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