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
This commit is contained in:
CRTkafa 2026-10-13 04:03:09 +08:00
parent 614c32bcba
commit 36a8bf9625
73 changed files with 1195 additions and 251 deletions

View file

@ -37,6 +37,7 @@ import { adbAppLocaleArgs, adbDemoExitArgs, adbDemoModeArgs, adbForceStopArgs, a
import { type CopyFile, copyText, type FrameDims, frameDims } from './lib';
import { captureWeb, defaultConcurrency, hashPaths, hashServed, planWeb, readCache, selectJobs, summaryLines, writeCaptureState } from './capture-web';
import { imageInfo } from './spec';
import * as rt from '../lib/rt';
type Device = 'phone' | 'tablet';
@ -56,7 +57,7 @@ export function adbPath(): string {
/** Throws the install hint when a tool is neither a path that exists nor on PATH. */
export function requireTool(cmd: string, name = cmd): void {
if (existsSync(cmd) || Bun.which(cmd)) return;
if (existsSync(cmd) || rt.which(cmd)) return;
throw new Error(TOOL_HINTS[name] ?? `${name} not found on PATH`);
}
@ -150,7 +151,7 @@ export async function run(ctx: Ctx): Promise<void> {
if (source === 'ios-sim') {
const ios = need(cap.ios, 'shots.capture.ios');
if (!dry) { requireTool('xcrun'); requireTool('ffmpeg'); }
const list = dry && !Bun.which('xcrun') ? '{"devices":{}}' : sh('xcrun', ['simctl', 'list', 'devices', '-j']).out.toString();
const list = dry && !rt.which('xcrun') ? '{"devices":{}}' : sh('xcrun', ['simctl', 'list', 'devices', '-j']).out.toString();
for (const device of devices) {
const want = simTarget(ios, device);
const todo = shotsFor(cap, device, only);
@ -171,16 +172,16 @@ export async function run(ctx: Ctx): Promise<void> {
if (bootedByUs && warmup > 0) {
log('shots', `warming up ${ios.bundleId} (first launch after boot, ${warmup} ms; shots.capture.ios.warmupMs)`);
exec('xcrun', simLaunchArgs(sim.udid, ios.bundleId, { lang: langs[0]!, locale: localeOf(config, langs[0]!, 'ios-sim'), shot: '', baseArgs: ios.args }));
if (!dry) await Bun.sleep(warmup);
if (!dry) await rt.sleep(warmup);
}
for (const lang of langs) for (const [name, s] of todo) {
try {
const locale = localeOf(config, lang, 'ios-sim');
exec('xcrun', simLaunchArgs(sim.udid, ios.bundleId, { lang, locale, shot: name, baseArgs: ios.args, args: s.args }));
if (s.url) { if (!dry) await Bun.sleep(1500); exec('xcrun', simOpenUrlArgs(sim.udid, fill(s.url, { lang, locale, shot: name }))); }
if (s.url) { if (!dry) await rt.sleep(1500); exec('xcrun', simOpenUrlArgs(sim.udid, fill(s.url, { lang, locale, shot: name }))); }
const out = target(lang, device, name);
if (dry) { progress(`${lang}/${device}/${name}.png (${sim.name}, would wait ${s.delayMs ?? 2500} ms)`); continue; }
await Bun.sleep(s.delayMs ?? 2500);
await rt.sleep(s.delayMs ?? 2500);
const tmp = join(tmpDir, `sim-${name}.png`);
sh('xcrun', simScreenshotArgs(sim.udid, tmp));
storeFrame(tmp, out, cap.cropTop, device, dims[device]);
@ -221,7 +222,7 @@ export async function run(ctx: Ctx): Promise<void> {
exec(adb, adbStartArgs(and.package, { lang, locale, shot: name, url: s.url, activity: and.activity, extras: s.extras }, serial));
const out = target(lang, device, name);
if (dry) { progress(`${lang}/${device}/${name}.png (would wait ${s.delayMs ?? 2500} ms, screencap)`); continue; }
await Bun.sleep(s.delayMs ?? 2500);
await rt.sleep(s.delayMs ?? 2500);
const tmp = join(tmpDir, `adb-${name}.png`);
writeFileSync(tmp, sh(adb, adbScreencapArgs(serial)).out);
storeFrame(tmp, out, cap.cropTop, device, dims[device]);