POC: `glab orbit` Command Integration
## Context `gkg` (Graph Knowledge Graph) is a local binary that indexes repositories into a structured knowledge graph and can serve them via a local server/MCP endpoint. We want to expose its functionality through `glab orbit ...` subcommands, following similar patterns to the `duo cli` integration — but adapted for `gkg`'s richer command surface (both short-lived and long-running commands). **Future consideration**: There will eventually be two modes — local binary (current) and remote API (future). Subcommands need enough explicit structure so `--local`/`--remote` routing can be added per-command later without a rewrite. Binary management (download/install/update) is out of scope for this iteration. --- ## gkg Command Surface | glab orbit command | gkg invocation | Behavior | |---------------------------|------------------------------|--------------------| | `glab orbit index [...]` | `gkg index [args...]` | Runs to completion | | `glab orbit server [...]` | `gkg server [args...]` | start: long-running; stop: quick | | `glab orbit remove [...]` | `gkg remove [args...]` | Runs to completion | | `glab orbit clean` | `gkg clean` | Runs to completion | `server` is a pass-through to `gkg server [args...]`, so `glab orbit server start [...]` and `glab orbit server stop` both work without needing separate cobra commands for each. --- ## Approach: Top-Level Pass-Through (~5 files) Each top-level subcommand (`index`, `server`, `remove`, `clean`) is an explicit cobra command with `DisableFlagParsing: true`. It prepends the appropriate gkg subcommand name and passes everything else through to the binary verbatim. This gives us: - Glab-native `--help` listing the top-level subcommands - The right anchor points to add `--local`/`--remote` routing in each command's `RunE` later - No per-sub-subcommand files (e.g. no separate `server_start.go` / `server_stop.go`) --- ## File Structure ``` internal/commands/orbit/ ├── orbit.go # Parent cmd + binary resolution + shared runGkg() helper ├── index.go # glab orbit index → gkg index [args...] ├── server.go # glab orbit server → gkg server [args...] ├── remove.go # glab orbit remove → gkg remove [args...] └── clean.go # glab orbit clean → gkg clean [args...] ``` --- ## Key Design Details ### Binary Resolution (`orbit.go`) ```go func gkgPath() (string, error) { if p := os.Getenv("GLAB_GKG_PATH"); p != "" { return p, nil } return exec.LookPath("gkg") } ``` `GLAB_GKG_PATH` env var allows override (useful for testing and future binary management). ### Shared Execution Helper (`orbit.go`) ```go func runGkg(ctx context.Context, io *iostreams.IOStreams, gkgArgs []string) error { bin, err := gkgPath() if err != nil { return fmt.Errorf("gkg binary not found on PATH (install it or set GLAB_GKG_PATH): %w", err) } cmd := exec.CommandContext(ctx, bin, gkgArgs...) cmd.Stdin = io.In cmd.Stdout = io.StdOut cmd.Stderr = io.StdErr if err := cmd.Run(); err != nil { if exitErr, ok := err.(*exec.ExitError); ok { os.Exit(exitErr.ExitCode()) } return err } return nil } ``` Uses `exec.Command` subprocess (not `syscall.Exec`) on all platforms — no platform-split files needed. Ctrl+C naturally sends SIGINT to the whole process group so `gkg server start` (foreground) stops cleanly. Exit codes are preserved. ### Each Subcommand Pattern ```go // example: server.go cmd := &cobra.Command{ Use: "server [command] [flags]", Short: "Manage the gkg server", DisableFlagParsing: true, RunE: func(cmd *cobra.Command, args []string) error { return runGkg(cmd.Context(), f.IO, append([]string{"server"}, args...)) }, } ``` ### Help Behavior `DisableFlagParsing: true` means cobra does **not** intercept `--help` or `-h` — they become raw args passed to `RunE`, which forwards them to gkg. - `glab orbit --help` → cobra shows glab-native help listing the 4 subcommands (the parent `orbit` command has normal flag parsing) - `glab orbit server --help` → calls `gkg server --help` → shows gkg's native server help - `glab orbit index -h` → calls `gkg index -h` → shows gkg's native index help ### Future `--local`/`--remote` Routing When the remote API variant arrives, the RunE evolves to something like: ```go RunE: func(cmd *cobra.Command, args []string) error { if remote { return runRemoteAPI(ctx, f, "server", args) } return runGkg(ctx, f.IO, append([]string{"server"}, args...)) }, ``` The cobra command stays the same; only the routing logic inside RunE changes. --- ## Files to Modify | File | Change | |------|--------| | `internal/commands/root.go` | Add import + `rootCmd.AddCommand(orbitCmd.NewCmd(f))` alongside `duoCmd` | ## Files to Create | File | Purpose | |------|---------| | `internal/commands/orbit/orbit.go` | Parent cmd, binary resolution, `runGkg()` helper | | `internal/commands/orbit/index.go` | `glab orbit index` | | `internal/commands/orbit/server.go` | `glab orbit server` | | `internal/commands/orbit/remove.go` | `glab orbit remove` | | `internal/commands/orbit/clean.go` | `glab orbit clean` | --- ## Verification 1. **Binary not found**: `GLAB_GKG_PATH=/nonexistent glab orbit index` → clear error 2. **Override path**: `GLAB_GKG_PATH=$(which gkg) glab orbit index .` → works 3. **Short-lived command**: `glab orbit index .` → streams gkg output, exits with same code 4. **Foreground server**: `glab orbit server start` → streams logs, Ctrl+C stops cleanly 5. **Detached server**: `glab orbit server start --detached` → returns to prompt 6. **Stop server**: `glab orbit server stop` → runs to completion 7. **All flags pass through**: `glab orbit index --verbose --threads 4 /path` → flags reach gkg unchanged 8. **Help**: `glab orbit --help` lists the 4 subcommands; `glab orbit server --help` shows gkg's own help for `server` 9. **Build check**: `go build ./...` 10. **Unit tests**: `go test ./internal/commands/orbit/...`
issue