The Go CLI¶
limabean is being rewritten from a bash script into a Go binary. The VM
templates don't change; only the tool that drives them does. This page is the
design. User-facing docs live in cli.md.
Goals¶
- Behave like the bash CLI it replaces: same commands, config file, VM layout and Claude Desktop and Keychain conventions, so existing setups keep working.
- Make secrets easy to add, including in a single form when a VM is created.
- Ship as one binary (
brew install,go install), with no runtime dependency on gum. - Be tested: parsing, quoting and file edits have unit tests, and command wiring is checked automatically.
Non-goal: importing Lima's Go packages. github.com/lima-vm/lima/v2 is
importable but makes no promise of a stable API, and it would tie limabean to
one Lima version while users run whatever limactl they have installed.
limactl is the boundary, and limactl list --format json gives structured
data.
Layout¶
cmd/limabean/main.go entry point: cli.Execute()
internal/cli/ Cobra commands, one per file (root.go, new.go, ls.go,
secrets_set.go, …); groups (github, secrets, claude,
network, snapshot, autostop, images) have a file for
the parent and one per subcommand; mcp.go is the MCP
server, which runs these commands in-process;
images.go builds and finds the images VMs are
copied from
internal/config/ Viper: config file, LIMABEAN_* environment, flags
internal/lima/ limactl runner (list, start, stop, delete, disks, ssh)
internal/pins/ pinned versions in mise TOML files, for bump
internal/project/ .limabean.yaml: find, parse and check, for up
internal/templates/ the templates directory, packs and environments
internal/secrets/ reference files: parse, merge, resolve, render
internal/keychain/ macOS Keychain via `security`, values on stdin
internal/claude/ sshConfigs entries in ~/.claude/settings.json
internal/github/ token checks against the GitHub API
internal/ui/ lipgloss styles, huh forms, spinners, plain mode
internal/atomic/ atomic file writes for limabean's own files
templates.go embeds templates/ into the binary
Conventions¶
- One command per file. Each file defines a single
cobra.Commandconstructor such asnewNewCmd(app *App), and registers it with its parent inroot.goor in the group's file. - Complete help. Every command has
Short,LongandExample, written for someone who has never used limabean. A test walks the command tree and fails if any of them is empty. - Flags bound through Viper, so each setting can come from a flag, a
LIMABEAN_*environment variable or the config file, in that order. - No secret values in argv. Values reach
security,sshand other processes on stdin.
Configuration¶
Viper reads ~/.config/limabean/config, the existing KEY="value" file, using
its dotenv format, so configs written by the bash init keep working.
| Key | Default | Flag | Environment |
|---|---|---|---|
GIT_CONFIG_DIR |
~/.config/git |
LIMABEAN_GIT_CONFIG_DIR |
|
CODE_DIR |
~/code |
LIMABEAN_CODE_DIR |
|
CODE_DISK |
50 (GiB) |
LIMABEAN_CODE_DISK |
|
CPUS / MEMORY / DISK |
template defaults (2, 4 GiB, 10 GiB) | --cpus / --memory / --disk on up |
LIMABEAN_CPUS / LIMABEAN_MEMORY / LIMABEAN_DISK |
PACKS |
empty: only core |
--with / --without on up |
LIMABEAN_PACKS |
IMAGES |
yes |
LIMABEAN_IMAGES |
|
CLAUDE_DESKTOP |
yes |
LIMABEAN_CLAUDE_DESKTOP |
|
AWS_DIR |
~/.aws |
LIMABEAN_AWS_DIR |
|
IDE |
empty: the first installed | --ide on code |
LIMABEAN_IDE |
Global flags: --config, --plain (also LIMABEAN_PLAIN=1), --templates
(use a templates directory instead of the embedded copy, for development).
Templates¶
templates/ is embedded with go:embed. Lima needs real files, and it
resolves file: and base: references relative to the template. So on
first use the tree is written to ~/.cache/limabean/templates/<hash>/, named
by a hash of its contents, and limactl start reads it from there. Your own
environments in ~/.config/limabean/templates/*.yaml are copied alongside
it, so they can use the built-in layers. --templates ./templates points at
a checkout instead.
Interface¶
- In a terminal: lipgloss styles in the README palette, a spinner for slow
steps (Lima's output is shown only on failure), huh forms and selects for
prompts, and a table for
ls. - Without a terminal, or with
--plain: plain lines, no prompts. Missing arguments are errors, and questions take their defaults.
Secrets¶
References live on the Mac, never values:
~/.config/limabean/secrets.env every VM
~/.config/limabean/secrets.<vm>.env one VM; overrides the shared file
One NAME=reference per line. A reference is keychain:<service>,
op://vault/item/field (1Password CLI), env:<VAR>, cmd:<command>, or a
plain value for non-secret settings. limabean secrets sync resolves them on
the Mac and writes export NAME='value' lines to
~/.config/limabean/secrets.env (mode 600) in each VM. The VM's env.sh
loads that file.
Commands: secrets ls, secrets set, secrets rm, secrets edit and
secrets sync. secrets set NAME, given no reference, prompts for the value
and stores it in the Keychain as limabean-secret-NAME.
At creation, limabean up shows a form: the name, the environment, the
existing secrets to include (all shared ones are pre-selected), and "Add a
secret…", which asks for a name and a source (paste into the Keychain, a
1Password reference, an environment variable or a command). Choices made here
are written to secrets.<vm>.env. For scripts, --secret NAME=reference
does the same.
GitHub token. limabean github login stores the token in the Keychain
(limabean-github-token) and sets GITHUB_TOKEN=keychain:limabean-github-token
in the shared file, so the token is just another secret. github status,
sync and logout remain as shortcuts.
Testing¶
- Unit tests for reference parsing and merging, shell quoting of values,
Claude settings edits (other keys kept, invalid JSON untouched, in-place
writes), config loading, and
limactl listJSON parsing through a fake runner. - The help-completeness test described under Conventions.
- CI runs
go test ./...,go vetand golangci-lint, alongside the existing shellcheck and template validation. - End-to-end runs against real VMs before each release.
Migration¶
Done. The Go CLI reads the same config, writes the same Claude Desktop
entries and Keychain items, and creates VMs from the same templates, so
existing setups keep working. The bash bin/limabean is gone. VMs created by
it load secrets after limabean rebuild.
Releases¶
GoReleaser (.goreleaser.yaml) builds one universal
macOS binary, with the version set through -X main.version. Pull requests
get snapshot builds as workflow artifacts (snapshot.yml). A v* tag runs
release.yml, which tests, builds and publishes a GitHub release with the
archive, checksums and a changelog, using the workflow's own GITHUB_TOKEN.
No other secrets are needed.