feat: add demo subcommand with seeded test data #12

Merged
lerko merged 1 commits from feat/demo-command into develop 2026-05-16 17:15:17 +00:00
2 changed files with 272 additions and 0 deletions
Showing only changes of commit 5bb6e89523 - Show all commits
+139
View File
@@ -0,0 +1,139 @@
package cmd
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"time"
"github.com/lerko/nib/internal/db"
"github.com/spf13/cobra"
)
var demoCmd = &cobra.Command{
Use: "demo",
Short: "start server with pre-populated demo data",
RunE: runDemo,
}
func init() {
rootCmd.AddCommand(demoCmd)
}
type demoEntity struct {
Body string `json:"body"`
Glyph string `json:"glyph"`
Title *string `json:"title,omitempty"`
Description *string `json:"description,omitempty"`
TimeAnchor *string `json:"time_anchor,omitempty"`
Pinned bool `json:"pinned"`
Completed bool `json:"completed"`
Deleted bool `json:"deleted"`
CardType *string `json:"card_type,omitempty"`
CardData *string `json:"card_data,omitempty"`
Tags []string `json:"tags"`
}
func runDemo(_ *cobra.Command, _ []string) error {
tmpDir, err := os.MkdirTemp("", "nib-demo-*")
if err != nil {
return err
}
dbPath := filepath.Join(tmpDir, "demo.db")
fmt.Printf("demo db: %s\n", dbPath)
store, err := db.Open(dbPath)
if err != nil {
return err
}
if err := seedDemo(store); err != nil {
store.Close()
return fmt.Errorf("seed demo data: %w", err)
}
store.Close()
os.Setenv("NIB_DB", dbPath)
return runServe(nil, nil)
}
func seedDemo(store *db.Store) error {
data, err := findDemoFile()
if err != nil {
return err
}
var entries []demoEntity
if err := json.Unmarshal(data, &entries); err != nil {
return fmt.Errorf("parse demo.json: %w", err)
}
now := time.Now().UTC()
for i, entry := range entries {
e := &db.Entity{
Body: entry.Body,
Glyph: db.Glyph(entry.Glyph),
Tags: entry.Tags,
}
if entry.Title != nil {
e.Title = entry.Title
}
if entry.Description != nil {
e.Description = entry.Description
}
if entry.TimeAnchor != nil {
e.TimeAnchor = entry.TimeAnchor
}
if entry.Pinned {
e.Pinned = true
}
if entry.Completed {
t := now.Add(-time.Duration(i) * time.Hour)
e.CompletedAt = &t
}
if err := store.Create(e); err != nil {
return fmt.Errorf("entity %d: %w", i, err)
}
if entry.CardType != nil {
ct := db.CardType(*entry.CardType)
if err := store.Promote(e.ID, ct, entry.CardData); err != nil {
return fmt.Errorf("promote entity %d: %w", i, err)
}
}
if entry.Deleted {
store.SoftDelete(e.ID)
}
}
fmt.Printf("seeded %d entities\n", len(entries))
return nil
}
func findDemoFile() ([]byte, error) {
candidates := []string{
"testdata/demo.json",
filepath.Join(execDir(), "testdata", "demo.json"),
}
for _, path := range candidates {
data, err := os.ReadFile(path)
if err == nil {
return data, nil
}
}
return nil, fmt.Errorf("demo.json not found (looked in: %v)", candidates)
}
func execDir() string {
exe, err := os.Executable()
if err != nil {
return "."
}
return filepath.Dir(exe)
}
+133
View File
@@ -0,0 +1,133 @@
[
{
"body": "Buy milk, eggs, and bread",
"glyph": "todo",
"tags": ["errands", "grocery"]
},
{
"body": "Fix leaking kitchen faucet",
"glyph": "todo",
"tags": ["home", "plumbing"]
},
{
"body": "Review pull request for auth refactor",
"glyph": "todo",
"tags": ["work", "code-review"],
"pinned": true
},
{
"body": "Dentist appointment",
"glyph": "event",
"time_anchor": "2026-05-20T10:00:00Z",
"tags": ["health"]
},
{
"body": "Team standup",
"glyph": "event",
"time_anchor": "2026-05-19T09:00:00Z",
"tags": ["work", "meetings"]
},
{
"body": "Kubernetes clusters use etcd as the backing store for all cluster data including state, config, and metadata.",
"glyph": "note",
"tags": ["devops", "k8s"]
},
{
"body": "The Go scheduler uses M:N threading — M goroutines multiplexed onto N OS threads.",
"glyph": "note",
"tags": ["golang", "til"]
},
{
"body": "Solar panel installation — get 3 quotes before June",
"glyph": "note",
"tags": ["home", "solar"],
"pinned": true
},
{
"body": "Submit quarterly tax estimate",
"glyph": "todo",
"time_anchor": "2026-06-15T00:00:00Z",
"tags": ["finance"]
},
{
"body": "Backup NAS to offsite",
"glyph": "todo",
"completed": true,
"tags": ["homelab", "backups"]
},
{
"body": "version: '3'\nservices:\n traefik:\n image: traefik:v2.10\n ports:\n - \"${host_port:-443}:443\"\n volumes:\n - /var/run/docker.sock:/var/run/docker.sock\n environment:\n - CF_DNS_API_TOKEN=${cf_token}\n labels:\n - traefik.http.routers.dashboard.rule=Host(`${dashboard_domain}`)",
"glyph": "note",
"title": "Traefik Reverse Proxy",
"description": "Production-ready compose with auto-TLS renewal",
"card_type": "snippet",
"card_data": "{\"language\":\"yaml\",\"source\":\"personal\"}",
"tags": ["homelab", "docker", "traefik"]
},
{
"body": "## Weekly Review\n- [ ] Clear inbox\n- [ ] Review calendar\n- [ ] Update project boards\n- [ ] Plan next week",
"glyph": "note",
"title": "Weekly Review Checklist",
"card_type": "checklist",
"card_data": "{\"items\":4,\"completed\":0}",
"tags": ["productivity", "routine"]
},
{
"body": "PRAGMA journal_mode = WAL;\nPRAGMA busy_timeout = ${timeout_ms:-5000};\nPRAGMA synchronous = ${sync_mode:-NORMAL};",
"glyph": "note",
"title": "SQLite Concurrency",
"description": "Key settings for multi-reader single-writer",
"card_type": "snippet",
"card_data": "{\"language\":\"sql\",\"source\":\"docs\"}",
"tags": ["sqlite", "til"]
},
{
"body": "Decided to use CalVer (YYYY.0M.MICRO) instead of SemVer for nib releases. Rationale: nib is an app not a library, no API stability contract needed.",
"glyph": "note",
"title": "Versioning Strategy",
"card_type": "decision",
"card_data": "{\"status\":\"accepted\",\"date\":\"2026-04-01\"}",
"tags": ["nib", "decisions"]
},
{
"body": "https://github.com/charmbracelet/bubbletea",
"glyph": "note",
"title": "Bubbletea TUI Framework",
"description": "Go TUI framework based on Elm architecture",
"card_type": "link",
"card_data": "{\"url\":\"https://github.com/charmbracelet/bubbletea\",\"domain\":\"github.com\"}",
"tags": ["golang", "tui", "libraries"]
},
{
"body": "Remember to rotate API keys every 90 days",
"glyph": "todo",
"time_anchor": "2026-07-01T00:00:00Z",
"tags": ["security", "homelab"]
},
{
"body": "Interesting idea: build a CLI that converts natural language to nib captures using local LLM",
"glyph": "note",
"tags": ["ideas", "nib", "ai"]
},
{
"body": "Garage door opener warranty expires in August",
"glyph": "event",
"time_anchor": "2026-08-15T00:00:00Z",
"tags": ["home"]
},
{
"body": "Consolidate all docker services to single compose file",
"glyph": "todo",
"tags": ["homelab", "docker"],
"deleted": true
},
{
"body": "## ${project_name}\n- [ ] Create repo at ${git_host}/${org}/${project_name}\n- [ ] Add CI pipeline\n- [ ] Write README\n- [ ] Add LICENSE (${license_type})\n- [ ] First release tag",
"glyph": "note",
"title": "Project Bootstrap",
"description": "Standard checklist for starting new projects",
"card_type": "template",
"card_data": "{\"items\":5}",
"tags": ["productivity", "dev"]
}
]