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

@ -21,6 +21,7 @@ import { STORAGE_READ_SCOPE } from './lib/google-auth';
import { done, log, printJson, warn } from './lib/log';
import { pct, textTable } from './lib/table';
import { googleClient } from './play/google';
import * as rt from './lib/rt';
const TAG = 'analytics';
const day = (d: Date) => d.toISOString().slice(0, 10);
@ -42,7 +43,7 @@ async function reportRows(c: Asc, report: J, start: string, tag: string): Promis
const r = await fetch(seg.attributes.url);
if (!r.ok) throw new Error(`segment ${seg.id}: HTTP ${r.status}`);
const gz = new Uint8Array(await r.arrayBuffer());
const raw = gz[0] === 0x1f && gz[1] === 0x8b ? Bun.gunzipSync(gz) : gz;
const raw = gz[0] === 0x1f && gz[1] === 0x8b ? rt.gunzipSync(gz) : gz;
// Apple documents an md5 checksum per segment without saying of which bytes: accept the file or its content.
if (seg.attributes.checksum && !checksumOk(gz, seg.attributes.checksum) && !checksumOk(raw, seg.attributes.checksum)) throw new Error(`segment ${seg.id}: md5 checksum mismatch`);
rows.push(...parseReport(decodeText(raw)));
@ -145,7 +146,7 @@ export async function run(ctx: Ctx): Promise<void> {
const series = mergeSeries(snaps);
const first = [series.apple[0]?.date, series.play[0]?.date].filter(Boolean).sort()[0] ?? snaps[0]!.window.start;
const paths = a.list('paths').length ? a.list('paths') : ['store', 'storebound.config.ts', ...(config.shots?.copy ? [config.shots.copy] : [])];
const g = Bun.spawnSync(['git', 'log', `--since=${first}`, '--date=short', '--format=%x1e%h%x1f%ad%x1f%s', '--name-only', '--', ...paths], { cwd: root, stdout: 'pipe', stderr: 'pipe' });
const g = rt.spawnSync(['git', 'log', `--since=${first}`, '--date=short', '--format=%x1e%h%x1f%ad%x1f%s', '--name-only', '--', ...paths], { cwd: root, stdout: 'pipe', stderr: 'pipe' });
if (g.exitCode !== 0) throw new Error(`git log: ${g.stderr.toString().trim()}`);
const events = parseGitLog(g.stdout.toString()).reverse();
log(TAG, `${snaps.length} snapshot(s), ${series.apple.length} App Store day(s), ${series.play.length} Play day(s), ${events.length} store change(s) since ${first}`);