start adding cli

This commit is contained in:
Chris Wanstrath 2026-01-28 22:36:23 -08:00
parent 2d544e9bd3
commit 95d4348560
5 changed files with 122 additions and 5 deletions

View File

@ -18,9 +18,14 @@
## cli ## cli
[ ] `toes --help` [x] `toes --help`
[ ] `toes --version` [x] `toes --version`
[ ] `toes list` [x] `toes list`
[x] `toes start <app>`
[x] `toes stop <app>`
[x] `toes restart <app>`
[ ] `toes open <app>`
[ ] `toes logs <app>`
[ ] `toes new` [ ] `toes new`
[ ] `toes pull` [ ] `toes pull`
[ ] `toes push` [ ] `toes push`

3
bin/toes Executable file
View File

@ -0,0 +1,3 @@
#!/usr/bin/env bun
import '../src/cli/index.ts'

View File

@ -5,6 +5,7 @@
"": { "": {
"name": "toes", "name": "toes",
"dependencies": { "dependencies": {
"commander": "^14.0.2",
"forge": "git+https://git.nose.space/defunkt/forge", "forge": "git+https://git.nose.space/defunkt/forge",
"hype": "git+https://git.nose.space/defunkt/hype", "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=="], "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"], "forge": ["forge@git+https://git.nose.space/defunkt/forge#debfd73ab22c50f66ccc93cb41164c234f78a920", { "peerDependencies": { "typescript": "^5" } }, "debfd73ab22c50f66ccc93cb41164c234f78a920"],
"hono": ["hono@4.11.7", "", {}, "sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw=="], "hono": ["hono@4.11.7", "", {}, "sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw=="],

View File

@ -14,7 +14,8 @@
"typescript": "^5.9.2" "typescript": "^5.9.2"
}, },
"dependencies": { "dependencies": {
"hype": "git+https://git.nose.space/defunkt/hype", "commander": "^14.0.2",
"forge": "git+https://git.nose.space/defunkt/forge" "forge": "git+https://git.nose.space/defunkt/forge",
"hype": "git+https://git.nose.space/defunkt/hype"
} }
} }

105
src/cli/index.ts Normal file
View File

@ -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<string, string> = {
running: '●',
starting: '◎',
stopped: '◯',
invalid: '◌',
}
async function get<T>(url: string): Promise<T | undefined> {
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<T, B = unknown>(url: string, body?: B): Promise<T | undefined> {
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('<name>', 'app name')
.action(startApp)
program
.command('stop')
.description('Stop an app')
.argument('<name>', 'app name')
.action(stopApp)
program
.command('restart')
.description('Restart an app')
.argument('<name>', 'app name')
.action(restartApp)
program
.command('new')
.description('Create a new app')
.argument('<name>', 'app name')
.action(name => {
// ...
})
program
.command('push')
.description('Push app to server')
.option('-f, --force', 'force overwrite')
.action(options => {
// ...
})
program.parse()