From 5c88eff46f6853f0bed7a9a4e5c4d6c07dd02e6e Mon Sep 17 00:00:00 2001 From: Mathias Bergqvist Date: Sun, 3 May 2026 21:21:08 +0200 Subject: [PATCH] feat(hyperguild): subcommand router skeleton Lays down the cmd/hyperguild/ entry point. Defines the subcommand contract (ctx, args, stdin, stdout, stderr) error, the dispatch() function that's testable without os.Exit, and stubs for tier / brain / mode that return errNotImplemented. Subsequent commits replace each stub. Part of Plan 4 (hyperguild CLI) of the hyperguild migration. --- cmd/hyperguild/main.go | 79 +++++++++++++++++++++++++++++++++++++ cmd/hyperguild/main_test.go | 42 ++++++++++++++++++++ 2 files changed, 121 insertions(+) create mode 100644 cmd/hyperguild/main.go create mode 100644 cmd/hyperguild/main_test.go diff --git a/cmd/hyperguild/main.go b/cmd/hyperguild/main.go new file mode 100644 index 0000000..222b7cf --- /dev/null +++ b/cmd/hyperguild/main.go @@ -0,0 +1,79 @@ +// Package main implements the hyperguild CLI: tier probe, brain HTTP REST +// access, and .mcp.json mode bootstrap. See docs/superpowers/specs/. +package main + +import ( + "context" + "errors" + "fmt" + "io" + "os" +) + +// subcommand is the contract every hyperguild subcommand satisfies. +// Functions take an explicit context, args (without the subcommand name +// itself), and explicit IO so tests can exercise full flows without +// touching os.Stdin / os.Stdout / os.Exit. +type subcommand func(ctx context.Context, args []string, stdin io.Reader, stdout, stderr io.Writer) error + +// errNotImplemented is returned by stub subcommands until their task lands. +var errNotImplemented = errors.New("not implemented") + +func notYet(ctx context.Context, args []string, stdin io.Reader, stdout, stderr io.Writer) error { + return errNotImplemented +} + +func subcommands() map[string]subcommand { + return map[string]subcommand{ + "tier": notYet, + "brain": notYet, + "mode": notYet, + } +} + +const usage = `Usage: hyperguild [options] + +Subcommands: + tier Probe Anthropic + LiteLLM, print current operating tier. + brain query BM25 search the brain (HTTP REST). + brain write + Write stdin as a knowledge entry of type , slug . + mode Bootstrap .mcp.json for a chosen mode: + cloud | client-local | sovereign + +Environment: + BRAIN_URL Brain HTTP REST + MCP base URL. + Default: http://koala:30330 + ANTHROPIC_PROBE_URL Tier probe URL for the Anthropic API. + Default: https://api.anthropic.com + LITELLM_BASE_URL Tier probe URL for the LiteLLM gateway. + Required for tier probe; no default. +` + +// dispatch routes args to a subcommand and returns the process exit code. +// Split from main() so tests can drive it without process exit. +func dispatch(ctx context.Context, args []string, stdin io.Reader, stdout, stderr io.Writer) int { + if len(args) == 0 { + fmt.Fprint(stderr, usage) //nolint:errcheck + return 2 + } + switch args[0] { + case "-h", "--help", "help": + fmt.Fprint(stdout, usage) //nolint:errcheck + return 0 + } + cmd, ok := subcommands()[args[0]] + if !ok { + fmt.Fprintf(stderr, "hyperguild: unknown subcommand: %s\n%s", args[0], usage) //nolint:errcheck + return 2 + } + if err := cmd(ctx, args[1:], stdin, stdout, stderr); err != nil { + fmt.Fprintf(stderr, "hyperguild %s: %v\n", args[0], err) //nolint:errcheck + return 1 + } + return 0 +} + +func main() { + os.Exit(dispatch(context.Background(), os.Args[1:], os.Stdin, os.Stdout, os.Stderr)) +} diff --git a/cmd/hyperguild/main_test.go b/cmd/hyperguild/main_test.go new file mode 100644 index 0000000..7c91bd0 --- /dev/null +++ b/cmd/hyperguild/main_test.go @@ -0,0 +1,42 @@ +package main + +import ( + "bytes" + "context" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestDispatch_Help_PrintsUsageAndReturnsZero(t *testing.T) { + var out, errBuf bytes.Buffer + code := dispatch(context.Background(), []string{"--help"}, strings.NewReader(""), &out, &errBuf) + assert.Equal(t, 0, code) + assert.Contains(t, out.String(), "Usage: hyperguild") + assert.Contains(t, out.String(), "tier") + assert.Contains(t, out.String(), "brain") + assert.Contains(t, out.String(), "mode") +} + +func TestDispatch_NoArgs_PrintsUsageAndReturnsTwo(t *testing.T) { + var out, errBuf bytes.Buffer + code := dispatch(context.Background(), []string{}, strings.NewReader(""), &out, &errBuf) + assert.Equal(t, 2, code) + assert.Contains(t, errBuf.String(), "Usage: hyperguild") +} + +func TestDispatch_UnknownSubcommand_ReturnsTwo(t *testing.T) { + var out, errBuf bytes.Buffer + code := dispatch(context.Background(), []string{"bogus"}, strings.NewReader(""), &out, &errBuf) + assert.Equal(t, 2, code) + assert.Contains(t, errBuf.String(), "unknown subcommand: bogus") +} + +func TestDispatch_KnownSubcommand_RoutesAndReturnsExitCode(t *testing.T) { + var out, errBuf bytes.Buffer + code := dispatch(context.Background(), []string{"tier"}, strings.NewReader(""), &out, &errBuf) + // At Task 1 time, tier returns the not-implemented error → exit 1. + assert.Equal(t, 1, code) + assert.Contains(t, errBuf.String(), "not implemented") +}