From 5bb6e895235fad8d888467131ffb68fa445d1d8b Mon Sep 17 00:00:00 2001 From: Tyler Koenig Date: Sat, 16 May 2026 13:13:05 -0400 Subject: [PATCH] feat: add demo subcommand with seeded test data nib demo starts server with temp DB populated from testdata/demo.json. Covers all glyphs, card types, tags, pins, completions, deletes, and template fill placeholders. --- cmd/demo.go | 139 +++++++++++++++++++++++++++++++++++++++++++++ testdata/demo.json | 133 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 272 insertions(+) create mode 100644 cmd/demo.go create mode 100644 testdata/demo.json diff --git a/cmd/demo.go b/cmd/demo.go new file mode 100644 index 0000000..ca3bf50 --- /dev/null +++ b/cmd/demo.go @@ -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) +} diff --git a/testdata/demo.json b/testdata/demo.json new file mode 100644 index 0000000..57c23ca --- /dev/null +++ b/testdata/demo.json @@ -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"] + } +]