Accepted 2026-10-31: the owner approved the spec stop of
@stackra/console (“handle all, all
approved”, Linear STA-1730), and the leader approved the class style and the optional gen. Number:
0506, reserved under the slug console-command-kind in numbers.yaml (STA-1732). Amends
0294 (the module and app
group tables, decisions 13 and 14, and the kind table, decision 16) and
0127 (its 2026-10-24 note, which folded
console into cli).Context
The three CLIs of 0500 (stackra, runway, figentra) are
hand-built: each parses its own arguments, prints its own help and errors, and styles its output its own way. There is
no shared command framework, and no declared shape for a command that a package could expose. ADR 0294 decision 23 says
“Command means a CLI command only”, but the kind table has no row for one. ADR 0127’s 2026-10-24 note folded a console
package into cli on that reading, which put every command inside one CLI package. The basalt port of
@stackra/console (Academorix spec 030) used a provider API (defineConsoleCommand, string tokens) that v3 retires
(0145, 0294
decision 17). Commands need the v3 shape: a class, decorated fields, constructor collaborators, and a folder of their own.
Decision
The kind
-
Commandis a kind. A CLI command is a class that extendsCommand({ name, description, hidden?, aliases? })from@stackra/console. Its name and aliases are written once, each made of[a-z0-9]segments joined by-, and the segments joined by:(tenant:seed). The kind options are read without constructing the class, as every kind’s are.hiddenkeeps a command out of the list; it still runs by name. -
A command is a binding. Its collaborators come in through its constructor, and gen (or the caller’s container)
builds it (0294 decision 17).
handle(io)runs it and returns an exit code from 0 to 255, nothing, or an object or array that becomes the--jsondata. -
Fields carry the arguments and options.
@Argument(options?)binds a field to the next positional token, in field order.@Option(options?)binds a field to--<kebab-name>, and to-<short>whenshortis given. Both are field decorators made withcreateDecoratorthat target thecommandkind only (0294 decision 6). The value kind comes from@Str,@Int,@Numor@Listof@stackra/validation, whoseSHAPE_KINDSalready includescommand. A field with none of them is a boolean option, or a string argument. A field’s default is its initialiser. An argument with no default is required unless it is@Argument({ optional: true }), because the runtime cannot see a?without gen. A@Listargument comes last and takes the rest. -
The
commands/group. A command file sits incommands/: in a module (a service’ssrc/modules/<key>/commands/), in a package’ssrc/node/commands/, and in an app’ssrc/commands/. The file is<name>.command.ts, its class isPascalCase(stem) + Command(greet.command.tsdeclaresGreetCommand), one class per file, and its test sits beside it in__tests__/(0022).
The framework and its look
-
@stackra/consoleis the framework. Its package ispackages/developer/console(@stackra/console,tier:lib, categorydeveloper, runtimesisomorphicandnode). It holds theCommandkind, its two decorators, the kernel (createConsole), the output, the prompts, the testing harness and the themes. It is the framework and the terminal look of the three CLIs of 0500. It holds no module’s commands: each package exposes its own, from its owncommands/group (phase 2, STA-1731). It depends on@stackra/container,@stackra/contractsand@stackra/validation, and never on@stackra/gen. Its own bin,stackra-console, has one command,create, which writes a standalone console app. -
Registries. A standalone app passes its commands to the kernel:
createConsole({ name, version, commands: [...] }). A member that runs gen gets#stackra/commands, an explicit list of static imports that gen writes from thecommands/groups it uses, with duplicate names and aliases refused at gen time, naming both classes. Nothing scans at runtime. gen is never a runtime or peer dependency of@stackra/consoleor of a package that ships commands. -
One presentation. Every product shares one layout (the gutter, the symbol column, dotted task lines, rounded
boxes, one error screen). A product changes only its theme: the name, the ASCII logo, the gradient, the accent, the
glyph and the ASCII glyph. The rules below hold for every CLI:
- Colour is off under
NO_COLOR,CI,--no-ansi, a non-terminal stdout andTERM=dumb;--ansiandFORCE_COLORturn it on against those. With colour off, nothing is written but text, and boxes are drawn in ASCII. - Prompts are asked only when stdin and stdout are terminals,
CIis empty, and neither--no-interactionnor--jsonis given. Otherwise a prompt takes its default, and a prompt with no default fails the run with exit code 2 and names the option that answers it. --jsonwrites exactly one document to stdout:{ "ok": true, "data": ... }, or{ "ok": false, "error": { "code", "message" } }where the code is the error’s class name in snake case withoutError.- Widths are measured by grapheme cluster. Characters are never reordered, and the components write no FSI/PDI
isolates (many terminals draw them as boxes);
isolateBidiis offered for callers. On a terminal without bidi or Arabic shaping (TERM_PROGRAMclaude-desktop, vscode, iTerm.app, Hyper, Tabby), right-to-left runs are written shaped and in visual order;STACKRA_BIDI=visual|logicaloverrides the choice, and pipes and--jsonalways get logical text.
- Colour is off under
-
Prompts are ours. The prompter runs on
node:readlinein raw mode, and colours come fromnode:utilstyleTextwith two escape helpers for 256 and true colour. No new dependency is added. Basalt’s@clack/promptsis not carried over. -
Themes.
ConsoleThemeis{ name, logo, gradient, accent, glyph, asciiGlyph }.@stackra/consoleshipsSTACKRA_THEMEonly.themeFromBrandTokens(tokens, choice)builds a theme from a brand kit’s colour tokens, sorunwayandfigentrabuild theirs from brand tokens they already pull. The package depends on no brand package.
Consequences
- A command lives in the package that owns its capability, and the framework adds no domain dependency.
- Two command shapes exist for a while.
@stackra/clikeeps itsCliCommanddata shape until it moves onto@stackra/console, which.agents/specs/sta-1730-console/adoption.mdplans. STA-1730 migrates no CLI, so 0500 decision 7 (output is kept) holds. - A command’s constructor must only store its collaborators. The kernel builds a throwaway instance with no arguments to
read the field defaults; if the constructor throws, the reader catches it and reads no defaults, so the options keep
their
undefinedvalues silently. - A standalone app has no
.stackra/and passes its registry. A gen member gets its registry from gen. Both paths reach one kernel. @stackra/consoleis a new free package on the Stackra registry (ADR 0453).
Enforcement
packages/developer/console/src/core/functions/read-command.function.tsrefuses a bad declaration (a name outside the form above, a required argument after an optional one, two fields with one flag, a global option’s name) withConsoleDeclarationError, whencreateConsoleruns and before any command runs.packages/developer/console/src/core/functions/build-registry.function.tsrefuses two commands that claim one name or alias, withDuplicateCommandError.- planned — the unit tests of those refusals, under
packages/developer/console/src/core/**/__tests__/(spec SC-002). - planned — STA-1731: the gen collector
emit-commandsrefuses duplicate names at gen time and writes.stackra/gen/commands.ts. - planned — STA-338: the validator’s
kind-pathcheck forcommands/and.command.ts, andbase-matches-suffixfor theCommandbase. - planned — oxlint
unicorn/filename-case, for kebab-case command files.
Supersedes and sources
Supersedes: none. Amends:- 0294: decision 13 (the module table gains
commands/), decision 14 (the app table gainscommands/) and decision 16 (the kind table gainsCommand), by a dated note on that page. Decision 23 stands:Commandmeans a CLI command only. - 0127: its 2026-10-24 (STA-348) note folded
consoleintocli. This reverses that fold for the command framework:@stackra/consoleis its owndeveloperpackage. The reason of that note, that a Command is a CLI command only, stands. A dated note on that page says so.
@stackra/console spec (.agents/specs/sta-1730-console/spec.md, the
decisions taken at the spec stop) and its design (plan.md); the leader’s approval of the class style and the optional
gen; Linear STA-1730 (phase 1), STA-1731 (phase 2) and STA-1732 (the number reservation); the basalt port, Academorix
spec 030.