cli

CLI command structure, Cobra patterns, execution flow, hidden internal commands

SKILL.md

GitHub

CLI Architecture Skill

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
  • internal/app/commandsvc/ - command execution service and CLI-facing service errors
  • internal/app/commandadapters/ - runtime/dependency/discovery adapters used by CLI execution
  • internal/app/deps/ - dependency validation that renders through CLI paths
  • internal/app/execute/ - runtime selection and execution-context construction
  • Adding new CLI commands or subcommands
  • Modifying command output format
  • TUI component integration
  • Error handling and exit codes

Normative Quick Rules

  • .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/skills/go/SKILL.md is the source of truth for Go comments, ordering, and error-handling conventions.
  • This skill documents CLI architecture and implementation details; if this file conflicts with a rule, the rule wins.

Command Hierarchy Structure

The CLI is organized under root.go. Derive the current command tree before editing or documenting it:

go run . --help
go run . module --help
go run . tui --help
rg -n 'func new.*Command|Use:|Hidden:' cmd/invowk

Current major groups are expected to include:

Command Description
invowk cmd Dynamic command execution (discovered from invowkfiles/modules)
invowk module Module management; inspect cmd/invowk/module*.go for current subcommands
invowk validate Unified validation (workspace, invowkfile, or module)
invowk audit Module supply-chain/security audit
invowk agent Agent workflow helpers
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

Hidden Internal Commands Policy

CRITICAL: All invowk internal * commands MUST remain hidden.

&cobra.Command{
    Use:    "internal",
    Hidden: true,  // ALWAYS true for internal commands
}

Rules:

  • Internal commands are for inter-process communication and subprocess execution
  • Do NOT document in website docs
  • Only mention in .agents/ and README.md
  • Used by container runtime, SSH server, TUI server internals

Current internal commands: derive from cmd/invowk/internal*.go before making changes:

rg -n 'newInternal|Use:|Hidden: true' cmd/invowk/internal*.go

Expected categories include virtual runtime subprocess entrypoints and container/runtime dependency probes. Keep all of them under hidden invowk internal command paths.


Dynamic Command Registration

The discovery → registration flow (cmd_discovery.go):

Discovery → Validation → Command Registration
                ↓
        DiscoveredCommandSet
         ├── Commands: all discovered
         ├── AmbiguousNames: conflicts
         └── SourceOrder: sorted sources

Startup Gate (Important)

Dynamic registration must stay lazy for startup-sensitive paths.

  • Register discovered commands only when argv targets the cmd subtree (or completion requests for it).
  • Do not trigger discovery registration for unrelated root paths like --version or --help.
  • This keeps non-cmd startup latency low while preserving full invowk cmd behavior.

Transparent Namespace

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

Hierarchical Tree Building

Multi-word commands (e.g., "deploy staging") are built into a command tree with automatically created parent commands.


TUI Component Wrapper Pattern

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)
}

Available TUI Commands

Rebuild this inventory from cmd/invowk/tui*.go when changing TUI command behavior:

rg -n 'func newTUI.*Command|Use:|Flags\\(\\)' cmd/invowk/tui*.go
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 style Apply terminal text styling
tui write Multi-line text editor

Benefits:

  • Commands work both standalone and nested in TUI server
  • Output to stdout for piping/variable assignment
  • Consistent behavior across execution contexts

Discovery → Runtime → Execution Flow

The execution path spans the Cobra adapter, command service, command adapters, and runtime packages. Re-read these files before editing flow-sensitive code:

  • cmd/invowk/cmd*.go for Cobra flag parsing, dynamic command registration, disambiguation, watch mode, and service-error rendering.
  • internal/app/commandsvc/service.go, dispatch.go, inputs.go, errors.go, and watch.go for service orchestration and error classification.
  • internal/app/commandadapters/ for dependency probes, runtime registry construction, discovery adapters, and interactive subprocess/session bridges.
  • internal/app/execute/ for runtime selection and execution-context construction.

At a high level, execution still follows this shape:

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()     ← Uses command adapters/runtime registry, then executes:
        ├── 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()

Error Classification Pipeline

Execution error handling is two-stage: classifyExecutionError() (internal/app/commandsvc/errors.go) maps runtime errors to service-owned ErrorKind values, then the CLI layer maps those kinds to issue catalog IDs in cmd/invowk/app.go.

