Variadic Arguments

Use ... for variadic arguments:


cli.command('install <packages...>', 'Install packages')
  .action((packages: string[]) => {
    console.log(`Installing: ${packages.join(', ')}`)
  })

Multiple Arguments

Combine multiple arguments:


cli.command('move <source> <destination> [options...]', 'Move files')
  .action((source: string, destination: string, options: string[]) => {
    console.log(`Moving ${source} to ${destination}`)
    if (options.length > 0) {
      console.log(`Options: ${options.join(', ')}`)
    }
  })

Options

Boolean Options


cli.command('build', 'Build the project')
  .option('--verbose, -v', 'Enable verbose output')
  .option('--watch, -w', 'Watch for changes')
  .option('--minify, -m', 'Minify output')
  .action((options) => {
    if (options.verbose) {
      console.log('Verbose mode enabled')
    }
    // Build logic
  })

Options with Values


cli.command('serve', 'Start the server')
  .option('--port, -p <port>', 'Port to listen on', { default: 3000 })
  .option('--host, -h <host>', 'Host to bind to', { default: 'localhost' })
  .action((options) => {
    console.log(`Starting server on ${options.host}:${options.port}`)
  })

Required Options


cli.command('deploy', 'Deploy the application')
  .option('--environment, -e <env>', 'Deployment environment', { required: true })
  .action((options) => {
    console.log(`Deploying to ${options.environment}`)
  })

Option Types


cli.command('process', 'Process data')
  // String option
  .option('--format, -f <format>', 'Output format', { default: 'json' })

  // Number option (parsed automatically)
  .option('--count, -c <count>', 'Number of items', { default: 10 })

  // Boolean option (flag)
  .option('--dry-run, -d', 'Perform dry run')

  // Array option
  .option('--include, -i <patterns...>', 'Patterns to include')

  .action((options) => {
    console.log('Format:', options.format)    // string
    console.log('Count:', options.count)       // number
    console.log('Dry run:', options.dryRun)   // boolean
    console.log('Include:', options.include)   // string[]
  })

Option Validation


cli.command('resize', 'Resize an image')
  .option('--width, -w <width>', 'Image width', {
    default: 100,
    validate: (value) => {
      const num = parseInt(value, 10)
      if (isNaN(num) || num < 1) {
        return 'Width must be a positive number'
      }
      return true
    },
  })
  .action((options) => {
    console.log(`Resizing to width: ${options.width}`)
  })

Input/Output

Console Output


import { log } from '@stacksjs/cli'

export default function (cli: CLI) {
  cli.command('status', 'Check status')
    .action(() => {
      // Standard log levels
      log.info('Information message')
      log.success('Success message')
      log.warn('Warning message')
      log.error('Error message')
      log.debug('Debug message')

      // Formatted output
      console.log('')  // Empty line
      console.log('Plain output')
    })
}

Styled Output


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

export default function (cli: CLI) {
  cli.command('styled', 'Show styled output')
    .action(() => {
      console.log(bold('Bold text'))
      console.log(green('Green text'))
      console.log(red('Red text'))
      console.log(yellow('Yellow text'))
      console.log(italic('Italic text'))
      console.log(dim('Dimmed text'))

      // Combine styles
      console.log(bold(green('Bold and green')))
    })
}

Progress Indicators


import { spinner } from '@stacksjs/cli'

export default function (cli: CLI) {
  cli.command('download', 'Download files')
    .action(async () => {
      const spin = spinner('Downloading...')
      spin.start()

      try {
        await downloadFiles()
        spin.succeed('Download complete')
      } catch (error) {
        spin.fail('Download failed')
      }
    })
}

Interactive Prompts


import { prompt, confirm, select, multiselect } from '@stacksjs/cli'

export default function (cli: CLI) {
  cli.command('setup', 'Interactive setup')
    .action(async () => {
      // Text input
      const name = await prompt('What is your name?')

      // Confirmation
      const proceed = await confirm('Do you want to continue?')

      // Single selection
      const framework = await select('Choose a framework:', [
        'Vue',
        'React',
        'Svelte',
      ])

      // Multiple selection
      const features = await multiselect('Select features:', [
        { name: 'TypeScript', value: 'ts', checked: true },
        { name: 'ESLint', value: 'eslint', checked: true },
        { name: 'Prettier', value: 'prettier' },
      ])

      console.log({ name, proceed, framework, features })
    })
}

Command Aliases

Single Alias


cli.command('inspire', 'Inspire yourself')
  .alias('insp')
  .action(() => {
    // Can be run as `buddy inspire` or `buddy insp`
  })

Multiple Aliases


cli.command('generate:component', 'Generate a component')
  .alias('g:c')
  .alias('gc')
  .action(() => {
    // Can be run as any of the aliases
  })

Subcommands

Creating Subcommands


