A powerful command-line interface framework for building interactive CLI applications, featuring command parsing, prompts, spinners, and beautiful output formatting.
Installation
bunadd@stacksjs/cli
Basic Usage
import{CLI,log}from'@stacksjs/cli'// Create a simple commandconstcli=newCLI('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'exportdefaultdefineCommand({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:
exportdefaultdefineCommand({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
constcommand=newCommand('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
constcli=newCLI('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'constname=awaitprompts.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
constpassword=awaitprompts.password({message:'Enter your password:',mask:'*',validate:(value)=>{if(value.length<8)return'Password must be at least 8 characters'}})
Confirm
constconfirmed=awaitprompts.confirm({message:'Are you sure you want to continue?',initialValue:false})if(confirmed){// Proceed}
Select (Single Choice)
constframework=awaitprompts.select({message:'Choose a framework:',options:[{value:'stx',label:'STX',hint:'Recommended'},{value:'react',label:'React'},{value:'svelte',label:'Svelte'},],initialValue:'stx'})
Multi-Select
constfeatures=awaitprompts.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
constproject=awaitprompt.autocomplete({message:'Select a project:',options:async(input)=>{constprojects=awaitfetchProjects(input)returnprojects.map(p=>({value:p.id,label:p.name}))},placeholder:'Type to search...'})
constfile=awaitprompt.path({message:'Select a file:',type:'file',validate:(path)=>{if(!path.endsWith('.ts'))return'Must be a TypeScript file'}})constdirectory=awaitprompt.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'// Colorsconsole.log(red('Error text'))console.log(green('Success text'))console.log(yellow('Warning text'))console.log(blue('Info text'))console.log(cyan('Highlighted text'))// Formattingconsole.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` chainconsole.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!')
import{runCommand,exec}from'@stacksjs/cli'// Run a command and get resultconstresult=awaitrunCommand('npm install')if(result.isOk){console.log('Success:',result.value)}else{console.error('Error:',result.error)}// Execute with optionsconstoutput=awaitexec('ls -la',{cwd:'/path/to/dir',env:{NODE_ENV:'production'}})
Streaming Output
import{exec}from'@stacksjs/cli'constproc=exec('npm run build',{stdout:'pipe',stderr:'pipe'})forawait(constchunkofproc.stdout){process.stdout.write(chunk)}
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'awaitdump(someObject)awaitdump('Label',anotherObject)// Dump and exitawaitdd(object)
constcli=newCLI('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 commandscli.command('build').action((options)=>{if(options.verbose)log.info('Verbose mode enabled')})
Help Generation
constcli=newCLI('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{awaitdeploy()}catch(error){log.error('Deployment failed:',error.message)process.exit(1)}})// Global error handlercli.catch((error)=>{log.error('An unexpected error occurred:',error)process.exit(1)})
Edge Cases
Handling Cancellation
constname=awaitprompts.text({message:'Enter name:'})// User pressed Ctrl+Cif(prompt.isCancel(name)){log.warn('Operation cancelled')process.exit(0)}
Handling Empty Input
constvalue=awaitprompts.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 pipedlog.info('Running in non-interactive mode')// Use default values instead of prompts}else{constname=awaitprompts.text({message:'Name:'})}