Go CLI for Verda Cloud. Cobra commands + Bubble Tea TUI + lipgloss styling.
make build # Build binary to ./bin/verda
make test # Run all tests (go test -race)
make lint # Lint only (golangci-lint); also run by pre-commit hooks
make pre-commit # Full pre-commit suiteNever use raw go test ./... — always make test (go test -race). Lint is separate: make lint; the pre-commit hooks run both.
cmd/verda/ # Entrypoint
internal/verda-cli/
cmd/cmd.go # Root command, command groups
cmd/util/ # Factory, IOStreams, helpers, pricing, hostname
cmd/<domain>/ # One dir per domain (see per-command docs below)
CLAUDE.md # Domain knowledge, gotchas, edge cases
README.md # Usage examples, flags, architecture notes
options/ # Global CLI options, credentials
internal/skills/ # Embedded AI skill files (go:embed)
pkg/ # In-tree TUI core, log, version (formerly verdagostack)
Each command directory has its own CLAUDE.md (domain knowledge) and README.md (usage/architecture). These are the source of truth for command-specific behavior.
| Directory | Docs | Description |
|---|---|---|
cmd/vm/ |
CLAUDE.md, README.md | VM create/list/describe/action, wizard, templates |
cmd/template/ |
CLAUDE.md, README.md | Template create/edit/list/show/delete |
cmd/auth/ |
CLAUDE.md, README.md | Login, logout, show credentials |
cmd/volume/ |
CLAUDE.md, README.md | Volume lifecycle, trash, actions |
cmd/sshkey/ |
CLAUDE.md, README.md | SSH key management |
cmd/startupscript/ |
CLAUDE.md, README.md | Startup script management |
cmd/registry/ |
CLAUDE.md, README.md | Container registry (vccr.io): configure, configure-docker (alias login), show, ls, tags, push, copy, delete — beta (enabled by default, marked (beta) in verda --help) |
cmd/update/ |
CLAUDE.md, README.md | CLI self-update |
cmd/settings/ |
CLAUDE.md, README.md | CLI settings management |
cmd/objectstorage/ |
CLAUDE.md, README.md | S3-style object storage: configure, mb/rb, cp/mv/sync/ls/rm, uploads, presign |
cmd/serverless/ |
CLAUDE.md, README.md | Serverless containers and batch jobs |
cmd/doctor/ |
— | Environment diagnostics |
cmd/availability/ |
— | Instance availability by location |
cmd/cost/ |
— | Balance, running costs, estimates |
cmd/images/ |
— | OS image listing |
cmd/instancetypes/ |
— | Instance type catalog |
cmd/locations/ |
— | Datacenter locations |
cmd/status/ |
— | Status dashboard |
cmd/ssh/ |
— | SSH into instances |
cmd/mcp/ |
CLAUDE.md, README.md | MCP server (AI-agent tool surface; confirm gates, accepted/completed semantics) |
cmd/skills/ |
— | AI skills management |
cmd/completion/ |
— | Shell completions |
- Factory (
cmd/util/factory.go): DI for Prompter, Status, VerdaClient, Debug, AgentMode, OutputFormat - Wizard engine (
pkg/tui/wizard): Multi-step interactive flows - Lazy client (
clientFunc): API client resolved on first use, not at init - API cache (
apiCache): Shared across wizard steps to avoid redundant calls
pkg/tui(+bubbletea,wizard,testing),pkg/log,pkg/versionlive in-tree — edit them directly like any other code in this repo (they were copied fromverdagostackv1.4.2, which this repo no longer depends on)- Bubble Tea v2 (
charm.land/bubbletea/v2), lipgloss v2 (charm.land/lipgloss/v2) - Never use v1 imports — they won't compile
The repo lints with golangci-lint via make lint (also enforced by the pre-commit hooks, not by make test). These are the patterns the linters enforce — write them correctly the first time instead of fixing them in a second pass:
- HTTP bodies — use
http.NoBodyfor GET/DELETE/etc., nevernil. Close withdefer func() { _ = resp.Body.Close() }(), not baredefer resp.Body.Close()(errcheck). - American English —
behavior,canceled,artifact,checkered,gray.misspellruns withlocale: USand rejects British spellings in code and comments. - Reuse constants — before writing a string literal that might repeat, grep for an existing one. Current package-level constants worth knowing:
defaultTag("latest") incmd/registry/refname.go,progressJSON("json") incmd/registry/push.go,untaggedLabel("<untagged>") incmd/registry/format.go.goconstfails on ≥3 occurrences. - Strings over fmt.Sprintf —
"prefix " + sbeatsfmt.Sprintf("prefix %s", s)when there's only one substitution (perfsprint). - Range indexing for structs ≥96 B —
for i := range xs { x := &xs[i] }avoids the per-iteration copyfor _, x := range xsincurs (gocritic rangeValCopy).ArtifactInfo,VMDescribeResult, etc. all cross the threshold. - Intentional
return nilafter error — prompter cancellation returns an error that we deliberately swallow. Annotate withreturn nil //nolint:nilerr // intentional: prompter cancel is a clean exitsonilerrdoesn't flag it and the reason survives. - No blank line after
{—whitespacelinter flags it. Go straight into the first statement. - Type inference over explicit declaration —
rt := http.DefaultTransportovervar rt http.RoundTripper = http.DefaultTransport(staticcheck ST1023). - Complexity budgets —
gocyclotrips at 20,nestifat 5. Extract helpers before you hit them; refactoring after the fact is more churn.
.golangci.yaml is the authoritative list — all of the above come from linters enabled there.
- Default to no comment. Well-named identifiers carry the meaning. Add a comment only when the why is non-obvious: an invariant, a workaround, a gotcha a future reader would miss, an evolution point.
- One line, identifier-first.
// resolveContainerName: args[0], else picker; agent requires <name>.beats a three-line paragraph. - Never narrate WHAT.
// Loop over deployments and build labelsis noise — delete it. - Capture invariants, not history.
// Describe still succeeds if status RPC fails.is durable.// Added for ticket VC-1234rots — put it in the commit message. - Flag known evolution points.
// if SDK gains json:"status", switch to explicit fields.documents a future-failure mode so the next reader doesn't have to rediscover it. - Delete when the reason expires. Workaround landed, gotcha fixed, SDK gap closed → remove the comment in the same commit.
- Timeout context:
ctx, cancel := context.WithTimeout(cmd.Context(), f.Options().Timeout)for control-plane calls. Data-plane transfers (registry push/copy, object-storage cp/mv/sync) run oncmd.Context()— Ctrl+C is the stop signal; a multi-GB transfer legitimately outlives--timeout. Interactive prompts also getcmd.Context(), and work resumed after a prompt re-bounds its API ctx so prompt think-time can't drain the budget. The sharedhttp.Clientcarries NOTimeout— the client cap covers whole-body reads and would clamp transfers. - Spinner: Show spinner during API calls, stop before handling result
- Debug output:
cmdutil.DebugJSON(ioStreams.ErrOut, f.Debug(), "label:", data) - Dual mode: Work with flags (non-interactive) AND prompts (interactive) — no partial wizard
- Output separation: Data →
ioStreams.Out, prompts/warnings/debug →ioStreams.ErrOut
- Show warning styling (red bold) before confirmation
- Require
prompter.Confirm()— return nil on cancel or Esc - In agent mode (
f.AgentMode()): require--yesflag, never auto-confirm
- Show the hint bar at the bottom of every direct
Prompter.Select— passtui.WithShowHints(true)to render↑/↓ navigate · type to filter · enter select · esc back · ctrl+c exitbelow the choices. Same forMultiSelectvia the equivalent option. Wizard step Loaders are exempt — the wizard composite already renders its own hint bar; double-rendering is a bug. - Treat Ctrl+C as a hard exit, Esc as a soft back — never show a confirmation dialog on either. Unix users expect Ctrl+C to be terminal; an "Exit?" prompt is friction, and confirmation dialogs themselves can be cancelled which makes the design contradictory. Use
cmdutil.IsPromptInterrupt(err)for Ctrl+C andcmdutil.IsPromptBack(err)for Esc when the two need different handling (e.g. in a "Back to list / Exit" gate, Esc returns to the list while Ctrl+C exits the whole loop). Both are cleanly distinguishable viacmdutil.IsPromptCancel(err)if a flow doesn't care which key triggered it. - Use
cmdutil.IsPromptCancel(err)— never bare-return nilon prompter errors; distinguish clean Ctrl+C / Esc from real I/O failures and propagate the latter.
- Instance
price_per_hourfrom the API (instances AND instance-types endpoints) is the TOTAL hourly price of the instance. Never multiply by GPU/vCPU count. Verified live on staging 2026-08-09 (temp/docs/c1-ondemand-instance.json; review C1). - Burn rate = plain sum of instance
price_per_hourtotals (+ volumebase_hourly_cost). - A per-unit price shown to the user is total divided by units (GPU count or vCPU count) — division only, and only for display.
- Volume hourly:
cmdutil.VolumeHourlyPrice(monthlyPerGB, sizeGB)=ceil(monthlyPerGB * sizeGB / HoursInMonth * 10000) / 10000— the only sanctioned formula (MCP and all CLI surfaces use it). - Volume monthly:
cmdutil.VolumeMonthlyPrice(monthlyPerGB, sizeGB); hourly→monthly estimates usecmdutil.HoursInMonth(730 = 365*24/12, matching the web frontend).
- AWS-style INI at
~/.verda/credentialswithverda_prefixed keys - Profile support via
[profile_name]sections f.VerdaClient()handles resolution — returns clear error if not authenticated
- Read the nearest
CLAUDE.mdin the command directory (e.g.cmd/vm/CLAUDE.md) — domain knowledge, gotchas, edge cases - Read the nearest
README.mdin the command directory — usage examples, flags, architecture - Read
.ai/skills/new-command.mdfor the full checklist when adding/modifying commands - If touching pricing, auth, or agent-mode: plan first, don't code immediately
Per-command docs are auto-maintained by /update-command-knowledge skill.
Manual update: claude -p "/update-command-knowledge --all" --model sonnet --dangerously-skip-permissions
| Change Type | Approach |
|---|---|
| Rename, typo, flag default | Just do it |
| New list/describe command | Follow .ai/skills/new-command.md checklist |
| New create/wizard flow | Plan first — wizard steps, cache strategy, step dependencies |
| Refactor shared util | Check all callers, run full test suite |
| Pricing logic | Deep think — verify formula against API docs, test with real numbers |
| Auth flow changes | Deep think — test all profiles, expired tokens, missing creds |
Agent-mode (--agent) changes |
Deep think — JSON output contract, structured errors, no prompts |
Before considering any change complete:
make build # Must compile
make test # Must pass (go test -race)
make lint # Must pass (golangci-lint; also run by pre-commit hooks)Never report work as complete with lint failures outstanding. Fix them before the "done" message; don't defer to the pre-commit hook. See the "Go House Style" section above for the patterns that prevent the common hits.
If you modified a command, also verify:
./bin/verda <command> --helprenders correctly- Interactive mode works (prompts appear)
- Non-interactive mode works (flags only, no prompts)
--agent -o jsonmode works (structured output, no TUI)--debugshows request/response payloads
This repo targets Claude Code and OpenAI Codex. Claude auto-loads this file; Codex auto-loads AGENTS.md (execution contract). A .cursor/rules/main.mdc pointer exists for Cursor users but is not a primary target — if Cursor drops out of the stack, delete it rather than letting it drift.