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

@ -29,11 +29,12 @@ import { describeReq } from './jsonapi';
import { createOnce } from '../lib/upsert';
import { elapsed, heartbeat, type HeartbeatOptions, mb, share, withHeartbeat } from '../lib/progress';
import { pump } from '../lib/runner';
import * as rt from '../lib/rt';
const TAG = 'upload';
function md5File(path: string): string {
const h = new Bun.CryptoHasher('md5');
const h = rt.hasher('md5');
const fd = openSync(path, 'r');
try {
const buf = Buffer.alloc(8 * 1048576);
@ -81,7 +82,7 @@ export function uploadTimeout(sent: number, size: number, partsDone: number, par
export async function sendParts(o: SendPartsOptions): Promise<void> {
const say = o.say ?? ((m: string) => log(TAG, m));
const doFetch = o.fetch ?? ((u: string, i: RequestInit) => fetch(u, i));
const sleep = o.sleep ?? Bun.sleep;
const sleep = o.sleep ?? rt.sleep;
const now = o.now ?? Date.now;
const started = now();
const n = o.ops.length;
@ -159,7 +160,7 @@ async function apiUpload(c: Asc, appId: string, ipa: string, info: ReturnType<ty
if (Date.now() > deadline) throw Object.assign(new Error(`upload still ${st.state ?? 'pending'} at the timeout; the file is sent, App Store Connect is still checking it: storebound apple testflight wait --build ${info.build} --version ${info.version} goes on waiting`), { exit: WAIT_EXIT.timeout });
delay = nextPollDelay(delay, { first: 15_000, max: 60_000 });
log(TAG, `upload ${st.state ?? 'pending'}, next check in ${Math.round(delay / 1000)} s`);
await Bun.sleep(delay);
await rt.sleep(delay);
}
}
@ -172,14 +173,14 @@ const uploadsOf = (c: Asc, appId: string, info: { version: string; build: string
* stopped at the deadline (exit 7 with the hint).
*/
async function altoolUpload(root: string, ipa: string, deadline: number, o: HeartbeatOptions = {}): Promise<void> {
if (process.platform !== 'darwin' || !Bun.which('xcrun')) throw new Error('--via altool needs macOS with Xcode; use --via api elsewhere');
if (process.platform !== 'darwin' || !rt.which('xcrun')) throw new Error('--via altool needs macOS with Xcode; use --via api elsewhere');
const k = ascKeyFile(root);
try {
const argv = altoolArgs(ipa, k);
const size = statSync(ipa).size;
log(TAG, `$ ${showCmd(argv, k)}`);
log(TAG, `uploading ${mb(size)} with altool (it reports only at the end)`);
const p = Bun.spawn(argv, { stdout: 'pipe', stderr: 'pipe' });
const p = rt.spawn(argv, { stdout: 'pipe', stderr: 'pipe' });
let timedOut = false;
const kill = setTimeout(() => { timedOut = true; p.kill(); }, Math.max(0, deadline - Date.now()));
const lines: string[] = [];