- name
- stacks-cli
- description
- 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/.
- license
- MIT
- compatibility
- Bun >= 1.3.0, TypeScript
- allowed-tools
- Read Edit Write Bash Grep Glob
# 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.
```typescript
// 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()`:
```typescript
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:
```typescript
export const aliases = ['emails', 'mail'] // extra aliases
export const enabled = false // keep the file, hide the command
```
### The 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.
```typescript
import { defineCommands } from '@stacksjs/cli'
export default defineCommands({
'send-emails <type>': { file: 'SendEmails', aliases: ['emails'] },
'legacy': { file: 'Legacy', enabled: false },
})
```
### Registration Methods
1. **Drop a file in `app/Commands/`** - auto-discovered, nothing else to do
2. **Event listeners** - CLI events in `app/Listeners/Console.ts`
3. **`app/Commands.ts`** - optional, for ordering / aliases / disabling
## CLI Event Listeners
```typescript
// 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)` and
`await 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):
```typescript
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)
```typescript
{
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
```bash
buddy build:cli # build CLI binary
# Compiles for: linux-x64, linux-arm64, windows-x64, darwin-x64, darwin-arm64
```
## Gotchas
- Application commands go in `app/Commands/`, framework commands in `storage/framework/core/buddy/src/commands/`
- Commands are auto-discovered from `app/Commands/`; `app/Commands.ts` is 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.
## Native command ownership
Import `spawnOwnedProcess` from `@stacksjs/cli/owned-process` for a command whose
normally inherited descendants belong to the same lifetime. This subpath uses
only native Bun/Node primitives, with no CLI or application bootstrap.
A controlled owner establishes a POSIX process group or native Windows Job
Object before admitting the command. `started` resolves its actual command PID;
`pid` identifies the stable lifecycle owner. Consumer IPC is forwarded with the
verified command PID. `exited` preserves command status and settles after tree
disposal. `stop()` is idempotent, and signaling the owner also initiates cleanup.
Compiled callers supply their installed Bun path as `ownerRuntime`.
This is lifecycle ownership, not a sandbox: deliberate POSIX breakaway is outside
the group contract. Native admission or disposal failures reject rather than
falling back to direct-child cleanup. `runBoundedProcess` in `cli/process` retains
its separate output-byte/deadline contract; it is not this ownership API.
Auf GitHub ansehen