// app/Commands/User.ts
export default function (cli: CLI) {
  // Main command group
  cli.command('user', 'User management commands')

  // Subcommands
  cli.command('user:create <email>', 'Create a new user')
    .option('--admin', 'Create as admin')
    .action(async (email, options) => {
      await createUser(email, options.admin)
    })

  cli.command('user:delete <id>', 'Delete a user')
    .option('--force', 'Skip confirmation')
    .action(async (id, options) => {
      if (!options.force) {
        const confirmed = await confirm('Are you sure?')
        if (!confirmed) return
      }
      await deleteUser(id)
    })

  cli.command('user:list', 'List all users')
    .option('--format, -f <format>', 'Output format', { default: 'table' })
    .action(async (options) => {
      const users = await getUsers()
      displayUsers(users, options.format)
    })

  // Catch unknown subcommands
  cli.on('user:*', () => {
    log.error('Invalid command. See --help for available commands.')
    process.exit(1)
  })

  return cli
}

Calling Commands Programmatically

From Actions


// app/Actions/MaintenanceAction.ts
import { runCommand } from '@stacksjs/cli'

export default new Action({
  async handle() {
    // Run a CLI command
    await runCommand('buddy cache:clear')
    await runCommand('buddy queue:restart')

    return response.json({ message: 'Maintenance complete' })
  },
})

From Other Commands


export default function (cli: CLI) {
  cli.command('reset', 'Reset the application')
    .action(async () => {
      const { runCommand } = await import('@stacksjs/cli')

      log.info('Clearing cache...')
      await runCommand('buddy cache:clear')

      log.info('Running migrations...')
      await runCommand('buddy migrate:fresh')

      log.info('Seeding database...')
      await runCommand('buddy db:seed')

      log.success('Application reset complete!')
    })
}

Using exec


import { exec } from '@stacksjs/cli'

export default function (cli: CLI) {
  cli.command('deploy', 'Deploy application')
    .action(async () => {
      // Run shell commands
      const result = await exec('git status')

      if (result.isOk()) {
        console.log(result.value.stdout)
      } else {
        console.error(result.error)
      }
    })
}

Error Handling

Exit Codes


import { ExitCode } from '@stacksjs/types'

export default function (cli: CLI) {
  cli.command('check', 'Check something')
    .action(() => {
      try {
        // Check logic
        process.exit(ExitCode.Success)        // 0
      } catch (error) {
        process.exit(ExitCode.FatalError)     // 1
      }
    })
}

Graceful Error Handling


import { handleError } from '@stacksjs/error-handling'

export default function (cli: CLI) {
  cli.command('risky', 'Risky operation')
    .action(async () => {
      try {
        await riskyOperation()
        log.success('Operation completed')
      } catch (error) {
        handleError(error)
        log.error('Operation failed')
        process.exit(1)
      }
    })
}

Validation Errors


export default function (cli: CLI) {
  cli.command('process <file>', 'Process a file')
    .action(async (file) => {
      // Validate input
      if (!file.endsWith('.json')) {
        log.error('File must be a JSON file')
        process.exit(1)
      }

      // Check file exists
      if (!await exists(file)) {
        log.error(`File not found: ${file}`)
        process.exit(1)
      }

      // Process file
      await processFile(file)
    })
}

Best Practices

Command Structure


// Recommended command structure
export default function (cli: CLI) {
  cli
    .command('namespace:action', 'Description of what this command does')
    .option('--option, -o <value>', 'Option description', { default: 'default' })
    .alias('n:a')
    .action(async (options) => {
      try {
        // 1. Validate inputs
        validateInputs(options)

        // 2. Show progress
        const spin = spinner('Processing...')
        spin.start()

        // 3. Perform action
        const result = await performAction(options)

        // 4. Show success
        spin.succeed('Complete!')
        displayResult(result)

        // 5. Exit cleanly
        process.exit(ExitCode.Success)
      } catch (error) {
        // 6. Handle errors gracefully
        handleError(error)
        process.exit(ExitCode.FatalError)
      }
    })

  return cli
}

Help Text


cli.command('complex', 'A complex command')
  .option('--input, -i <file>', 'Input file path')
  .option('--output, -o <file>', 'Output file path')
  .example('buddy complex -i data.json -o result.json')
  .example('buddy complex --input=data.json --output=result.json')

Edge Cases and Gotchas

Option Name Conflicts

Avoid using reserved option names:


// Avoid these (used by CLI framework)
// --help, -h
// --version, -v (if version is enabled)

Async Actions

Always handle async operations properly:


// Correct: await async operations
cli.command('async', 'Async command')
  .action(async () => {
    await someAsyncOperation()
    process.exit(0)
  })

// Incorrect: missing await causes early exit
cli.command('async', 'Async command')
  .action(() => {
    someAsyncOperation()  // Command exits before this completes
    process.exit(0)
  })

Environment Variables

Access environment variables in commands:


cli.command('config', 'Show configuration')
  .action(() => {
    const env = process.env.NODE_ENV || 'development'
    const debug = process.env.DEBUG === 'true'

    console.log(`Environment: ${env}`)
    console.log(`Debug: ${debug}`)
  })

API Reference

CLI Methods

MethodDescription
command(name, description)Define a command
option(flags, description, config?)Add an option
alias(name)Add a command alias
action(handler)Set the action handler
example(text)Add an example
on(event, handler)Listen for events

Exit Codes

CodeConstantDescription
0ExitCode.SuccessSuccessful execution
1ExitCode.FatalErrorGeneral error

Log Methods

MethodDescription
log.info(message)Information message
log.success(message)Success message
log.warn(message)Warning message
log.error(message)Error message
log.debug(message)Debug message