From 95d4348560aa0563128255ee318769bfe4d3aa41 Mon Sep 17 00:00:00 2001 From: Chris Wanstrath Date: Wed, 28 Jan 2026 22:36:23 -0800 Subject: [PATCH] start adding cli --- TODO.txt | 11 +++-- bin/toes | 3 ++ bun.lock | 3 ++ package.json | 5 ++- src/cli/index.ts | 105 +++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 122 insertions(+), 5 deletions(-) create mode 100755 bin/toes create mode 100644 src/cli/index.ts diff --git a/TODO.txt b/TODO.txt index 684a0fd..6761318 100644 --- a/TODO.txt +++ b/TODO.txt @@ -18,9 +18,14 @@ ## cli -[ ] `toes --help` -[ ] `toes --version` -[ ] `toes list` +[x] `toes --help` +[x] `toes --version` +[x] `toes list` +[x] `toes start ` +[x] `toes stop ` +[x] `toes restart ` +[ ] `toes open ` +[ ] `toes logs ` [ ] `toes new` [ ] `toes pull` [ ] `toes push` diff --git a/bin/toes b/bin/toes new file mode 100755 index 0000000..fd37539 --- /dev/null +++ b/bin/toes @@ -0,0 +1,3 @@ +#!/usr/bin/env bun + +import '../src/cli/index.ts' diff --git a/bun.lock b/bun.lock index 0ba53ce..3f7abd2 100644 --- a/bun.lock +++ b/bun.lock @@ -5,6 +5,7 @@ "": { "name": "toes", "dependencies": { + "commander": "^14.0.2", "forge": "git+https://git.nose.space/defunkt/forge", "hype": "git+https://git.nose.space/defunkt/hype", }, @@ -23,6 +24,8 @@ "bun-types": ["bun-types@1.3.7", "", { "dependencies": { "@types/node": "*" } }, "sha512-qyschsA03Qz+gou+apt6HNl6HnI+sJJLL4wLDke4iugsE6584CMupOtTY1n+2YC9nGVrEKUlTs99jjRLKgWnjQ=="], + "commander": ["commander@14.0.2", "", {}, "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ=="], + "forge": ["forge@git+https://git.nose.space/defunkt/forge#debfd73ab22c50f66ccc93cb41164c234f78a920", { "peerDependencies": { "typescript": "^5" } }, "debfd73ab22c50f66ccc93cb41164c234f78a920"], "hono": ["hono@4.11.7", "", {}, "sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw=="], diff --git a/package.json b/package.json index 308648e..edc310f 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,8 @@ "typescript": "^5.9.2" }, "dependencies": { - "hype": "git+https://git.nose.space/defunkt/hype", - "forge": "git+https://git.nose.space/defunkt/forge" + "commander": "^14.0.2", + "forge": "git+https://git.nose.space/defunkt/forge", + "hype": "git+https://git.nose.space/defunkt/hype" } } diff --git a/src/cli/index.ts b/src/cli/index.ts new file mode 100644 index 0000000..dc02928 --- /dev/null +++ b/src/cli/index.ts @@ -0,0 +1,105 @@ +import { program } from 'commander' +import { join } from 'path' +import type { App } from '@types' +import { APPS_DIR } from '$apps' + +const HOST = `http://localhost:${process.env.PORT ?? 3000}` + +const STATE_ICONS: Record = { + running: '●', + starting: '◎', + stopped: '◯', + invalid: '◌', +} + +async function get(url: string): Promise { + try { + const res = await fetch(join(HOST, url)) + if (!res.ok) throw new Error(`${res.status} ${res.statusText}`) + return await res.json() + } catch (error) { + console.error(error) + } +} + +async function post(url: string, body?: B): Promise { + try { + const res = await fetch(join(HOST, url), { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }) + if (!res.ok) throw new Error(`${res.status} ${res.statusText}`) + return await res.json() + } catch (error) { + console.error(error) + } +} + +async function listApps() { + const apps: App[] | undefined = await get('/api/apps') + if (!apps) return + + for (const app of apps) { + console.log(`${STATE_ICONS[app.state] ?? '◯'} ${app.name}`) + } +} + +const startApp = async (app: string) => { + await post(`/api/apps/${app}/start`) +} + +const stopApp = async (app: string) => { + await post(`/api/apps/${app}/stop`) +} + +const restartApp = async (app: string) => { + await post(`/api/apps/${app}/restart`) +} + + +program + .name('toes') + .version('0.0.1', '-v, --version') + +program + .command('list') + .description('List all apps') + .action(listApps) + +program + .command('start') + .description('Start an app') + .argument('', 'app name') + .action(startApp) + +program + .command('stop') + .description('Stop an app') + .argument('', 'app name') + .action(stopApp) + + +program + .command('restart') + .description('Restart an app') + .argument('', 'app name') + .action(restartApp) + +program + .command('new') + .description('Create a new app') + .argument('', 'app name') + .action(name => { + // ... + }) + +program + .command('push') + .description('Push app to server') + .option('-f, --force', 'force overwrite') + .action(options => { + // ... + }) + +program.parse() \ No newline at end of file