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('status') .helpGroup('Apps:') .description('Show status of all apps, or details for a specific app') .argument('[name]', 'app name (uses current directory if omitted)') .option('-t, --tools', 'show only tools') .option('-a, --apps', 'show only apps (exclude tools)') .action((name?: string, options?: { apps?: boolean; tools?: boolean }) => { if (name) return infoApp(name) return listApps(options ?? {}) }) program .command('list', { hidden: true }) .option('-t, --tools', 'show only tools') .option('-a, --apps', 'show only apps (exclude tools)') .action(listApps) program .command('info', { hidden: true }) .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('', '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 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 ', '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) // 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('', '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) // Shell program .command('shell') .description('Interactive shell') .action(async () => { const { shell } = await import('./shell') await shell() }) // Hide and disable commands that don't work over SSH if (process.env.USER === 'cli') { const disabled = ['shell', 'get', 'open'] for (const name of disabled) { const cmd = program.commands.find((c) => c.name() === name) if (!cmd) continue cmd.helpInformation = () => '' ;(cmd as any)._hidden = true cmd.action(() => { console.error(`"${name}" is not available over SSH`) process.exit(1) }) } } export { program }