stacksjs

stacks-cli

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/.

stacksjs 636 20 Updated 5d ago
GitHub

Install

npx skillscat add stacksjs/stacks/stacks-cli

Install via the SkillsCat registry.

SKILL.md

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 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.

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

// 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):

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-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.