crabbox/internal/cli/app.go
2026-05-06 07:52:15 +01:00

253 lines
8.4 KiB
Go

package cli
import (
"context"
"errors"
"flag"
"fmt"
"io"
"os"
)
type App struct {
Stdout io.Writer
Stderr io.Writer
}
func Run(ctx context.Context, args []string) error {
app := App{Stdout: os.Stdout, Stderr: os.Stderr}
return app.Run(ctx, args)
}
func (a App) Run(ctx context.Context, args []string) error {
if len(args) == 0 {
a.printHelp()
return exit(2, "missing command")
}
switch args[0] {
case "-h", "--help":
a.printHelp()
return nil
case "help":
if len(args) > 1 {
return a.runKong(ctx, args)
}
a.printHelp()
return nil
}
if help, ok := a.directCommandHelp(ctx, args); ok {
return help
}
return a.runKong(ctx, args)
}
func (a App) directCommandHelp(ctx context.Context, args []string) (error, bool) {
if len(args) < 2 || !isHelpArg(args[1]) || isKongCommandGroup(args[0]) {
return nil, false
}
helpArgs := []string{"--help"}
switch args[0] {
case "init":
return a.initProject(ctx, helpArgs), true
case "login":
return a.login(ctx, helpArgs), true
case "logout":
return a.logout(ctx, helpArgs), true
case "whoami":
return a.whoami(ctx, helpArgs), true
case "doctor":
return a.doctor(ctx, helpArgs), true
case "warmup":
return a.warmup(ctx, helpArgs), true
case "run":
return a.runCommand(ctx, helpArgs), true
case "sync-plan":
return a.syncPlan(ctx, helpArgs), true
case "history":
return a.history(ctx, helpArgs), true
case "logs":
return a.logs(ctx, helpArgs), true
case "events":
return a.events(ctx, helpArgs), true
case "attach":
return a.attach(ctx, helpArgs), true
case "results":
return a.results(ctx, helpArgs), true
case "status":
return a.status(ctx, helpArgs), true
case "list":
return a.list(ctx, helpArgs), true
case "usage":
return a.usage(ctx, helpArgs), true
case "ssh":
return a.ssh(ctx, helpArgs), true
case "vnc":
return a.vnc(ctx, helpArgs), true
case "webvnc":
return a.webvnc(ctx, helpArgs), true
case "code":
return a.webCode(ctx, helpArgs), true
case "screenshot":
return a.screenshot(ctx, helpArgs), true
case "inspect":
return a.inspect(ctx, helpArgs), true
case "stop", "release":
return a.stop(ctx, helpArgs), true
case "cleanup":
return a.cleanup(ctx, helpArgs), true
default:
return nil, false
}
}
func isHelpArg(arg string) bool {
return arg == "-h" || arg == "--help" || arg == "help"
}
func (a App) printHelp() {
fmt.Fprintln(a.Stdout, `Crabbox leases remote test boxes, syncs your dirty checkout, runs commands, and cleans up.
Usage:
crabbox <command> [flags]
crabbox run [flags] -- <command...>
Start Here:
crabbox login
Open GitHub login and store broker credentials.
crabbox doctor
Check local tools, config, broker, and provider access.
crabbox init
Add repo-local Crabbox config, GitHub workflow, and agent skill.
crabbox warmup --class beast
Lease a reusable box and print a cbx_... id plus friendly slug.
crabbox run --id blue-lobster -- pnpm test:changed
Sync this checkout to the box and run a command.
crabbox warmup --desktop --browser --code
Lease a UI-capable box with a browser and web code editor.
Commands:
init Onboard the current repo for Crabbox
login Open GitHub login, store broker credentials, verify access
logout Remove the stored broker token
whoami Show broker identity
doctor Check local and broker/provider readiness
warmup Lease a box and wait until it is ready
run Sync the repo, run a remote command, stream output
desktop Launch apps into a visible desktop session
media Create preview artifacts from recorded desktop videos
sync-plan Show local sync manifest size hotspots
history List recorded remote runs
logs Print recorded run logs
events Print recorded run events
attach Follow recorded events for an active run
results Show recorded test result summaries
cache Inspect, purge, or warm remote caches
status Show lease state; add --wait to block until ready
list List Crabbox machines
image Create or promote brokered AWS runner images
usage Show cost and usage estimates by user, org, or fleet
admin Lease admin controls for trusted operators
actions Register GitHub Actions runners or dispatch workflows
ssh Print the SSH command for a lease
vnc Print or open VNC connection details for a desktop lease
webvnc Bridge a desktop lease into the authenticated web portal
code Bridge a code lease into the authenticated web portal
screenshot Capture a PNG from a desktop lease
inspect Print lease/provider details; add --json for scripts
stop Release a lease or delete a direct-provider machine
cleanup Sweep expired direct-provider machines
config Show or update user config
Common Flows:
crabbox run --class beast -- pnpm check
crabbox warmup
crabbox status --id blue-lobster --wait
crabbox run --id blue-lobster --shell 'pnpm install --frozen-lockfile && pnpm test'
crabbox ssh --id blue-lobster
crabbox vnc --id blue-lobster --open
crabbox desktop launch --id blue-lobster --browser --url https://example.com --webvnc --open
crabbox media preview --input desktop.mp4 --output desktop-preview.gif --trimmed-video-output desktop-change.mp4
crabbox webvnc --id blue-lobster --open
crabbox code --id blue-lobster --open
crabbox screenshot --id blue-lobster --output desktop.png
crabbox inspect --id blue-lobster --json
crabbox history --lease cbx_abcdef123456
crabbox logs run_123
crabbox events run_123
crabbox attach run_123
crabbox results run_123
crabbox cache stats --id blue-lobster
crabbox usage --scope org
crabbox admin leases --state active
crabbox warmup --actions-runner
crabbox actions hydrate --id blue-lobster
crabbox actions dispatch -f testbox_id=cbx_abcdef123456
crabbox run --provider ssh --target macos --static-host mac.local -- echo ok
crabbox run --provider ssh --target windows --windows-mode normal --static-host win.local -- pwsh -NoProfile -Command '$PSVersionTable'
crabbox stop blue-lobster
Global:
-h, --help Show help
--version Print version
Config:
crabbox login [--url <url>] [--provider aws|hetzner] [--no-browser]
crabbox login --url <url> --token-stdin [--provider aws|hetzner]
crabbox config path
crabbox config show [--json]
crabbox config set-broker --url <url> --token-stdin [--provider aws|hetzner]
Environment:
CRABBOX_COORDINATOR Broker URL
CRABBOX_COORDINATOR_TOKEN Broker bearer token
CRABBOX_COORDINATOR_ADMIN_TOKEN
Broker admin bearer token
CRABBOX_ACCESS_CLIENT_ID Cloudflare Access service token client ID
CRABBOX_ACCESS_CLIENT_SECRET Cloudflare Access service token client secret
CRABBOX_ACCESS_TOKEN Cloudflare Access JWT for protected routes
CRABBOX_PROVIDER hetzner, aws, ssh, blacksmith-testbox, daytona, or islo
CRABBOX_TARGET linux, macos, or windows
CRABBOX_WINDOWS_MODE normal or wsl2
CRABBOX_DESKTOP Provision or require desktop/VNC capability
CRABBOX_BROWSER Provision or require browser capability
CRABBOX_CODE Provision or require web code capability
CRABBOX_STATIC_HOST Static SSH host for provider=ssh
CRABBOX_OWNER Usage owner override
CRABBOX_ORG Usage org override
CRABBOX_CONFIG Optional config path
CRABBOX_IDLE_TIMEOUT Default idle expiry, e.g. 30m
CRABBOX_TTL Maximum lease lifetime, e.g. 90m
CRABBOX_AWS_REGION Default eu-west-1
CRABBOX_AWS_SSH_CIDRS Comma-separated AWS SSH source CIDRs
CRABBOX_SSH_FALLBACK_PORTS Comma-separated SSH fallback ports, or none
CRABBOX_CAPACITY_MARKET spot or on-demand
CRABBOX_CAPACITY_REGIONS Comma-separated AWS Spot placement candidates
HCLOUD_TOKEN/HETZNER_TOKEN Direct Hetzner mode
Aliases:
crabbox release <id-or-slug> Alias for stop
crabbox pool list Alias for list
crabbox machine cleanup Alias for cleanup
Docs:
docs/commands/README.md`)
}
func newFlagSet(name string, stderr io.Writer) *flag.FlagSet {
fs := flag.NewFlagSet(name, flag.ContinueOnError)
fs.SetOutput(stderr)
return fs
}
func parseFlags(fs *flag.FlagSet, args []string) error {
if err := fs.Parse(args); err != nil {
if errors.Is(err, flag.ErrHelp) {
return ExitError{Code: 0}
}
return exit(2, "%v", err)
}
return nil
}