toes/src/cli/setup.ts
2026-03-05 07:43:59 -08:00

238 lines
6.1 KiB
TypeScript

import { program } from 'commander'
import color from 'kleur'
import pkg from '../../package.json'
import { SHA } from './sha'
import {
cronList,
cronLog,
cronRun,
cronStatus,
envList,
envRm,
envSet,
getApp,
infoApp,
listApps,
logApp,
newApp,
openApp,
renameApp,
restartApp,
rmApp,
shareApp,
startApp,
metricsApp,
stopApp,
unshareApp,
} from './commands'
program
.name('toes')
.version(`v${pkg.version}-${SHA}`, '-v, --version')
.addHelpText('beforeAll', (ctx) => {
if (ctx.command === program) {
return color.bold().cyan('🐾 Toes') + color.gray(' - personal web appliance\n')
}
return ''
})
.addHelpCommand(false)
.configureOutput({
writeOut: (str) => {
const colored = str
.replace(/^([A-Z][\w ]*:)/gm, color.yellow('$1'))
process.stdout.write(colored)
},
})
program
.command('version', { hidden: true })
.action(() => console.log(program.version()))
// Apps
program
.command('list')
.helpGroup('Apps:')
.description('List all apps')
.option('-t, --tools', 'show only tools')
.option('-a, --apps', 'show only apps (exclude tools)')
.action(listApps)
program
.command('info')
.helpGroup('Apps:')
.description('Show info for an app')
.argument('[name]', 'app name (uses current directory if omitted)')
.action(infoApp)
program
.command('new')
.helpGroup('Apps:')
.description('Create a new toes app')
.argument('[name]', 'app name (uses current directory if omitted)')
.option('--ssr', 'SSR template with pages directory (default)')
.option('--bare', 'minimal template with no pages')
.option('--spa', 'single-page app with client-side rendering')
.action(newApp)
program
.command('get')
.helpGroup('Apps:')
.description('Clone an app from the server')
.argument('<name>', 'app name')
.argument('[directory]', 'target directory (defaults to app name)')
.action(getApp)
program
.command('open')
.helpGroup('Apps:')
.description('Open an app in browser')
.argument('[name]', 'app name (uses current directory if omitted)')
.action(openApp)
program
.command('rename')
.helpGroup('Apps:')
.description('Rename an app')
.argument('[name]', 'app name (uses current directory if omitted)')
.argument('<new-name>', 'new app name')
.action(renameApp)
program
.command('rm')
.helpGroup('Apps:')
.description('Remove an app from the server')
.argument('[name]', 'app name (uses current directory if omitted)')
.action(rmApp)
// Lifecycle
program
.command('start')
.helpGroup('Lifecycle:')
.description('Start an app')
.argument('[name]', 'app name (uses current directory if omitted)')
.action(startApp)
program
.command('stop')
.helpGroup('Lifecycle:')
.description('Stop an app')
.argument('[name]', 'app name (uses current directory if omitted)')
.action(stopApp)
program
.command('restart')
.helpGroup('Lifecycle:')
.description('Restart an app')
.argument('[name]', 'app name (uses current directory if omitted)')
.action(restartApp)
program
.command('share')
.helpGroup('Lifecycle:')
.description('Share an app via public tunnel')
.argument('[name]', 'app name (uses current directory if omitted)')
.action(shareApp)
program
.command('unshare')
.helpGroup('Lifecycle:')
.description('Stop sharing an app')
.argument('[name]', 'app name (uses current directory if omitted)')
.action(unshareApp)
program
.command('logs')
.helpGroup('Lifecycle:')
.description('Show logs for an app')
.argument('[name]', 'app name (uses current directory if omitted)')
.option('-f, --follow', 'follow log output')
.option('-d, --date <date>', 'show logs from a specific date (YYYY-MM-DD)')
.option('-s, --since <duration>', 'show logs since duration (e.g., 1h, 2d)')
.option('-g, --grep <pattern>', 'filter logs by pattern')
.action(logApp)
program
.command('log', { hidden: true })
.argument('[name]', 'app name (uses current directory if omitted)')
.option('-f, --follow', 'follow log output')
.option('-d, --date <date>', 'show logs from a specific date (YYYY-MM-DD)')
.option('-s, --since <duration>', 'show logs since duration (e.g., 1h, 2d)')
.option('-g, --grep <pattern>', 'filter logs by pattern')
.action(logApp)
program
.command('metrics')
.helpGroup('Lifecycle:')
.description('Show CPU, memory, and disk metrics for apps')
.argument('[name]', 'app name (uses current directory if omitted)')
.action(metricsApp)
const cron = program
.command('cron')
.helpGroup('Lifecycle:')
.description('Manage cron jobs')
.argument('[app]', 'app name (list jobs for specific app)')
.action(cronList)
cron
.command('log')
.description('Show cron job logs')
.argument('[target]', 'app name or job (app:name)')
.option('-f, --follow', 'follow log output')
.action(cronLog)
cron
.command('status')
.description('Show detailed status for a job')
.argument('<job>', 'job identifier (app:name)')
.action(cronStatus)
cron
.command('run')
.description('Run a job immediately')
.argument('<job>', 'job identifier (app:name)')
.action(cronRun)
// Config
const env = program
.command('env')
.helpGroup('Config:')
.description('Manage environment variables')
.argument('[name]', 'app name (uses current directory if omitted)')
.option('-g, --global', 'manage global variables shared by all apps')
.action(envList)
env
.command('set')
.description('Set an environment variable')
.argument('[name]', 'app name (uses current directory if omitted)')
.argument('<key>', 'variable name')
.argument('[value]', 'variable value (or use KEY=value format)')
.option('-g, --global', 'set a global variable shared by all apps')
.action(envSet)
env
.command('rm')
.description('Remove an environment variable')
.argument('[name]', 'app name (uses current directory if omitted)')
.argument('<key>', 'variable name to remove')
.option('-g, --global', 'remove a global variable')
.action(envRm)
// Shell
program
.command('shell')
.description('Interactive shell')
.action(async () => {
const { shell } = await import('./shell')
await shell()
})
export { program }