@tokenoftrust/cli 1.4.0-rc.16 → 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 +101 -11
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
|
@@ -756,12 +756,15 @@ export async function submitCandidate(client, { repo, changeId, changeSummary, p
|
|
|
756
756
|
}
|
|
757
757
|
|
|
758
758
|
/** Print the candidate_open result: the PR the approver reviews, or the MCP's own
|
|
759
|
-
* refusal message when it couldn't open/update one. `quiet` (--json) suppresses it.
|
|
759
|
+
* refusal message when it couldn't open/update one. `quiet` (--json) suppresses it.
|
|
760
|
+
* Deliberately does NOT print `result.url` — that's the INTERNAL forge (Gitea) PR
|
|
761
|
+
* link, plumbing a developer never needs to see (DZ, 2026-08-15); the product
|
|
762
|
+
* surface is the shareable /preview/<tenant>/pr/<N> URL printed right after
|
|
763
|
+
* (shareablePrUrl). The forge URL still rides the --json payload for tooling. */
|
|
760
764
|
function reportCandidate(result, changeId, { quiet = false } = {}) {
|
|
761
765
|
if (quiet) return;
|
|
762
766
|
if (result && typeof result.prNumber === "number") {
|
|
763
767
|
console.log(`\n ✓ candidate ${result.changeId || changeId} — PR #${result.prNumber} (${result.state || "open"})`);
|
|
764
|
-
if (result.url) console.log(` ${result.url}`);
|
|
765
768
|
return;
|
|
766
769
|
}
|
|
767
770
|
const msg = result?.message || (result?.raw && String(result.raw)) || JSON.stringify(result ?? null);
|
|
@@ -815,6 +818,7 @@ export function buildJsonResult({ ok, ref = null, commit = null, changeId = null
|
|
|
815
818
|
shipped: status?.shipped ?? null,
|
|
816
819
|
dispatched: status?.dispatched ?? null,
|
|
817
820
|
notDispatched: status?.notDispatched ?? false,
|
|
821
|
+
forwardFailed: status?.forwardFailed ?? false,
|
|
818
822
|
delivery: status?.delivery ?? null,
|
|
819
823
|
previewPrUrl,
|
|
820
824
|
...(error ? { error } : {}),
|
|
@@ -1005,7 +1009,15 @@ export async function run(argv, ctx, { verb = "preview" } = {}) {
|
|
|
1005
1009
|
if (out.trim()) console.error(redactUrl(out.trim()));
|
|
1006
1010
|
} catch (pushErr) {
|
|
1007
1011
|
const msg = `push failed: ${redactUrl(String(pushErr.stderr || pushErr.message || pushErr))}`;
|
|
1008
|
-
|
|
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));
|
|
1009
1021
|
emitJson(args, buildJsonResult({ ok: false, ref, commit, changeId, error: msg }));
|
|
1010
1022
|
return 1;
|
|
1011
1023
|
}
|
|
@@ -1013,7 +1025,7 @@ export async function run(argv, ctx, { verb = "preview" } = {}) {
|
|
|
1013
1025
|
printChangeSummary(changeSummary, { quiet: args.json });
|
|
1014
1026
|
const note = e instanceof AuthUnavailableError
|
|
1015
1027
|
? `sign in to see the reconcile/compliance/preview result — ${e.hint || "developer sign-in pending"}`
|
|
1016
|
-
: `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)}`;
|
|
1017
1029
|
if (!args.json) {
|
|
1018
1030
|
console.log(` (${note})`);
|
|
1019
1031
|
console.log(` Your push is in; the preview updates once reconcile completes.`);
|
|
@@ -1154,14 +1166,14 @@ export async function run(argv, ctx, { verb = "preview" } = {}) {
|
|
|
1154
1166
|
}
|
|
1155
1167
|
// --json also skips the browser auto-open (open: !args.noOpen && !args.json)
|
|
1156
1168
|
// — automation doesn't want a browser popping up.
|
|
1157
|
-
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 });
|
|
1158
1170
|
emitJson(args, buildJsonResult({ ok: status?.status !== "failed", ref, commit, changeId, candidate, status, previewPrUrl }));
|
|
1159
1171
|
return status?.status === "failed" ? 1 : 0;
|
|
1160
1172
|
} catch (e) {
|
|
1161
1173
|
progress?.stop();
|
|
1162
1174
|
const note = e instanceof AuthUnavailableError
|
|
1163
1175
|
? `sign in to see the reconcile/compliance/preview result — ${e.hint || "developer sign-in pending"}`
|
|
1164
|
-
: `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)}`;
|
|
1165
1177
|
if (!args.json) {
|
|
1166
1178
|
console.log(` (${note})`);
|
|
1167
1179
|
console.log(` Your push is in; the preview updates once reconcile completes.`);
|
|
@@ -1247,6 +1259,19 @@ export async function pollPreviewStatus(
|
|
|
1247
1259
|
if (last.dispatched) everDispatched = true;
|
|
1248
1260
|
last.everDispatched = everDispatched;
|
|
1249
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
|
+
}
|
|
1250
1275
|
// Never-dispatched dead-end: still pending, no delivery has EVER been observed
|
|
1251
1276
|
// for this commit, and we're past the startup grace — the reconcile will never
|
|
1252
1277
|
// arrive. Return honestly instead of continuing to show "still reconciling".
|
|
@@ -1306,6 +1331,34 @@ export function shareablePrUrl(base, tenant, prNumber) {
|
|
|
1306
1331
|
return `${String(base).replace(/\/+$/, "")}/preview/${tenant}/pr/${prNumber}`;
|
|
1307
1332
|
}
|
|
1308
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
|
+
|
|
1309
1362
|
/**
|
|
1310
1363
|
* Build the printed lines for the headline "share this with your reviewer" block —
|
|
1311
1364
|
* the whole point of U14: on a successful preview, the SHAREABLE deep link
|
|
@@ -1349,14 +1402,24 @@ export function formatShareableUrlBlock(s, tenant) {
|
|
|
1349
1402
|
* @param {string} tenant @param {string} [verb]
|
|
1350
1403
|
* @returns {string[]}
|
|
1351
1404
|
*/
|
|
1352
|
-
export function formatNotDispatchedBlock({ commit = null, ref = null } = {}, tenant, verb = "preview") {
|
|
1405
|
+
export function formatNotDispatchedBlock({ commit = null, ref = null, noChanges = false } = {}, tenant, verb = "preview") {
|
|
1353
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
|
+
];
|
|
1354
1418
|
return [
|
|
1355
1419
|
`\n ⚠ No reconcile was dispatched for ${short} on ${tenant}.`,
|
|
1356
1420
|
` Your push landed${ref ? ` on ${ref}` : ""}, but nothing picked it up to build a preview —`,
|
|
1357
1421
|
` re-running \`tot ${verb}\` will NOT change that. This usually means one of:`,
|
|
1358
|
-
|
|
1359
|
-
` • your session is scoped to a different store than the one you pushed.`,
|
|
1422
|
+
...causes,
|
|
1360
1423
|
` Next:`,
|
|
1361
1424
|
` • \`tot grants\` — confirm ${tenant} is active for you;`,
|
|
1362
1425
|
` • check the preview dashboard for ${tenant} (it will read "Last reconcile: never" until a job runs);`,
|
|
@@ -1364,6 +1427,26 @@ export function formatNotDispatchedBlock({ commit = null, ref = null } = {}, ten
|
|
|
1364
1427
|
];
|
|
1365
1428
|
}
|
|
1366
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
|
+
|
|
1367
1450
|
/**
|
|
1368
1451
|
* Print the reconcile/compliance/preview result and, on a clean reconcile with a
|
|
1369
1452
|
* preview URL, open it in the browser (unless opts.open === false). `quiet`
|
|
@@ -1372,7 +1455,7 @@ export function formatNotDispatchedBlock({ commit = null, ref = null } = {}, ten
|
|
|
1372
1455
|
* --json — automation doesn't want a browser popping up). `commit`/`ref`/`verb`
|
|
1373
1456
|
* feed the honest never-dispatched block.
|
|
1374
1457
|
*/
|
|
1375
|
-
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 } = {}) {
|
|
1376
1459
|
if (!s || s.status === "unknown") {
|
|
1377
1460
|
if (!quiet) {
|
|
1378
1461
|
console.log(
|
|
@@ -1382,9 +1465,16 @@ function reportStatus(s, tenant, { open = true, quiet = false, commit = null, re
|
|
|
1382
1465
|
}
|
|
1383
1466
|
return;
|
|
1384
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
|
+
}
|
|
1385
1475
|
// Never-dispatched dead-end — the honest replacement for false "still reconciling".
|
|
1386
1476
|
if (s.notDispatched) {
|
|
1387
|
-
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);
|
|
1388
1478
|
return;
|
|
1389
1479
|
}
|
|
1390
1480
|
if (s.status === "pending") {
|