Use when building CLI commands or tools with Stacks - the @stacksjs/cli package for creating commands with argument parsing, option handling, colored output, tables, progress indicators, prompts, or integrating with the buddy command system. Covers @stacksjs/cli and app/Commands/.
Install
npx skillscat add stacksjs/stacks/stacks-cli Install via the SkillsCat registry.
Stacks CLI Framework
The @stacksjs/cli package provides the foundation for building CLI commands, used internally by the buddy CLI.
Key Paths
- Core package:
storage/framework/core/cli/src/ - CLI configuration:
config/cli.ts - Application commands:
app/Commands/ - Optional registry:
app/Commands.ts(not needed - see below)
Creating Commands
Every .ts file in app/Commands/ is a command. There is no registration step
and no generated registry file: drop the file in and it is live, nested
directories included (app/Commands/Archive/Run.ts).
Declarative form (preferred)
defineCommand() infers the handler's options from the flags declared above
it, so there is no hand-written options interface to drift out of step.
// app/Commands/Greet.ts
import { defineCommand, log } from '@stacksjs/cli'
export default defineCommand({
name: 'greet <who>',
description: 'Greet a user',
aliases: ['g'],
options: {
'--loud, -l': { description: 'Shout the greeting', default: false },
'--title <title>': 'Prefix the name',
'--times <n>': { description: 'How often', default: 1, type: [Number] },
},
handle(options, who) {
// options.loud -> boolean
// options.title -> string | undefined
// options.times -> number
const greeting = `Hello, ${options.title ?? ''}${who}!`
for (let i = 0; i < options.times; i++)
log.info(options.loud ? greeting.toUpperCase() : greeting)
},
})Flag to property: --dry-run -> dryRun, --two, -t -> two, --no-cache
-> cache. Value type: <x> -> string, [x] -> string | true, <x...>
-> string[], no argument -> boolean. A default makes the property always
present; a type: [Number] makes it a number.
Imperative form
For a file that registers several commands or needs cli.on():
import { defineCommand, log } from '@stacksjs/cli'
export default defineCommand((cli) => {
cli.command('inspire', 'Inspire yourself').alias('insp').action(() => {})
cli.command('inspire:two', 'Two quotes').action(() => {})
cli.on('inspire:*', () => log.error('Invalid command'))
})Per-file configuration
Named exports beside the default one, so a command owns its own configuration:
export const aliases = ['emails', 'mail'] // extra aliases
export const enabled = false // keep the file, hide the commandThe optional registry (app/Commands.ts)
Only worth keeping when you want to control the order commands are listed in,
or to alias/disable a command without editing its file. A command file the
registry never mentions still loads.
import { defineCommands } from '@stacksjs/cli'
export default defineCommands({
'send-emails <type>': { file: 'SendEmails', aliases: ['emails'] },
'legacy': { file: 'Legacy', enabled: false },
})Registration Methods
- Drop a file in
app/Commands/- auto-discovered, nothing else to do - Event listeners - CLI events in
app/Listeners/Console.ts app/Commands.ts- optional, for ordering / aliases / disabling
CLI Event Listeners
// app/Listeners/Console.ts
export default function(cli: CLI) {
cli.on('custom:command', () => {
log.info('Custom command executed!')
})
cli.on('my:*', () => {
// Wildcard - matches my:anything
})
cli.on('unknown:!', () => {
// Default/fallback handler
})
}Plain command output
Import print and printError from @stacksjs/cli/output for command results,
test tallies and diagnostics that must be visible even when logging is filtered.
They write synchronously to stdout and stderr, preserve Node formatting and
inspection, append a newline, and complete before an immediate process.exit().
The narrow import needs no application configuration. Use log for application
logging with levels, context and transports.
For large results or piped output, use await printAsync(...args) andawait printErrorAsync(...args) from the same entrypoint. Their signatures are(...args: unknown[]) => Promise<void>. They append a newline, preserve Node
formatting, wait for stream writes under backpressure, and reject write errors
such as EPIPE. Await them before process.exit(). The synchronous helpers
remain for small, bounded immediate-exit messages and may throw EAGAIN when
a nonblocking pipe is full. These helpers are also exported by @stacksjs/cli.
Output Formatting
Via @stacksjs/utils color functions (available in CLI context):
import { bold, green, red, yellow, dim, underline } from '@stacksjs/utils'
print(green('✓ Success'))
printError(red('✗ Error'))
console.log(yellow('⚠ Warning'))
console.log(bold('Important'))
console.log(dim('Subtle info'))config/cli.ts (BinaryConfig)
{
name: 'My Custom CLI',
command: 'mycli',
description: 'My custom CLI tool',
deploy: true
}CLI Commands
buddy make:command [name]- scaffold a new command
Compiled Binaries
buddy build:cli # build CLI binary
# Compiles for: linux-x64, linux-arm64, windows-x64, darwin-x64, darwin-arm64Gotchas
- Application commands go in
app/Commands/, framework commands instorage/framework/core/buddy/src/commands/ - Commands are auto-discovered from
app/Commands/;app/Commands.tsis optional and additive - CLI events support wildcards (
*) and default handlers (!) - The buddy CLI lazy-loads commands - not all load at startup
- Output formatting uses ANSI colors from
@stacksjs/utils - Custom CLIs can be compiled to platform-specific binaries
- The CLI framework is separate from buddy - buddy uses it internally
Native terminal primitives
The public @stacksjs/cli entry re-exports table, progress, tasks, note,
getTerminalSize and isInteractive from core/cli/src/terminal.ts. Import
dump/dd/echo from @stacksjs/logging, where those debug helpers live. Read their
options/types before building a new terminal formatter or adding another CLI
dependency. Commands can also use the native prompt, spinner, exec and signal
helpers from the same package. A command file is discovered recursively; its
handler's argument order comes from defineCommand, not a copied CAC action shape.
Source evidence: storage/framework/core/cli/src/index.ts,commands.ts and terminal.ts; command discovery is owned by buddy's
runtime registry. buddy list and buddy <command> --help are the
authoritative command/flag inventory, including package-contributed commands.