Skip to content

Creating Commands ​

This guide covers advanced techniques for creating and managing commands in your clapp applications.

Command Architecture ​

Understanding the architecture behind clapp commands will help you build more sophisticated CLI applications.

Command Lifecycle ​

Each command goes through a lifecycle:

  1. Registration: Command is defined and registered with the CLI
  2. Parsing: Command arguments and options are parsed
  3. Validation: Inputs are validated against defined rules
  4. Execution: Command action is executed with provided inputs
  5. Completion: Command completes and returns results

Advanced Arguments ​

Variadic Arguments ​

Collect multiple arguments with variadic parameters:

ts
command('install')
  .description('Install packages')
  .argument('<...packages>', 'Packages to install')
  .action((packages) => {
    console.log(`Installing packages: ${packages.join(', ')}`)
  })

Typed Arguments ​

Define argument types for automatic conversion and validation:

ts
command('launch')
  .description('Launch application')
  .argument('<port:number>', 'Port to listen on')
  .argument('[hostname:string]', 'Hostname', 'localhost')
  .action((port, hostname) => {
    // port is automatically converted to a number
    console.log(`Launching on ${hostname}:${port}`)
  })

Advanced Options ​

Negatable Options ​

Create options that can be negated:

ts
command('build')
  .description('Build the project')
  .option('--minify/--no-minify', 'Minify output', true)
  .action((options) => {
    console.log(`Minification: ${options.minify ? 'enabled' : 'disabled'}`)
  })

Option Choices ​

Restrict option values to a predefined set:

ts
command('deploy')
  .description('Deploy application')
  .option('-e, --environment <env>', 'Deployment environment', {
    choices: ['development', 'staging', 'production'],
    default: 'development',
  })
  .action((options) => {
    console.log(`Deploying to ${options.environment}`)
  })

Coercion ​

Convert option values to specific types:

ts
command('resize')
  .description('Resize image')
  .option('-w, --width <width>', 'Width in pixels', {
    coerce: value => Number.parseInt(value, 10),
  })
  .option('-h, --height <height>', 'Height in pixels', {
    coerce: value => Number.parseInt(value, 10),
  })
  .action((options) => {
    // options.width and options.height are numbers
    console.log(`Resizing to ${options.width}x${options.height}`)
  })

Option Dependencies ​

Create options that depend on other options:

ts
command('deploy')
  .description('Deploy application')
  .option('-e, --environment <env>', 'Deployment environment')
  .option('-c, --config <path>', 'Config file')
  .dependsOn('config', ['environment'])
  .action((options) => {
    // Requires --environment if --config is provided
    console.log(`Deploying with config: ${options.config}`)
  })

Command Groups ​

Command Group Structure ​

Create hierarchical command structures:

ts
// Main parent command
const db = command('db')
  .description('Database operations')

// First-level subcommands
db.command('migrate')
  .description('Database migrations')

db.command('seed')
  .description('Seed database')

// Second-level subcommands
const migrateCmd = db.commands.find(cmd => cmd.name === 'migrate')
migrateCmd.command('up')
  .description('Run migrations')

migrateCmd.command('down')
  .description('Rollback migrations')

Shared Options ​

Share options across related commands:

ts
import { cli, command, createOption } from '@stacksjs/clapp'

const app = cli({
  name: 'mycli',
})

// Create a shared option
const verboseOption = createOption('-v, --verbose', 'Enable verbose output')

// Apply to multiple commands
command('build')
  .description('Build project')
  .addOption(verboseOption)
  .action((options) => {
    if (options.verbose)
      console.log('Verbose mode enabled for build')
    // Build logic...
  })

command('deploy')
  .description('Deploy project')
  .addOption(verboseOption)
  .action((options) => {
    if (options.verbose)
      console.log('Verbose mode enabled for deploy')
    // Deploy logic...
  })

Command Middleware ​

Use middleware to run code before or after commands:

ts
import { cli, command } from '@stacksjs/clapp'

const app = cli({
  name: 'mycli',
})

