CLI Package

On this page 51

A powerful command-line interface framework for building interactive CLI applications, featuring command parsing, prompts, spinners, and beautiful output formatting.

Installation

bun add @stacksjs/cli

Basic Usage

import { CLI, log } from '@stacksjs/cli'

// Create a simple command
const cli = new CLI('myapp')
  .command('greet', 'Greet a user')
  .option('-n, --name <name>', 'Name to greet')
  .action((options) => {
    log.info(`Hello, ${options.name || 'World'}!`)
  })

cli.run()

Creating Commands

Basic Command

There is no Command class. A command is a definition object passed to defineCommand(), which is what app/Commands/*.ts export:

import { defineCommand } from '@stacksjs/cli'

export default defineCommand({
  name: 'deploy',
  description: 'Deploy the application',
  handle() {
    console.log('Deploying...')
  },
})

Command with Options

The declarative form INFERS the handler's options from the flags it declares, so there is no hand-written options interface to keep in sync:

export default defineCommand({
  name: 'build',
  description: 'Build the application',
  options: {
    '-e, --env <environment>': { description: 'Target environment', default: 'production' },
    '-m, --minify': { description: 'Minify output', default: false },
    '-w, --watch': 'Watch for changes',
  },
  handle(options) {
    console.log(`Building for ${options.env}`)
    if (options.minify) console.log('Minification enabled')
    if (options.watch) console.log('Watch mode enabled')
  })

Command with Arguments

const command = new Command('generate')
  .description('Generate a resource')
  .argument('<type>', 'Resource type (model, controller, action)')
  .argument('<name>', 'Resource name')
  .argument('[path]', 'Optional path')
  .action((type, name, path, options) => {
    console.log(`Generating ${type}: ${name}`)
    if (path) console.log(`At path: ${path}`)
  })

Subcommands

const cli = new CLI('buddy')

cli.command('make')
  .description('Generate resources')
  .command('model')
    .description('Generate a model')
    .argument('<name>')
    .action((name) => {
      console.log(`Creating model: ${name}`)
    })
  .command('controller')
    .description('Generate a controller')
    .argument('<name>')
    .action((name) => {
      console.log(`Creating controller: ${name}`)
    })

Prompts

Text Input

import { prompts } from '@stacksjs/cli'

const name = await prompts.text({
  message: 'What is your name?',
  placeholder: 'Enter your name',
  defaultValue: 'Anonymous',
  validate: (value) => {
    if (value.length < 2) return 'Name must be at least 2 characters'
  }
})

Password Input

const password = await prompts.password({
  message: 'Enter your password:',
  mask: '*',
  validate: (value) => {
    if (value.length < 8) return 'Password must be at least 8 characters'
  }
})

Confirm

const confirmed = await prompts.confirm({
  message: 'Are you sure you want to continue?',
  initialValue: false
})

if (confirmed) {
  // Proceed
}

Select (Single Choice)

const framework = await prompts.select({
  message: 'Choose a framework:',
  options: [
    { value: 'stx', label: 'STX', hint: 'Recommended' },
    { value: 'react', label: 'React' },
    { value: 'svelte', label: 'Svelte' },
  ],
  initialValue: 'stx'
})

Multi-Select

const features = await prompts.multiselect({
  message: 'Select features to install:',
  options: [
    { value: 'auth', label: 'Authentication' },
    { value: 'api', label: 'API Routes' },
    { value: 'queue', label: 'Queue System' },
    { value: 'cache', label: 'Caching' },
  ],
  required: true,
  initialValues: ['auth']
})

Autocomplete

const project = await prompt.autocomplete({
  message: 'Select a project:',
  options: async (input) => {
    const projects = await fetchProjects(input)
    return projects.map(p => ({
      value: p.id,
      label: p.name
    }))
  },
  placeholder: 'Type to search...'
})

Group Prompts

const answers = await prompt.group({
  name: () => prompts.text({ message: 'Project name:' }),
  type: () => prompts.select({
    message: 'Project type:',
    options: ['app', 'library', 'plugin']
  }),
  features: () => prompts.multiselect({
    message: 'Features:',
    options: ['typescript', 'eslint', 'prettier']
  }),
}, {
  onCancel: () => {
    log.error('Setup cancelled')
    process.exit(1)
  }
})

Path Selection

const file = await prompt.path({
  message: 'Select a file:',
  type: 'file',
  validate: (path) => {
    if (!path.endsWith('.ts')) return 'Must be a TypeScript file'
  }
})

const directory = await prompt.path({
  message: 'Select output directory:',
  type: 'directory'
})

Output and Logging

Log Levels

import { log } from '@stacksjs/cli'

log.info('Information message')
log.success('Operation completed successfully')
log.warn('Warning: Something might be wrong')
log.error('Error: Something went wrong')
log.debug('Debug information')

Styled Output

import { bold, blue, cyan, dim, green, italic, log, red, underline, yellow } from '@stacksjs/cli'

// Colors
console.log(red('Error text'))
console.log(green('Success text'))
console.log(yellow('Warning text'))
console.log(blue('Info text'))
console.log(cyan('Highlighted text'))

// Formatting
console.log(bold('Bold text'))
console.log(dim('Dimmed text'))
console.log(italic('Italic text'))
console.log(underline('Underlined text'))

// Combinations compose as functions - there is no `.bold.red` chain
console.log(bold(red('Bold red text')))
console.log(dim(yellow('Dim yellow text')))

Notes and Messages

import { note, outro, intro } from '@stacksjs/cli'

intro('Welcome to the CLI')

note('Some important information', 'Note')

outro('Setup complete!')

Spinners

Basic Spinner

import { spinner } from '@stacksjs/cli'

const spin = spinner('Loading...')
spin.start()

try {
  await performTask()
  spin.succeed('Task completed!')
} catch (error) {
  spin.fail('Task failed!')
}

Spinner with Messages

const spin = spinner('Initializing...')

spin.update('Downloading dependencies...')
await downloadDeps()

spin.update('Building project...')
await build()

spin.update('Running tests...')
await runTests()

spin.succeed('All done!')

Multiple Spinners

import { Spinner } from '@stacksjs/cli'

const spinner1 = new Spinner('Task 1')
const spinner2 = new Spinner('Task 2')

spinner1.start()
spinner2.start()

await Promise.all([
  task1().then(() => spinner1.success('Task 1 done')),
  task2().then(() => spinner2.success('Task 2 done'))
])

Progress Bars

import { progress } from '@stacksjs/cli'

const bar = progress({
  total: 100,
  format: 'Progress |{bar}| {percentage}% | {value}/{total}'
})

for (let i = 0; i <= 100; i++) {
  bar.update(i)
  await sleep(50)
}

bar.stop()

Tasks

Task Lists

import { tasks } from '@stacksjs/cli'

await tasks([
  {
    title: 'Installing dependencies',
    task: async () => {
      await installDependencies()
    }
  },
  {
    title: 'Building project',
    task: async () => {
      await buildProject()
    }
  },
  {
    title: 'Running tests',
    enabled: (ctx) => ctx.runTests,
    task: async () => {
      await runTests()
    }
  }
])

Task with Context

await tasks([
  {
    title: 'Fetch data',
    task: async (ctx) => {
      ctx.data = await fetchData()
    }
  },
  {
    title: 'Process data',
    task: async (ctx) => {
      await processData(ctx.data)
    }
  }
], { concurrent: false })

Command Execution

Running Commands

import { runCommand, exec } from '@stacksjs/cli'

// Run a command and get result
const result = await runCommand('npm install')

if (result.isOk) {
  console.log('Success:', result.value)
} else {
  console.error('Error:', result.error)
}

// Execute with options
const output = await exec('ls -la', {
  cwd: '/path/to/dir',
  env: { NODE_ENV: 'production' }
})

Streaming Output

import { exec } from '@stacksjs/cli'

const proc = exec('npm run build', {
  stdout: 'pipe',
  stderr: 'pipe'
})

for await (const chunk of proc.stdout) {
  process.stdout.write(chunk)
}

Argument Parsing

import { parseArgs } from '@stacksjs/cli'

const args = parseArgs(process.argv.slice(2), {
  string: ['name', 'output'],
  boolean: ['verbose', 'force'],
  alias: {
    n: 'name',
    o: 'output',
    v: 'verbose',
    f: 'force'
  },
  default: {
    verbose: false
  }
})

// buddy command -n myproject -v --force
// args = { name: 'myproject', verbose: true, force: true, _: ['command'] }

Helper Functions

Dump and Die

These live in @stacksjs/logging, not here - they go through the logger, so a dump lands in the log file as well as the terminal. Both are async: await them before a process.exit, or the pending write is dropped and the line vanishes.

import { dd, dump } from '@stacksjs/logging'

await dump(someObject)
await dump('Label', anotherObject)

// Dump and exit
await dd(object)

Echo

import { echo } from '@stacksjs/logging'

await echo('Simple output message')
await echo(object)

CLI Configuration

Global Options

const cli = new CLI('myapp')
  .version('1.0.0')
  .description('My awesome CLI application')
  .option('-v, --verbose', 'Enable verbose output')
  .option('-c, --config <path>', 'Config file path')

// Global options are available to all commands
cli.command('build')
  .action((options) => {
    if (options.verbose) log.info('Verbose mode enabled')
  })

Help Generation

const cli = new CLI('myapp')
  .command('serve')
  .description('Start the development server')
  .option('-p, --port <number>', 'Port to listen on', '3000')
  .option('-h, --host <hostname>', 'Host to bind to', 'localhost')
  .example('myapp serve --port 8080')
  .example('myapp serve -h 0.0.0.0')

Error Handling

cli.command('deploy')
  .action(async () => {
    try {
      await deploy()
    } catch (error) {
      log.error('Deployment failed:', error.message)
      process.exit(1)
    }
  })

// Global error handler
cli.catch((error) => {
  log.error('An unexpected error occurred:', error)
  process.exit(1)
})

Edge Cases

Handling Cancellation

const name = await prompts.text({
  message: 'Enter name:'
})

// User pressed Ctrl+C
if (prompt.isCancel(name)) {
  log.warn('Operation cancelled')
  process.exit(0)
}

Handling Empty Input

const value = await prompts.text({
  message: 'Enter value:',
  validate: (v) => {
    if (!v || v.trim() === '') {
      return 'Value cannot be empty'
    }
  }
})

Terminal Size

import { getTerminalSize } from '@stacksjs/cli'

const { columns, rows } = getTerminalSize()

if (columns < 80) {
  log.warn('Terminal is too narrow for optimal display')
}

Non-Interactive Mode

import { isInteractive } from '@stacksjs/cli'

if (!isInteractive()) {
  // Running in CI or piped
  log.info('Running in non-interactive mode')
  // Use default values instead of prompts
} else {
  const name = await prompts.text({ message: 'Name:' })
}

API Reference

CLI Class

MethodDescription
command(name)Add a command
option(flags, desc, default?)Add global option
version(version)Set version
description(desc)Set description
run()Parse and execute
catch(handler)Set error handler

Command Class

MethodDescription
description(desc)Set description
argument(name, desc?)Add argument
option(flags, desc, default?)Add option
action(handler)Set action handler
example(text)Add example
alias(name)Add command alias

Prompt Functions

FunctionDescription
prompts.text(options)Text input
prompts.password(options)Password input
prompts.confirm(options)Yes/no confirmation
prompts.select(options)Single selection
prompts.multiselect(options)Multiple selection
prompt.autocomplete(options)Autocomplete input
prompt.path(options)File/directory path
prompt.group(prompts)Group multiple prompts

Log Functions

FunctionDescription
log.info(msg)Info message
log.success(msg)Success message
log.warn(msg)Warning message
log.error(msg)Error message
log.debug(msg)Debug message
log.dump(obj)Dump object
log.dd(obj)Dump and die

Spinner Functions

MethodDescription
spinner(message)Create spinner
spinner().start()Start spinning
spinner().stop()Stop spinning
spinner().succeed(msg)Stop with success
spinner().fail(msg)Stop with error
spinner().update(msg)Update message
withSpinner(msg, fn)Run fn under a spinner

Execution Functions

FunctionDescription
runCommand(cmd)Run shell command
exec(cmd, opts)Execute with options
parseArgs(args, opts)Parse CLI arguments