CLI Design - axnic/rtunk GitHub Wiki

CLI Design

Note

Contributor documentation. Describes the command surface rtunk is built to, not a user manual โ€” see Command Reference for that.

This document is the authoritative target design for rtunk's command surface and the run semantics of check, fmt and run. Terminal rendering lives in Terminal-UX-Design.md; staging lives in ROADMAP.md; project principles live in AGENTS.md.

This file's audience is now contributors and maintainers verifying implementation status against design; the public, user-facing command reference lives at Command Reference.

Implementation status. v0.8 (CLI reshape) is implemented, under internal/cli (now one file per command): .rtunk over .trunk precedence and the project root rule (findConfig in internal/cli/shared.go), file selection and --from (internal/cli/shared.go), exit codes (cmd/rtunk/main.go), fmt working-tree-only behavior with --force, linters {list,enable,disable}, git-hooks sync|unsync, plugins print, the hidden toolbox group, logs list|show|clean, help [--all], and cache clean|prune. v0.9 item 1 is implemented: check and fmt render through the plain renderer in internal/cli/render (see Terminal-UX-Design.md), with --no-progress; this is a breaking change, the former file:line severity [rule] message lines and N issue(s) in M file(s) summary are gone. v0.9 item 2 is implemented: --format human|sarif|json and ANSI color. v0.9 item 4 is implemented: the filtered linters list / actions list (internal/cli/linters_list.go, internal/cli/actions_list.go, sharing shared.go's buildLintersList/buildActionsList), a breaking change, the former * name description format is gone. v0.9 item 3 is implemented: the TTY live view on stderr, with --ascii and --live-height (RTUNK_LIVE_HEIGHT); all four v0.9 items are done. Still planned: v1.1 (rtunk.lock).

Cross-cutting rules

Project root

check, fmt and run only execute in a directory that has an ancestor (itself included) containing a config (.trunk or .rtunk). If no root is found, rtunk refuses with an explicit message. This guards against running in $HOME, including via rtunk check .. (Implemented.)

Precedence when .trunk and .rtunk coexist: .rtunk wins over .trunk. Exactly one of the two is read, with no merge. (Implemented.)

File selection

Without a path (rtunk check, rtunk fmt):

Situation Files processed
In git, branch has an upstream diff from merge-base(upstream, HEAD) to the working tree (staged, unstaged, untracked-not-ignored)
In git, no upstream diff from HEAD to the working tree (staged, unstaged, untracked-not-ignored)
Not in git error: explicit paths are required

--from <ref> forces the diff base. It exists for CI (detached HEAD, no upstream). When the selection is legitimately empty inside git, rtunk prints rtunk: no files to check (check) or rtunk: no files to format (fmt) and exits 0; outside git with no explicit paths, rtunk exits 1 with an error explaining that explicit paths are required. (Implemented, internal/cli/shared.go.)

With explicit path(s) (rtunk check .), every file under the path is processed: git ls-files -co --exclude-standard in git, everything otherwise.

Default-to-changed-files is rtunk's performance lever: the common case checks a handful of files, not the whole repository.

Configuration validation

check and fmt validate the resolved, enabled configuration before running anything:

  • A linter using the old, pre-commands: single type/command shape is refused outright โ€” the run exits 1 naming the linter and its own deprecated: migration message, if it has one.
  • A linter or command carrying a deprecated: migration message (in the modern shape) prints a warning to stderr naming it, but still runs.

config print and linters list resolve the same configuration without either check, so a broken configuration can still be inspected in order to fix it. (Implemented, pkg/trunk/config/ deprecations.go, internal/cli/shared.go's checkDeprecations, called from check_run.go and fmt.go.)

Output

--no-progress (check and fmt, implemented) suppresses the per-linter progress lines on stderr; warnings and errors are still printed. It also disables the live view. The default output is the plain renderer described in Terminal-UX-Design.md.

Live view (check and fmt, implemented): when stderr is a terminal, --no-progress is not set and TERM is not dumb, a live area is drawn on stderr while the run progresses and erased before the stdout report; otherwise the per-linter progress lines are emitted unchanged. stdout, exit codes and run logs are not affected.

  • --ascii: ASCII glyphs in the live view. Also chosen automatically for a non-UTF-8 locale (first non-empty of LC_ALL, LC_CTYPE, LANG must contain utf-8 or utf8).
  • --live-height <n> (env RTUNK_LIVE_HEIGHT): maximum height of the live area in lines; default half the terminal height, minimum 3. Not a config file key.
  • Ctrl+C erases the area and re-raises SIGINT.

--format human|sarif|json|github (human and json for check and fmt, sarif and github for check only; default human):

  • human: the report described in Terminal-UX-Design.md. ANSI color is added only when stdout is a terminal and NO_COLOR is empty; stderr progress lines are never colored. Output outside a terminal is uncolored.
  • json: one document on stdout: {version: 1, command: "check"|"fmt", elapsed_ms, run_log, files_checked, linters, suppressed, issues: [{file, line, column, severity, message, linter, rule, url}], failures: [{linter, error}], skipped: [], changed: []}. All keys are always present, empty arrays are [] (never null), line/column are 0 when unknown; suppressed counts findings hidden by rtunk-ignore/trunk-ignore comments (see Ignoring-Issues.md).
  • sarif: SARIF 2.1.0 for CI, check only: one run, tool.driver rtunk, deduplicated rules, one result per issue, failures as toolExecutionNotifications with executionSuccessful: false; the run carries properties.suppressed when at least one finding was suppressed. fmt --format sarif is refused with --format sarif is only supported by check (exit 1) before anything runs.
  • github: GitHub Actions workflow commands for check only, on stdout: one ::error, ::warning or ::notice per finding (file, line, col, title=linter/rule properties, each omitted when unknown; paths relative to $GITHUB_WORKSPACE), ::error per failed linter and ::warning per skipped one. Every finding is emitted; GitHub caps what it displays. With $GITHUB_STEP_SUMMARY set, a Markdown job summary is appended to that file. Never selected automatically from $GITHUB_ACTIONS. fmt --format github is a usage error.

Progress lines stay on stderr for every format (--no-progress silences them); stdout carries only the report or document. check --fix/--format-before-check under json/sarif emit a single document reflecting the final checking pass (json puts every writing pass's files โ€” formatted, fix-commanded, or inline-fixed โ€” under changed). When no file is selected no document is written (empty stdout, exit 0). Exit codes are unchanged for every format.

Exit codes

Identical to trunk's for check, fmt and run. trunk's public documentation publishes no table; measured on trunk 1.25.0 (throwaway repo, shfmt linter):

Case Code
check without finding 0
check with findings 1
check on a nonexistent path 1
check with an invalid config 1
check where a tool fails to run (e.g. install impossible) 1
fmt without change 0
fmt that fixes files 0

Only 0 and 1 exist, with no distinction between "findings" and "error". A fmt that fixes files does not return an error. For run and fmt with a failing tool (not measured on trunk): 0 on success, non-zero on error. Verified: cmd/rtunk/main.go exits 1 on any returned error and 0 otherwise; a fmt that fixes files exits 0. (Implemented.)

fmt

fmt writes to the working tree only and never touches the index (no git add). Partially staged files are skipped with a warning unless --force is given. (Implemented.)

check --fix and --format-before-check

check --fix (-y) applies every enabled fix command (a linter's own in-place, non-formatter autofix) and every finding's own inline fix to what the checking pass found, then reports whatever remains โ€” it never runs a formatter. check --format-before-check runs every enabled formatter first, then checks the reformatted files โ€” the combined "format, then check" sequence --fix used to mean; --verify-stable now applies to --format-before-check's own formatting pass, same as fmt --verify-stable. The two compose: given together, the order is format pass, checking pass, fix application, final checking pass. (Implemented, internal/cli/check_run.go.)

Hidden commands

toolbox and other internal commands are callable but absent from the default help. rtunk help --all lists everything. (Implemented.)

Everyday commands

rtunk check [--from <ref>] [--no-progress] [--ascii] [--live-height <n>] [--format human|sarif|json|github] [<path>...]
  -> read the config
  => [in parallel]
    -> download runtimes if needed
    -> determine the files to check (see "File selection")
  -> download the required tools
  -> run the check
  -> output the result

rtunk fmt [--no-progress] [--ascii] [--live-height <n>] [--format human|json] [<path>...] follows the same flow, with the fmt rules above.

rtunk [actions] run <id> runs an action in the current directory.

check and fmt take paths only. There are no trunk-style aliases. (Implemented.)

Configuration inspection

  • rtunk config print: print the current, fully resolved configuration.
  • rtunk plugins print: print all configuration available across all plugins, resolved. Can be very large; useful as a registry dump. Replaces config print --all, which is removed. (Implemented.)

The originally planned config {plugins,lint,actions,tools,runtimes} list|show were never shipped and stay dropped: linters/actions listing and plugins print cover the need.

Downloads

  • rtunk download [<category> [<id>[@version]...]]: download ahead of time what check, fmt and run otherwise fetch lazily. No argument downloads every enabled tool and runtime. <category> is runtime, tools or lint (a linter id, expanded to its tools) and requires at least one id. Skips installed items, prints Downloaded N item(s). or Everything is already downloaded., and shows the live install view on a terminal. Same command as rtunk toolbox download. Printed by linters enable as a hint.

Internal commands (advanced, hidden)

  • rtunk toolbox download [{runtime,tools,lint} <id>[@version]...]: alias of the public rtunk download.
  • rtunk toolbox where {runtime,tools} <id>[@version]: absolute path of the item's install directory (not its shim).
  • rtunk toolbox exec|x {runtime,tools} <id>[@version] -- <cmd> [<args>...]: run <cmd>, which is the item's shim when equal to <id>, else an executable in its install directory. --interactive binds stdin/stdout, for instance to open a python or node shell from a runtime; otherwise stdin is unbound.
  • rtunk lock: see "Download integrity" below (planned, v1.1).

toolbox exec and where are implemented and replace the former top-level exec|x and where (v0.2), narrowed to {runtime,tools}; the old names are removed, with no compat aliases. download has since been made public again (see above).

Administration

Cache

  • rtunk cache clean: remove the downloads, plugins, logs and registry subtrees of the cache root (not the whole directory, which may be shared); per-subtree progress and space freed on a terminal. (Implemented.)
  • rtunk cache prune: removes cache entries no currently-existing, currently-configured repository still needs. Driven by a per-repository usage registry (download.RecordUsage, written on every check/fmt run, not actions): a repository that no longer exists on disk is dropped from the registry, taking with it everything it alone was keeping alive; a repository that still exists keeps exactly what its most recent registry entry lists. There is no age or duration flag -- staleness is "no live repository needs this," not "unused for N days." (Implemented.)

Linters and actions

The two groups are symmetric:

  • rtunk linters {list,enable,disable} <id>[@version]: list linters grouped as enabled (id@version), available for this repo (matching at least one repository file, not enabled) and, with --all, the rest; add or remove a linter in the config. list [--all] [--format human|json]. (list implemented, internal/cli/linters_list.go.)
  • rtunk actions {list,enable,disable,history}: list actions (list [--format human|json], groups Enabled and Available (not enabled), no --all); enable or disable them; show the history of actions in this repo (history [--id=STRING] [--limit=INT], restricting to one action id via --id, not a positional argument).

linters list and actions list share one layout (see Terminal-UX-Design.md). linters ... replaces check enable|disable|list (implemented; check takes paths only). File counts come from every repository file matched against the linter's files: criteria.

Miscellaneous

  • rtunk git-hooks sync|unsync: enable or disable the git hooks defined by actions (implemented; install, uninstall and the alias are removed, with no compat aliases).
  • rtunk init: initialize rtunk: scaffolds .rtunk/rtunk.yaml, or migrates an existing .trunk/ into .rtunk/ and removes it. On a terminal it then runs the linters and actions pickers and downloads what is enabled; otherwise it prints the next commands. Then links .rtunk/{logs,tools,plugins}.
  • rtunk deinit: remove rtunk's configuration and installed artifacts (the inverse of init).
  • rtunk logs list [<file>...], rtunk logs show <uid>|latest [<file>...], rtunk logs clean: inspect and clean per-run logs. Implemented in internal/cli/logs_list.go, logs_show.go, logs_clean.go.

Renovate

  • rtunk renovate enable|disable: turn Renovate annotations in the configuration on or off (enable is the former annotate; disable strips the annotations). Both warn on stderr when no Renovate config file at the repo root contains the regexManager. (Implemented.)
  • rtunk renovate config: print the Renovate configuration with the custom regexManager used for version management. Eventually includes postUpgradeTasks: rtunk lock. (Implemented, without the postUpgradeTasks entry.)

These replace the former rtunk renovate annotate|config (v1.1).

End-to-end verification

scripts/verify-renovate-e2e.sh proves the printed regexManager snippet and the "# renovate: ..." annotations actually work against a real Renovate run, not just a plausible-looking regex that was never executed: it builds rtunk, gets the real config snippet from rtunk renovate config and a real annotated fixture from rtunk renovate enable (a local plugin declaring a tool backed by koalaman/shellcheck, pinned at an old real tag), then runs the actual renovate CLI (via npx) against them and asserts its debug output shows a proposed update for that dependency.

It is a manual script, not part of go test ./... or CI: it needs network access, a real GitHub token, and downloads the real renovate npm package via npx (a few hundred MB, cached after the first run) โ€” a real external dependency with real latency that would make the default test suite flaky and slow for everyone. Run it with:

GITHUB_COM_TOKEN="$(gh auth token)" ./scripts/verify-renovate-e2e.sh

Last verified 2026-09-28: found koalaman/shellcheck pinned at v0.7.0 โ†’ Renovate's own dry-run=lookup proposed a real update to v0.11.0 ("datasource": "github-releases", "updateType": "minor"), via the exact regexManager rtunk renovate config prints.

Download integrity

Current model

  • HTTPS-only fetch (http rejected, redirects included);
  • stream passed through a SHA256 hasher into a temporary file;
  • renamed to blobs/sha256/<hex> only once the hash is known (content-addressing, TOFU).

trunk plugin downloads: recipes carry no upstream checksum.

Known limit: TOFU. The first download is accepted as is; a source compromised at that moment is not detected.

Next step (after check/fmt/run, non-blocking for v1; roadmap v1.1)

A versioned rtunk.lock in the repo, id@version@platform -> sha256 (go.sum style):

  • entry present: mismatch is a blocking error, temp file deleted, no fallback;
  • entry absent locally: download and record;
  • entry absent with --locked or CI=true: error;
  • hidden rtunk lock precomputes hashes for the other platforms;
  • Renovate: postUpgradeTasks: rtunk lock, to be added to renovate config.

Later (v1+)

Reuse checksums and signatures from the aqua-registry.

Where to go next

โš ๏ธ **GitHub.com Fallback** โš ๏ธ