lstk is LocalStack's new CLI (v2) - a Go-based command-line interface for starting and managing LocalStack instances via Docker (and more runtimes in the future).
make build # Compiles to bin/lstk
make test # Run unit tests (cmd/ and internal/) via gotestsum
make test-integration # Run integration tests (builds first, requires Docker)
make lint # Run golangci-lint
make mock-generate # Run go generate to regenerate mocks
make clean # Remove build artifactsRun a single integration test:
make test-integration RUN=TestStartCommandSucceedsWithValidTokenNote: Integration tests require LOCALSTACK_AUTH_TOKEN environment variable for valid token tests.
main.go- Entry pointcmd/- CLI wiring only (Cobra framework), no business logicinternal/- All business logic goes herecontainer/- Handling different emulator containersruntime/- Abstraction for container runtimes (Docker, Kubernetes, etc.) - currently only Docker implementedauth/- Authentication (env var token or browser-based login)config/- Viper-based TOML config loading and path resolutionoutput/- Generic event and sink abstractions for CLI/TUI/non-interactive renderingui/- Bubble Tea views for interactive outputupdate/- Self-update logic: version check via GitHub API, binary/Homebrew/npm update paths, archive extractionlog/- Internal diagnostic logging (not for user-facing output — useoutput/for that)
lstk always writes diagnostic logs to $CONFIG_DIR/lstk.log (appends across runs, cleared at 1 MB). Two log levels: Info and Error.
log.Loggeris injected as a dependency (viaStartOptionsor constructor params). Uselog.Nop()in tests.- This is separate from
output.Sink— the logger is for internal diagnostics, the sink is for user-facing output.
Uses Viper with TOML format. lstk uses the first config.toml found in this order:
./.lstk/config.toml(project-local)$HOME/.config/lstk/config.toml- macOS:
$HOME/Library/Application Support/lstk/config.toml/ Windows:%AppData%\lstk\config.toml
When no config file exists, lstk creates one at $HOME/.config/lstk/config.toml if $HOME/.config/ already exists, otherwise at the OS default (#3). This means #3 is only reached on macOS when $HOME/.config/ didn't exist at first run.
Use lstk config path to print the resolved config file path currently in use.
When adding a new command that depends on configuration, wire config initialization explicitly in that command (PreRunE: initConfig). Keep side-effect-free commands (e.g., version, config path) without config initialization.
Created automatically on first run with defaults. Supports emulator types (aws, snowflake, azure) - currently only aws is implemented.
Use lstk setup <emulator> to set up CLI integration for an emulator type:
lstk setup aws— Sets up AWS CLI profile in~/.aws/configand~/.aws/credentials
This naming avoids AWS-specific "profile" terminology and uses a clear verb for mutation operations.
The deprecated lstk config profile command still works but points users to lstk setup aws.
Environment variables:
LOCALSTACK_AUTH_TOKEN- Auth token (skips browser login if set)
- Don't add comments for self-explanatory code. Only comment when the "why" isn't obvious from the code itself.
- Do not remove comments added by someone else than yourself.
- Errors returned by functions should always be checked unless in test files.
- Terminology: in user-facing CLI/help/docs, prefer
emulatorovercontainer/runtime; usecontainer/runtimeonly for internal implementation details. - Avoid package-level global variables. Use constructor functions that return fresh instances and inject dependencies explicitly. This keeps packages testable in isolation and prevents shared mutable state between tests.
- Never print directly to stdout/stderr (e.g.,
fmt.Fprintf(os.Stderr, …)). For user-facing output, emit events throughoutput.Sink. For internal diagnostics, uselog.Logger. If neither is available (e.g., during logger setup), return errors to the caller and let them decide. - Do not call
config.Get()from domain/business-logic packages. Instead, extract the values you need at the command boundary (cmd/) and pass them as explicit function arguments. This keeps domain functions testable without requiring Viper/config initialization.
- Prefer integration tests to cover most cases. Use unit tests when integration tests are not practical.
- When fixing a bug, always add an integration test that fails before the fix and passes after. This prevents regressions and documents the exact scenario that was broken.
- Integration tests that run the CLI binary with Bubble Tea must use a PTY (
github.com/creack/pty) since Bubble Tea requires a terminal. Usepty.Start(cmd)instead ofcmd.CombinedOutput(), read output withio.Copy(), and send keystrokes by writing to the PTY (e.g.,ptmx.Write([]byte("\r"))for Enter).
- Emit typed events through
internal/output(EmitInfo,EmitSuccess,EmitNote,EmitWarning,EmitStatus,EmitProgress, etc.) instead of printing from domain/command handlers. - Keep
output.Sinksealed (unexportedemit); sink implementations belong ininternal/output. - Reuse
FormatEventLine(event any)for all line-oriented rendering so plain and TUI output stay consistent. - Select output mode at the command boundary in
cmd/: interactive TTY runs Bubble Tea, non-interactive mode usesoutput.NewPlainSink(...). - Keep non-TTY mode non-interactive (no stdin prompts or input waits).
- Domain packages must not import Bubble Tea or UI packages.
- Any feature/workflow package that produces user-visible progress should accept an
output.Sinkdependency and emit events throughinternal/output. - Do not pass UI callbacks like
onProgress func(...)through domain layers; prefer typed output events. - Event payloads should be domain facts (phase/status/progress), not pre-rendered UI strings.
- When adding a new event type, update all of:
internal/output/events.go(event type +Eventunion constraint + emit helper)internal/output/plain_format.go(line formatting fallback)- tests in
internal/output/*_test.gofor formatter/sink behavior parity
Domain code must never read from stdin or wait for user input directly. Instead:
-
Emit a
UserInputRequestEventviaoutput.EmitUserInputRequest()with:Prompt: message to displayOptions: available choices (e.g.,{Key: "enter", Label: "Press ENTER to continue"})ResponseCh: channel to receive the user's response
-
Wait on the
ResponseChfor anInputResponsecontaining:SelectedKey: which option was selectedCancelled: true if user cancelled (e.g., Ctrl+C)
-
The TUI (
internal/ui/app.go) handles these events by showing the prompt and sending the response when the user interacts. -
In non-interactive mode, commands requiring user input should fail early with a helpful error (e.g., "set LOCALSTACK_AUTH_TOKEN or run in interactive mode").
Example flow in auth login:
responseCh := make(chan output.InputResponse, 1)
output.EmitUserInputRequest(sink, output.UserInputRequestEvent{
Prompt: "Waiting for authentication...",
Options: []output.InputOption{{Key: "enter", Label: "Press ENTER when complete"}},
ResponseCh: responseCh,
})
select {
case resp := <-responseCh:
if resp.Cancelled {
return "", context.Canceled
}
// proceed with user's choice
case <-ctx.Done():
return "", ctx.Err()
}internal/ui/- Bubble Tea app model and run orchestrationinternal/ui/components/- Reusable presentational componentsinternal/ui/styles/- Lipgloss style definitions and palette constants
- Keep components small and focused (single concern each).
- Keep UI as presentation/orchestration only; business logic stays in domain packages.
- Long-running work must run outside
Update()(goroutine or command path), with UI updates sent asynchronously. - Bubble Tea updates from background work should flow through
Program.Send()viaoutput.NewTUISink(...). Update()must stay non-blocking.- UI should consume shared output events directly; add UI-only wrapper/control messages only when needed, and suffix them with
...Msg. - Keep message/history state bounded (for example, capped line buffer).
- Define styles with semantic names in
internal/ui/styles/styles.go. - Preserve the Nimbo palette constants (
#3F51C7,#5E6AD2,#7E88EC) unless intentionally changing branding. - If changing palette constants, update/add tests to guard against accidental drift.
Custom skills are available in .claude/skills/:
/add-command <name>— Scaffold a new CLI subcommand with proper cmd/ wiring, domain logic, sink handling, and tests/add-event <EventName>— Add a new output event type to the event/sink system with format parity/add-component <name>— Scaffold a new Bubble Tea TUI component/review-pr <number>— Review a PR against architectural patterns/create-pr— Create a PR with conventional format and Linear ticket linking
When making significant changes to the codebase (new commands, architectural changes, build process updates, new patterns), update this CLAUDE.md file to reflect them.