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