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.
This commit is contained in:
79
cmd/hyperguild/main.go
Normal file
79
cmd/hyperguild/main.go
Normal file
@@ -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 <subcommand> [options]
|
||||
|
||||
Subcommands:
|
||||
tier Probe Anthropic + LiteLLM, print current operating tier.
|
||||
brain query <q> BM25 search the brain (HTTP REST).
|
||||
brain write <t> <s>
|
||||
Write stdin as a knowledge entry of type <t>, slug <s>.
|
||||
mode <name> 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))
|
||||
}
|
||||
42
cmd/hyperguild/main_test.go
Normal file
42
cmd/hyperguild/main_test.go
Normal file
@@ -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")
|
||||
}
|
||||
Reference in New Issue
Block a user