@tokenoftrust/cli 1.4.0-rc.17 → 1.4.0-rc.18
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/submit.mjs +96 -9
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tokenoftrust/cli",
|
|
3
|
-
"version": "1.4.0-rc.
|
|
3
|
+
"version": "1.4.0-rc.18",
|
|
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/submit.mjs
CHANGED
|
@@ -818,6 +818,7 @@ export function buildJsonResult({ ok, ref = null, commit = null, changeId = null
|
|
|
818
818
|
shipped: status?.shipped ?? null,
|
|
819
819
|
dispatched: status?.dispatched ?? null,
|
|
820
820
|
notDispatched: status?.notDispatched ?? false,
|
|
821
|
+
forwardFailed: status?.forwardFailed ?? false,
|
|
821
822
|
delivery: status?.delivery ?? null,
|
|
822
823
|
previewPrUrl,
|
|
823
824
|
...(error ? { error } : {}),
|
|
@@ -1008,7 +1009,15 @@ export async function run(argv, ctx, { verb = "preview" } = {}) {
|
|
|
1008
1009
|
if (out.trim()) console.error(redactUrl(out.trim()));
|
|
1009
1010
|
} catch (pushErr) {
|
|
1010
1011
|
const msg = `push failed: ${redactUrl(String(pushErr.stderr || pushErr.message || pushErr))}`;
|
|
1011
|
-
|
|
1012
|
+
// This is the NO-SESSION path pushing the clone-time embedded credential —
|
|
1013
|
+
// which rotation kills the moment any fresh mint happens elsewhere. An auth
|
|
1014
|
+
// failure here is therefore almost always "you're not signed in IN THIS
|
|
1015
|
+
// SHELL", not a network problem; the old remote-is-reachable hint sent a
|
|
1016
|
+
// human down the wrong path live (Trello-13075 polish).
|
|
1017
|
+
const hint = isForgeAuthError(pushErr?.stderr || pushErr?.message || pushErr)
|
|
1018
|
+
? "you're not signed in in this shell, and the checkout's embedded credential has likely been rotated — run `tot login` (check TOT_PROFILE if you use per-terminal identities), then re-run"
|
|
1019
|
+
: "check your commit and that the checkout's remote is reachable, then re-run";
|
|
1020
|
+
console.error(fail(msg, hint));
|
|
1012
1021
|
emitJson(args, buildJsonResult({ ok: false, ref, commit, changeId, error: msg }));
|
|
1013
1022
|
return 1;
|
|
1014
1023
|
}
|
|
@@ -1016,7 +1025,7 @@ export async function run(argv, ctx, { verb = "preview" } = {}) {
|
|
|
1016
1025
|
printChangeSummary(changeSummary, { quiet: args.json });
|
|
1017
1026
|
const note = e instanceof AuthUnavailableError
|
|
1018
1027
|
? `sign in to see the reconcile/compliance/preview result — ${e.hint || "developer sign-in pending"}`
|
|
1019
|
-
: `couldn't reach Token of Trust for the result read-back: ${
|
|
1028
|
+
: `couldn't reach Token of Trust for the result read-back: ${describeReadbackError(e)}`;
|
|
1020
1029
|
if (!args.json) {
|
|
1021
1030
|
console.log(` (${note})`);
|
|
1022
1031
|
console.log(` Your push is in; the preview updates once reconcile completes.`);
|
|
@@ -1157,14 +1166,14 @@ export async function run(argv, ctx, { verb = "preview" } = {}) {
|
|
|
1157
1166
|
}
|
|
1158
1167
|
// --json also skips the browser auto-open (open: !args.noOpen && !args.json)
|
|
1159
1168
|
// — automation doesn't want a browser popping up.
|
|
1160
|
-
reportStatus(status, tenant, { open: !args.noOpen && !args.json, quiet: args.json, commit, ref, verb });
|
|
1169
|
+
reportStatus(status, tenant, { open: !args.noOpen && !args.json, quiet: args.json, commit, ref, verb, noChanges: patchEntries.length === 0 });
|
|
1161
1170
|
emitJson(args, buildJsonResult({ ok: status?.status !== "failed", ref, commit, changeId, candidate, status, previewPrUrl }));
|
|
1162
1171
|
return status?.status === "failed" ? 1 : 0;
|
|
1163
1172
|
} catch (e) {
|
|
1164
1173
|
progress?.stop();
|
|
1165
1174
|
const note = e instanceof AuthUnavailableError
|
|
1166
1175
|
? `sign in to see the reconcile/compliance/preview result — ${e.hint || "developer sign-in pending"}`
|
|
1167
|
-
: `reconcile is running — the result read-back isn't available yet: ${
|
|
1176
|
+
: `reconcile is running — the result read-back isn't available yet: ${describeReadbackError(e)}`;
|
|
1168
1177
|
if (!args.json) {
|
|
1169
1178
|
console.log(` (${note})`);
|
|
1170
1179
|
console.log(` Your push is in; the preview updates once reconcile completes.`);
|
|
@@ -1250,6 +1259,19 @@ export async function pollPreviewStatus(
|
|
|
1250
1259
|
if (last.dispatched) everDispatched = true;
|
|
1251
1260
|
last.everDispatched = everDispatched;
|
|
1252
1261
|
if (onTick) onTick(last, i);
|
|
1262
|
+
// Terminally-failed forward: the delivery record settled with forwarded:false
|
|
1263
|
+
// (and it isn't the at-receipt `pending` marker) — the control plane could not
|
|
1264
|
+
// deliver this commit to the reconciler, and polling longer cannot change that.
|
|
1265
|
+
// Only a NEW push produces a new delivery. Stop and say so (Trello-13075
|
|
1266
|
+
// honesty discipline: never spin on a state that cannot progress).
|
|
1267
|
+
if (
|
|
1268
|
+
last.status === "pending" &&
|
|
1269
|
+
last.delivery?.actual &&
|
|
1270
|
+
last.delivery.actual.forwarded === false &&
|
|
1271
|
+
!last.delivery.actual.pending
|
|
1272
|
+
) {
|
|
1273
|
+
return { ...last, forwardFailed: true };
|
|
1274
|
+
}
|
|
1253
1275
|
// Never-dispatched dead-end: still pending, no delivery has EVER been observed
|
|
1254
1276
|
// for this commit, and we're past the startup grace — the reconcile will never
|
|
1255
1277
|
// arrive. Return honestly instead of continuing to show "still reconciling".
|
|
@@ -1309,6 +1331,34 @@ export function shareablePrUrl(base, tenant, prNumber) {
|
|
|
1309
1331
|
return `${String(base).replace(/\/+$/, "")}/preview/${tenant}/pr/${prNumber}`;
|
|
1310
1332
|
}
|
|
1311
1333
|
|
|
1334
|
+
/**
|
|
1335
|
+
* Humanize a result-read-back failure. MCP auth errors arrive as a JSON blob
|
|
1336
|
+
* whose `data.self_repair` carries a summary + step list — dumping that raw
|
|
1337
|
+
* into the terminal (observed live: a wall of escaped JSON mid-submit) buries
|
|
1338
|
+
* the one thing the developer needs: sign in again. Detect that shape and
|
|
1339
|
+
* reduce it to the summary's first sentence + the concrete next step; anything
|
|
1340
|
+
* else passes through unchanged. Pure — unit-tested.
|
|
1341
|
+
* @param {unknown} e
|
|
1342
|
+
* @returns {string}
|
|
1343
|
+
*/
|
|
1344
|
+
export function describeReadbackError(e) {
|
|
1345
|
+
const msg = String(e?.message || e || "");
|
|
1346
|
+
const jsonStart = msg.indexOf("{");
|
|
1347
|
+
if (jsonStart >= 0 && msg.includes("self_repair")) {
|
|
1348
|
+
try {
|
|
1349
|
+
const body = JSON.parse(msg.slice(jsonStart));
|
|
1350
|
+
const repair = body?.data?.self_repair;
|
|
1351
|
+
const summaryFirst = String(repair?.summary || body?.message || "").split(/(?<=\.)\s/)[0];
|
|
1352
|
+
if (summaryFirst) {
|
|
1353
|
+
return `${summaryFirst} Next: run \`tot login\` in this shell (check TOT_PROFILE), then re-run.`;
|
|
1354
|
+
}
|
|
1355
|
+
} catch {
|
|
1356
|
+
// Not the shape we thought — fall through to the raw message.
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
return msg;
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1312
1362
|
/**
|
|
1313
1363
|
* Build the printed lines for the headline "share this with your reviewer" block —
|
|
1314
1364
|
* the whole point of U14: on a successful preview, the SHAREABLE deep link
|
|
@@ -1352,14 +1402,24 @@ export function formatShareableUrlBlock(s, tenant) {
|
|
|
1352
1402
|
* @param {string} tenant @param {string} [verb]
|
|
1353
1403
|
* @returns {string[]}
|
|
1354
1404
|
*/
|
|
1355
|
-
export function formatNotDispatchedBlock({ commit = null, ref = null } = {}, tenant, verb = "preview") {
|
|
1405
|
+
export function formatNotDispatchedBlock({ commit = null, ref = null, noChanges = false } = {}, tenant, verb = "preview") {
|
|
1356
1406
|
const short = commit ? commit.slice(0, 9) : "(unknown commit)";
|
|
1407
|
+
// Empty-diff cause FIRST when we know it applies (live-testing finding: an
|
|
1408
|
+
// empty candidate submit structurally CANNOT build — no diff → no candidate PR
|
|
1409
|
+
// → no pull_request webhook — and blaming webhooks/scope for it sent a human
|
|
1410
|
+
// down two wrong debugging paths).
|
|
1411
|
+
const causes = [
|
|
1412
|
+
...(noChanges
|
|
1413
|
+
? [` • your submit contained NO content changes — a candidate with no diff opens no PR and builds nothing (make an edit, or move the shared ref: \`tot ${verb} --ref preview\`),`]
|
|
1414
|
+
: []),
|
|
1415
|
+
` • the store's reconcile webhook isn't registered yet (an operator must (re-)provision it), or`,
|
|
1416
|
+
` • your session is scoped to a different store than the one you pushed.`,
|
|
1417
|
+
];
|
|
1357
1418
|
return [
|
|
1358
1419
|
`\n ⚠ No reconcile was dispatched for ${short} on ${tenant}.`,
|
|
1359
1420
|
` Your push landed${ref ? ` on ${ref}` : ""}, but nothing picked it up to build a preview —`,
|
|
1360
1421
|
` re-running \`tot ${verb}\` will NOT change that. This usually means one of:`,
|
|
1361
|
-
|
|
1362
|
-
` • your session is scoped to a different store than the one you pushed.`,
|
|
1422
|
+
...causes,
|
|
1363
1423
|
` Next:`,
|
|
1364
1424
|
` • \`tot grants\` — confirm ${tenant} is active for you;`,
|
|
1365
1425
|
` • check the preview dashboard for ${tenant} (it will read "Last reconcile: never" until a job runs);`,
|
|
@@ -1367,6 +1427,26 @@ export function formatNotDispatchedBlock({ commit = null, ref = null } = {}, ten
|
|
|
1367
1427
|
];
|
|
1368
1428
|
}
|
|
1369
1429
|
|
|
1430
|
+
/**
|
|
1431
|
+
* The honest "the delivery FAILED to forward" block — the delivery record settled
|
|
1432
|
+
* `forwarded: false` (not the at-receipt pending marker), so the control plane
|
|
1433
|
+
* could not deliver this commit to the reconciler; polling longer cannot change
|
|
1434
|
+
* that, and ONLY a new push produces a new delivery. Distinct from
|
|
1435
|
+
* `formatNotDispatchedBlock` (nothing was ever dispatched) — here the plumbing
|
|
1436
|
+
* fired and died in transit, so the recovery differs. Pure — unit-tested.
|
|
1437
|
+
* @param {{ commit?: string|null }} ctx @param {string} tenant
|
|
1438
|
+
* @returns {string[]}
|
|
1439
|
+
*/
|
|
1440
|
+
export function formatForwardFailedBlock({ commit = null } = {}, tenant) {
|
|
1441
|
+
const short = commit ? commit.slice(0, 9) : "(unknown commit)";
|
|
1442
|
+
return [
|
|
1443
|
+
`\n ⚠ The reconcile delivery for ${short} on ${tenant} FAILED in transit (network/timeout at the control plane).`,
|
|
1444
|
+
` Waiting longer will not help — only a NEW push produces a new delivery.`,
|
|
1445
|
+
` Next: commit again (an empty commit works: git commit --allow-empty -m retry) and re-push;`,
|
|
1446
|
+
` if it fails the same way twice, share this with support: commit ${short}, tenant ${tenant}.`,
|
|
1447
|
+
];
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1370
1450
|
/**
|
|
1371
1451
|
* Print the reconcile/compliance/preview result and, on a clean reconcile with a
|
|
1372
1452
|
* preview URL, open it in the browser (unless opts.open === false). `quiet`
|
|
@@ -1375,7 +1455,7 @@ export function formatNotDispatchedBlock({ commit = null, ref = null } = {}, ten
|
|
|
1375
1455
|
* --json — automation doesn't want a browser popping up). `commit`/`ref`/`verb`
|
|
1376
1456
|
* feed the honest never-dispatched block.
|
|
1377
1457
|
*/
|
|
1378
|
-
function reportStatus(s, tenant, { open = true, quiet = false, commit = null, ref = null, verb = "preview" } = {}) {
|
|
1458
|
+
function reportStatus(s, tenant, { open = true, quiet = false, commit = null, ref = null, verb = "preview", noChanges = false } = {}) {
|
|
1379
1459
|
if (!s || s.status === "unknown") {
|
|
1380
1460
|
if (!quiet) {
|
|
1381
1461
|
console.log(
|
|
@@ -1385,9 +1465,16 @@ function reportStatus(s, tenant, { open = true, quiet = false, commit = null, re
|
|
|
1385
1465
|
}
|
|
1386
1466
|
return;
|
|
1387
1467
|
}
|
|
1468
|
+
// Terminally-failed forward — the delivery fired and died in transit; a re-push
|
|
1469
|
+
// (new delivery) is the only recovery. Checked BEFORE notDispatched: a settled
|
|
1470
|
+
// failed forward IS a dispatch, just a doomed one.
|
|
1471
|
+
if (s.forwardFailed) {
|
|
1472
|
+
if (!quiet) for (const line of formatForwardFailedBlock({ commit }, tenant)) console.log(line);
|
|
1473
|
+
return;
|
|
1474
|
+
}
|
|
1388
1475
|
// Never-dispatched dead-end — the honest replacement for false "still reconciling".
|
|
1389
1476
|
if (s.notDispatched) {
|
|
1390
|
-
if (!quiet) for (const line of formatNotDispatchedBlock({ commit, ref }, tenant, verb)) console.log(line);
|
|
1477
|
+
if (!quiet) for (const line of formatNotDispatchedBlock({ commit, ref, noChanges }, tenant, verb)) console.log(line);
|
|
1391
1478
|
return;
|
|
1392
1479
|
}
|
|
1393
1480
|
if (s.status === "pending") {
|