cli: make bundled help self-describing

This commit is contained in:
2026-03-22 23:37:38 +08:00
parent 5859ff219e
commit 4d8c90eb26
49 changed files with 792 additions and 29 deletions
@@ -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