storebound/cli/doctor.ts
CRT 36a8bf9625 CLI runs on Node 20+: npx storebound (bunx keeps working)
- cli/lib/rt.ts: the runtime layer on node: modules (spawn / spawnSync with Bun's defaults, exit codes and
  "Executable not found", which with PATHEXT, sleep, serve, Glob, hasher, gunzip, stdin, SQLite, packageRoot,
  config imports with a type-stripping fallback for Node 20); every Bun.* use in cli/ goes through it
- tools/gen-embedded.ts -> cli/lib/embedded.ts: templates/, cli/shots/*.html and Play's Data safety form embedded
  (replaces Bun text imports; the bundle and the compiled sidecar carry them), checked fresh by bun run ci
- package.json: bin dist/storebound.mjs (Node ESM bundle, tools/build-node.ts, prepack), engines node >=20
- bun run ci: node bundle + node smoke (real node: --version, help, check same as Bun, scan, release plan --help,
  init, MCP handshake + tool call); Forgejo CI sets up Node 20; tests/node-compat.test.ts guards cli/
- install writes npx MCP commands when run with Node; bin/storebound falls back to Node
- docs: npx storebound init (Node 20+) or bunx storebound init
2026-10-13 04:03:28 +08:00

60 lines
3.7 KiB
TypeScript

// What this machine has for storebound, and how to get what is missing. The same detection as the desktop app's
// Dependencies screen (lib/tools.ts). Store work (listings, products, releases, reviews) needs none of these tools;
// screenshots, app previews, icons and simulator / emulator captures do. Read-only: it prints commands, it runs none.
// storebound doctor [--dir .] [--json]
import { existsSync } from 'node:fs';
import { join, resolve } from 'node:path';
import pkg from '../package.json' with { type: 'json' };
import type { Args } from './lib/args';
import { CONFIG_NAMES } from './lib/config';
import { log } from './lib/log';
import { textTable } from './lib/table';
import { androidSdk, type AndroidSdkLookup, detectTools, findPlaywright, installCommands, type ToolStatus } from './lib/tools';
import * as rt from './lib/rt';
export interface DoctorRow {
id: ToolStatus['id'] | 'playwright';
name: string;
found: boolean;
version: string | null;
path: string | null;
source: ToolStatus['source'];
usedFor: string;
/** Install commands for this OS (empty when found). */
install: string[];
url: string | null;
}
export interface DoctorResult {
storebound: string; bun: string | null; runtime: string; platform: string; project: string | null; tools: DoctorRow[]; missing: number;
/** The Android SDK `play build` hands to Gradle, where it came from, and every place looked at. */
androidSdk: AndroidSdkLookup;
}
export function doctor(cwd: string, env: Record<string, string | undefined> = process.env): DoctorResult {
const tools: DoctorRow[] = detectTools(env).map(t => ({ id: t.id, name: t.name, found: t.found, version: t.version, path: t.path, source: t.source, usedFor: t.usedFor,
install: t.found ? [] : installCommands(t.id), url: t.install.url }));
const pw = findPlaywright([cwd, rt.packageRoot()]);
tools.splice(3, 0, { id: 'playwright', name: 'playwright', found: !!pw, version: null, path: pw, source: pw ? 'system' : null,
usedFor: 'screenshot renders and web captures from a terminal (the desktop app brings its own)', install: pw ? [] : installCommands('playwright'), url: 'https://playwright.dev/docs/intro' });
const project = CONFIG_NAMES.some(n => existsSync(join(cwd, n))) ? cwd : null;
const sdk = androidSdk([join(cwd, 'android'), cwd], env);
return { storebound: pkg.version, bun: rt.bunVersion, runtime: `${rt.runtime.name} ${rt.runtime.version}`, platform: `${process.platform}-${process.arch}`, project, tools, missing: tools.filter(t => !t.found).length, androidSdk: sdk };
}
export async function run(args: Args): Promise<void> {
const r = doctor(resolve(args.str('dir', '.')));
if (args.has('json')) { console.log(JSON.stringify(r, null, 2)); return; }
log('doctor', `storebound ${r.storebound} · ${r.runtime} · ${r.platform}${r.project ? ' · project config found' : ''}`);
console.log(textTable(['tool', 'status', 'version', 'used for'], r.tools.map(t => [t.name, t.found ? (t.source === 'app' ? 'ok (app copy)' : 'ok') : 'missing', t.version ?? '', t.usedFor])));
log('doctor', r.androidSdk.path ? `Android SDK: ${r.androidSdk.path} (from ${r.androidSdk.source})`
: `Android SDK: not found (play build needs it). Checked: ${r.androidSdk.checked.join('; ')}`);
const missing = r.tools.filter(t => !t.found);
if (!missing.length) { log('doctor', 'everything storebound can use is here'); return; }
console.log('\nTo install what is missing (pick one per tool; storebound never runs these for you):');
for (const t of missing) {
console.log(` ${t.name}`);
for (const c of t.install) console.log(` ${c}`);
if (t.url) console.log(` ${t.url}`);
}
log('doctor', `${missing.length} missing. Store work needs none of them; screenshots, previews, icons and captures do.`);
}