// Authentication middleware
async function requireAuth(next) {
  console.log('Checking authentication...')
  // Check authentication logic here
  const isAuthenticated = true

  if (isAuthenticated) {
    // Continue to the command
    await next()
  }
  else {
    console.error('Authentication failed')
    process.exit(1)
  }
}

// Timing middleware
async function timeExecution(next) {
  const start = Date.now()
  await next()
  const end = Date.now()
  console.log(`Command executed in ${end - start}ms`)
}

// Apply middleware to a command
command('deploy')
  .description('Deploy application')
  .middleware([requireAuth, timeExecution])
  .action(() => {
    console.log('Deploying application...')
  })

Dynamic Commands ​

Create commands dynamically at runtime:

ts
import * as fs from 'node:fs'
import { cli, command } from '@stacksjs/clapp'

const app = cli({
  name: 'mycli',
})

// Generate commands from configuration
function loadCommands() {
  const config = JSON.parse(fs.readFileSync('./commands.json', 'utf8'))

  config.commands.forEach((cmd) => {
    command(cmd.name)
      .description(cmd.description)
      .action(() => {
        console.log(`Executing ${cmd.name}: ${cmd.script}`)
        // Execute cmd.script
      })
  })
}

loadCommands()
app.run()

Command Aliases ​

Create aliases for commonly used commands:

ts
import { cli, command } from '@stacksjs/clapp'

const app = cli({
  name: 'mycli',
})

command('install')
  .alias('i') // shorthand alias
  .description('Install dependencies')
  .action(() => {
    console.log('Installing dependencies...')
  })

// Users can now use either:
// $ mycli install
// $ mycli i

Command Hooks ​

Register hooks that run at different points in the command lifecycle:

ts
import { cli, command } from '@stacksjs/clapp'

const app = cli({
  name: 'mycli',
})

command('deploy')
  .description('Deploy application')
  .beforeRun(() => {
    console.log('Before deploy...')
    // Run setup tasks
  })
  .afterRun(() => {
    console.log('After deploy...')
    // Run cleanup tasks
  })
  .action(() => {
    console.log('Deploying application...')
  })

Command Composition ​

Compose complex commands from smaller, reusable parts:

ts
import { cli, command, createOption } from '@stacksjs/clapp'

// Reusable command parts
function withVerboseOption(cmd) {
  return cmd.option('-v, --verbose', 'Enable verbose output')
}

function withForceOption(cmd) {
  return cmd.option('-f, --force', 'Force operation without confirmation')
}

function withLogging(cmd) {
  return cmd
    .beforeRun(() => {
      console.log(`Running ${cmd.name}...`)
    })
    .afterRun(() => {
      console.log(`${cmd.name} completed`)
    })
}

// Create a command with composition
const buildCmd = command('build')
  .description('Build the project')
  .action((options) => {
    console.log(`Building with options:`, options)
  })

// Apply composable parts
withVerboseOption(buildCmd)
withForceOption(buildCmd)
withLogging(buildCmd)

Error Handling ​

Handle command errors gracefully:

ts
import { cli, command } from '@stacksjs/clapp'

const app = cli({
  name: 'mycli',
})

command('risky')
  .description('Run a risky operation')
  .action(async () => {
    try {
      // Attempt risky operation
      const result = await riskyOperation()
      console.log('Operation succeeded:', result)
    }
    catch (err) {
      // Handle specific errors
      if (err.code === 'NETWORK_ERROR') {
        console.error('Network error occurred. Check your connection.')
        process.exit(1)
      }

      // Handle other errors
      console.error('An unexpected error occurred:', err.message)
      process.exit(1)
    }
  })

app.run()

Command Customization ​

Customize command appearance and behavior:

ts
import { cli, command, style } from '@stacksjs/clapp'

const app = cli({
  name: 'mycli',
})

command('status')
  .description('Show system status')
  .help({
    header: style.blue('STATUS COMMAND'),
    usage: 'mycli status [options]',
    examples: [
      'mycli status',
      'mycli status --json',
      'mycli status --detailed',
    ],
    footer: style.dim('For more information, visit our docs.'),
  })
  .action(() => {
    console.log('System is running')
  })

For more information, see the Commands API Reference.

Released under the MIT License.