CLI command structure, Cobra patterns, execution flow, hidden internal commands. Use when working on cmd/invowk/ files, adding CLI commands, or modifying Cobra command trees.
This skill covers the CLI implementation in Invowk, including Cobra command structure, dynamic command registration, TUI component wrappers, and the execution flow.
Use this skill when working on:
cmd/invowk/ - CLI commands and structure.agents/rules/commands.md is the source of truth for hidden internal command policy..agents/rules/testing.md is the source of truth for CLI test coverage and mirror expectations..agents/rules/go-patterns.md is the source of truth for Go comments, ordering, and error-handling conventions.The CLI is organized under root.go with these main command groups:
| Command | Description |
|---|---|
invowk cmd | Dynamic command execution (discovered from invowkfiles/modules) |
invowk module | Module management (validate, create, alias, deps) |
invowk validate | Unified validation (workspace, invowkfile, or module) |
invowk config | Configuration management |
invowk init | Initialize new invowkfiles |
invowk tui | Interactive terminal UI components (gum-like) |
invowk internal | Hidden internal commands |
invowk completion | Shell completion |
CRITICAL: All invowk internal * commands MUST remain hidden.
&cobra.Command{
Use: "internal",
Hidden: true, // ALWAYS true for internal commands
}
Rules:
.agents/ and README.mdCurrent internal commands:
invowk internal exec-virtual — Runs virtual shell in subprocess contextinvowk internal check-cmd <name> — Returns exit 0 if command is discoverable, exit 1 otherwise. Used by runtime-level cmds dependency validation inside containers to verify auto-provisioning worked.The discovery → registration flow (cmd_discovery.go):
Discovery → Validation → Command Registration
↓
DiscoveredCommandSet
├── Commands: all discovered
├── AmbiguousNames: conflicts
└── SourceOrder: sorted sources
Dynamic registration must stay lazy for startup-sensitive paths.
cmd subtree (or completion requests for it).--version or --help.cmd startup latency low while preserving full invowk cmd behavior.Unambiguous commands are registered under their SimpleName:
# Only one source defines "build" → user can run directly
invowk cmd build
# Multiple sources define "deploy" → requires disambiguation
invowk cmd @foo deploy # Using @source prefix
invowk cmd --ivk-from foo deploy # Using --ivk-from flag
Multi-word commands (e.g., "deploy staging") are built into a command tree with automatically created parent commands.
All TUI components follow a dual-layer delegation pattern (tui_*.go):
func runTuiInput(cmd *cobra.Command, args []string) error {
// Layer 1: Check if running under parent TUI server
if client := tuiserver.NewClientFromEnv(); client != nil {
// Delegate to parent TUI server via HTTP/IPC
result, err := client.Input(tuiserver.InputRequest{...})
return handleResult(result)
}
// Layer 2: Direct TUI rendering
result, err := tui.Input(tui.InputOptions{...})
return handleResult(result)
}
| Command | Purpose |
|---|---|
tui input | Single-line text input |
tui choose | Single/multi-select from list |
tui confirm | Yes/no confirmation |
tui spin | Spinner with command execution |
tui filter | Fuzzy filter |
tui file | File picker |
tui table | Display/select from table |
tui pager | Scrollable content viewer |
tui format | Markdown/code/emoji formatting |
tui write | Multi-line text editor |
Benefits:
The execution is decomposed into a pipeline of focused methods on commandsvc.Service (internal/app/commandsvc/service.go), with runtime selection and context construction delegated to internal/app/execute/:
commandService.Execute(ctx, req)
│
├── discoverCommand() ← Uses req.ResolvedCommand when provided by CLI/disambiguation path;
│ otherwise loads config and calls s.discovery.GetCommand(ctx, name)
│
├── resolveDefinitions() ← Resolves flag/arg defs with fallbacks
│
├── validateInputs() ← Validates flags, args, platform compatibility
│
├── resolveRuntime() ← Delegates to appexec.ResolveRuntime() (3-tier precedence),
│ wraps errors as ServiceError
│
├── ensureSSHIfNeeded() ← Conditional SSH server start for container host access
│
├── buildExecContext() ← Delegates to appexec.BuildExecutionContext() for env var projection
│ (includes INVOWK_CMD_NAME, INVOWK_RUNTIME, INVOWK_SOURCE, INVOWK_PLATFORM)
│
├── [DRY-RUN SHORT-CIRCUIT] ← If req.DryRun: renderDryRun() and return (no execution)
│
└── dispatchExecution() ← Calls runtime.BuildRegistry(), then executes pipeline:
├── Container init fail-fast (via runtimeRegistryResult.ContainerInitErr)
├── Timeout validation (parse-only, fail-fast on invalid strings)
├── Timeout wrapping (context.WithTimeout)
├── Dependency validation (validateAndRenderDeps)
├── Interactive mode (alternate screen + TUI server) OR standard execution
└── Error classification via classifyExecutionError()
classifyExecutionError() (internal/app/commandsvc/errors.go) maps runtime errors to issue catalog IDs using type-safe errors.Is() chains:
switch {
case errors.Is(err, container.ErrNoEngineAvailable): → ContainerEngineNotFoundId
case errors.Is(err, runtime.ErrRuntimeNotAvailable): → RuntimeNotAvailableId
case errors.Is(err, os.ErrPermission): → PermissionDeniedId
default (ActionableError "find shell"): → ShellNotFoundId