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:
parent
614c32bcba
commit
36a8bf9625
73 changed files with 1195 additions and 251 deletions
|
|
@ -39,6 +39,7 @@ import { openAcross, withAccountIds } from './accounts';
|
|||
import { appleAppId, withAppleAppId } from './app-id';
|
||||
import { bareUrl } from './debug';
|
||||
import { type ConsoleBrowser, GOOGLE_REJECTED, googleRejected, openConsoleBrowser, parseUseChromeSession } from './session';
|
||||
import * as rt from '../lib/rt';
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
export type Page = any;
|
||||
|
|
@ -686,7 +687,7 @@ export async function poll<T>(ms: number, fn: () => Promise<T | null>, stop?: St
|
|||
try { const r = await fn(); if (r !== null) return r; } catch { /* navigating */ }
|
||||
if (Date.now() >= end) return null;
|
||||
if (page && WAITS.quietMs > 0 && await quietFor(page) >= WAITS.quietMs) return null;
|
||||
await Bun.sleep(WAITS.pollMs);
|
||||
await rt.sleep(WAITS.pollMs);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -989,7 +990,7 @@ export async function fillSession(b: ConsoleBrowser, o: FillSessionOpts): Promis
|
|||
// the type's last step was saved: what comes next is the App Privacy page again
|
||||
const closing = !!t && def.sections.every((x, i) => x.type !== t || done.has(i));
|
||||
progress(t && !closing ? `the "${t}" dialog step after "${name}"` : `the page after "${t ? `${t}: ` : ''}${name}"`);
|
||||
if (o.debugEveryStep) { const p = active(); if (p) void Bun.sleep(tick).then(() => dump(p, t && !/^Set Up /.test(name) ? `${t} after ${name}` : `after ${name}`)).catch(() => { /* gone */ }); }
|
||||
if (o.debugEveryStep) { const p = active(); if (p) void rt.sleep(tick).then(() => dump(p, t && !/^Set Up /.test(name) ? `${t} after ${name}` : `after ${name}`)).catch(() => { /* gone */ }); }
|
||||
};
|
||||
const meta = formMeta(form, config), res = consoleResult(form, config);
|
||||
const active = (): Page | undefined => b.pages().at(-1);
|
||||
|
|
@ -1056,7 +1057,7 @@ export async function fillSession(b: ConsoleBrowser, o: FillSessionOpts): Promis
|
|||
}
|
||||
} else if (p && reached(p)) { if (++ok >= 2) { if (told) say('signed in'); lastWhy = ''; return true; } }
|
||||
else ok = 0;
|
||||
await Bun.sleep(tick);
|
||||
await rt.sleep(tick);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
|
@ -1297,13 +1298,13 @@ export async function fillSession(b: ConsoleBrowser, o: FillSessionOpts): Promis
|
|||
while (!b.isClosed() && !finished()) {
|
||||
const p = active();
|
||||
if (!await watch(p)) break;
|
||||
if (!p) { await Bun.sleep(tick); continue; }
|
||||
if (!p) { await rt.sleep(tick); continue; }
|
||||
const w = await signInReason(p).catch(() => ({ rule: 'the page could not be read', url: bareUrl(p.url()) }));
|
||||
if (w) {
|
||||
sayWhy(w);
|
||||
if (!signedOut && plan.length) signInHint();
|
||||
signedOut = true;
|
||||
await Bun.sleep(tick); continue;
|
||||
await rt.sleep(tick); continue;
|
||||
}
|
||||
// sign-in finished while watching: open the form again from step 1 (nothing filled yet)
|
||||
if (signedOut) {
|
||||
|
|
@ -1328,13 +1329,13 @@ export async function fillSession(b: ConsoleBrowser, o: FillSessionOpts): Promis
|
|||
}
|
||||
}
|
||||
let s: FillStep;
|
||||
try { s = await fillPage(p, def, pageAnswers, done, false, currentType); } catch { await Bun.sleep(tick); continue; } // navigating
|
||||
try { s = await fillPage(p, def, pageAnswers, done, false, currentType); } catch { await rt.sleep(tick); continue; } // navigating
|
||||
if (s.section !== null) {
|
||||
// a page still loading can miss a field for a moment: only a miss that lasts (3 rounds, at least WAITS.missMs) is a UI change
|
||||
if (s.fallback && (++misses < 3 || Date.now() - firstMiss < WAITS.missMs)) { if (misses === 1) firstMiss = Date.now(); await Bun.sleep(tick); continue; }
|
||||
if (s.fallback && (++misses < 3 || Date.now() - firstMiss < WAITS.missMs)) { if (misses === 1) firstMiss = Date.now(); await rt.sleep(tick); continue; }
|
||||
misses = 0;
|
||||
// the miss lasts: fill every field that was found, leave the missing ones to the owner (only all-missing is a UI change)
|
||||
if (s.fallback) { try { s = await fillPage(p, def, pageAnswers, done, true, currentType); } catch { await Bun.sleep(tick); continue; } }
|
||||
if (s.fallback) { try { s = await fillPage(p, def, pageAnswers, done, true, currentType); } catch { await rt.sleep(tick); continue; } }
|
||||
if (!s.fallback && s.missing.length) warn(TAG, `${s.title}: could not find ${s.missing.map(id => labels.get(id) ?? id).join(', ')} on the page (the console UI may have changed): filling ${s.missing.length === 1 ? 'it' : 'them'} is left to ${agentPart(form)}`);
|
||||
if (s.fallback) {
|
||||
fallback = true;
|
||||
|
|
@ -1367,7 +1368,7 @@ export async function fillSession(b: ConsoleBrowser, o: FillSessionOpts): Promis
|
|||
continue; // more of the form may be on this page
|
||||
}
|
||||
if (waitFor) {
|
||||
if (!await answered(waitFor.pending)) { await Bun.sleep(tick); continue; }
|
||||
if (!await answered(waitFor.pending)) { await rt.sleep(tick); continue; }
|
||||
say(`"${waitFor.title}" answered: storebound goes on`);
|
||||
progress(`the next part after "${waitFor.title}"`);
|
||||
waitFor = null;
|
||||
|
|
@ -1376,7 +1377,7 @@ export async function fillSession(b: ConsoleBrowser, o: FillSessionOpts): Promis
|
|||
pendingAdvance = false;
|
||||
const moved = await advance(p).catch(() => false);
|
||||
if (moved === 'draft') pendingAdvance = true; // Next follows the saved draft
|
||||
if (moved) { await Bun.sleep(tick); continue; }
|
||||
if (moved) { await rt.sleep(tick); continue; }
|
||||
}
|
||||
// an App Privacy dialog step with nothing to answer (the "Let's define two important terms" page between linked and
|
||||
// tracking): no inputs, only Back / Cancel / Next -> Next is navigation
|
||||
|
|
@ -1386,10 +1387,10 @@ export async function fillSession(b: ConsoleBrowser, o: FillSessionOpts): Promis
|
|||
if (info && (infoRounds += 1) >= 2) {
|
||||
infoRounds = 0;
|
||||
const next = await controlWhere(info, FLOW_NAMES.next, () => true, dataTypes);
|
||||
if (next && await flowClick(next, form, dataTypes, say, clicked)) { say('an information-only step of the dialog: storebound pressed Next'); progress('the next step of the dialog'); await Bun.sleep(tick); continue; }
|
||||
if (next && await flowClick(next, form, dataTypes, say, clicked)) { say('an information-only step of the dialog: storebound pressed Next'); progress('the next step of the dialog'); await rt.sleep(tick); continue; }
|
||||
}
|
||||
if (privacy) await privacyStep(p).catch(() => { /* navigating */ });
|
||||
await Bun.sleep(tick);
|
||||
await rt.sleep(tick);
|
||||
}
|
||||
if (timedOut) {
|
||||
say(`nothing storebound knows happened for ${mins(idleMs)}: it stops here and closes the window${o.debugDir && dumps ? ` (debug: ${o.debugDir})` : ''}. Answers saved as drafts stay; run it again when the page is fixed.`);
|
||||
|
|
@ -1403,7 +1404,7 @@ export async function fillSession(b: ConsoleBrowser, o: FillSessionOpts): Promis
|
|||
// the owner reviews and presses the final button, then closes the window: a reminder every 10 heartbeats, at most finalMs
|
||||
const end = Date.now() + finalMs;
|
||||
while (!b.isClosed() && Date.now() < end) {
|
||||
await Promise.race([b.closed, Bun.sleep(Math.min(10 * heartbeatMs, Math.max(0, end - Date.now())))]);
|
||||
await Promise.race([b.closed, rt.sleep(Math.min(10 * heartbeatMs, Math.max(0, end - Date.now())))]);
|
||||
if (!b.isClosed() && Date.now() < end) say('still open: storebound waits for you to review, press the final button and close the window');
|
||||
}
|
||||
if (!b.isClosed()) { say(`the window has been open for ${mins(finalMs)} after the fill: storebound closes it now`); await b.close(); }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue