From 2ef00c9d53bb967f7f4188a88c371165e3a5a33a Mon Sep 17 00:00:00 2001 From: Chris Wanstrath Date: Wed, 4 Feb 2026 09:51:29 -0800 Subject: [PATCH] /ok --- apps/basic/20260130-000000/index.tsx | 1 + apps/clock/20260130-000000/index.tsx | 2 + .../code/20260130-000000/src/server/index.tsx | 2 + apps/cron/20260201-000000/index.tsx | 2 + apps/env/20260130-000000/index.tsx | 2 + apps/profile/20260130-000000/index.tsx | 1 + .../todo/20260130-181927/src/server/index.tsx | 2 + apps/versions/20260130-000000/index.tsx | 2 + src/cli/commands/index.ts | 1 + src/cli/commands/stats.ts | 103 ++++++++++++++++++ src/cli/setup.ts | 7 ++ src/server/apps.ts | 2 +- templates/bare/index.tsx | 1 + templates/spa/src/server/index.tsx | 1 + 14 files changed, 128 insertions(+), 1 deletion(-) create mode 100644 src/cli/commands/stats.ts diff --git a/apps/basic/20260130-000000/index.tsx b/apps/basic/20260130-000000/index.tsx index 301eab6..fd2b077 100644 --- a/apps/basic/20260130-000000/index.tsx +++ b/apps/basic/20260130-000000/index.tsx @@ -3,6 +3,7 @@ import { Hype } from '@because/hype' const app = new Hype app.get('/', c => c.html(

Hi there!

)) +app.get('/ok', c => c.text('ok')) const apps = () => { } diff --git a/apps/clock/20260130-000000/index.tsx b/apps/clock/20260130-000000/index.tsx index 2986716..5e4b9f9 100644 --- a/apps/clock/20260130-000000/index.tsx +++ b/apps/clock/20260130-000000/index.tsx @@ -45,6 +45,8 @@ app.get('/styles.css', c => c.text(stylesToCSS(), 200, { 'Content-Type': 'text/css; charset=utf-8', })) +app.get('/ok', c => c.text('ok')) + app.get('/', c => c.html( diff --git a/apps/code/20260130-000000/src/server/index.tsx b/apps/code/20260130-000000/src/server/index.tsx index e2c0e06..cd303d1 100644 --- a/apps/code/20260130-000000/src/server/index.tsx +++ b/apps/code/20260130-000000/src/server/index.tsx @@ -319,6 +319,8 @@ function Layout({ title, children, highlight, editable }: LayoutProps) { ) } +app.get('/ok', c => c.text('ok')) + app.get('/styles.css', c => c.text(baseStyles + stylesToCSS(), 200, { 'Content-Type': 'text/css; charset=utf-8', })) diff --git a/apps/cron/20260201-000000/index.tsx b/apps/cron/20260201-000000/index.tsx index 678eb16..b3fa2cd 100644 --- a/apps/cron/20260201-000000/index.tsx +++ b/apps/cron/20260201-000000/index.tsx @@ -217,6 +217,8 @@ function statusColor(job: CronJob): string { } // Routes +app.get('/ok', c => c.text('ok')) + app.get('/styles.css', c => c.text(baseStyles + stylesToCSS(), 200, { 'Content-Type': 'text/css; charset=utf-8', })) diff --git a/apps/env/20260130-000000/index.tsx b/apps/env/20260130-000000/index.tsx index 75da411..42186fc 100644 --- a/apps/env/20260130-000000/index.tsx +++ b/apps/env/20260130-000000/index.tsx @@ -248,6 +248,8 @@ function appEnvPath(appName: string): string { return join(ENV_DIR, `${appName}.env`) } +app.get('/ok', c => c.text('ok')) + app.get('/styles.css', c => c.text(baseStyles + stylesToCSS(), 200, { 'Content-Type': 'text/css; charset=utf-8', })) diff --git a/apps/profile/20260130-000000/index.tsx b/apps/profile/20260130-000000/index.tsx index 4f4f488..9213c19 100644 --- a/apps/profile/20260130-000000/index.tsx +++ b/apps/profile/20260130-000000/index.tsx @@ -3,5 +3,6 @@ import { Hype } from '@because/hype' const app = new Hype app.get('/', c => c.html(

My Profile!!!

)) +app.get('/ok', c => c.text('ok')) export default app.defaults diff --git a/apps/todo/20260130-181927/src/server/index.tsx b/apps/todo/20260130-181927/src/server/index.tsx index 2835e2e..92ee55c 100644 --- a/apps/todo/20260130-181927/src/server/index.tsx +++ b/apps/todo/20260130-181927/src/server/index.tsx @@ -190,6 +190,8 @@ function serializeTodo(todo: ParsedTodo): string { return lines.join('\n') + '\n' } +app.get('/ok', c => c.text('ok')) + app.get('/styles.css', c => c.text(baseStyles + todoStyles + stylesToCSS(), 200, { 'Content-Type': 'text/css; charset=utf-8', })) diff --git a/apps/versions/20260130-000000/index.tsx b/apps/versions/20260130-000000/index.tsx index 62a0afa..a8c9f14 100644 --- a/apps/versions/20260130-000000/index.tsx +++ b/apps/versions/20260130-000000/index.tsx @@ -121,6 +121,8 @@ function Layout({ title, subtitle, children }: LayoutProps) { ) } +app.get('/ok', c => c.text('ok')) + app.get('/styles.css', c => c.text(baseStyles + stylesToCSS(), 200, { 'Content-Type': 'text/css; charset=utf-8', })) diff --git a/src/cli/commands/index.ts b/src/cli/commands/index.ts index e802e72..d36c55a 100644 --- a/src/cli/commands/index.ts +++ b/src/cli/commands/index.ts @@ -12,4 +12,5 @@ export { startApp, stopApp, } from './manage' +export { statsApp } from './stats' export { cleanApp, diffApp, getApp, pullApp, pushApp, rollbackApp, stashApp, stashListApp, stashPopApp, statusApp, syncApp, versionsApp } from './sync' diff --git a/src/cli/commands/stats.ts b/src/cli/commands/stats.ts new file mode 100644 index 0000000..646c075 --- /dev/null +++ b/src/cli/commands/stats.ts @@ -0,0 +1,103 @@ +import color from 'kleur' +import { get } from '../http' +import { resolveAppName } from '../name' + +interface AppStats { + name: string + state: string + port?: number + pid?: number + cpu?: number + memory?: number + rss?: number + tool?: boolean +} + +function formatRss(kb?: number): string { + if (kb === undefined) return '-' + if (kb < 1024) return `${kb} KB` + if (kb < 1024 * 1024) return `${(kb / 1024).toFixed(1)} MB` + return `${(kb / 1024 / 1024).toFixed(2)} GB` +} + +function formatPercent(value?: number): string { + if (value === undefined) return '-' + return `${value.toFixed(1)}%` +} + +function pad(str: string, len: number, right = false): string { + if (right) return str.padStart(len) + return str.padEnd(len) +} + +export async function statsApp(arg?: string) { + // If arg is provided, show stats for that app only + if (arg) { + const name = resolveAppName(arg) + if (!name) return + + const stats: AppStats | undefined = await get(`/api/tools/stats/api/stats/${name}`) + if (!stats) { + console.error(`App not found: ${name}`) + return + } + + console.log(`${color.bold(stats.name)} ${stats.tool ? color.gray('[tool]') : ''}`) + console.log(` State: ${stats.state}`) + if (stats.pid) console.log(` PID: ${stats.pid}`) + if (stats.port) console.log(` Port: ${stats.port}`) + if (stats.cpu !== undefined) console.log(` CPU: ${formatPercent(stats.cpu)}`) + if (stats.memory !== undefined) console.log(` Memory: ${formatPercent(stats.memory)}`) + if (stats.rss !== undefined) console.log(` RSS: ${formatRss(stats.rss)}`) + return + } + + // Show stats for all apps + const stats: AppStats[] | undefined = await get('/api/tools/stats/api/stats') + if (!stats || stats.length === 0) { + console.log('No apps found') + return + } + + // Sort: running first, then by name + stats.sort((a, b) => { + if (a.state === 'running' && b.state !== 'running') return -1 + if (a.state !== 'running' && b.state === 'running') return 1 + return a.name.localeCompare(b.name) + }) + + // Calculate column widths + const nameWidth = Math.max(4, ...stats.map(s => s.name.length + (s.tool ? 7 : 0))) + const stateWidth = Math.max(5, ...stats.map(s => s.state.length)) + + // Header + console.log( + color.gray( + `${pad('NAME', nameWidth)} ${pad('STATE', stateWidth)} ${pad('PID', 7, true)} ${pad('CPU', 7, true)} ${pad('MEM', 7, true)} ${pad('RSS', 10, true)}` + ) + ) + + // Rows + for (const s of stats) { + const name = s.tool ? `${s.name} ${color.gray('[tool]')}` : s.name + const stateColor = s.state === 'running' ? color.green : s.state === 'invalid' ? color.red : color.gray + const state = stateColor(s.state) + + const pid = s.pid ? String(s.pid) : '-' + const cpu = formatPercent(s.cpu) + const mem = formatPercent(s.memory) + const rss = formatRss(s.rss) + + console.log( + `${pad(name, nameWidth)} ${pad(state, stateWidth)} ${pad(pid, 7, true)} ${pad(cpu, 7, true)} ${pad(mem, 7, true)} ${pad(rss, 10, true)}` + ) + } + + // Summary + const running = stats.filter(s => s.state === 'running') + const totalCpu = running.reduce((sum, s) => sum + (s.cpu ?? 0), 0) + const totalRss = running.reduce((sum, s) => sum + (s.rss ?? 0), 0) + + console.log() + console.log(color.gray(`${running.length} running, ${formatPercent(totalCpu)} CPU, ${formatRss(totalRss)} total`)) +} diff --git a/src/cli/setup.ts b/src/cli/setup.ts index 8f53d21..4e88969 100644 --- a/src/cli/setup.ts +++ b/src/cli/setup.ts @@ -24,6 +24,7 @@ import { stashListApp, stashPopApp, startApp, + statsApp, statusApp, stopApp, syncApp, @@ -109,6 +110,12 @@ program .option('-g, --grep ', 'filter logs by pattern') .action(logApp) +program + .command('stats') + .description('Show CPU and memory stats for apps') + .argument('[name]', 'app name (uses current directory if omitted)') + .action(statsApp) + program .command('open') .description('Open an app in browser') diff --git a/src/server/apps.ts b/src/server/apps.ts index e749955..c032be7 100644 --- a/src/server/apps.ts +++ b/src/server/apps.ts @@ -715,7 +715,7 @@ function startHealthChecks(app: App, port: number) { const controller = new AbortController() const timeout = setTimeout(() => controller.abort(), HEALTH_CHECK_TIMEOUT) - const response = await fetch(`http://localhost:${port}/`, { + const response = await fetch(`http://localhost:${port}/ok`, { signal: controller.signal, }) diff --git a/templates/bare/index.tsx b/templates/bare/index.tsx index 724f3a9..e836863 100644 --- a/templates/bare/index.tsx +++ b/templates/bare/index.tsx @@ -3,5 +3,6 @@ import { Hype } from '@because/hype' const app = new Hype() app.get('/', c => c.text('$$APP_NAME$$')) +app.get('/ok', c => c.text('ok')) export default app.defaults diff --git a/templates/spa/src/server/index.tsx b/templates/spa/src/server/index.tsx index 99049e2..55041a0 100644 --- a/templates/spa/src/server/index.tsx +++ b/templates/spa/src/server/index.tsx @@ -4,5 +4,6 @@ const app = new Hype({ layout: false }) // custom routes go here // app.get("/my-custom-routes", (c) => c.text("wild, wild stuff")) +app.get('/ok', c => c.text('ok')) export default app.defaults