cli: make bundled help self-describing
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
@@ -30,6 +31,21 @@ func Execute(args []string, stdout, stderr io.Writer) int {
|
||||
usage()
|
||||
return 2
|
||||
}
|
||||
if args[0] == "help" {
|
||||
if len(args) == 1 {
|
||||
usage()
|
||||
return 0
|
||||
}
|
||||
if err := runCommand([]string{args[1], "--help"}); err != nil {
|
||||
_, _ = fmt.Fprintln(commandStderr, err)
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
if isHelpToken(args[0]) {
|
||||
usage()
|
||||
return 0
|
||||
}
|
||||
|
||||
if err := runCommand(args); err != nil {
|
||||
_, _ = fmt.Fprintln(commandStderr, err)
|
||||
@@ -39,6 +55,15 @@ func Execute(args []string, stdout, stderr io.Writer) int {
|
||||
return 0
|
||||
}
|
||||
|
||||
func isHelpToken(value string) bool {
|
||||
switch strings.TrimSpace(value) {
|
||||
case "-h", "--help":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func runCommand(args []string) error {
|
||||
switch args[0] {
|
||||
case "init":
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRepoMemoryRootHelpShowsWorkflowAndCommands(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
stdout, stderr, exitCode := executeRepoMemoryCommand("--help")
|
||||
if exitCode != 0 {
|
||||
t.Fatalf("expected help exit 0, got %d\nstderr:\n%s\nstdout:\n%s", exitCode, stderr, stdout)
|
||||
}
|
||||
|
||||
combined := stdout + stderr
|
||||
if !strings.Contains(combined, "Store durable repository knowledge in SQLite") {
|
||||
t.Fatalf("expected root help to explain purpose, got:\n%s", combined)
|
||||
}
|
||||
if !strings.Contains(combined, "Constraints:") {
|
||||
t.Fatalf("expected root help to include constraints section, got:\n%s", combined)
|
||||
}
|
||||
if !strings.Contains(combined, "repo-memory verify --db ~/.codex/data/repo-memory.db --repo /path/to/repo") {
|
||||
t.Fatalf("expected root help to include workflow example, got:\n%s", combined)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepoMemoryCommandHelpWorksThroughHelpSubcommand(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
stdout, stderr, exitCode := executeRepoMemoryCommand("help", "add")
|
||||
if exitCode != 0 {
|
||||
t.Fatalf("expected help exit 0, got %d\nstderr:\n%s\nstdout:\n%s", exitCode, stderr, stdout)
|
||||
}
|
||||
|
||||
combined := stdout + stderr
|
||||
if !strings.Contains(combined, "Insert or update one durable knowledge entry") {
|
||||
t.Fatalf("expected add help summary, got:\n%s", combined)
|
||||
}
|
||||
if !strings.Contains(combined, "Constraints:") {
|
||||
t.Fatalf("expected add help to include constraints section, got:\n%s", combined)
|
||||
}
|
||||
if !strings.Contains(combined, "--kind") || !strings.Contains(combined, "--dep") {
|
||||
t.Fatalf("expected add help to print flags, got:\n%s", combined)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepoMemoryCommandHelpWorksWithDashHelp(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
stdout, stderr, exitCode := executeRepoMemoryCommand("search", "--help")
|
||||
if exitCode != 0 {
|
||||
t.Fatalf("expected help exit 0, got %d\nstderr:\n%s\nstdout:\n%s", exitCode, stderr, stdout)
|
||||
}
|
||||
|
||||
combined := stdout + stderr
|
||||
if !strings.Contains(combined, "Search stored repository knowledge before a deeper code dive") {
|
||||
t.Fatalf("expected search help summary, got:\n%s", combined)
|
||||
}
|
||||
if !strings.Contains(combined, `--query "actionCode fill"`) {
|
||||
t.Fatalf("expected search help example, got:\n%s", combined)
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
@@ -32,8 +33,19 @@ func (s *stringSliceFlag) Set(value string) error {
|
||||
func runInit(args []string) error {
|
||||
fs := flag.NewFlagSet("init", flag.ContinueOnError)
|
||||
fs.SetOutput(commandStderr)
|
||||
setCommandUsage(fs, "init",
|
||||
"Create or migrate the SQLite schema for one repo-memory database.",
|
||||
[]string{
|
||||
"Run init once before first real use on a new database path.",
|
||||
"init is safe to rerun when you need to ensure the schema exists before another command.",
|
||||
},
|
||||
`repo-memory init --db ~/.codex/data/repo-memory.db`,
|
||||
)
|
||||
dbPath := fs.String("db", "repo-memory.db", "SQLite database path")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
if errors.Is(err, flag.ErrHelp) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -54,10 +66,21 @@ func runInit(args []string) error {
|
||||
func runIngest(args []string) error {
|
||||
fs := flag.NewFlagSet("ingest", flag.ContinueOnError)
|
||||
fs.SetOutput(commandStderr)
|
||||
setCommandUsage(fs, "ingest",
|
||||
"Scan markdown knowledge under one repository and import it into repo-memory.",
|
||||
[]string{
|
||||
"ingest expects a repository root and scans markdown under the configured relative path.",
|
||||
"Use ingest for curated docs; use add when you want to record one specific fact manually.",
|
||||
},
|
||||
`repo-memory ingest --db ~/.codex/data/repo-memory.db --repo /path/to/repo --path docs/ai`,
|
||||
)
|
||||
dbPath := fs.String("db", "repo-memory.db", "SQLite database path")
|
||||
repoPath := fs.String("repo", "", "Repository root")
|
||||
scanPath := fs.String("path", "docs/ai", "Relative path under repo to scan for markdown")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
if errors.Is(err, flag.ErrHelp) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
if strings.TrimSpace(*repoPath) == "" {
|
||||
@@ -120,6 +143,14 @@ func runIngest(args []string) error {
|
||||
func runAdd(args []string) error {
|
||||
fs := flag.NewFlagSet("add", flag.ContinueOnError)
|
||||
fs.SetOutput(commandStderr)
|
||||
setCommandUsage(fs, "add",
|
||||
"Insert or update one durable knowledge entry with evidence, aliases, and dependencies.",
|
||||
[]string{
|
||||
"add requires a repository root because entries are anchored to one repo.",
|
||||
"Prefer confirmed only for well-supported facts; use dependencies and evidence fields whenever possible.",
|
||||
},
|
||||
`repo-memory add --db ~/.codex/data/repo-memory.db --repo /path/to/repo --kind term --key AITask --summary "Plan task model" --source-path app/AITask.java --source-line 42 --status confirmed --alias "AI Task" --dep file:app/AITask.java:hard`,
|
||||
)
|
||||
dbPath := fs.String("db", "repo-memory.db", "SQLite database path")
|
||||
repoPath := fs.String("repo", "", "Repository root")
|
||||
kind := fs.String("kind", "", "Knowledge kind, e.g. term|chain|danger")
|
||||
@@ -139,6 +170,9 @@ func runAdd(args []string) error {
|
||||
fs.Var(&aliases, "alias", "Alias for this entry (repeatable)")
|
||||
fs.Var(&deps, "dep", "Dependency in type:locator[:hard|soft] format (repeatable)")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
if errors.Is(err, flag.ErrHelp) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
if strings.TrimSpace(*repoPath) == "" {
|
||||
@@ -202,11 +236,22 @@ func runAdd(args []string) error {
|
||||
func runSearch(args []string) error {
|
||||
fs := flag.NewFlagSet("search", flag.ContinueOnError)
|
||||
fs.SetOutput(commandStderr)
|
||||
setCommandUsage(fs, "search",
|
||||
"Search stored repository knowledge before a deeper code dive.",
|
||||
[]string{
|
||||
"search requires a non-empty query string.",
|
||||
"Use repo filtering when you want to narrow results to one repository name or path fragment.",
|
||||
},
|
||||
`repo-memory search --db ~/.codex/data/repo-memory.db --repo zeus --query "actionCode fill" --limit 10`,
|
||||
)
|
||||
dbPath := fs.String("db", "repo-memory.db", "SQLite database path")
|
||||
query := fs.String("query", "", "Search query")
|
||||
repo := fs.String("repo", "", "Optional repo path filter (substring match)")
|
||||
limit := fs.Int("limit", 10, "Result limit")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
if errors.Is(err, flag.ErrHelp) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
if strings.TrimSpace(*query) == "" {
|
||||
@@ -243,8 +288,18 @@ func runSearch(args []string) error {
|
||||
func runRepos(args []string) error {
|
||||
fs := flag.NewFlagSet("repos", flag.ContinueOnError)
|
||||
fs.SetOutput(commandStderr)
|
||||
setCommandUsage(fs, "repos",
|
||||
"List repositories currently tracked in one repo-memory database.",
|
||||
[]string{
|
||||
"repos reads only from the memory database; it does not rescan the filesystem.",
|
||||
},
|
||||
`repo-memory repos --db ~/.codex/data/repo-memory.db`,
|
||||
)
|
||||
dbPath := fs.String("db", "repo-memory.db", "SQLite database path")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
if errors.Is(err, flag.ErrHelp) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -272,12 +327,23 @@ func runRepos(args []string) error {
|
||||
func runList(args []string) error {
|
||||
fs := flag.NewFlagSet("list", flag.ContinueOnError)
|
||||
fs.SetOutput(commandStderr)
|
||||
setCommandUsage(fs, "list",
|
||||
"List entries with optional repo, kind, and status filters.",
|
||||
[]string{
|
||||
"list is for broad inspection; use search when you need ranked text matching.",
|
||||
"Filters are optional and can be combined to narrow the result set.",
|
||||
},
|
||||
`repo-memory list --db ~/.codex/data/repo-memory.db --repo zeus --kind term --status confirmed --limit 20`,
|
||||
)
|
||||
dbPath := fs.String("db", "repo-memory.db", "SQLite database path")
|
||||
repo := fs.String("repo", "", "Optional repo path filter (substring match)")
|
||||
kind := fs.String("kind", "", "Optional knowledge kind filter")
|
||||
status := fs.String("status", "", "Optional status filter")
|
||||
limit := fs.Int("limit", 20, "Result limit")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
if errors.Is(err, flag.ErrHelp) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -311,6 +377,14 @@ func runList(args []string) error {
|
||||
func runEvents(args []string) error {
|
||||
fs := flag.NewFlagSet("events", flag.ContinueOnError)
|
||||
fs.SetOutput(commandStderr)
|
||||
setCommandUsage(fs, "events",
|
||||
"Show verification and status-change history for one entry.",
|
||||
[]string{
|
||||
"Resolve the entry either by --id or by the combination of --repo, --kind, and --key.",
|
||||
"events is history-only; it does not modify entries.",
|
||||
},
|
||||
`repo-memory events --db ~/.codex/data/repo-memory.db --id 1`,
|
||||
)
|
||||
dbPath := fs.String("db", "repo-memory.db", "SQLite database path")
|
||||
id := fs.Int64("id", 0, "Entry id")
|
||||
repo := fs.String("repo", "", "Repo root when resolving by kind/key")
|
||||
@@ -318,6 +392,9 @@ func runEvents(args []string) error {
|
||||
key := fs.String("key", "", "Knowledge key when resolving by kind/key")
|
||||
limit := fs.Int("limit", 20, "Result limit")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
if errors.Is(err, flag.ErrHelp) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -361,11 +438,22 @@ func runEvents(args []string) error {
|
||||
func runLink(args []string) error {
|
||||
fs := flag.NewFlagSet("link", flag.ContinueOnError)
|
||||
fs.SetOutput(commandStderr)
|
||||
setCommandUsage(fs, "link",
|
||||
"Create a relationship edge between two stored entries.",
|
||||
[]string{
|
||||
"link expects two existing entry IDs.",
|
||||
"Use links for durable relationships such as related_to, depends_on, or implements.",
|
||||
},
|
||||
`repo-memory link --db ~/.codex/data/repo-memory.db --from-id 1 --to-id 2 --relation related_to`,
|
||||
)
|
||||
dbPath := fs.String("db", "repo-memory.db", "SQLite database path")
|
||||
fromID := fs.Int64("from-id", 0, "From entry id")
|
||||
toID := fs.Int64("to-id", 0, "To entry id")
|
||||
relation := fs.String("relation", "", "Link relation")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
if errors.Is(err, flag.ErrHelp) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -386,9 +474,20 @@ func runLink(args []string) error {
|
||||
func runVerify(args []string) error {
|
||||
fs := flag.NewFlagSet("verify", flag.ContinueOnError)
|
||||
fs.SetOutput(commandStderr)
|
||||
setCommandUsage(fs, "verify",
|
||||
"Re-check stored entries against current repository state and downgrade stale knowledge.",
|
||||
[]string{
|
||||
"verify uses current git state to detect changed or missing hard dependencies.",
|
||||
"Pass --repo to verify one repository; omit it to verify every tracked repository in the database.",
|
||||
},
|
||||
`repo-memory verify --db ~/.codex/data/repo-memory.db --repo /path/to/repo`,
|
||||
)
|
||||
dbPath := fs.String("db", "repo-memory.db", "SQLite database path")
|
||||
repo := fs.String("repo", "", "Optional repo root to verify; if omitted, verify all known repos")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
if errors.Is(err, flag.ErrHelp) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -468,18 +567,64 @@ func usage() {
|
||||
_, _ = fmt.Fprintf(commandStderr, `repo-memory: repo memory CLI
|
||||
|
||||
Usage:
|
||||
repo-memory init --db repo-memory.db
|
||||
repo-memory add --db repo-memory.db --repo /path/to/repo --kind term --key AITask --summary "..."
|
||||
repo-memory ingest --db repo-memory.db --repo /path/to/repo [--path docs/ai]
|
||||
repo-memory search --db repo-memory.db --query "actionCode fill" [--repo zeus]
|
||||
repo-memory list --db repo-memory.db [--repo zeus] [--kind term] [--status confirmed]
|
||||
repo-memory events --db repo-memory.db --id 1
|
||||
repo-memory link --db repo-memory.db --from-id 1 --to-id 2 --relation related_to
|
||||
repo-memory verify --db repo-memory.db [--repo /path/to/repo]
|
||||
repo-memory repos --db repo-memory.db
|
||||
repo-memory <command> [flags]
|
||||
|
||||
Purpose:
|
||||
Store durable repository knowledge in SQLite so agents can search prior findings,
|
||||
ingest curated docs, add confirmed facts, inspect history, and verify stale entries.
|
||||
|
||||
Constraints:
|
||||
- Write durable repository knowledge, not short-lived chat conclusions.
|
||||
- Prefer search before add so repeated work starts from existing knowledge.
|
||||
- Use verify when code has moved enough that stored entries may be stale.
|
||||
|
||||
Commands:
|
||||
init Initialize or migrate one SQLite database
|
||||
ingest Import markdown knowledge from docs under one repo
|
||||
add Insert or update one durable knowledge entry
|
||||
search Search stored knowledge by query text
|
||||
list List entries with optional filters
|
||||
events Show history for one entry
|
||||
link Link two entries together
|
||||
verify Re-check entries against current repo state
|
||||
repos List tracked repositories
|
||||
|
||||
Examples:
|
||||
repo-memory init --db ~/.codex/data/repo-memory.db
|
||||
repo-memory search --db ~/.codex/data/repo-memory.db --repo zeus --query "router auth"
|
||||
repo-memory add --db ~/.codex/data/repo-memory.db --repo /path/to/repo --kind term --key AuthRouter --summary "..."
|
||||
repo-memory verify --db ~/.codex/data/repo-memory.db --repo /path/to/repo
|
||||
|
||||
More help:
|
||||
repo-memory --help
|
||||
repo-memory help add
|
||||
repo-memory add --help
|
||||
`)
|
||||
}
|
||||
|
||||
func setCommandUsage(fs *flag.FlagSet, name, summary string, constraints []string, example string) {
|
||||
fs.Usage = func() {
|
||||
_, _ = fmt.Fprintf(commandStderr, "repo-memory %s\n\n", name)
|
||||
_, _ = fmt.Fprintf(commandStderr, "Usage:\n repo-memory %s [flags]\n\n", name)
|
||||
_, _ = fmt.Fprintf(commandStderr, "Purpose:\n %s\n\n", summary)
|
||||
if len(constraints) > 0 {
|
||||
_, _ = fmt.Fprintln(commandStderr, "Constraints:")
|
||||
for _, constraint := range constraints {
|
||||
if strings.TrimSpace(constraint) == "" {
|
||||
continue
|
||||
}
|
||||
_, _ = fmt.Fprintf(commandStderr, " - %s\n", constraint)
|
||||
}
|
||||
_, _ = fmt.Fprintln(commandStderr)
|
||||
}
|
||||
if strings.TrimSpace(example) != "" {
|
||||
_, _ = fmt.Fprintf(commandStderr, "Example:\n %s\n\n", example)
|
||||
}
|
||||
_, _ = fmt.Fprintln(commandStderr, "Flags:")
|
||||
fs.PrintDefaults()
|
||||
}
|
||||
}
|
||||
|
||||
type gitState struct {
|
||||
branch string
|
||||
commit string
|
||||
|
||||
Reference in New Issue
Block a user