@tokenoftrust/cli 1.4.1 → 2.0.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/README.md +5 -0
- package/bin/tot.mjs +58 -79
- package/package.json +6 -1
- package/src/activity.mjs +15 -14
- package/src/app-scaffold.mjs +4 -4
- package/src/auth.mjs +13 -5
- package/src/candidate-state.mjs +3 -3
- package/src/commands/accept.mjs +65 -38
- package/src/commands/app/dev.mjs +8 -4
- package/src/commands/app/index.mjs +3 -3
- package/src/commands/app/scaffold.mjs +1 -1
- package/src/commands/branches.mjs +4 -3
- package/src/commands/cleanup.mjs +7 -11
- package/src/commands/clone.mjs +23 -20
- package/src/commands/dev.mjs +42 -24
- package/src/commands/doctor.mjs +4 -4
- package/src/commands/git-credential.mjs +2 -2
- package/src/commands/go-live.mjs +9 -5
- package/src/commands/grants.mjs +7 -5
- package/src/commands/hotfix.mjs +1 -1
- package/src/commands/ideas.mjs +2 -2
- package/src/commands/link.mjs +2 -2
- package/src/commands/login.mjs +5 -6
- package/src/commands/pr.mjs +33 -19
- package/src/commands/preview-build.mjs +6 -6
- package/src/commands/preview-doctor.mjs +225 -0
- package/src/commands/preview-retry-evidence.mjs +156 -0
- package/src/commands/preview.mjs +19 -3
- package/src/commands/revert.mjs +5 -5
- package/src/commands/rollback.mjs +18 -16
- package/src/commands/ship.mjs +29 -12
- package/src/commands/start.mjs +61 -51
- package/src/commands/submit.mjs +360 -50
- package/src/commands/sync.mjs +2 -2
- package/src/commands/validate.mjs +4 -3
- package/src/commands/whoami.mjs +1 -1
- package/src/dev-heartbeat.mjs +3 -2
- package/src/dev-logs.mjs +2 -2
- package/src/errors.mjs +11 -4
- package/src/git-credential.mjs +94 -21
- package/src/last-tenant.mjs +1 -1
- package/src/mcp.mjs +6 -1
- package/src/merge-doctor-report.mjs +208 -0
- package/src/oauth.mjs +18 -14
- package/src/obstacle-beacon.cjs +2 -2
- package/src/obstacle.mjs +1 -1
- package/src/plan.mjs +16 -21
- package/src/sample.mjs +4 -4
- package/src/validate.mjs +135 -15
- package/src/vendor/private-apps-devkit.mjs +3 -3
- package/src/viewer-session.mjs +118 -0
- package/template/private-app/README.md +12 -6
- package/src/commands/retire.mjs +0 -203
package/src/commands/dev.mjs
CHANGED
|
@@ -147,11 +147,11 @@ export async function run(argv, ctx) {
|
|
|
147
147
|
}
|
|
148
148
|
|
|
149
149
|
/**
|
|
150
|
-
* Standalone-checkout entrypoint: native runtime by default (
|
|
150
|
+
* Standalone-checkout entrypoint: native runtime by default (no Docker
|
|
151
151
|
* prerequisite), Docker on explicit request, and Docker as an automatic
|
|
152
152
|
* fallback when the native artifact can't be fetched (e.g. the MCP's
|
|
153
153
|
* dev_renderer_artifact isn't configured on this deployment yet) — so `tot dev`
|
|
154
|
-
* keeps working
|
|
154
|
+
* keeps working instead of hard-failing.
|
|
155
155
|
*/
|
|
156
156
|
async function runStandalone(workspace, args, ctx) {
|
|
157
157
|
if (args.docker) return runContainer(workspace, args, ctx);
|
|
@@ -266,7 +266,7 @@ function runMonorepo(ctx, argv) {
|
|
|
266
266
|
* Thrown when the native runtime can't be used for a reason that's about the
|
|
267
267
|
* artifact/service, not the developer's checkout — runStandalone catches this
|
|
268
268
|
* specifically and falls back to Docker instead of hard-failing. Exported so
|
|
269
|
-
* `tot start`
|
|
269
|
+
* `tot start` can catch it too and fall back the same way.
|
|
270
270
|
*/
|
|
271
271
|
export class NativeArtifactUnavailableError extends Error {}
|
|
272
272
|
|
|
@@ -326,7 +326,7 @@ export function bootNative(runnerDir, workspace, port, url, args) {
|
|
|
326
326
|
const handle = spawnNativeDev(runnerDir, workspace, port, { stdio: "piped", env: bridgeEnv });
|
|
327
327
|
streamDevLogs(handle.child, { cockpitUrl: cockpitUrlFrom(bridgeEnv.TOT_DEV_ACTIVITY_URL) });
|
|
328
328
|
|
|
329
|
-
// Heartbeat the hosted cockpit
|
|
329
|
+
// Heartbeat the hosted cockpit with the CLI version + this live localhost
|
|
330
330
|
// URL while the runner runs — CLI-side, using the SAME bridge credential the
|
|
331
331
|
// runner's file-save path uses (no-op in --sample, which threads no credential).
|
|
332
332
|
// `cwd` (the checkout root) lets the cockpit show the FULL path to edit.
|
|
@@ -409,7 +409,7 @@ export function printSampleBanner({ url }, mode = "sample") {
|
|
|
409
409
|
* Exported so sample/tests can drive the selection directly.
|
|
410
410
|
* @param {ReturnType<typeof parseArgs>} args
|
|
411
411
|
* @param {{ client?: any }} [opts] an already-authenticated MCP client (entitled path)
|
|
412
|
-
* @returns {Promise<{kind:
|
|
412
|
+
* @returns {Promise<{kind:string,version:string,url:string,strip:number,cacheKey:string,integrity:string|null}>}
|
|
413
413
|
*/
|
|
414
414
|
export async function resolveRendererSource(args, { client } = {}) {
|
|
415
415
|
if (args.sample) return resolvePublicRendererSource(args);
|
|
@@ -577,6 +577,11 @@ function compareStableAsc(a, b) {
|
|
|
577
577
|
* override with `--renderer-version` / TOT_RUNNER_VERSION. Package/registry
|
|
578
578
|
* overridable via env for testing.
|
|
579
579
|
*/
|
|
580
|
+
/**
|
|
581
|
+
* @param {any} args
|
|
582
|
+
* @param {NodeJS.ProcessEnv} [env]
|
|
583
|
+
* @param {{ declaredVersion?: string|null }} [opts]
|
|
584
|
+
*/
|
|
580
585
|
export async function resolvePublicRendererSource(args, env = process.env, { declaredVersion = null } = {}) {
|
|
581
586
|
const pkg = env.TOT_RUNNER_PACKAGE || PUBLIC_RUNNER_PACKAGE;
|
|
582
587
|
const registry = (env.TOT_NPM_REGISTRY || DEFAULT_NPM_REGISTRY).replace(/\/$/, "");
|
|
@@ -644,8 +649,9 @@ function isCompatibleMaintenancePatch(cliVersion, runnerVersion) {
|
|
|
644
649
|
/**
|
|
645
650
|
* ENTITLED source: the MCP's `dev_renderer_artifact` signed URL, gated on the
|
|
646
651
|
* developer entitlement (same gate as the Docker pull token). Reuses an
|
|
647
|
-
* already-authenticated `client` when provided
|
|
652
|
+
* already-authenticated `client` when provided, else establishes its own.
|
|
648
653
|
*/
|
|
654
|
+
/** @param {any} args @param {{ client?: any }} [opts] */
|
|
649
655
|
export async function resolveEntitledRendererSource(args, { client: providedClient } = {}) {
|
|
650
656
|
const baseUrl = args.mcp || process.env.MCP_BASE_URL || process.env.TOT_MCP_URL || DEFAULT_MCP_URL;
|
|
651
657
|
const client = providedClient || createMcpClient(baseUrl);
|
|
@@ -680,11 +686,13 @@ export async function resolveEntitledRendererSource(args, { client: providedClie
|
|
|
680
686
|
* anything that should fall back to Docker (MCP unreachable, not entitled, not
|
|
681
687
|
* configured, no corepack/pnpm on this host) rather than hard-failing `tot dev`.
|
|
682
688
|
*
|
|
683
|
-
* Exported + accepts an already-authenticated `client` (
|
|
684
|
-
*
|
|
689
|
+
* Exported + accepts an already-authenticated `client` (`tot start` reuses
|
|
690
|
+
* its own session and overlaps this with the checkout clone
|
|
685
691
|
* instead of paying for a second client.initialize()+establishSession() — same
|
|
686
692
|
* pattern as ensureRegistryLogin's `providedClient`). `tot dev` standalone
|
|
687
693
|
* omits it and this establishes its own, as before.
|
|
694
|
+
* @param {any} args
|
|
695
|
+
* @param {{ client?: any }} [opts]
|
|
688
696
|
* @returns {Promise<string>} the cached, installed runner tree's root directory.
|
|
689
697
|
*/
|
|
690
698
|
export async function ensureRendererArtifact(args, { client: providedClient } = {}) {
|
|
@@ -865,11 +873,11 @@ export function probeRunnerVersion(runnerDir, { timeoutMs = 4000 } = {}) {
|
|
|
865
873
|
* @returns {Promise<string>} the runner tree's root directory (has scripts/tot-dev.mjs).
|
|
866
874
|
*/
|
|
867
875
|
export async function ensureSampleRenderer(args, ctx, { env = process.env, cacheRoot = RENDERER_CACHE_ROOT } = {}) {
|
|
868
|
-
const src = resolveLocalRendererSource({
|
|
876
|
+
const src = /** @type {any} */ (resolveLocalRendererSource({
|
|
869
877
|
env,
|
|
870
878
|
mode: ctx?.mode,
|
|
871
879
|
repoRoot: ctx?.repoRoot,
|
|
872
|
-
});
|
|
880
|
+
}));
|
|
873
881
|
|
|
874
882
|
if (src.kind === "dir") {
|
|
875
883
|
if (!existsSync(join(src.dir, "scripts", "tot-dev.mjs"))) {
|
|
@@ -1063,7 +1071,7 @@ export async function installRunnerTarball(
|
|
|
1063
1071
|
// of stopping a first run at an error only `rm -rf` folklore could clear.
|
|
1064
1072
|
// Bounded to one retry so a genuinely-broken source still fails loudly.
|
|
1065
1073
|
for (let attempt = 1; ; attempt++) {
|
|
1066
|
-
const archivePath = isUrl ? join(tmpdir(), `tot-renderer-${process.pid}-${attempt}.tar.gz`) : localSource;
|
|
1074
|
+
const archivePath = /** @type {string} */ (isUrl ? join(tmpdir(), `tot-renderer-${process.pid}-${attempt}.tar.gz`) : localSource);
|
|
1067
1075
|
const stagingDir = `${runnerDir}.staging-${process.pid}`;
|
|
1068
1076
|
try {
|
|
1069
1077
|
if (isUrl) {
|
|
@@ -1133,7 +1141,7 @@ export async function installRunnerTarball(
|
|
|
1133
1141
|
exitCode: 2,
|
|
1134
1142
|
},
|
|
1135
1143
|
);
|
|
1136
|
-
err.permanent = true;
|
|
1144
|
+
/** @type {any} */ (err).permanent = true;
|
|
1137
1145
|
throw err;
|
|
1138
1146
|
}
|
|
1139
1147
|
writeCacheMarker(runnerDir, { version, integrity: contentId });
|
|
@@ -1285,7 +1293,7 @@ async function downloadFile(url, destPath) {
|
|
|
1285
1293
|
if (!res.ok || !res.body) {
|
|
1286
1294
|
throw new Error(`download failed: HTTP ${res.status} ${res.statusText}`);
|
|
1287
1295
|
}
|
|
1288
|
-
await pipeline(Readable.fromWeb(res.body), createWriteStream(destPath));
|
|
1296
|
+
await pipeline(Readable.fromWeb(/** @type {any} */ (res.body)), createWriteStream(destPath));
|
|
1289
1297
|
}
|
|
1290
1298
|
|
|
1291
1299
|
/**
|
|
@@ -1334,7 +1342,8 @@ export function ensureCorepackPnpm(runnerDir, { logPath, spawnFn = spawnSync } =
|
|
|
1334
1342
|
for (const args of [["enable"], ["prepare", pm, "--activate"]]) {
|
|
1335
1343
|
const r = spawnFn("corepack", args, { stdio: ["ignore", fd ?? "ignore", fd ?? "ignore"] });
|
|
1336
1344
|
if (fd !== null && (r.error || r.status !== 0)) {
|
|
1337
|
-
|
|
1345
|
+
const rerr = /** @type {any} */ (r.error);
|
|
1346
|
+
writeSync(fd, `[tot] corepack ${args.join(" ")} → ${rerr?.code || rerr?.message || `exit ${r.status}`}\n`);
|
|
1338
1347
|
}
|
|
1339
1348
|
}
|
|
1340
1349
|
} finally {
|
|
@@ -1378,6 +1387,10 @@ function spawnAsyncResult(cmd, args, opts = {}) {
|
|
|
1378
1387
|
});
|
|
1379
1388
|
}
|
|
1380
1389
|
|
|
1390
|
+
/**
|
|
1391
|
+
* @param {string} runnerDir
|
|
1392
|
+
* @param {{ logPath?: string, spawnFn?: (cmd: string, args: string[], opts?: any) => any }} [opts]
|
|
1393
|
+
*/
|
|
1381
1394
|
export async function runPnpmInstall(runnerDir, { logPath, spawnFn = spawnAsyncResult } = {}) {
|
|
1382
1395
|
const fd = logPath ? openSync(logPath, "a") : null;
|
|
1383
1396
|
const installArgs = ["install", "--config.dangerouslyAllowAllBuilds=true"];
|
|
@@ -1415,7 +1428,7 @@ export async function runPnpmInstall(runnerDir, { logPath, spawnFn = spawnAsyncR
|
|
|
1415
1428
|
stdio: ["ignore", fd ?? "ignore", fd ?? "ignore"],
|
|
1416
1429
|
});
|
|
1417
1430
|
if (r.status === 0) return; // installed
|
|
1418
|
-
if (r.error?.code === "ENOENT") {
|
|
1431
|
+
if (/** @type {any} */ (r.error)?.code === "ENOENT") {
|
|
1419
1432
|
// This launcher isn't on the machine — record it and try the next one.
|
|
1420
1433
|
if (fd !== null) writeSync(fd, `[tot] ${cmd} not found (ENOENT) — trying the next launcher\n`);
|
|
1421
1434
|
continue;
|
|
@@ -1446,7 +1459,7 @@ export async function runPnpmInstall(runnerDir, { logPath, spawnFn = spawnAsyncR
|
|
|
1446
1459
|
(logPath ? `\n details: ${logPath}` : ""),
|
|
1447
1460
|
{ next: "install pnpm with `npm i -g pnpm` (or `corepack enable`), then re-run `tot start`" },
|
|
1448
1461
|
);
|
|
1449
|
-
err.permanent = true;
|
|
1462
|
+
/** @type {any} */ (err).permanent = true;
|
|
1450
1463
|
throw err;
|
|
1451
1464
|
} finally {
|
|
1452
1465
|
if (fd !== null) closeSync(fd);
|
|
@@ -1501,6 +1514,10 @@ function missingPackage404(logPath) {
|
|
|
1501
1514
|
* fs-watch HMR. Returns a handle shaped like spawnDevContainer's, so run()'s
|
|
1502
1515
|
* auto-open-browser logic works unchanged for either runtime. Exported (and
|
|
1503
1516
|
* `stdio` overridable) so `tot start` can pipe the logs instead of inheriting.
|
|
1517
|
+
* @param {string} runnerDir
|
|
1518
|
+
* @param {string} workspace
|
|
1519
|
+
* @param {string} port
|
|
1520
|
+
* @param {{ stdio?: any, env?: Record<string, any> }} [opts]
|
|
1504
1521
|
*/
|
|
1505
1522
|
export function spawnNativeDev(runnerDir, workspace, port, { stdio = "inherit", env = {} } = {}) {
|
|
1506
1523
|
const script = join(runnerDir, "scripts", "tot-dev.mjs");
|
|
@@ -1512,7 +1529,7 @@ export function spawnNativeDev(runnerDir, workspace, port, { stdio = "inherit",
|
|
|
1512
1529
|
// `env` (e.g. activityBridgeEnv()) merges OVER process.env — {} is a pure
|
|
1513
1530
|
// passthrough, identical to the old no-env-key behavior. Append `--no-warnings`
|
|
1514
1531
|
// to NODE_OPTIONS so the runner's Node process never emits the scary
|
|
1515
|
-
// `(node:NNNN) ExperimentalWarning: …` boot noise to the developer
|
|
1532
|
+
// `(node:NNNN) ExperimentalWarning: …` boot noise to the developer;
|
|
1516
1533
|
// appended (not replaced) so any host-set NODE_OPTIONS is preserved.
|
|
1517
1534
|
const mergedEnv = { ...process.env, ...env };
|
|
1518
1535
|
mergedEnv.NODE_OPTIONS = `${mergedEnv.NODE_OPTIONS ? `${mergedEnv.NODE_OPTIONS} ` : ""}--no-warnings`;
|
|
@@ -1521,7 +1538,7 @@ export function spawnNativeDev(runnerDir, workspace, port, { stdio = "inherit",
|
|
|
1521
1538
|
[script, "--workspace", workspace, "--port", port],
|
|
1522
1539
|
{ cwd: runnerDir, stdio: stdioArr, env: mergedEnv },
|
|
1523
1540
|
);
|
|
1524
|
-
const handle = { child, exited: false, done: null };
|
|
1541
|
+
const handle = { child, exited: false, done: /** @type {any} */ (null) };
|
|
1525
1542
|
handle.done = new Promise((resolvePromise) => {
|
|
1526
1543
|
child.on("exit", (code) => {
|
|
1527
1544
|
handle.exited = true;
|
|
@@ -1555,7 +1572,7 @@ async function runContainer(workspace, args, ctx) {
|
|
|
1555
1572
|
const handle = await spawnDevContainer(plan, args, { stdio: "piped" });
|
|
1556
1573
|
streamDevLogs(handle.child, { cockpitUrl: cockpitUrlFrom(activityBridgeEnv().TOT_DEV_ACTIVITY_URL) });
|
|
1557
1574
|
|
|
1558
|
-
// Heartbeat the hosted cockpit
|
|
1575
|
+
// Heartbeat the hosted cockpit CLI-side while the container runs — the
|
|
1559
1576
|
// container reports file-saves via the threaded env, but the CLI owns the
|
|
1560
1577
|
// version + live URL. Same bridge credential (activityBridgeEnv), no-op absent.
|
|
1561
1578
|
const stopHeartbeat = startHeartbeatFromEnv(activityBridgeEnv(), { url: plan.url, cwd: plan.workspace });
|
|
@@ -1580,7 +1597,7 @@ async function runContainer(workspace, args, ctx) {
|
|
|
1580
1597
|
|
|
1581
1598
|
/**
|
|
1582
1599
|
* Resolve the runner image ref the same way buildContainerPlan does — exported
|
|
1583
|
-
* so a caller (`tot start
|
|
1600
|
+
* so a caller (`tot start`) can know the image, and therefore prefetch
|
|
1584
1601
|
* a registry login (below), before the checkout dir exists.
|
|
1585
1602
|
*/
|
|
1586
1603
|
export function resolveDevImage(args, env = process.env) {
|
|
@@ -1676,9 +1693,9 @@ export async function spawnDevContainer(plan, args, { stdio = "inherit" } = {})
|
|
|
1676
1693
|
|
|
1677
1694
|
const stdioArr =
|
|
1678
1695
|
stdio === "piped" ? ["ignore", "pipe", "pipe"] : ["inherit", "inherit", "inherit"];
|
|
1679
|
-
const child = spawn("docker", plan.dockerArgs, { stdio: stdioArr });
|
|
1696
|
+
const child = spawn("docker", plan.dockerArgs, { stdio: /** @type {any} */ (stdioArr) });
|
|
1680
1697
|
|
|
1681
|
-
const handle = { child, exited: false, done: null };
|
|
1698
|
+
const handle = { child, exited: false, done: /** @type {any} */ (null) };
|
|
1682
1699
|
handle.done = new Promise((resolvePromise) => {
|
|
1683
1700
|
child.on("exit", (code) => {
|
|
1684
1701
|
handle.exited = true;
|
|
@@ -1714,10 +1731,11 @@ export function isPrivateRegistryImage(image) {
|
|
|
1714
1731
|
* the session, or the tool isn't available yet, warn softly and continue (an
|
|
1715
1732
|
* existing `docker login`, or the docker pull error, takes over).
|
|
1716
1733
|
*
|
|
1717
|
-
* Exported + accepts an already-authenticated `client` (
|
|
1734
|
+
* Exported + accepts an already-authenticated `client` (`tot start` reuses
|
|
1718
1735
|
* its own session and overlaps this with the checkout clone instead of paying
|
|
1719
1736
|
* for a second client.initialize()+establishSession() serially afterward).
|
|
1720
1737
|
* `tot dev` standalone omits it and this establishes its own, as before.
|
|
1738
|
+
* @param {string} image @param {any} args @param {{ client?: any }} [opts]
|
|
1721
1739
|
*/
|
|
1722
1740
|
export async function ensureRegistryLogin(image, args, { client: providedClient } = {}) {
|
|
1723
1741
|
const registry = String(image).split("/")[0];
|
|
@@ -1767,7 +1785,7 @@ export function dockerAvailable() {
|
|
|
1767
1785
|
* thing `open -a` can launch unattended; other platforms report false and
|
|
1768
1786
|
* leave it to the caller's usual next-command message). Returns true once
|
|
1769
1787
|
* Docker answers, false if it doesn't within the wait window. Shared by
|
|
1770
|
-
* `tot start`'s preflight and `tot doctor --fix
|
|
1788
|
+
* `tot start`'s preflight and `tot doctor --fix`.
|
|
1771
1789
|
*/
|
|
1772
1790
|
export async function tryStartDocker() {
|
|
1773
1791
|
if (process.platform !== "darwin") return false;
|
package/src/commands/doctor.mjs
CHANGED
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
* auth identity is available, and Docker — plus reports the detected context
|
|
7
7
|
* so a developer knows which mode `tot` will use here.
|
|
8
8
|
*
|
|
9
|
-
* Docker is informational-only (not blocking):
|
|
9
|
+
* Docker is informational-only (not blocking): the native runtime is
|
|
10
10
|
* the default for `tot dev`/`tot start`, so a Docker that isn't running no
|
|
11
11
|
* longer stops the loop — it only matters if you pass --docker or the native
|
|
12
12
|
* artifact can't be fetched (see dev.mjs#NativeArtifactUnavailableError).
|
|
13
13
|
*
|
|
14
|
-
* `--fix`
|
|
14
|
+
* `--fix` auto-remediates what it safely can, each fix announced as it
|
|
15
15
|
* runs: starts Docker (macOS, opportunistically — harmless even though it's
|
|
16
16
|
* no longer required), creates ~/.tot if it's missing, and signs you in when
|
|
17
17
|
* there's no usable session at all. Anything it can't fix (or won't — a stale
|
|
@@ -135,9 +135,9 @@ export function collectChecks(_ctx, env = process.env) {
|
|
|
135
135
|
|
|
136
136
|
/**
|
|
137
137
|
* Auto-remediate the checks that are safely fixable without human judgment
|
|
138
|
-
*
|
|
138
|
+
* — announcing each one as it runs. A stale-but-refreshable sign-in isn't
|
|
139
139
|
* handled here because it isn't a failing check: the developer session refreshes
|
|
140
|
-
* it silently the next time it's actually used
|
|
140
|
+
* it silently the next time it's actually used.
|
|
141
141
|
*/
|
|
142
142
|
async function applyFixes(checks, env) {
|
|
143
143
|
const totDir = join(env.TOT_HOME || homedir(), ".tot");
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `tot git-credential` — git's own credential-helper protocol (see `git help
|
|
3
3
|
* gitcredentials`), implemented over the developer's cached `tot login`
|
|
4
|
-
* session
|
|
4
|
+
* session. `tot
|
|
5
5
|
* clone` configures a fresh checkout's `credential.helper` to run this (see
|
|
6
6
|
* ../git-credential.mjs's CREDENTIAL_HELPER), so `git fetch`/`git push`/`git
|
|
7
7
|
* pull` — run DIRECTLY by the developer, not just through `tot preview` —
|
|
@@ -93,7 +93,7 @@ export function readStdin() {
|
|
|
93
93
|
* globally-scoped helper (or a checkout with a submodule / unrelated remote)
|
|
94
94
|
* must not become a way to exfiltrate the token to an arbitrary host.
|
|
95
95
|
* @param {{
|
|
96
|
-
* env?: NodeJS.ProcessEnv, cwd?: string, expectedHost?: string,
|
|
96
|
+
* env?: NodeJS.ProcessEnv, cwd?: string, expectedHost?: string|null,
|
|
97
97
|
* createClient?: typeof createMcpClient,
|
|
98
98
|
* establish?: typeof establishSession,
|
|
99
99
|
* checkout?: typeof checkoutTenant,
|
package/src/commands/go-live.mjs
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `tot go-live` — cut the apex domain over to the storefront
|
|
2
|
+
* `tot go-live` — cut the apex domain over to the storefront. The CLI
|
|
3
3
|
* counterpart to the admin Publish tab's Domain "Connect" button: it drives the
|
|
4
4
|
* SAME go-live CI/dispatch the button does (the storefront `/api/domain/dispatch`
|
|
5
5
|
* → `www-domain` repository_dispatch → `cutover-dns.mjs`), gated by the SAME
|
|
6
6
|
* server-authoritative readiness gate the admin display reads
|
|
7
|
-
* (`GET /api/domain/readiness
|
|
7
|
+
* (`GET /api/domain/readiness`). This command CONSUMES those seams — it
|
|
8
8
|
* does NOT reimplement the DNS cutover, the readiness policy, or the revert.
|
|
9
9
|
*
|
|
10
10
|
* tot go-live show apex readiness, then (if ready) connect the apex
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* revert). Fail-closed: a CONNECT is refused unless the server reports
|
|
20
20
|
* `ready:true` — the CLI never recomputes readiness, it reads the same gate
|
|
21
21
|
* the cutover is governed by, so they cannot diverge.
|
|
22
|
-
* 2. Owner-only: the server resolves owner capability
|
|
22
|
+
* 2. Owner-only: the server resolves owner capability; a non-owner is
|
|
23
23
|
* DENIED with the itemized reason. The CLI does not assert ownership itself.
|
|
24
24
|
* 3. ALWAYS require ONE explicit [y/N] confirm (default NO). There is NO
|
|
25
25
|
* `--yes`/`--force`; in a NON-TTY (CI, piped) it REFUSES rather than
|
|
@@ -196,6 +196,9 @@ export function renderReadiness({ appDomain, readiness, action }) {
|
|
|
196
196
|
const lines = ["", ` Apex ${action === "rollback" ? "rollback" : "cutover"} readiness for ${appDomain ?? "this store"}:`];
|
|
197
197
|
for (const c of readiness.checks) {
|
|
198
198
|
lines.push(` ${c.ok ? "✓" : "✗"} ${c.label}${c.detail ? ` — ${c.detail}` : ""}`);
|
|
199
|
+
// Blocked checks carry a "how to clear this" remedy from the server gate —
|
|
200
|
+
// the same guidance the admin panel shows, so the two surfaces never diverge.
|
|
201
|
+
if (!c.ok && c.remedy) lines.push(` → ${c.remedy}`);
|
|
199
202
|
}
|
|
200
203
|
if (action === "connect") {
|
|
201
204
|
lines.push(
|
|
@@ -217,6 +220,7 @@ export function renderReadiness({ appDomain, readiness, action }) {
|
|
|
217
220
|
* assuming it from the dispatch. Injectable delay/attempts. Returns the last
|
|
218
221
|
* normalised run status (may still be "dispatched" if CI is slow — reported honestly).
|
|
219
222
|
* @param {{ get:(path:string)=>Promise<any> }} http
|
|
223
|
+
* @param {{ attempts?: number, delayMs?: number, sleep?: Function }} [opts]
|
|
220
224
|
*/
|
|
221
225
|
export async function pollRunTerminal(http, { attempts = 8, delayMs = 2000, sleep } = {}) {
|
|
222
226
|
const wait = sleep || ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
@@ -391,7 +395,7 @@ function cap(s) {
|
|
|
391
395
|
* ship floor. Throws on a non-2xx with the server's error message (so the caller
|
|
392
396
|
* surfaces the real refusal). `fetchImpl` is injectable for tests.
|
|
393
397
|
* @param {string} base
|
|
394
|
-
* @param {{ token
|
|
398
|
+
* @param {{ token?:string, owner?:string, capability?:string, fetchImpl?:typeof fetch }} [auth]
|
|
395
399
|
*/
|
|
396
400
|
export function createStorefrontHttp(base, { token, owner, capability = "ship-on-behalf", fetchImpl } = {}) {
|
|
397
401
|
const root = base.replace(/\/+$/, "");
|
|
@@ -476,7 +480,7 @@ export async function run(argv, ctx) {
|
|
|
476
480
|
const http = createStorefrontHttp(base, { token, owner });
|
|
477
481
|
return await runGoLive(
|
|
478
482
|
http,
|
|
479
|
-
{ appDomain: owner, action: args.action, rehearsal: args.rehearsal, noOpen: args.noOpen },
|
|
483
|
+
{ appDomain: owner, action: /** @type {any} */ (args.action), rehearsal: args.rehearsal, noOpen: args.noOpen },
|
|
480
484
|
{ openUrl: (u) => openBrowser(u) },
|
|
481
485
|
);
|
|
482
486
|
}
|
package/src/commands/grants.mjs
CHANGED
|
@@ -61,9 +61,10 @@ function parseArgs(argv) {
|
|
|
61
61
|
* exp: number|null, revokedAt: string|null }> }}
|
|
62
62
|
*/
|
|
63
63
|
export function normalizeGrantRows(resp) {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const
|
|
64
|
+
const rr = /** @type {any} */ (resp);
|
|
65
|
+
if (resp && typeof resp === "object" && !Array.isArray(resp) && Array.isArray(rr.grants)) {
|
|
66
|
+
const activeTenant = typeof rr.activeTenant === "string" ? rr.activeTenant : null;
|
|
67
|
+
const rows = rr.grants
|
|
67
68
|
.map((g) => {
|
|
68
69
|
const tenant = g?.tenant ?? g?.id ?? null;
|
|
69
70
|
return {
|
|
@@ -83,7 +84,7 @@ export function normalizeGrantRows(resp) {
|
|
|
83
84
|
|
|
84
85
|
// Fallback: client_list — store ids + environment + the selected marker only. No
|
|
85
86
|
// grant detail is knowable here (grantActive/capability/tier/exp/revokedAt stay null).
|
|
86
|
-
const clients = Array.isArray(resp) ? resp :
|
|
87
|
+
const clients = Array.isArray(resp) ? resp : rr?.clients || rr?.tenants || [];
|
|
87
88
|
const rows = (Array.isArray(clients) ? clients : [])
|
|
88
89
|
.map((c) => ({
|
|
89
90
|
tenant: c?.tenant ?? c?.id ?? c?.clientId ?? c?.appDomain ?? null,
|
|
@@ -147,6 +148,7 @@ export function formatExpiry(exp, { now = Date.now() } = {}) {
|
|
|
147
148
|
export function describeGrantRow(row, { now = Date.now() } = {}) {
|
|
148
149
|
const status = grantStatus(row, { now });
|
|
149
150
|
if (status === "unknown") return "grant detail unavailable (server introspection not enabled)";
|
|
151
|
+
/** @type {string[]} */
|
|
150
152
|
const parts = [status];
|
|
151
153
|
if (row.capability) parts.push(`capability=${row.capability}`);
|
|
152
154
|
if (row.tier) parts.push(`tier=${row.tier}`);
|
|
@@ -238,7 +240,7 @@ export async function run(argv, _ctx) {
|
|
|
238
240
|
return 1;
|
|
239
241
|
}
|
|
240
242
|
if (rows.length === 0) {
|
|
241
|
-
// Status-aware, like whoami/checkout
|
|
243
|
+
// Status-aware, like whoami/checkout: an UNLINKED identity is told to
|
|
242
244
|
// link, not given the "may still be propagating" copy — that's only the genuine
|
|
243
245
|
// linked-but-zero-grants case.
|
|
244
246
|
const g = noStoresGuidance(resp);
|
package/src/commands/hotfix.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `tot hotfix`
|
|
2
|
+
* `tot hotfix` — the OWNER-ONLY EXCEPTION LANE: release an urgent
|
|
3
3
|
* production fix from `main` to live WHILE `preview` still contains other unshipped
|
|
4
4
|
* work, EXCLUDING that unshipped preview head, then automatically forward-integrate
|
|
5
5
|
* `main` into `preview` and re-validate.
|
package/src/commands/ideas.mjs
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `tot ideas` — a gallery of copy-paste reskin prompts that reliably wow
|
|
2
|
+
* `tot ideas` — a gallery of copy-paste reskin prompts that reliably wow.
|
|
3
3
|
* Not a script to run — inspiration for the AI moment: paste one into Claude
|
|
4
4
|
* (after `tot start`'s "Connect Claude?" prompt, or `claude mcp add` directly)
|
|
5
5
|
* and watch your store change and reload.
|
|
6
6
|
*
|
|
7
7
|
* IDEAS[0] doubles as the seeded first prompt `tot start` ends on and drops
|
|
8
|
-
* you into (
|
|
8
|
+
* you into (see commands/start.mjs) — one list, no drift between the two.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
/** Reskin prompts, ordered strongest-first. Pure data — exported for reuse + tests. */
|
package/src/commands/link.mjs
CHANGED
|
@@ -54,7 +54,7 @@ Run this when \`tot whoami\` / \`tot start\` say your identity isn't linked yet.
|
|
|
54
54
|
* @returns {{ authUrl: string|null, pollHandle: string|null }}
|
|
55
55
|
*/
|
|
56
56
|
export function linkBeginFields(res) {
|
|
57
|
-
const c = res && typeof res === "object" && !Array.isArray(res) ? res : {};
|
|
57
|
+
const c = /** @type {any} */ (res && typeof res === "object" && !Array.isArray(res) ? res : {});
|
|
58
58
|
const authUrl =
|
|
59
59
|
c.authUrl ||
|
|
60
60
|
c.url ||
|
|
@@ -77,7 +77,7 @@ export function linkBeginFields(res) {
|
|
|
77
77
|
* @returns {"linked"|"pending"|string}
|
|
78
78
|
*/
|
|
79
79
|
export function linkPollStatus(res) {
|
|
80
|
-
const c = res && typeof res === "object" && !Array.isArray(res) ? res : {};
|
|
80
|
+
const c = /** @type {any} */ (res && typeof res === "object" && !Array.isArray(res) ? res : {});
|
|
81
81
|
if (c.linked === true || c.done === true || c.complete === true) return "linked";
|
|
82
82
|
const raw =
|
|
83
83
|
(typeof c.status === "string" && c.status) || (typeof c.state === "string" && c.state) || "";
|
package/src/commands/login.mjs
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* TOT_MCP_URL, else the production MCP) so the cached token matches where it's
|
|
12
12
|
* spent — point both at qa with `--mcp https://mcp.qa.tokenoftrust.com` or the env.
|
|
13
13
|
*
|
|
14
|
-
*
|
|
14
|
+
* `--device` forces the RFC 8628 device-code flow (headless/SSH/no-browser:
|
|
15
15
|
* print a code, poll until it's approved elsewhere). It's also the automatic
|
|
16
16
|
* fallback for a bare `tot login` when there's no browser opener on this box at
|
|
17
17
|
* all (oauth.mjs's loopback flow would otherwise hang forever waiting for a
|
|
@@ -116,11 +116,11 @@ After signing in, run \`tot whoami\` to confirm, then \`tot clone\` / \`tot subm
|
|
|
116
116
|
* device-code flow if `device` is set or the loopback can't be reached at
|
|
117
117
|
* all) and cache the result, reusing a previously-registered client for THIS
|
|
118
118
|
* MCP so we don't re-register (and burn the DCR rate limit) on every login.
|
|
119
|
-
* Exported so `tot doctor --fix`
|
|
119
|
+
* Exported so `tot doctor --fix` can re-login as an auto-remediation
|
|
120
120
|
* without duplicating this.
|
|
121
121
|
* @returns {Promise<object>} the credentials written to disk.
|
|
122
122
|
*/
|
|
123
|
-
export async function loginAndCache(mcpUrl, env = process.env, { log = () => {}, device = false } = {}) {
|
|
123
|
+
export async function loginAndCache(mcpUrl, env = process.env, { log = /** @type {(m?: string) => void} */ (() => {}), device = false } = {}) {
|
|
124
124
|
const path = defaultCredentialsPath(env);
|
|
125
125
|
const prior = readCredentials(path);
|
|
126
126
|
const clientId = prior && prior.mcpUrl === mcpUrl ? prior.clientId : undefined;
|
|
@@ -196,11 +196,10 @@ export async function offerSignIn(mcpUrl, env = process.env, {
|
|
|
196
196
|
* `log` carries the fingerprint + "waiting for approval" lines to the terminal.
|
|
197
197
|
* @returns {Promise<object>} the credentials written to disk.
|
|
198
198
|
*/
|
|
199
|
-
export async function redeemAndCache(mcpUrl, code, env = process.env, { log = () => {} } = {}) {
|
|
199
|
+
export async function redeemAndCache(mcpUrl, code, env = process.env, { log = /** @type {(m?: string) => void} */ (() => {}) } = {}) {
|
|
200
200
|
const path = defaultCredentialsPath(env);
|
|
201
201
|
const prior = readCredentials(path);
|
|
202
|
-
const
|
|
203
|
-
const creds = await rendezvousLoginFlow({ mcpUrl, clientId, code, log });
|
|
202
|
+
const creds = await rendezvousLoginFlow({ mcpUrl, code, log });
|
|
204
203
|
const merged = mergeActivityBridge(prior, mcpUrl, creds);
|
|
205
204
|
writeCredentials(path, merged);
|
|
206
205
|
return merged;
|
package/src/commands/pr.mjs
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* tot pr close <N|id> close (reject) a candidate without merging
|
|
8
8
|
*
|
|
9
9
|
* A candidate PR is the reviewable unit `tot submit` creates. By default a
|
|
10
|
-
* re-submit UPDATES your open candidate; `tot submit --
|
|
10
|
+
* re-submit UPDATES your open candidate; `tot submit --fork-candidate` forks another. This
|
|
11
11
|
* command fills the gap the raw submit loop left — a first-party way to list your
|
|
12
12
|
* open candidates and to close one (candidate close was otherwise gated to
|
|
13
13
|
* version-control apps only).
|
|
@@ -17,9 +17,9 @@
|
|
|
17
17
|
* `candidate_status` / `candidate_close` tools. Dependency-free (global fetch +
|
|
18
18
|
* `git`).
|
|
19
19
|
*
|
|
20
|
-
* OPERATOR MODE
|
|
20
|
+
* OPERATOR MODE. `tot pr list --tenant <t>` lists a tenant's OPEN
|
|
21
21
|
* candidate queue WITHOUT a checkout — the read-only companion to `tot ship --pr <N>
|
|
22
|
-
* --tenant <t
|
|
22
|
+
* --tenant <t>`. It reuses that command's exact transport: the SAME `GET /api/changes`
|
|
23
23
|
* HTTP endpoint + operator-secret Bearer auth (`resolveOperatorSecret`,
|
|
24
24
|
* `normalizeChangesQueue` from ship.mjs). `view`/`close` stay developer-only.
|
|
25
25
|
*/
|
|
@@ -28,9 +28,8 @@ import { createMcpClient } from "../mcp.mjs";
|
|
|
28
28
|
import { establishSession, AuthUnavailableError } from "../auth.mjs";
|
|
29
29
|
import { fail } from "../errors.mjs";
|
|
30
30
|
import { repoNameFromRemote, currentBranch } from "./submit.mjs";
|
|
31
|
-
//
|
|
32
|
-
// same queue normalisation) so the
|
|
33
|
-
// one wire, not two.
|
|
31
|
+
// `tot pr list` reuses `tot ship --pr`'s operator-secret transport helpers verbatim
|
|
32
|
+
// (same env precedence, same queue normalisation) so the two speak one wire, not two.
|
|
34
33
|
import { resolveOperatorSecret, normalizeChangesQueue } from "./ship.mjs";
|
|
35
34
|
import {
|
|
36
35
|
defaultCandidateStatePath,
|
|
@@ -71,7 +70,7 @@ const USAGE = `tot pr — see and manage candidate PRs
|
|
|
71
70
|
|
|
72
71
|
DEVELOPER (default) — run inside your OWN store checkout: lists/manages the
|
|
73
72
|
candidates \`tot submit\` opens. A re-submit updates your open one by default;
|
|
74
|
-
\`tot submit --
|
|
73
|
+
\`tot submit --fork-candidate\` forks another.
|
|
75
74
|
|
|
76
75
|
OPERATOR — \`tot pr list --tenant <appDomain>\` lists ANY tenant's open candidate
|
|
77
76
|
queue WITHOUT a checkout (the read-only companion to \`tot ship --pr <N> --tenant\`).
|
|
@@ -135,8 +134,8 @@ export function matchCandidate(candidates, target) {
|
|
|
135
134
|
|
|
136
135
|
/**
|
|
137
136
|
* One-line candidate summary for `tot pr list` — surfaces branch ↔ PR# ↔ preview
|
|
138
|
-
* URL so a dev sees, at a glance, which git branch each candidate belongs to
|
|
139
|
-
* branch-bound candidates) and where its preview lives. ONLY `previewUrl` (the
|
|
137
|
+
* URL so a dev sees, at a glance, which git branch each candidate belongs to
|
|
138
|
+
* (branch-bound candidates) and where its preview lives. ONLY `previewUrl` (the
|
|
140
139
|
* storefront-owned `/preview/<tenant>/pr/<N>` link) — NEVER `url` (the forge/
|
|
141
140
|
* Gitea `html_url`), which must never reach a terminal (2026-08-18 incident:
|
|
142
141
|
* a raw forge PR URL reached an owner). `active` marks the one
|
|
@@ -153,10 +152,10 @@ export function formatCandidateLine(c, { active = false } = {}) {
|
|
|
153
152
|
return ` PR ${pr} ${branch} ${c.changeId} [${c.state ?? "?"}]${urlPart}${activePart}`;
|
|
154
153
|
}
|
|
155
154
|
|
|
156
|
-
// ─── Operator queue listing
|
|
155
|
+
// ─── Operator queue listing ──────────────────────────────────────────────────────────
|
|
157
156
|
//
|
|
158
|
-
// The read-only companion to `tot ship --pr <N> --tenant <t
|
|
159
|
-
// OPEN candidate queue WITHOUT a checkout. It reuses
|
|
157
|
+
// The read-only companion to `tot ship --pr <N> --tenant <t>`: list a tenant's
|
|
158
|
+
// OPEN candidate queue WITHOUT a checkout. It reuses that command's exact transport — the SAME
|
|
160
159
|
// `GET /api/changes` endpoint (operator accept queue) + Bearer operator-secret auth
|
|
161
160
|
// (`resolveOperatorSecret` / `normalizeChangesQueue` from ship.mjs) — so a listing and
|
|
162
161
|
// a ship read one wire. NO merge/deploy/side-effect; pure listing.
|
|
@@ -172,8 +171,15 @@ export function formatCandidateLine(c, { active = false } = {}) {
|
|
|
172
171
|
* sees it's listable but must `tot preview build` before it can ship.
|
|
173
172
|
* Adapts `formatCandidateLine`'s house style to the `GET /api/changes` shape (`status`
|
|
174
173
|
* not `state`; `previewUrl`; a `headSha` to short-render). Pure — unit-tested.
|
|
174
|
+
*
|
|
175
|
+
* A built row whose `evidence.pushFailed` is set (the MCP `change_report_evidence`
|
|
176
|
+
* push never landed — fb-1787938036559-ppmu4e) gets a `[evidence push failed]` tag
|
|
177
|
+
* + a `tot preview retry-evidence` hint, so the gap between "storefront thinks this
|
|
178
|
+
* is promotable" and "MCP has no evidence for it" is visible right where an
|
|
179
|
+
* operator is already looking, not just in the activity log.
|
|
175
180
|
* @param {{prNumber?:number|null, status?:string|null, changeId?:string|null,
|
|
176
|
-
* headSha?:string|null, previewUrl?:string|null, built?:boolean
|
|
181
|
+
* headSha?:string|null, previewUrl?:string|null, built?:boolean,
|
|
182
|
+
* evidence?:{pushFailed?:boolean}|null}} c
|
|
177
183
|
* @returns {string}
|
|
178
184
|
*/
|
|
179
185
|
export function formatOperatorCandidateLine(c) {
|
|
@@ -190,6 +196,13 @@ export function formatOperatorCandidateLine(c) {
|
|
|
190
196
|
}
|
|
191
197
|
const status = c.status ?? "?";
|
|
192
198
|
const urlPart = c.previewUrl ? ` ${c.previewUrl}` : "";
|
|
199
|
+
if (c.evidence?.pushFailed) {
|
|
200
|
+
const retryHint =
|
|
201
|
+
typeof c.prNumber === "number"
|
|
202
|
+
? ` → tot preview retry-evidence --pr ${c.prNumber}`
|
|
203
|
+
: " → tot preview retry-evidence";
|
|
204
|
+
return ` PR ${pr} [${status}] [evidence push failed] ${c.changeId} ${head}${urlPart}${retryHint}`;
|
|
205
|
+
}
|
|
193
206
|
return ` PR ${pr} [${status}] ${c.changeId} ${head}${urlPart}`;
|
|
194
207
|
}
|
|
195
208
|
|
|
@@ -210,7 +223,7 @@ export function sortQueueByPrDesc(changes) {
|
|
|
210
223
|
/**
|
|
211
224
|
* The OPERATOR `tot pr list --tenant <t>` flow: fetch the tenant's OPEN candidate
|
|
212
225
|
* queue over `GET /api/changes` (Bearer operator secret + `X-Tot-Owner` +
|
|
213
|
-
* `x-tot-capability: ship-on-behalf
|
|
226
|
+
* `x-tot-capability: ship-on-behalf`) and print each candidate one per
|
|
214
227
|
* line, sorted by PR number desc. Fail-closed: no secret → honest refusal (exit 2)
|
|
215
228
|
* BEFORE any network. `fetch` is injected so it's unit-tested with no live network.
|
|
216
229
|
*
|
|
@@ -225,8 +238,9 @@ export async function runPrListOperator({ tenant, secret, storefrontUrl = null }
|
|
|
225
238
|
if (!secret) {
|
|
226
239
|
console.error(
|
|
227
240
|
fail(
|
|
228
|
-
"
|
|
229
|
-
|
|
241
|
+
"no operator secret — view this tenant's queue from the storefront admin instead.",
|
|
242
|
+
`open https://${tenant}/admin → Publish tab to see the candidate queue and act on it. ` +
|
|
243
|
+
"Operators/CI: set PREVIEW_RECONCILE_SECRET (or GRANTS_ADMIN_SECRET / TOT_OPERATOR_SECRET), or pass --secret.",
|
|
230
244
|
),
|
|
231
245
|
);
|
|
232
246
|
return 2;
|
|
@@ -287,12 +301,12 @@ export async function run(argv, ctx) {
|
|
|
287
301
|
console.log(USAGE);
|
|
288
302
|
return 0;
|
|
289
303
|
}
|
|
290
|
-
if (!SUBCOMMANDS.includes(args.sub)) {
|
|
304
|
+
if (!SUBCOMMANDS.includes(/** @type {any} */ (args.sub))) {
|
|
291
305
|
console.error(fail(`unknown subcommand: \`tot pr ${args.sub}\``, "tot pr list | view <N> | close <N>"));
|
|
292
306
|
return 2;
|
|
293
307
|
}
|
|
294
308
|
|
|
295
|
-
// OPERATOR MODE
|
|
309
|
+
// OPERATOR MODE: `--tenant <t>` lists that tenant's OPEN candidate queue with
|
|
296
310
|
// NO checkout, over the same HTTP transport `tot ship --pr` uses. Only `list` has an
|
|
297
311
|
// operator path today — `view`/`close` stay developer-only (checkout-bound).
|
|
298
312
|
if (args.tenant && `${args.tenant}`.trim()) {
|
|
@@ -347,7 +361,7 @@ export async function run(argv, ctx) {
|
|
|
347
361
|
const baseUrl = args.mcp || env.MCP_BASE_URL || env.TOT_MCP_URL || DEFAULT_MCP_URL;
|
|
348
362
|
const storefrontUrl = args.url || env.TOT_STOREFRONT_URL || env.STOREFRONT_BASE_URL || DEFAULT_STOREFRONT_URL;
|
|
349
363
|
const statePath = defaultCandidateStatePath(env);
|
|
350
|
-
// Branch-bound
|
|
364
|
+
// Branch-bound: the active-pointer namespace is scoped to the current git
|
|
351
365
|
// branch, so the "← active" marker reflects THIS branch's candidate.
|
|
352
366
|
const scope = { mcpUrl: baseUrl, repo, branch: currentBranch(gitSafe) };
|
|
353
367
|
const client = createMcpClient(baseUrl);
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `tot preview build --tenant <t> --pr <N>` — OPERATOR build-on-demand
|
|
2
|
+
* `tot preview build --tenant <t> --pr <N>` — OPERATOR build-on-demand.
|
|
3
3
|
*
|
|
4
4
|
* Materialize ANY PR's hosted preview (including one that isn't yours, and one that
|
|
5
5
|
* was orphaned — its webhook never processed so it has no ReviewEnvironment). It
|
|
6
|
-
* calls the session-authenticated storefront endpoint `POST /api/preview/build
|
|
7
|
-
*
|
|
6
|
+
* calls the session-authenticated storefront endpoint `POST /api/preview/build`,
|
|
7
|
+
* which runs the EXISTING `reconcileCandidate` against the PR head. The
|
|
8
8
|
* CLI passes the KNOWN descriptor (the head sha it holds), so there is NO forge
|
|
9
9
|
* PR-read dependency: reconcile reads the tenant files at that sha (content-addressed).
|
|
10
10
|
*
|
|
@@ -22,9 +22,9 @@
|
|
|
22
22
|
* URL) and confirms before acting (`--yes` to skip; a non-TTY without `--yes`
|
|
23
23
|
* aborts rather than acting silently). Build-on-demand is INERT — it flips no shared
|
|
24
24
|
* channel and is not go-live — so this is a materialize, not a deploy. The plan
|
|
25
|
-
* itself is built by the SHARED plan module (`../plan.mjs
|
|
26
|
-
* affordance every other mutating operator verb (accept/ship/
|
|
27
|
-
* `/admin` confirm dialog use
|
|
25
|
+
* itself is built by the SHARED plan module (`../plan.mjs`) — the same
|
|
26
|
+
* affordance every other mutating operator verb (accept/ship/reject) and the
|
|
27
|
+
* `/admin` confirm dialog use.
|
|
28
28
|
*
|
|
29
29
|
* Dependency-free (global fetch + `git` for the optional head-sha default).
|
|
30
30
|
*/
|