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.
This commit is contained in:
+139
@@ -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)
|
||||
}
|
||||
Vendored
+133
@@ -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"]
|
||||
}
|
||||
]
|
||||
Reference in New Issue
Block a user