From 362cc0057ef23e9d94d51c5a17ad0016dcbfce1d Mon Sep 17 00:00:00 2001 From: isink17 <39876158+isink17@users.noreply.github.com> Date: Wed, 22 Apr 2026 15:16:29 +0200 Subject: [PATCH 1/2] cli: add registry-driven root help (--help/-h/help) --- internal/cli/app.go | 50 +++++++++++++++++++++++++++++----------- internal/cli/app_test.go | 42 +++++++++++++++++++++++++++++++++ internal/cli/commands.go | 11 +++++++++ 3 files changed, 90 insertions(+), 13 deletions(-) diff --git a/internal/cli/app.go b/internal/cli/app.go index 1f28856..09fc203 100644 --- a/internal/cli/app.go +++ b/internal/cli/app.go @@ -50,8 +50,8 @@ func (s *stringListFlag) Set(value string) error { func Run(ctx context.Context, args []string, stdout, stderr io.Writer) error { startupVersionCheck(ctx, stderr) - if len(args) == 0 { - printUsage(stdout) + if len(args) == 0 || isRootHelpFlag(args[0]) { + printRootHelp(stdout) return nil } @@ -70,6 +70,15 @@ func Run(ctx context.Context, args []string, stdout, stderr io.Writer) error { return cmd.run(ctx, globalCfg, stdout, stderr, args[1:]) } +func isRootHelpFlag(arg string) bool { + switch arg { + case "-h", "--help": + return true + default: + return false + } +} + func runDoctor(stdout io.Writer, args []string) error { fs := flag.NewFlagSet("doctor", flag.ContinueOnError) fs.SetOutput(io.Discard) @@ -1132,19 +1141,34 @@ func runAffectedTests(ctx context.Context, cfg config.Config, stdout io.Writer, return nil } -func printUsage(w io.Writer) { - fmt.Fprintln(w, "codegraph commands:") +func printUsage(w io.Writer) { printRootHelp(w) } + +func printRootHelp(w io.Writer) { + fmt.Fprintf(w, "%s - local-first code context engine and MCP server\n\n", appname.BinaryName) + fmt.Fprintln(w, "Usage:") + fmt.Fprintf(w, " %s [args]\n", appname.BinaryName) + fmt.Fprintf(w, " %s --help\n", appname.BinaryName) + fmt.Fprintf(w, " %s help\n\n", appname.BinaryName) + + fmt.Fprintln(w, "Commands:") for _, cmd := range commandList { - lines := cmd.usageLines - if len(lines) == 0 { - lines = []string{" " + cmd.name} + // Use the first usage line as the synopsis so help stays stable even if + // additional notes exist below it (jsonl hints, etc.). + synopsis := cmd.name + if len(cmd.usageLines) > 0 { + synopsis = strings.TrimSpace(cmd.usageLines[0]) } - for i, line := range lines { - if i == 0 && cmd.description != "" { - fmt.Fprintf(w, "%s - %s\n", line, cmd.description) - continue - } - fmt.Fprintln(w, line) + if cmd.description != "" { + fmt.Fprintf(w, " %s - %s\n", synopsis, cmd.description) + } else { + fmt.Fprintf(w, " %s\n", synopsis) } } + + fmt.Fprintln(w, "\nExamples:") + fmt.Fprintf(w, " %s install\n", appname.BinaryName) + fmt.Fprintf(w, " %s index .\n", appname.BinaryName) + fmt.Fprintf(w, " %s stats .\n", appname.BinaryName) + fmt.Fprintf(w, " %s find-symbol . MySymbol\n", appname.BinaryName) + fmt.Fprintf(w, " %s serve --repo-root .\n", appname.BinaryName) } diff --git a/internal/cli/app_test.go b/internal/cli/app_test.go index 184b541..5c1a4d8 100644 --- a/internal/cli/app_test.go +++ b/internal/cli/app_test.go @@ -232,6 +232,48 @@ func TestRunDoctorFix(t *testing.T) { } } +func TestRunRootHelpFlagsAndCommand(t *testing.T) { + prev := startupVersionCheck + startupVersionCheck = func(context.Context, io.Writer) {} + t.Cleanup(func() { + startupVersionCheck = prev + }) + + for _, args := range [][]string{ + {}, + {"--help"}, + {"-h"}, + } { + t.Run(strings.Join(append([]string{"root"}, args...), "_"), func(t *testing.T) { + var out bytes.Buffer + var errOut bytes.Buffer + if err := Run(context.Background(), args, &out, &errOut); err != nil { + t.Fatalf("Run(%v) error = %v", args, err) + } + if got := out.String(); !strings.Contains(got, "Usage:") || !strings.Contains(got, "Commands:") { + t.Fatalf("help output missing sections, output:\n%s", got) + } + }) + } +} + +func TestRunHelpCommand(t *testing.T) { + prev := startupVersionCheck + startupVersionCheck = func(context.Context, io.Writer) {} + t.Cleanup(func() { + startupVersionCheck = prev + }) + + var out bytes.Buffer + var errOut bytes.Buffer + if err := Run(context.Background(), []string{"help"}, &out, &errOut); err != nil { + t.Fatalf("Run(help) error = %v", err) + } + if got := out.String(); !strings.Contains(got, "Usage:") || !strings.Contains(got, "Commands:") { + t.Fatalf("help output missing sections, output:\n%s", got) + } +} + func TestParseBenchmarkMetrics(t *testing.T) { output := ` goos: windows diff --git a/internal/cli/commands.go b/internal/cli/commands.go index 4a0ef76..c193fdc 100644 --- a/internal/cli/commands.go +++ b/internal/cli/commands.go @@ -61,6 +61,17 @@ func registerCommand(reg map[string]*command, c *command) { func newCommandList() []*command { return []*command{ + { + name: "help", + description: "show help", + usageLines: []string{" help"}, + run: func(ctx context.Context, cfg config.Config, stdout, stderr io.Writer, args []string) error { + // Keep this minimal for now. Per-command help can be added later without + // changing the dispatch surface. + printRootHelp(stdout) + return nil + }, + }, { name: "install", description: "install codegraph", From a33d7b7c92e9d5e689b81d2c8c6e657b9a9d2126 Mon Sep 17 00:00:00 2001 From: isink17 <39876158+isink17@users.noreply.github.com> Date: Wed, 22 Apr 2026 15:17:12 +0200 Subject: [PATCH 2/2] cli: add registry-driven root help (--help/-h/help) --- internal/cli/app.go | 2 +- internal/cli/commands.go | 19 +++++++++++++++++-- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/internal/cli/app.go b/internal/cli/app.go index 09fc203..b1addbb 100644 --- a/internal/cli/app.go +++ b/internal/cli/app.go @@ -1151,7 +1151,7 @@ func printRootHelp(w io.Writer) { fmt.Fprintf(w, " %s help\n\n", appname.BinaryName) fmt.Fprintln(w, "Commands:") - for _, cmd := range commandList { + for _, cmd := range commands() { // Use the first usage line as the synopsis so help stays stable even if // additional notes exist below it (jsonl hints, etc.). synopsis := cmd.name diff --git a/internal/cli/commands.go b/internal/cli/commands.go index c193fdc..2abb35f 100644 --- a/internal/cli/commands.go +++ b/internal/cli/commands.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "io" + "sync" "github.com/isink17/codegraph/internal/config" ) @@ -17,15 +18,29 @@ type command struct { } var ( - commandList = newCommandList() - commandByName = newCommandRegistry(commandList) + commandInitOnce sync.Once + commandList []*command + commandByName map[string]*command ) func lookupCommand(name string) (*command, bool) { + ensureCommandsInit() c, ok := commandByName[name] return c, ok } +func commands() []*command { + ensureCommandsInit() + return commandList +} + +func ensureCommandsInit() { + commandInitOnce.Do(func() { + commandList = newCommandList() + commandByName = newCommandRegistry(commandList) + }) +} + func newCommandRegistry(cmds []*command) map[string]*command { reg := map[string]*command{}