classifyExecutionError(err) → commandsvc.ErrorKind
mapServiceErrorKindToIssueID(kind) → issue.ID

ServiceError & renderServiceError()

ServiceError (service_error.go) carries optional rendering info for the CLI layer. The extracted renderServiceError() DRYs the identical rendering pattern used in both executeRequest() and runDisambiguatedCommand():

renderServiceError(stderr, svcErr)
  → prints styled message (if present)
  → renders issue catalog help section (if IssueID set)

Disambiguation Pattern

Two methods for specifying command source:

@source Prefix (First Argument)

invowk cmd @foo deploy      # Run deploy from foo.invowkmod
invowk cmd @invowkfile build  # Run build from invowkfile.cue

--ivk-from Flag

invowk cmd --ivk-from foo deploy

Source Name Normalization

normalizeSourceName() handles various formats:

  • "foo" → "foo"
  • "foo.invowkmod" → "foo"
  • "invowkfile" → "invowkfile"
  • "invowkfile.cue" → "invowkfile"

Error Handling & Exit Codes

ExitError Type

type ExitError struct {
    Code types.ExitCode
    Err  error
}

Pattern:

  • Command RunE returns ExitError for non-zero exit codes
  • Root Execute() catches ExitError and calls os.Exit(exitErr.Code)
  • Prevents cascading error messages while maintaining proper exit codes

Dual-Channel Error Output

CLI handlers (e.g., runModuleRemove, runModuleSync) use two output channels simultaneously:

  1. stdout — Handler prints styled progress with icons via fmt.Printf (e.g., "• Removing dep...", "✗ Failed to remove module: ...")
  2. stderr — Handler returns the error to Cobra, which renders it with styled ERROR formatting

This means a handler error produces output on BOTH channels. When writing txtar tests for error paths, assert both:

! exec invowk module remove dep
stdout 'Failed to remove'        # Handler's styled output (stdout)
stderr '[Nn]o module found'      # Cobra's error rendering (stderr)

Testing only one channel misses bugs in the other (e.g., handler formatting breaks but Cobra rendering works, or vice versa).

Styled Error Rendering

cmd_render.go provides styled error messages for:

  • Argument validation failures
  • Dependency errors
  • Runtime mismatches
  • Ambiguous commands
  • Host platform compatibility issues

Styling System

Unified color palette (styles.go):

Color Hex Use
ColorPrimary #7C3AED Titles (purple)
ColorMuted #6B7280 Subtitles (gray)
ColorSuccess #10B981 Success (green)
ColorError #EF4444 Errors (red)
ColorWarning #F59E0B Warnings (amber)
ColorHighlight #3B82F6 Commands/links (blue)
ColorVerbose #9CA3AF Verbose output (light gray)

Reusable Styles:

  • TitleStyle, SubtitleStyle, SuccessStyle, ErrorStyle
  • WarningStyle, CmdStyle, VerboseStyle

Global Flags

Root Command (Persistent)

--ivk-verbose, -v     // Enable verbose output
--ivk-config          // Custom config file path
--ivk-interactive, -i // Run in alternate screen buffer

Cmd Command

--ivk-runtime, -r     // Override runtime (must be allowed)
--ivk-from            // Specify source for disambiguation
--ivk-force-rebuild   // Force container image rebuild
--ivk-container-name  // Override the persistent container target name
--ivk-dry-run         // Print resolved execution context without executing
--ivk-watch, -W       // Watch files for changes and re-execute

Configuration Loading

Configuration is request-scoped rather than initialized into global accessors:

  1. --ivk-config is stored in rootFlagValues and attached to the command context by contextWithConfigPath().
  2. Discovery and command execution load through the injected config provider; the request context also owns the per-request discovery/config cache.
  3. buildCommandExecuteRequest() carries the explicit config path and whether UI flags were explicitly set into the service request.
  4. resolveUIFlags() applies ui.verbose and ui.interactive only when the corresponding persistent CLI flag was not explicitly set. Config-load failures are rendered as warnings and leave the CLI values in place.

Priority: explicitly set CLI flags > config values > defaults.


Module Commands

Module management (module.go):

Command Purpose
module create Create new module
module list List discovered modules
module archive Package module as archive
module import Import external module
module vendor Vendor module dependencies

Dependency Management

