Distribution commands
A distribution can register top-level commands of its own beside setup, check, and undo. Use
one when an internal workflow belongs in the same branded executable your team already trusts —
acmedev sync rather than a second script nobody remembers to install.
Commands are distribution-owned, not plugin-owned. A command word is global real estate on the help screen, and the build-time distribution list is the one place that can arbitrate it.
Declare a command
Section titled “Declare a command”A command is data, not a framework class. The same definition builds the parser and renders every help screen, so the two cannot drift.
import type { CliCommandDefinition } from "@tryaura/aura-cli";
export const syncCommand: CliCommandDefinition = { word: "sync", summary: "Synchronize agent profiles with the Acme registry", examples: [ { args: "sync", text: "Synchronize every profile" }, { args: "sync --tag <tag>", text: "Synchronize one tag only" }, ], flags: [ { flag: "--force", kind: "boolean", description: "Sync even when nothing changed" }, { flag: "--name", kind: "string", placeholder: "<name>", description: "Profile to synchronize", }, { flag: "--tag", kind: "array", placeholder: "<tag>", description: "Tag to sync; repeatable" }, ], helpFooters: ["Exit codes: 0 synchronized · 2 invalid usage · 3 operational failures"], execute: async (invocation) => { invocation.stdout.write("Synchronized.\n"); return 0; },};Register it on the distribution beside the plugin list:
plugins: [...OFFICIAL_PLUGINS, internalPlugin],registry: OFFICIAL_REGISTRY_OPTIONS,commands: [syncCommand],| Field | Purpose |
|---|---|
word |
Top-level command word, such as sync. |
summary |
Row text on the root and unknown-command screens. No trailing period. |
examples |
Rows under “Everyday use”, most common first. Absent, the bare word is shown. |
flags |
Long options the command accepts. |
helpFooters |
Footer lines on the command’s help screen, such as an exit-code legend. |
execute |
Runs the command. The returned 0, 1, 2, or 3 becomes the process status. |
Flags and positionals
Section titled “Flags and positionals”| Kind | Declaration | Parsed value |
|---|---|---|
boolean |
--force |
Always present; false when not given. |
string |
--name <name> |
undefined when not given. |
array |
--tag <tag>, repeatable |
Always present; empty when not given. |
Parsed values arrive on invocation.flags, keyed by the flag exactly as declared — flags["--tag"],
not flags.tag. Everything that is not a flag or its value arrives on invocation.positionals in
the order given.
The typed helpers narrow each value to its declared kind and throw on a flag the definition never
declared, so a misspelled key fails loudly instead of reading as undefined:
import { arrayFlag, booleanFlag, stringFlag } from "@tryaura/aura-cli";
const force = booleanFlag(invocation, "--force"); // booleanconst name = stringFlag(invocation, "--name"); // string | undefinedconst tags = arrayFlag(invocation, "--tag"); // readonly string[]Flags are long-form only; there are no short aliases, required flags, choices, or string defaults.
Validate what the command needs at the top of execute and return 2 for invalid usage, the same
code the built-ins use.
Reserved names
Section titled “Reserved names”- Command words:
check,setup, andundoare Aura’s;helpandversionare the command framework’s. - Flags:
--helpis claimed by the framework and--no-coloris consumed before any command parses. - A word is lowercase kebab-case, at most 24 characters, starting with a letter. A flag is
lowercase kebab-case after
--, at most 32 characters. - Every rendered text field — the summary, flag descriptions, examples, and footers — must be one non-empty line.
An invalid, reserved, or duplicated definition fails the run at startup with exit code 3 and
reports every problem in the list at once, so a distribution with several mistakes is fixed in one
pass. Nothing shadows a built-in at parse time.
What an invocation receives
Section titled “What an invocation receives”| Field | Purpose |
|---|---|
branding |
The distribution’s names, version, and documentation link. |
colorDepth |
Supported color depth; 0 means emit no escape sequences. |
environment |
The run’s injected Environment. |
flags, positionals |
Parsed command line. |
stdin, stdout, stderr |
The run’s streams, injected rather than the process’s own. |
telemetry |
Records run events against this command’s word. |
Environment
Section titled “Environment”invocation.environment is the same injected Environment the built-in commands and every plugin
run against, captured once at the process boundary: cwd, homeDir, now(), platform,
pathEntries, readVariable(), exec(), and httpGet(). A command that reads only from it never
touches process state directly, so it behaves the same under the testkit and under any embedder.
const result = await invocation.environment.exec({ args: ["rev-parse", "--abbrev-ref", "HEAD"], command: "git",});const token = invocation.environment.readVariable("ACME_REGISTRY_TOKEN");exec never spawns a shell, so argument values are not word-split or glob-expanded; it strips
loader variables such as NODE_OPTIONS from the child, bounds every command with a timeout, and
truncates runaway output. httpGet is the same bounded TLS-only client the kernel uses. Read a
credential from readVariable at the moment of use and let it leave scope with the call.
Help screens
Section titled “Help screens”The definition renders the whole help surface, so it cannot describe an option that does not parse.
- The root screen and the unknown-command screen list each registered command after the built-in rows, in declaration order.
acmedev sync --helpshows Everyday use, Options, Advanced, then the definition’s footers. Advanced carries--no-coloralone.- A misspelled word gets the redirect screen; a bad flag on a registered command keeps the parser’s message, which names the offending flag.
Telemetry
Section titled “Telemetry”A registered command records events through invocation.telemetry, which is a no-op unless the
distribution composed a telemetry sink.
invocation.telemetry.record({ event: "sync-run", outcome: "applied", durationMs: 12, exitCode: 0, counts: { profiles: 4 }, flags: { force: invocation.flags["--force"] === true },});| Field | Purpose |
|---|---|
event |
What happened, as a label such as sync-run. Required. |
outcome |
Fixed-vocabulary result owned by the distribution, such as applied. |
durationMs |
Milliseconds the measured work took. |
exitCode |
Exit code, when the event describes a finished run. |
counts |
Label-keyed counters. |
flags |
Label-keyed booleans, typically which options the run carried. |
Aura stamps the envelope: the command word, the distro-command event kind, the timestamp from the
run’s clock, and the distribution version. A command can neither send an unstamped event nor
attribute one to a command it does not own. A command that throws is recorded as command-failed
with its exit code and no error text, the same way the built-ins report an operational failure.
That label is reserved for the crash record: an event a command records under command-failed
itself is discarded.
Labels are lowercase kebab-case and bounded, and counts and flags accept at most 32 entries
each. That is the same rule every built-in event follows: identifiers, counts, booleans, and
durations chosen at build time — never paths, file contents, messages, or strings returned by an
external tool. DO_NOT_TRACK or AURA_TELEMETRY=off disables the sink before it sees an event.
Common failures
Section titled “Common failures”| Symptom | Cause | Fix |
|---|---|---|
The run exits 3 at startup naming a command word |
The word is reserved, malformed, or declared twice | Read the listed problems; every one is reported in the same message. |
A flag reads as undefined |
The key is the bare name rather than the flag as declared | Index with "--tag", not "tag" — or use the typed helpers, which throw on an undeclared flag. |
| The command sees the wrong home directory | --home applies to the built-in commands only |
Declare a flag of your own and apply it to the paths the command builds. |
| Events never reach the sink | No sink is composed, or the invoking environment opted out | Compose telemetry on the distribution and check DO_NOT_TRACK. |
| An event is rejected by an ingestion endpoint | A payload value is free text rather than a build-time label | Send identifiers, counts, and booleans; count what you cannot name. |