@tokenoftrust/cli 3.10.3 → 3.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/commands/dev.mjs +98 -15
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tokenoftrust/cli",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.11.0",
|
|
4
4
|
"description": "Token of Trust developer CLI — clone a tenant store, run it locally with save→reload, and submit it for preview. Installs the `tot` command.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Token of Trust",
|
package/src/commands/dev.mjs
CHANGED
|
@@ -913,7 +913,20 @@ export function rendererNativeBindingGaps(runnerDir, { platform = process.platfo
|
|
|
913
913
|
* `platform`/`arch` are injectable so this is testable off the host's real arch.
|
|
914
914
|
*/
|
|
915
915
|
export function rendererCacheHealthy(runnerDir, opts = {}) {
|
|
916
|
-
return rendererNativeBindingGaps(runnerDir, opts).length === 0;
|
|
916
|
+
return rendererNativeBindingGaps(runnerDir, opts).length === 0 && rendererDepsResolve(runnerDir);
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
/**
|
|
920
|
+
* Does the renderer's own dependency tree resolve from where it sits? `existsSync`
|
|
921
|
+
* follows links, so a `node_modules` entry whose target is gone (a junction left
|
|
922
|
+
* pointing into a renamed staging directory) reads as absent. A tree with no
|
|
923
|
+
* `apps/storefront/package.json` has nothing to check and passes.
|
|
924
|
+
* @param {string} runnerDir
|
|
925
|
+
*/
|
|
926
|
+
export function rendererDepsResolve(runnerDir) {
|
|
927
|
+
const app = join(runnerDir, "apps", "storefront");
|
|
928
|
+
if (!existsSync(join(app, "package.json"))) return true;
|
|
929
|
+
return existsSync(join(app, "node_modules", "astro", "package.json"));
|
|
917
930
|
}
|
|
918
931
|
|
|
919
932
|
/**
|
|
@@ -1266,7 +1279,7 @@ export async function installRunnerTarball(
|
|
|
1266
1279
|
// Bounded to one retry so a genuinely-broken source still fails loudly.
|
|
1267
1280
|
for (let attempt = 1; ; attempt++) {
|
|
1268
1281
|
const archivePath = /** @type {string} */ (isUrl ? join(tmpdir(), `tot-renderer-${process.pid}-${attempt}.tar.gz`) : localSource);
|
|
1269
|
-
const stagingDir = `${runnerDir}.staging-${process.pid}`;
|
|
1282
|
+
const stagingDir = installsInPlace() ? runnerDir : `${runnerDir}.staging-${process.pid}`;
|
|
1270
1283
|
try {
|
|
1271
1284
|
if (isUrl) {
|
|
1272
1285
|
// The fetch itself is otherwise silent (no per-byte output) and can run
|
|
@@ -1339,8 +1352,10 @@ export async function installRunnerTarball(
|
|
|
1339
1352
|
// Atomic-ish: only rename into the final, discoverable path once install
|
|
1340
1353
|
// succeeded, so a crashed/interrupted run never leaves a half-built cache
|
|
1341
1354
|
// entry that a later `tot dev` would treat as ready.
|
|
1342
|
-
|
|
1343
|
-
|
|
1355
|
+
if (stagingDir !== runnerDir) {
|
|
1356
|
+
rmSync(runnerDir, { recursive: true, force: true });
|
|
1357
|
+
renameSync(stagingDir, runnerDir);
|
|
1358
|
+
}
|
|
1344
1359
|
// Fence a fresh install against npm/cli#4828: if the installer left the wrong
|
|
1345
1360
|
// arch's native bindings (or none) for this host, DON'T stamp the completion
|
|
1346
1361
|
// marker — an unmarked tree is never reused, so the next run reinstalls cleanly
|
|
@@ -1368,6 +1383,19 @@ export async function installRunnerTarball(
|
|
|
1368
1383
|
/** @type {any} */ (err).permanent = true;
|
|
1369
1384
|
throw err;
|
|
1370
1385
|
}
|
|
1386
|
+
if (!rendererDepsResolve(runnerDir)) {
|
|
1387
|
+
await emitObstacle("install-failed");
|
|
1388
|
+
const err = new CliError(
|
|
1389
|
+
`the store preview engine installed but its dependencies don't resolve from where it was installed` +
|
|
1390
|
+
`\n details: ${installLog}`,
|
|
1391
|
+
{
|
|
1392
|
+
next: `re-run \`tot start --reset-cache\`; if it repeats, file feedback naming preview engine ${version} and your OS`,
|
|
1393
|
+
exitCode: 2,
|
|
1394
|
+
},
|
|
1395
|
+
);
|
|
1396
|
+
/** @type {any} */ (err).permanent = true;
|
|
1397
|
+
throw err;
|
|
1398
|
+
}
|
|
1371
1399
|
writeCacheMarker(runnerDir, { version, integrity: contentId });
|
|
1372
1400
|
return runnerDir;
|
|
1373
1401
|
} catch (e) {
|
|
@@ -1547,9 +1575,9 @@ function extractTarball(archivePath, destDir, { strip = 0 } = {}) {
|
|
|
1547
1575
|
* missing or disabled (a locked-down host), pnpm install below falls through to
|
|
1548
1576
|
* the pnpm-missing obstacle instead of failing here.
|
|
1549
1577
|
* @param {string} runnerDir
|
|
1550
|
-
* @param {{ logPath?: string, spawnFn?:
|
|
1578
|
+
* @param {{ logPath?: string, spawnFn?: (cmd: string, args: string[], opts?: any) => any }} [opts]
|
|
1551
1579
|
*/
|
|
1552
|
-
export function ensureCorepackPnpm(runnerDir, { logPath, spawnFn =
|
|
1580
|
+
export function ensureCorepackPnpm(runnerDir, { logPath, spawnFn = spawnLauncherSync } = {}) {
|
|
1553
1581
|
const pkgPath = join(runnerDir, "package.json");
|
|
1554
1582
|
if (!existsSync(pkgPath)) return;
|
|
1555
1583
|
let pm;
|
|
@@ -1567,7 +1595,7 @@ export function ensureCorepackPnpm(runnerDir, { logPath, spawnFn = spawnSync } =
|
|
|
1567
1595
|
// WITHOUT the global shim (see runPnpmInstall's fallback). Capture both to the
|
|
1568
1596
|
// log — a silent corepack failure was why the install error carried no cause.
|
|
1569
1597
|
for (const args of [["enable"], ["prepare", pm, "--activate"]]) {
|
|
1570
|
-
const r = spawnFn("corepack", args, { stdio: ["ignore", fd ?? "ignore", fd ?? "ignore"]
|
|
1598
|
+
const r = spawnFn("corepack", args, { stdio: ["ignore", fd ?? "ignore", fd ?? "ignore"] });
|
|
1571
1599
|
if (fd !== null && (r.error || r.status !== 0)) {
|
|
1572
1600
|
const rerr = /** @type {any} */ (r.error);
|
|
1573
1601
|
writeSync(fd, `[tot] corepack ${args.join(" ")} → ${rerr?.code || rerr?.message || `exit ${r.status}`}\n`);
|
|
@@ -1598,22 +1626,68 @@ export function ensureCorepackPnpm(runnerDir, { logPath, spawnFn = spawnSync } =
|
|
|
1598
1626
|
* @param {string} cmd @param {string[]} args @param {object} opts
|
|
1599
1627
|
* @returns {Promise<{ status: number|null, error: Error|null, signal?: string|null }>}
|
|
1600
1628
|
*/
|
|
1629
|
+
/**
|
|
1630
|
+
* Windows installs the renderer where it will live. pnpm there writes `node_modules`
|
|
1631
|
+
* junctions with ABSOLUTE targets into the directory it installs in, so renaming a
|
|
1632
|
+
* staging directory into place leaves every one dangling. The completion marker is still
|
|
1633
|
+
* written last and an unmarked tree is never reused, so a half install cannot be served.
|
|
1634
|
+
* @param {string} [platform]
|
|
1635
|
+
*/
|
|
1636
|
+
export function installsInPlace(platform = process.platform) {
|
|
1637
|
+
return platform === "win32";
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1601
1640
|
/**
|
|
1602
1641
|
* On Windows `pnpm`, `corepack` and `npm` are `.cmd` shims, which Node refuses to
|
|
1603
|
-
* spawn without a shell
|
|
1604
|
-
*
|
|
1642
|
+
* spawn without a shell.
|
|
1643
|
+
* @param {string} [platform]
|
|
1605
1644
|
*/
|
|
1606
1645
|
export function launcherNeedsShell(platform = process.platform) {
|
|
1607
1646
|
return platform === "win32";
|
|
1608
1647
|
}
|
|
1609
1648
|
|
|
1649
|
+
const SAFE_LAUNCHER_ARG = /^[\w@.:+=/-]+$/;
|
|
1650
|
+
|
|
1651
|
+
/**
|
|
1652
|
+
* `[file, args, opts]` to spawn a package-manager launcher. On Windows that is one
|
|
1653
|
+
* command string run through a shell (an args array alongside `shell: true` trips
|
|
1654
|
+
* Node's DEP0190 warning). The shell sees every argument, so each must match a safe
|
|
1655
|
+
* alphabet; anything else throws instead of reaching it.
|
|
1656
|
+
* @param {string} cmd @param {string[]} args @param {object} [opts] @param {string} [platform]
|
|
1657
|
+
* @returns {[string, string[], object]}
|
|
1658
|
+
*/
|
|
1659
|
+
export function launcherSpawn(cmd, args, opts = {}, platform = process.platform) {
|
|
1660
|
+
if (!launcherNeedsShell(platform)) return [cmd, args, opts];
|
|
1661
|
+
for (const a of args) {
|
|
1662
|
+
if (!SAFE_LAUNCHER_ARG.test(a)) throw new Error(`refusing to pass an unsafe argument to ${cmd} through a shell: ${a}`);
|
|
1663
|
+
}
|
|
1664
|
+
return [[cmd, ...args].join(" "), [], { ...opts, shell: true }];
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
/**
|
|
1668
|
+
* Is the launcher on PATH? POSIX answers with ENOENT at spawn, so this is always true
|
|
1669
|
+
* there. Through cmd.exe a missing launcher is an ordinary non-zero exit that cannot be
|
|
1670
|
+
* told from a failed install, so Windows asks `where` (a real executable, no shell).
|
|
1671
|
+
* @param {string} cmd @param {string} [platform]
|
|
1672
|
+
*/
|
|
1673
|
+
export function launcherAvailable(cmd, platform = process.platform) {
|
|
1674
|
+
if (!launcherNeedsShell(platform)) return true;
|
|
1675
|
+
return spawnSync("where", [cmd], { stdio: "ignore" }).status === 0;
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
/** @param {string} cmd @param {string[]} args @param {object} [opts] */
|
|
1679
|
+
function spawnLauncherSync(cmd, args, opts = {}) {
|
|
1680
|
+
const [file, fileArgs, fileOpts] = launcherSpawn(cmd, args, opts);
|
|
1681
|
+
return spawnSync(file, fileArgs, fileOpts);
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1610
1684
|
function spawnAsyncResult(cmd, args, opts = {}) {
|
|
1611
1685
|
return new Promise((resolvePromise) => {
|
|
1612
1686
|
let settled = false;
|
|
1613
1687
|
const settle = (v) => { if (!settled) { settled = true; resolvePromise(v); } };
|
|
1614
1688
|
let child;
|
|
1615
1689
|
try {
|
|
1616
|
-
child = spawn(cmd, args, opts);
|
|
1690
|
+
child = spawn(...launcherSpawn(cmd, args, opts));
|
|
1617
1691
|
} catch (error) {
|
|
1618
1692
|
settle({ status: null, error });
|
|
1619
1693
|
return;
|
|
@@ -1625,10 +1699,10 @@ function spawnAsyncResult(cmd, args, opts = {}) {
|
|
|
1625
1699
|
|
|
1626
1700
|
/**
|
|
1627
1701
|
* @param {string} runnerDir
|
|
1628
|
-
* @param {{ logPath?: string, spawnFn?: (cmd: string, args: string[], opts?: any) => any }} [opts]
|
|
1702
|
+
* @param {{ logPath?: string, spawnFn?: (cmd: string, args: string[], opts?: any) => any, available?: (cmd: string) => boolean }} [opts]
|
|
1629
1703
|
* @returns {Promise<string>} the launcher that completed the install ("pnpm" | "corepack" | "npm").
|
|
1630
1704
|
*/
|
|
1631
|
-
export async function runPnpmInstall(runnerDir, { logPath, spawnFn = spawnAsyncResult } = {}) {
|
|
1705
|
+
export async function runPnpmInstall(runnerDir, { logPath, spawnFn = spawnAsyncResult, available = launcherAvailable } = {}) {
|
|
1632
1706
|
const fd = logPath ? openSync(logPath, "a") : null;
|
|
1633
1707
|
const installArgs = ["install", "--config.dangerouslyAllowAllBuilds=true"];
|
|
1634
1708
|
// pnpm FIRST: the renderer tree is a bundle of native optional deps (rolldown,
|
|
@@ -1653,6 +1727,10 @@ export async function runPnpmInstall(runnerDir, { logPath, spawnFn = spawnAsyncR
|
|
|
1653
1727
|
];
|
|
1654
1728
|
try {
|
|
1655
1729
|
for (const { cmd, args } of attempts) {
|
|
1730
|
+
if (!available(cmd)) {
|
|
1731
|
+
if (fd !== null) writeSync(fd, `[tot] ${cmd} not found on PATH — trying the next launcher\n`);
|
|
1732
|
+
continue;
|
|
1733
|
+
}
|
|
1656
1734
|
// Awaited: the default spawnFn (spawnAsyncResult) runs the launcher
|
|
1657
1735
|
// NON-BLOCKING so the caller's "installing…" spinner keeps ticking during
|
|
1658
1736
|
// the (long, one-time) npm install instead of freezing at "(0s)" — a
|
|
@@ -1663,15 +1741,20 @@ export async function runPnpmInstall(runnerDir, { logPath, spawnFn = spawnAsyncR
|
|
|
1663
1741
|
cwd: runnerDir,
|
|
1664
1742
|
// Send both streams to the log fd (or swallow them) — never inherit.
|
|
1665
1743
|
stdio: ["ignore", fd ?? "ignore", fd ?? "ignore"],
|
|
1666
|
-
shell: launcherNeedsShell(),
|
|
1667
1744
|
});
|
|
1668
1745
|
if (r.status === 0) return cmd; // installed — the launcher that did it
|
|
1669
|
-
|
|
1670
|
-
if (/** @type {any} */ (r.error)?.code === "ENOENT" || (launcherNeedsShell() && r.status === 9009)) {
|
|
1746
|
+
if (/** @type {any} */ (r.error)?.code === "ENOENT") {
|
|
1671
1747
|
// This launcher isn't on the machine — record it and try the next one.
|
|
1672
1748
|
if (fd !== null) writeSync(fd, `[tot] ${cmd} not found (ENOENT) — trying the next launcher\n`);
|
|
1673
1749
|
continue;
|
|
1674
1750
|
}
|
|
1751
|
+
// corepack is the one launcher that can run and still fail for a reason that has
|
|
1752
|
+
// nothing to do with the install (no write access to its shim directory, no
|
|
1753
|
+
// network to fetch pnpm); npm below is the escape hatch that always exists.
|
|
1754
|
+
if (cmd === "corepack") {
|
|
1755
|
+
if (fd !== null) writeSync(fd, `[tot] corepack exited ${r.status} — trying npm\n`);
|
|
1756
|
+
continue;
|
|
1757
|
+
}
|
|
1675
1758
|
// The launcher ran; the install itself failed. That's the actionable error.
|
|
1676
1759
|
// installRunnerTarball retries transient failures once from a clean slate;
|
|
1677
1760
|
// deterministic broken-release 404s are marked permanent below and skip it.
|