module add <git-url> <version>  # Add dependency
module remove <identifier>       # Remove dependency
module sync                      # Sync dependencies from invowkmod.cue (accepts 0 args)
module tidy                      # Add missing transitive requirements to invowkmod.cue
module update [identifier]       # Update all deps or one matching dependency
module deps                      # List dependencies from lock file (accepts 0 args)

Discovery Integration Points

disc := discovery.New(cfg)

// Discover and validate all commands (with ambiguity detection + diagnostics)
result, err := disc.DiscoverAndValidateCommandSet(ctx)
// result.Set is the DiscoveredCommandSet, result.Diagnostics are non-fatal warnings

// Get specific command info (for execution)
lookup, err := disc.GetCommand(ctx, cmdName)
// lookup.Command is the CommandInfo, lookup.Diagnostics are accumulated warnings

// Discover command set without tree validation (for listing)
result, err := disc.DiscoverCommandSet(ctx)

// List all discovered files (for module operations)
files, err := disc.DiscoverAll()  // or disc.LoadAll() to also parse

Design Principles

Principle Implementation
Transparent namespace Users don't specify source for unambiguous commands
Explicit disambiguation Require @source or --ivk-from for ambiguous commands
Platform-aware execution Different runtimes, validation per platform
Dual-layer TUI Support standalone and server-delegated rendering
Styled consistency Unified color palette across all output
Hidden internals Internal commands not documented to users
Configuration priority CLI flags > config file > defaults

File Organization

File Purpose
root.go Root command, global flags, config loading
cmd.go invowk cmd parent, executeRequest(), disambiguation entry point
cmd_discovery.go Dynamic command registration (registerDiscoveredCommands, buildLeafCommand)
cmd_execute_helpers.go runDisambiguatedCommand() (delegates to executeRequest()), checkAmbiguousCommand(), parseEnvVarFlags(), toEnvVarNames(), toDotenvFilePaths()
cmd_dryrun.go renderDryRun() — prints resolved execution context (script, env, runtime, platform, timeout) without executing
cmd_watch.go runWatchMode() — discovers command's WatchConfig, executes once, starts internal/watch/ watcher loop with re-execution callback
internal/app/execute/orchestrator.go RuntimeSelection, RuntimeNotAllowedError, ResolveRuntime() (3-tier precedence), BuildExecutionContext() (env var projection including INVOWK_CMD_NAME, INVOWK_RUNTIME, INVOWK_SOURCE, INVOWK_PLATFORM)
internal/discovery/validation.go ValidateCommandTree() for args/subcommand conflicts
internal/watch/watcher.go File watcher with fsnotify + doublestar glob matching, debouncing, default ignores
internal/runtime/registry_factory.go BuildRegistry(), BuildRegistryOptions, RegistryBuildResult, InitDiagnostic
internal/app/commandsvc/errors.go classifyExecutionError() — maps runtime errors to issue catalog IDs
service_error.go ServiceError type and renderServiceError()
cmd_render.go Styled error rendering (argument validation, deps, runtime, host support)
validate.go Unified invowk validate command (workspace, invowkfile, module auto-detection)
module.go Module commands
tui_*.go TUI component wrappers
styles.go Unified styling system
internal_*.go Hidden internal commands

Common Pitfalls

Pitfall Symptom Fix
New built-in command without txtar test TestBuiltinCommandTxtarCoverage fails Add a .txtar test in tests/cli/testdata/ with exec invowk <command>
Forgetting Hidden: true on internal cmd Users see internal commands Add Hidden: true to command
Hardcoded exit in RunE Error message not shown Return ExitError instead
Missing TUI server check TUI components fail in nested context Use dual-layer pattern
Not using styled output Inconsistent CLI appearance Use styles from styles.go
Wrong flag priority Config overrides CLI flag Check precedence logic
New execution flag omitted from request mapping Flag silently ignored in some paths Add it to cmdFlagValues, Cobra registration, and centralized buildCommandExecuteRequest() mapping; verify dynamic leaves, disambiguation, dry-run, and watch child requests
context.Background() in Cobra RunE handler Ctrl+C / timeout not propagated Extract cmd.Context() at closure boundary, pass as ctx context.Context first param to handler function
Unconditional discovered-command registration in root init invowk --version / --help startup regresses toward discovery cost Gate registration by argv so only cmd (and its completion paths) trigger registration