@tokenoftrust/cli 2.2.8 → 2.2.10
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 +110 -38
- package/src/commands/git-credential.mjs +24 -30
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tokenoftrust/cli",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.10",
|
|
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
|
@@ -760,65 +760,127 @@ export function pinnedPublicCacheDir(cacheRoot, version) {
|
|
|
760
760
|
}
|
|
761
761
|
|
|
762
762
|
/**
|
|
763
|
-
* List the installed
|
|
764
|
-
* normalised so a scoped package reads as
|
|
765
|
-
* store naming) regardless of the on-disk layout
|
|
766
|
-
*
|
|
767
|
-
*
|
|
763
|
+
* List the installed packages that could carry a platform-native binary, as
|
|
764
|
+
* `{ key, pkgJson }`: `key` is normalised so a scoped package reads as
|
|
765
|
+
* `@scope+name` (pnpm's virtual-store naming) regardless of the on-disk layout,
|
|
766
|
+
* and `pkgJson` is where that package's own manifest lives. Prefers the pnpm
|
|
767
|
+
* virtual store (`node_modules/.pnpm/<name>@<ver>/node_modules/<name>`); falls back
|
|
768
|
+
* to a shallow `node_modules` scan (one level into `@scope/`) for an npm/flat
|
|
769
|
+
* install. Best-effort — returns [] on any error.
|
|
768
770
|
*/
|
|
769
771
|
function collectNativePackageDirs(runnerDir) {
|
|
770
|
-
const pnpmDir = join(runnerDir, "node_modules", ".pnpm");
|
|
771
|
-
if (existsSync(pnpmDir)) return readdirSync(pnpmDir);
|
|
772
772
|
const nm = join(runnerDir, "node_modules");
|
|
773
|
+
const pnpmDir = join(nm, ".pnpm");
|
|
774
|
+
if (existsSync(pnpmDir)) {
|
|
775
|
+
return readdirSync(pnpmDir).map((entry) => {
|
|
776
|
+
const m = /^(@[^+]+)\+([^@]+)@/.exec(entry) || /^([^@]+)@/.exec(entry);
|
|
777
|
+
const pkgName = !m ? null : m[2] !== undefined ? `${m[1]}/${m[2]}` : m[1];
|
|
778
|
+
return {
|
|
779
|
+
key: entry,
|
|
780
|
+
pkgJson: pkgName ? join(pnpmDir, entry, "node_modules", pkgName, "package.json") : null,
|
|
781
|
+
};
|
|
782
|
+
});
|
|
783
|
+
}
|
|
773
784
|
if (!existsSync(nm)) return [];
|
|
774
|
-
const
|
|
785
|
+
const out = [];
|
|
775
786
|
for (const e of readdirSync(nm, { withFileTypes: true })) {
|
|
776
787
|
if (e.name.startsWith("@") && e.isDirectory()) {
|
|
777
|
-
for (const s of readdirSync(join(nm, e.name)))
|
|
788
|
+
for (const s of readdirSync(join(nm, e.name))) {
|
|
789
|
+
out.push({ key: `${e.name}+${s}`, pkgJson: join(nm, e.name, s, "package.json") });
|
|
790
|
+
}
|
|
778
791
|
} else {
|
|
779
|
-
|
|
792
|
+
out.push({ key: e.name, pkgJson: join(nm, e.name, "package.json") });
|
|
780
793
|
}
|
|
781
794
|
}
|
|
782
|
-
return
|
|
795
|
+
return out;
|
|
783
796
|
}
|
|
784
797
|
|
|
785
798
|
/**
|
|
786
|
-
*
|
|
787
|
-
*
|
|
788
|
-
*
|
|
789
|
-
* `
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
799
|
+
* The `<os>-<cpu>` targets a package's own manifest declares through its `os` and
|
|
800
|
+
* `cpu` fields — the authority npm and pnpm themselves install by. Null when the
|
|
801
|
+
* manifest is unreadable or declares no positive os AND cpu (negations such as
|
|
802
|
+
* `"!win32"` exclude rather than name a target).
|
|
803
|
+
*/
|
|
804
|
+
function declaredNativeTargets(pkgJsonPath) {
|
|
805
|
+
if (!pkgJsonPath) return null;
|
|
806
|
+
try {
|
|
807
|
+
const j = JSON.parse(readFileSync(pkgJsonPath, "utf8"));
|
|
808
|
+
const positive = (v) =>
|
|
809
|
+
(Array.isArray(v) ? v : typeof v === "string" ? [v] : []).filter(
|
|
810
|
+
(x) => typeof x === "string" && x && !x.startsWith("!"),
|
|
811
|
+
);
|
|
812
|
+
const os = positive(j.os);
|
|
813
|
+
const cpu = positive(j.cpu);
|
|
814
|
+
if (!os.length || !cpu.length) return null;
|
|
815
|
+
return os.flatMap((o) => cpu.map((c) => `${o}-${c}`));
|
|
816
|
+
} catch {
|
|
817
|
+
return null;
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
/**
|
|
822
|
+
* Package-name spellings of an OS / CPU that differ from Node's `process.platform`
|
|
823
|
+
* / `process.arch`. `@cloudflare/workerd-linux-64` is linux-x64; reading the name
|
|
824
|
+
* token literally makes every x64 host look like it is missing workerd.
|
|
825
|
+
*/
|
|
826
|
+
const NATIVE_OS_ALIASES = { windows: "win32" };
|
|
827
|
+
const NATIVE_CPU_ALIASES = { 64: "x64", amd64: "x64", x86_64: "x64", aarch64: "arm64", 32: "ia32", x86: "ia32", i386: "ia32" };
|
|
828
|
+
|
|
829
|
+
/**
|
|
830
|
+
* Every native-binding family that ships a variant for this host's OS but not for
|
|
831
|
+
* its OS+CPU — the exact shape of astro's `Cannot find native binding` boot crash.
|
|
832
|
+
* Platform-native packages are named `<family>-<os>-<cpu>[-<abi>]` (e.g.
|
|
833
|
+
* `@esbuild/darwin-arm64`, `lightningcss-linux-x64-gnu`, `@cloudflare/workerd-linux-64`);
|
|
834
|
+
* the name only GROUPS a package into its family. Which target a package is for
|
|
835
|
+
* comes from its manifest's `os`/`cpu` fields, falling back to the name tokens
|
|
836
|
+
* (aliases normalised) only when the manifest is unreadable. npm's optional-deps bug
|
|
837
|
+
* (npm/cli#4828) can materialise a DIFFERENT arch's variant than the host needs
|
|
838
|
+
* (e.g. darwin-x64 on an arm64 Mac). A family with no variant for our OS at all is
|
|
839
|
+
* a cross-platform optional dep that's correctly absent, so it never counts.
|
|
797
840
|
*
|
|
798
|
-
*
|
|
841
|
+
* @returns {{ family: string, want: string, found: string[] }[]} — `family` in
|
|
842
|
+
* npm spelling (`@scope/name`), `found` the targets installed for our OS.
|
|
843
|
+
* [] on any probe error, so a health probe can't itself break `tot dev`.
|
|
799
844
|
*/
|
|
800
|
-
export function
|
|
845
|
+
export function rendererNativeBindingGaps(runnerDir, { platform = process.platform, arch = process.arch } = {}) {
|
|
801
846
|
try {
|
|
802
|
-
const re = /^(.*?)[-+](darwin|linux|win32|freebsd|android|openharmony)-([a-z0-
|
|
847
|
+
const re = /^(.*?)[-+](darwin|linux|win32|windows|freebsd|android|openharmony)-([a-z0-9_]+)/;
|
|
803
848
|
const families = new Map(); // family -> Set("<os>-<cpu>")
|
|
804
|
-
for (const
|
|
805
|
-
const m =
|
|
849
|
+
for (const { key, pkgJson } of collectNativePackageDirs(runnerDir)) {
|
|
850
|
+
const m = key.match(re);
|
|
806
851
|
if (!m) continue;
|
|
807
852
|
const [, family, os, cpu] = m;
|
|
853
|
+
const targets = declaredNativeTargets(pkgJson) || [
|
|
854
|
+
`${NATIVE_OS_ALIASES[os] || os}-${NATIVE_CPU_ALIASES[cpu] || cpu}`,
|
|
855
|
+
];
|
|
808
856
|
if (!families.has(family)) families.set(family, new Set());
|
|
809
|
-
families.get(family).add(
|
|
857
|
+
for (const t of targets) families.get(family).add(t);
|
|
810
858
|
}
|
|
811
859
|
const want = `${platform}-${arch}`;
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
860
|
+
const gaps = [];
|
|
861
|
+
for (const [family, variants] of families) {
|
|
862
|
+
const ours = [...variants].filter((v) => v.startsWith(`${platform}-`));
|
|
863
|
+
if (ours.length && !variants.has(want)) {
|
|
864
|
+
gaps.push({ family: family.replace(/^(@[^+]+)\+/, "$1/"), want, found: ours.sort() });
|
|
865
|
+
}
|
|
815
866
|
}
|
|
816
|
-
return
|
|
867
|
+
return gaps;
|
|
817
868
|
} catch {
|
|
818
|
-
return
|
|
869
|
+
return [];
|
|
819
870
|
}
|
|
820
871
|
}
|
|
821
872
|
|
|
873
|
+
/**
|
|
874
|
+
* True unless the installed renderer is POISONED for this host's CPU arch — the
|
|
875
|
+
* "the dev server didn't come up" root cause (see rendererNativeBindingGaps).
|
|
876
|
+
* Never throws; on any probe error it returns true (trust the cache).
|
|
877
|
+
*
|
|
878
|
+
* `platform`/`arch` are injectable so this is testable off the host's real arch.
|
|
879
|
+
*/
|
|
880
|
+
export function rendererCacheHealthy(runnerDir, opts = {}) {
|
|
881
|
+
return rendererNativeBindingGaps(runnerDir, opts).length === 0;
|
|
882
|
+
}
|
|
883
|
+
|
|
822
884
|
/**
|
|
823
885
|
* True unless the installed runner package is missing source the Astro config
|
|
824
886
|
* chain needs to even boot — the "renderer resolves but `tot dev` can't load
|
|
@@ -1217,8 +1279,9 @@ export async function installRunnerTarball(
|
|
|
1217
1279
|
const installSpin = startProgress("installing the store preview engine…", {
|
|
1218
1280
|
stages: [{ afterMs: 20000, text: "still setting up the preview engine (first run only)…" }],
|
|
1219
1281
|
});
|
|
1282
|
+
let installer;
|
|
1220
1283
|
try {
|
|
1221
|
-
await runPnpmInstall(stagingDir, { logPath: installLog });
|
|
1284
|
+
installer = await runPnpmInstall(stagingDir, { logPath: installLog });
|
|
1222
1285
|
} finally {
|
|
1223
1286
|
installSpin.stop();
|
|
1224
1287
|
}
|
|
@@ -1234,12 +1297,20 @@ export async function installRunnerTarball(
|
|
|
1234
1297
|
// rather than swallow it as "the dev server didn't come up". Permanent: the
|
|
1235
1298
|
// same installer on the same host would just produce the same result, so the
|
|
1236
1299
|
// auto-retry is skipped.
|
|
1237
|
-
|
|
1300
|
+
const gaps = rendererNativeBindingGaps(runnerDir);
|
|
1301
|
+
if (gaps.length) {
|
|
1238
1302
|
await emitObstacle("renderer-native-bindings-missing");
|
|
1239
1303
|
const err = new CliError(
|
|
1240
|
-
`the store preview engine installed but is missing its native components for ${process.platform}-${process.arch}
|
|
1304
|
+
`the store preview engine installed but is missing its native components for ${process.platform}-${process.arch}` +
|
|
1305
|
+
` (${gaps.map((g) => `${g.family}: only ${g.found.join(", ")} installed`).join("; ")})` +
|
|
1306
|
+
`\n details: ${installLog}`,
|
|
1241
1307
|
{
|
|
1242
|
-
|
|
1308
|
+
// Only npm (the last-resort launcher) has the optional-deps bug that
|
|
1309
|
+
// installing pnpm cures; a pnpm-installed gap is not the developer's to fix.
|
|
1310
|
+
next:
|
|
1311
|
+
installer === "npm"
|
|
1312
|
+
? "install pnpm (`npm i -g pnpm`, or `corepack enable`) and re-run `tot start` — pnpm installs the platform-native bits npm can skip (npm/cli#4828)"
|
|
1313
|
+
: `re-run \`tot start --reset-cache\`; if it repeats, file feedback naming preview engine ${version} and the packages above — this is not something on your machine`,
|
|
1243
1314
|
exitCode: 2,
|
|
1244
1315
|
},
|
|
1245
1316
|
);
|
|
@@ -1495,6 +1566,7 @@ function spawnAsyncResult(cmd, args, opts = {}) {
|
|
|
1495
1566
|
/**
|
|
1496
1567
|
* @param {string} runnerDir
|
|
1497
1568
|
* @param {{ logPath?: string, spawnFn?: (cmd: string, args: string[], opts?: any) => any }} [opts]
|
|
1569
|
+
* @returns {Promise<string>} the launcher that completed the install ("pnpm" | "corepack" | "npm").
|
|
1498
1570
|
*/
|
|
1499
1571
|
export async function runPnpmInstall(runnerDir, { logPath, spawnFn = spawnAsyncResult } = {}) {
|
|
1500
1572
|
const fd = logPath ? openSync(logPath, "a") : null;
|
|
@@ -1532,7 +1604,7 @@ export async function runPnpmInstall(runnerDir, { logPath, spawnFn = spawnAsyncR
|
|
|
1532
1604
|
// Send both streams to the log fd (or swallow them) — never inherit.
|
|
1533
1605
|
stdio: ["ignore", fd ?? "ignore", fd ?? "ignore"],
|
|
1534
1606
|
});
|
|
1535
|
-
if (r.status === 0) return; // installed
|
|
1607
|
+
if (r.status === 0) return cmd; // installed — the launcher that did it
|
|
1536
1608
|
if (/** @type {any} */ (r.error)?.code === "ENOENT") {
|
|
1537
1609
|
// This launcher isn't on the machine — record it and try the next one.
|
|
1538
1610
|
if (fd !== null) writeSync(fd, `[tot] ${cmd} not found (ENOENT) — trying the next launcher\n`);
|
|
@@ -55,7 +55,7 @@ import { repoNameFromRemote, tagFromRepoName } from "./submit.mjs";
|
|
|
55
55
|
import { CliError, formatError } from "../errors.mjs";
|
|
56
56
|
import {
|
|
57
57
|
parseCredentialInput, formatCredentialOutput, splitAuthedRemote,
|
|
58
|
-
|
|
58
|
+
readCachedCredential, writeCachedCredential,
|
|
59
59
|
kitCredentialCachePath, dropCachedCredential, readCheckoutKind, READ_ONLY_KIT_KIND,
|
|
60
60
|
} from "../git-credential.mjs";
|
|
61
61
|
|
|
@@ -180,10 +180,21 @@ export async function mintOrCacheCredential({
|
|
|
180
180
|
const tenant = repoName || ctx.tenant;
|
|
181
181
|
const tag = tagFromRepoName(repoName, tenant);
|
|
182
182
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
183
|
+
// NEVER cache this credential (fb-1790975400361-eg8wxt). The push credential's mint
|
|
184
|
+
// (tot-mcp's checkoutRepo -> mintUserToken) ROTATES: it revokes EVERY prior token for
|
|
185
|
+
// this (developer, tenant) Gitea user on each call -- by design, so a leaked push
|
|
186
|
+
// credential stops working the moment a fresh one is issued (gitea-rest.ts:1559-1569).
|
|
187
|
+
// A local cache keyed only by (tenant, tag) -- not by checkout directory -- has no way
|
|
188
|
+
// to learn that ANOTHER checkout, another machine, or even a retry just rotated the
|
|
189
|
+
// token it is holding: it only knows its own TTL, so it keeps serving a credential
|
|
190
|
+
// Gitea already revoked until that TTL expires. That produced exactly the reported
|
|
191
|
+
// symptom -- a brand-new clone's first fetch fails "Failed to authenticate user" until
|
|
192
|
+
// the cache is cleared, because an earlier clone's mint had already rotated this one
|
|
193
|
+
// out from under it. The read-only kit-credential path avoids this by minting with
|
|
194
|
+
// `rotate: false` (gitea-rest.ts:571, "would yank a concurrent fetch's token") -- that
|
|
195
|
+
// option isn't available here without weakening the push credential's own leak-safety
|
|
196
|
+
// property, so the fix is on this side: mint fresh every time instead of trusting a
|
|
197
|
+
// local TTL the server can invalidate out of band.
|
|
187
198
|
const baseUrl = env.MCP_BASE_URL || env.TOT_MCP_URL || DEFAULT_MCP_URL;
|
|
188
199
|
const client = createClient(baseUrl);
|
|
189
200
|
try {
|
|
@@ -195,9 +206,7 @@ export async function mintOrCacheCredential({
|
|
|
195
206
|
const res = await checkout(client, { tenant, tag, cloneDir: null });
|
|
196
207
|
const cred = splitAuthedRemote(res.gitRemote || "");
|
|
197
208
|
if (!cred) return null;
|
|
198
|
-
|
|
199
|
-
writeCachedCredential(cachePath, fresh);
|
|
200
|
-
return fresh;
|
|
209
|
+
return { username: cred.username, password: cred.token };
|
|
201
210
|
} catch (e) {
|
|
202
211
|
// Same distinction as the kit-checkout catch above: `checkoutTenant` renders a
|
|
203
212
|
// CliError when the MCP (or this checkout's own state) has an authoritative answer
|
|
@@ -227,34 +236,19 @@ export function kitCheckout(cwd) {
|
|
|
227
236
|
}
|
|
228
237
|
|
|
229
238
|
/**
|
|
230
|
-
* git reported
|
|
231
|
-
*
|
|
239
|
+
* git reported a credential rejected (`erase`). Only the read-only kit credential is ever
|
|
240
|
+
* cached (see `mintOrCacheCredential` — the tenant push credential is never cached, because
|
|
241
|
+
* its mint rotates server-side out from under any local TTL), so this only has work to do
|
|
242
|
+
* for a kit checkout; a tenant checkout's `erase` is a correct no-op. Scoped to the
|
|
232
243
|
* checkout's own origin host, like `get`. Never throws.
|
|
233
244
|
* @param {{ env?: NodeJS.ProcessEnv, cwd?: string, expectedHost?: string|null }} [opts]
|
|
234
245
|
*/
|
|
235
246
|
export function forgetCredential({ env = process.env, cwd = process.cwd(), expectedHost = null } = {}) {
|
|
236
247
|
try {
|
|
237
248
|
const kit = kitCheckout(cwd);
|
|
238
|
-
if (kit)
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
return;
|
|
242
|
-
}
|
|
243
|
-
const ctx = detectContext(cwd);
|
|
244
|
-
if (ctx.mode !== "checkout" || !ctx.tenant || !ctx.workspacePath) return;
|
|
245
|
-
let originUrl = null;
|
|
246
|
-
try {
|
|
247
|
-
originUrl = execFileSync("git", ["-C", ctx.workspacePath, "remote", "get-url", "origin"], {
|
|
248
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
249
|
-
}).toString().trim();
|
|
250
|
-
} catch { /* no origin */ }
|
|
251
|
-
if (expectedHost && !hostMatches(originUrl, expectedHost)) return;
|
|
252
|
-
// Mirror `get`'s tenant resolution (remote-derived, falling back to the config
|
|
253
|
-
// file) so `erase` drops the SAME cache key `get` wrote, not a stale sibling.
|
|
254
|
-
const repoName = originUrl ? repoNameFromRemote(originUrl) : null;
|
|
255
|
-
const tenant = repoName || ctx.tenant;
|
|
256
|
-
const tag = tagFromRepoName(repoName, tenant);
|
|
257
|
-
dropCachedCredential(credentialCachePath(tenant, tag, env));
|
|
249
|
+
if (!kit) return;
|
|
250
|
+
if (expectedHost && !hostMatches(kit.originUrl, expectedHost)) return;
|
|
251
|
+
dropCachedCredential(kitCredentialCachePath(kit.target, env));
|
|
258
252
|
} catch {
|
|
259
253
|
/* erase is advisory — never fail git over it */
|
|
260
254
|
}
|