import { program } from 'commander' import color from 'kleur' import pkg from '../../package.json' import { withPager } from './pager' import { cleanApp, configShow, cronList, cronLog, cronRun, cronStatus, diffApp, envList, envRm, envSet, getApp, historyApp, infoApp, listApps, logApp, newApp, openApp, pullApp, pushApp, renameApp, restartApp, rmApp, rollbackApp, stashApp, stashListApp, stashPopApp, startApp, metricsApp, statusApp, stopApp, syncApp, versionsApp, } from './commands' program .name('toes') .version(`v${pkg.version}`, '-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('Download an app from server') .argument('', '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 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('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 ', 'show logs from a specific date (YYYY-MM-DD)') .option('-s, --since ', 'show logs since duration (e.g., 1h, 2d)') .option('-g, --grep ', '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 ', 'show logs from a specific date (YYYY-MM-DD)') .option('-s, --since ', 'show logs since duration (e.g., 1h, 2d)') .option('-g, --grep ', '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 identifier (app:name)') .action(cronStatus) cron .command('run') .description('Run a job immediately') .argument('', 'job identifier (app:name)') .action(cronRun) // Sync program .command('push') .helpGroup('Sync:') .description('Push local changes to server') .option('-f, --force', 'overwrite remote changes') .action(pushApp) program .command('pull') .helpGroup('Sync:') .description('Pull changes from server') .option('-f, --force', 'overwrite local changes') .action(pullApp) program .command('status') .helpGroup('Sync:') .description('Show what would be pushed/pulled') .action(statusApp) program .command('diff') .helpGroup('Sync:') .description('Show diff of changed files') .action(() => withPager(diffApp)) program .command('sync') .helpGroup('Sync:') .description('Watch and sync changes bidirectionally') .action(syncApp) program .command('clean') .helpGroup('Sync:') .description('Remove local files not on server') .option('-f, --force', 'skip confirmation') .option('-n, --dry-run', 'show what would be removed') .action(cleanApp) const stash = program .command('stash') .helpGroup('Sync:') .description('Stash local changes') .action(stashApp) stash .command('pop') .description('Restore stashed changes') .action(stashPopApp) stash .command('list') .description('List all stashes') .action(stashListApp) // Config program .command('config') .helpGroup('Config:') .description('Show current host configuration') .action(configShow) 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('', '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('', 'variable name to remove') .option('-g, --global', 'remove a global variable') .action(envRm) program .command('versions') .helpGroup('Config:') .description('List deployed versions') .argument('[name]', 'app name (uses current directory if omitted)') .action(versionsApp) program .command('history') .helpGroup('Config:') .description('Show file changes between versions') .argument('[name]', 'app name (uses current directory if omitted)') .action(historyApp) program .command('rollback') .helpGroup('Config:') .description('Rollback to a previous version') .argument('[name]', 'app name (uses current directory if omitted)') .option('-v, --version ', 'version to rollback to (prompts if omitted)') .action((name, options) => rollbackApp(name, options.version)) export { program }