@tokenoftrust/cli 1.4.0-rc.17 → 1.4.0-rc.19
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/bin/tot.mjs +49 -3
- package/package.json +1 -1
- package/src/activity.mjs +378 -0
- package/src/commands/clone.mjs +32 -0
- package/src/commands/dev.mjs +332 -97
- package/src/commands/pr.mjs +30 -7
- package/src/commands/ship.mjs +20 -1
- package/src/commands/submit.mjs +126 -9
- package/src/no-gitea-links.test.mjs +55 -0
package/src/commands/ship.mjs
CHANGED
|
@@ -47,6 +47,7 @@ import { fail } from "../errors.mjs";
|
|
|
47
47
|
import { planForAction, printPlanAndConfirm } from "../plan.mjs";
|
|
48
48
|
import { startProgress } from "../progress.mjs";
|
|
49
49
|
import { openBrowser } from "../open.mjs";
|
|
50
|
+
import { emitActivity } from "../activity.mjs";
|
|
50
51
|
|
|
51
52
|
const DEFAULT_STOREFRONT_URL = "https://storefront.tokenoftrust.store";
|
|
52
53
|
|
|
@@ -414,7 +415,25 @@ export async function runShip({ tenant, secret, storefrontUrl = null, yes = fals
|
|
|
414
415
|
const shipData = await readJsonSafe(shipRes);
|
|
415
416
|
progress?.stop();
|
|
416
417
|
|
|
417
|
-
|
|
418
|
+
const shipResult = normalizeShipResult(shipData);
|
|
419
|
+
// D3: emit the ship publish lifecycle event — `tot ship` does no LOCAL git op
|
|
420
|
+
// (it's HTTP-orchestrated), so this marks the outcome of the publish step
|
|
421
|
+
// itself, distinct from the outer command's invoked/result pair. Fire-and-
|
|
422
|
+
// forget best-effort: a silent no-op without a hosted-bridge credential, never
|
|
423
|
+
// awaited, never throws, never alters the ship. `errorClass` stays low-
|
|
424
|
+
// cardinality (the orchestrator's own terminal state/reason, never free text).
|
|
425
|
+
const shipped = shipResult.state === "shipped";
|
|
426
|
+
void emitActivity({
|
|
427
|
+
action: "cli.command.result",
|
|
428
|
+
outcome: {
|
|
429
|
+
status: shipped ? "succeeded" : "failed",
|
|
430
|
+
...(shipped ? {} : { errorClass: `ship_${shipResult.state}` }),
|
|
431
|
+
},
|
|
432
|
+
scope: { tenantId: tenant },
|
|
433
|
+
payload: { args: { command: "ship", subcommand: "ship.publish" } },
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
return reportShipResult(shipResult, { tenant, liveUrl, noOpen, openUrl: deps.openUrl });
|
|
418
437
|
}
|
|
419
438
|
|
|
420
439
|
/**
|
package/src/commands/submit.mjs
CHANGED
|
@@ -50,6 +50,7 @@ import { validateTenant, ERROR } from "../validate.mjs";
|
|
|
50
50
|
import { openBrowser } from "../open.mjs";
|
|
51
51
|
import { startProgress } from "../progress.mjs";
|
|
52
52
|
import { fail } from "../errors.mjs";
|
|
53
|
+
import { emitActivity } from "../activity.mjs";
|
|
53
54
|
import {
|
|
54
55
|
defaultCandidateStatePath,
|
|
55
56
|
readActiveChangeId,
|
|
@@ -81,6 +82,23 @@ export function candidateRefFor(changeId) {
|
|
|
81
82
|
return `${CANDIDATE_REF_PREFIX}${changeId}`;
|
|
82
83
|
}
|
|
83
84
|
|
|
85
|
+
/**
|
|
86
|
+
* D3: emit a git-op lifecycle event for `tot submit`/`tot preview` — one per git
|
|
87
|
+
* operation (commit / push), carrying its own success/failure, so the timeline sees
|
|
88
|
+
* the individual git steps, not just the outer command's invoked/result pair. Uses
|
|
89
|
+
* the `cli.command.result` catalog key with a `git.<op>` subcommand (a fixed, safe
|
|
90
|
+
* value). Fire-and-forget best-effort: a silent no-op without a hosted-bridge
|
|
91
|
+
* credential, never awaited, never throws, never alters the command. `errorClass` is
|
|
92
|
+
* a low-cardinality class (never a raw git stderr, which can carry a token/path).
|
|
93
|
+
*/
|
|
94
|
+
function emitGitOp(op, ok, { command = "submit", durationMs, errorClass } = {}) {
|
|
95
|
+
void emitActivity({
|
|
96
|
+
action: "cli.command.result",
|
|
97
|
+
outcome: { status: ok ? "succeeded" : "failed", ...(durationMs != null ? { durationMs } : {}), ...(errorClass ? { errorClass } : {}) },
|
|
98
|
+
payload: { args: { command, subcommand: `git.${op}`, ...(durationMs != null ? { durationMs } : {}) } },
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
84
102
|
export function parseArgs(argv) {
|
|
85
103
|
// `ref: null` — an explicit `--ref` always wins; otherwise the push target is
|
|
86
104
|
// derived per-run as YOUR OWN isolated candidate ref (resolvePushRef, below),
|
|
@@ -818,6 +836,7 @@ export function buildJsonResult({ ok, ref = null, commit = null, changeId = null
|
|
|
818
836
|
shipped: status?.shipped ?? null,
|
|
819
837
|
dispatched: status?.dispatched ?? null,
|
|
820
838
|
notDispatched: status?.notDispatched ?? false,
|
|
839
|
+
forwardFailed: status?.forwardFailed ?? false,
|
|
821
840
|
delivery: status?.delivery ?? null,
|
|
822
841
|
previewPrUrl,
|
|
823
842
|
...(error ? { error } : {}),
|
|
@@ -889,11 +908,13 @@ export async function run(argv, ctx, { verb = "preview" } = {}) {
|
|
|
889
908
|
try {
|
|
890
909
|
auto = autoCommitKnownTrees(git, { message: args.message, noCommit: args.noCommit });
|
|
891
910
|
} catch (e) {
|
|
911
|
+
emitGitOp("commit", false, { command: verb, errorClass: "git_commit_failed" });
|
|
892
912
|
const msg = `auto-commit failed: ${String(e?.stderr || e?.message || e)}`;
|
|
893
913
|
console.error(fail(msg, "commit your content manually (git add / git commit), or re-run with --no-commit"));
|
|
894
914
|
emitJson(args, buildJsonResult({ ok: false, error: msg }));
|
|
895
915
|
return 1;
|
|
896
916
|
}
|
|
917
|
+
if (auto.committed) emitGitOp("commit", true, { command: verb });
|
|
897
918
|
if (auto.refused) {
|
|
898
919
|
console.error(
|
|
899
920
|
fail(
|
|
@@ -1006,9 +1027,22 @@ export async function run(argv, ctx, { verb = "preview" } = {}) {
|
|
|
1006
1027
|
try {
|
|
1007
1028
|
const out = git(["push", "-f", "origin", `HEAD:refs/heads/${ref}`]);
|
|
1008
1029
|
if (out.trim()) console.error(redactUrl(out.trim()));
|
|
1030
|
+
emitGitOp("push", true, { command: verb });
|
|
1009
1031
|
} catch (pushErr) {
|
|
1032
|
+
emitGitOp("push", false, {
|
|
1033
|
+
command: verb,
|
|
1034
|
+
errorClass: isForgeAuthError(pushErr?.stderr || pushErr?.message || pushErr) ? "forge_auth" : "git_push_failed",
|
|
1035
|
+
});
|
|
1010
1036
|
const msg = `push failed: ${redactUrl(String(pushErr.stderr || pushErr.message || pushErr))}`;
|
|
1011
|
-
|
|
1037
|
+
// This is the NO-SESSION path pushing the clone-time embedded credential —
|
|
1038
|
+
// which rotation kills the moment any fresh mint happens elsewhere. An auth
|
|
1039
|
+
// failure here is therefore almost always "you're not signed in IN THIS
|
|
1040
|
+
// SHELL", not a network problem; the old remote-is-reachable hint sent a
|
|
1041
|
+
// human down the wrong path live (Trello-13075 polish).
|
|
1042
|
+
const hint = isForgeAuthError(pushErr?.stderr || pushErr?.message || pushErr)
|
|
1043
|
+
? "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"
|
|
1044
|
+
: "check your commit and that the checkout's remote is reachable, then re-run";
|
|
1045
|
+
console.error(fail(msg, hint));
|
|
1012
1046
|
emitJson(args, buildJsonResult({ ok: false, ref, commit, changeId, error: msg }));
|
|
1013
1047
|
return 1;
|
|
1014
1048
|
}
|
|
@@ -1016,7 +1050,7 @@ export async function run(argv, ctx, { verb = "preview" } = {}) {
|
|
|
1016
1050
|
printChangeSummary(changeSummary, { quiet: args.json });
|
|
1017
1051
|
const note = e instanceof AuthUnavailableError
|
|
1018
1052
|
? `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: ${
|
|
1053
|
+
: `couldn't reach Token of Trust for the result read-back: ${describeReadbackError(e)}`;
|
|
1020
1054
|
if (!args.json) {
|
|
1021
1055
|
console.log(` (${note})`);
|
|
1022
1056
|
console.log(` Your push is in; the preview updates once reconcile completes.`);
|
|
@@ -1057,7 +1091,12 @@ export async function run(argv, ctx, { verb = "preview" } = {}) {
|
|
|
1057
1091
|
try {
|
|
1058
1092
|
const { out } = await pushPreviewRef(git, mintRemote, { ref });
|
|
1059
1093
|
if (out && out.trim()) console.error(redactUrl(out.trim()));
|
|
1094
|
+
emitGitOp("push", true, { command: verb });
|
|
1060
1095
|
} catch (e) {
|
|
1096
|
+
emitGitOp("push", false, {
|
|
1097
|
+
command: verb,
|
|
1098
|
+
errorClass: isForgeAuthError(e?.stderr || e?.message || e) ? "forge_auth" : "git_push_failed",
|
|
1099
|
+
});
|
|
1061
1100
|
const msg = `push failed: ${redactUrl(String(e.stderr || e.message || e))}`;
|
|
1062
1101
|
console.error(fail(msg, "check your commit and that the checkout's remote is reachable, then re-run"));
|
|
1063
1102
|
emitJson(args, buildJsonResult({ ok: false, ref, commit, changeId, error: msg }));
|
|
@@ -1157,14 +1196,14 @@ export async function run(argv, ctx, { verb = "preview" } = {}) {
|
|
|
1157
1196
|
}
|
|
1158
1197
|
// --json also skips the browser auto-open (open: !args.noOpen && !args.json)
|
|
1159
1198
|
// — automation doesn't want a browser popping up.
|
|
1160
|
-
reportStatus(status, tenant, { open: !args.noOpen && !args.json, quiet: args.json, commit, ref, verb });
|
|
1199
|
+
reportStatus(status, tenant, { open: !args.noOpen && !args.json, quiet: args.json, commit, ref, verb, noChanges: patchEntries.length === 0 });
|
|
1161
1200
|
emitJson(args, buildJsonResult({ ok: status?.status !== "failed", ref, commit, changeId, candidate, status, previewPrUrl }));
|
|
1162
1201
|
return status?.status === "failed" ? 1 : 0;
|
|
1163
1202
|
} catch (e) {
|
|
1164
1203
|
progress?.stop();
|
|
1165
1204
|
const note = e instanceof AuthUnavailableError
|
|
1166
1205
|
? `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: ${
|
|
1206
|
+
: `reconcile is running — the result read-back isn't available yet: ${describeReadbackError(e)}`;
|
|
1168
1207
|
if (!args.json) {
|
|
1169
1208
|
console.log(` (${note})`);
|
|
1170
1209
|
console.log(` Your push is in; the preview updates once reconcile completes.`);
|
|
@@ -1250,6 +1289,19 @@ export async function pollPreviewStatus(
|
|
|
1250
1289
|
if (last.dispatched) everDispatched = true;
|
|
1251
1290
|
last.everDispatched = everDispatched;
|
|
1252
1291
|
if (onTick) onTick(last, i);
|
|
1292
|
+
// Terminally-failed forward: the delivery record settled with forwarded:false
|
|
1293
|
+
// (and it isn't the at-receipt `pending` marker) — the control plane could not
|
|
1294
|
+
// deliver this commit to the reconciler, and polling longer cannot change that.
|
|
1295
|
+
// Only a NEW push produces a new delivery. Stop and say so (Trello-13075
|
|
1296
|
+
// honesty discipline: never spin on a state that cannot progress).
|
|
1297
|
+
if (
|
|
1298
|
+
last.status === "pending" &&
|
|
1299
|
+
last.delivery?.actual &&
|
|
1300
|
+
last.delivery.actual.forwarded === false &&
|
|
1301
|
+
!last.delivery.actual.pending
|
|
1302
|
+
) {
|
|
1303
|
+
return { ...last, forwardFailed: true };
|
|
1304
|
+
}
|
|
1253
1305
|
// Never-dispatched dead-end: still pending, no delivery has EVER been observed
|
|
1254
1306
|
// for this commit, and we're past the startup grace — the reconcile will never
|
|
1255
1307
|
// arrive. Return honestly instead of continuing to show "still reconciling".
|
|
@@ -1309,6 +1361,34 @@ export function shareablePrUrl(base, tenant, prNumber) {
|
|
|
1309
1361
|
return `${String(base).replace(/\/+$/, "")}/preview/${tenant}/pr/${prNumber}`;
|
|
1310
1362
|
}
|
|
1311
1363
|
|
|
1364
|
+
/**
|
|
1365
|
+
* Humanize a result-read-back failure. MCP auth errors arrive as a JSON blob
|
|
1366
|
+
* whose `data.self_repair` carries a summary + step list — dumping that raw
|
|
1367
|
+
* into the terminal (observed live: a wall of escaped JSON mid-submit) buries
|
|
1368
|
+
* the one thing the developer needs: sign in again. Detect that shape and
|
|
1369
|
+
* reduce it to the summary's first sentence + the concrete next step; anything
|
|
1370
|
+
* else passes through unchanged. Pure — unit-tested.
|
|
1371
|
+
* @param {unknown} e
|
|
1372
|
+
* @returns {string}
|
|
1373
|
+
*/
|
|
1374
|
+
export function describeReadbackError(e) {
|
|
1375
|
+
const msg = String(e?.message || e || "");
|
|
1376
|
+
const jsonStart = msg.indexOf("{");
|
|
1377
|
+
if (jsonStart >= 0 && msg.includes("self_repair")) {
|
|
1378
|
+
try {
|
|
1379
|
+
const body = JSON.parse(msg.slice(jsonStart));
|
|
1380
|
+
const repair = body?.data?.self_repair;
|
|
1381
|
+
const summaryFirst = String(repair?.summary || body?.message || "").split(/(?<=\.)\s/)[0];
|
|
1382
|
+
if (summaryFirst) {
|
|
1383
|
+
return `${summaryFirst} Next: run \`tot login\` in this shell (check TOT_PROFILE), then re-run.`;
|
|
1384
|
+
}
|
|
1385
|
+
} catch {
|
|
1386
|
+
// Not the shape we thought — fall through to the raw message.
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
return msg;
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1312
1392
|
/**
|
|
1313
1393
|
* Build the printed lines for the headline "share this with your reviewer" block —
|
|
1314
1394
|
* the whole point of U14: on a successful preview, the SHAREABLE deep link
|
|
@@ -1352,14 +1432,24 @@ export function formatShareableUrlBlock(s, tenant) {
|
|
|
1352
1432
|
* @param {string} tenant @param {string} [verb]
|
|
1353
1433
|
* @returns {string[]}
|
|
1354
1434
|
*/
|
|
1355
|
-
export function formatNotDispatchedBlock({ commit = null, ref = null } = {}, tenant, verb = "preview") {
|
|
1435
|
+
export function formatNotDispatchedBlock({ commit = null, ref = null, noChanges = false } = {}, tenant, verb = "preview") {
|
|
1356
1436
|
const short = commit ? commit.slice(0, 9) : "(unknown commit)";
|
|
1437
|
+
// Empty-diff cause FIRST when we know it applies (live-testing finding: an
|
|
1438
|
+
// empty candidate submit structurally CANNOT build — no diff → no candidate PR
|
|
1439
|
+
// → no pull_request webhook — and blaming webhooks/scope for it sent a human
|
|
1440
|
+
// down two wrong debugging paths).
|
|
1441
|
+
const causes = [
|
|
1442
|
+
...(noChanges
|
|
1443
|
+
? [` • 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\`),`]
|
|
1444
|
+
: []),
|
|
1445
|
+
` • the store's reconcile webhook isn't registered yet (an operator must (re-)provision it), or`,
|
|
1446
|
+
` • your session is scoped to a different store than the one you pushed.`,
|
|
1447
|
+
];
|
|
1357
1448
|
return [
|
|
1358
1449
|
`\n ⚠ No reconcile was dispatched for ${short} on ${tenant}.`,
|
|
1359
1450
|
` Your push landed${ref ? ` on ${ref}` : ""}, but nothing picked it up to build a preview —`,
|
|
1360
1451
|
` 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.`,
|
|
1452
|
+
...causes,
|
|
1363
1453
|
` Next:`,
|
|
1364
1454
|
` • \`tot grants\` — confirm ${tenant} is active for you;`,
|
|
1365
1455
|
` • check the preview dashboard for ${tenant} (it will read "Last reconcile: never" until a job runs);`,
|
|
@@ -1367,6 +1457,26 @@ export function formatNotDispatchedBlock({ commit = null, ref = null } = {}, ten
|
|
|
1367
1457
|
];
|
|
1368
1458
|
}
|
|
1369
1459
|
|
|
1460
|
+
/**
|
|
1461
|
+
* The honest "the delivery FAILED to forward" block — the delivery record settled
|
|
1462
|
+
* `forwarded: false` (not the at-receipt pending marker), so the control plane
|
|
1463
|
+
* could not deliver this commit to the reconciler; polling longer cannot change
|
|
1464
|
+
* that, and ONLY a new push produces a new delivery. Distinct from
|
|
1465
|
+
* `formatNotDispatchedBlock` (nothing was ever dispatched) — here the plumbing
|
|
1466
|
+
* fired and died in transit, so the recovery differs. Pure — unit-tested.
|
|
1467
|
+
* @param {{ commit?: string|null }} ctx @param {string} tenant
|
|
1468
|
+
* @returns {string[]}
|
|
1469
|
+
*/
|
|
1470
|
+
export function formatForwardFailedBlock({ commit = null } = {}, tenant) {
|
|
1471
|
+
const short = commit ? commit.slice(0, 9) : "(unknown commit)";
|
|
1472
|
+
return [
|
|
1473
|
+
`\n ⚠ The reconcile delivery for ${short} on ${tenant} FAILED in transit (network/timeout at the control plane).`,
|
|
1474
|
+
` Waiting longer will not help — only a NEW push produces a new delivery.`,
|
|
1475
|
+
` Next: commit again (an empty commit works: git commit --allow-empty -m retry) and re-push;`,
|
|
1476
|
+
` if it fails the same way twice, share this with support: commit ${short}, tenant ${tenant}.`,
|
|
1477
|
+
];
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1370
1480
|
/**
|
|
1371
1481
|
* Print the reconcile/compliance/preview result and, on a clean reconcile with a
|
|
1372
1482
|
* preview URL, open it in the browser (unless opts.open === false). `quiet`
|
|
@@ -1375,7 +1485,7 @@ export function formatNotDispatchedBlock({ commit = null, ref = null } = {}, ten
|
|
|
1375
1485
|
* --json — automation doesn't want a browser popping up). `commit`/`ref`/`verb`
|
|
1376
1486
|
* feed the honest never-dispatched block.
|
|
1377
1487
|
*/
|
|
1378
|
-
function reportStatus(s, tenant, { open = true, quiet = false, commit = null, ref = null, verb = "preview" } = {}) {
|
|
1488
|
+
function reportStatus(s, tenant, { open = true, quiet = false, commit = null, ref = null, verb = "preview", noChanges = false } = {}) {
|
|
1379
1489
|
if (!s || s.status === "unknown") {
|
|
1380
1490
|
if (!quiet) {
|
|
1381
1491
|
console.log(
|
|
@@ -1385,9 +1495,16 @@ function reportStatus(s, tenant, { open = true, quiet = false, commit = null, re
|
|
|
1385
1495
|
}
|
|
1386
1496
|
return;
|
|
1387
1497
|
}
|
|
1498
|
+
// Terminally-failed forward — the delivery fired and died in transit; a re-push
|
|
1499
|
+
// (new delivery) is the only recovery. Checked BEFORE notDispatched: a settled
|
|
1500
|
+
// failed forward IS a dispatch, just a doomed one.
|
|
1501
|
+
if (s.forwardFailed) {
|
|
1502
|
+
if (!quiet) for (const line of formatForwardFailedBlock({ commit }, tenant)) console.log(line);
|
|
1503
|
+
return;
|
|
1504
|
+
}
|
|
1388
1505
|
// Never-dispatched dead-end — the honest replacement for false "still reconciling".
|
|
1389
1506
|
if (s.notDispatched) {
|
|
1390
|
-
if (!quiet) for (const line of formatNotDispatchedBlock({ commit, ref }, tenant, verb)) console.log(line);
|
|
1507
|
+
if (!quiet) for (const line of formatNotDispatchedBlock({ commit, ref, noChanges }, tenant, verb)) console.log(line);
|
|
1391
1508
|
return;
|
|
1392
1509
|
}
|
|
1393
1510
|
if (s.status === "pending") {
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// Regression guard: the `tot` CLI must never print/reference a raw Gitea
|
|
2
|
+
// forge URL to a developer's terminal. Gitea's API returns `html_url` when a
|
|
3
|
+
// PR is opened (e.g. `https://git.tokenoftrust.com/storefront/<repo>/pulls/<n>`)
|
|
4
|
+
// -- the CLI must surface the storefront-owned `/preview/<tenant>/pr/<n>` link
|
|
5
|
+
// instead (see apps/storefront's matching noGiteaLinks.test.ts and its header
|
|
6
|
+
// for the full architecture reasoning: apps/CLI never expose the forge
|
|
7
|
+
// directly, the MCP proxies every read/write). Precipitating incident
|
|
8
|
+
// (2026-08-18): a raw git.tokenoftrust.com PR URL reached an owner reviewing
|
|
9
|
+
// tokenoftrust.com. Test fixtures are exempt (they legitimately mock a Gitea
|
|
10
|
+
// URL to test the forge client), everything else in `src` must stay clean.
|
|
11
|
+
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
12
|
+
import { join, relative, dirname } from "node:path";
|
|
13
|
+
import { fileURLToPath } from "node:url";
|
|
14
|
+
import { test } from "node:test";
|
|
15
|
+
import assert from "node:assert/strict";
|
|
16
|
+
|
|
17
|
+
const SELF = fileURLToPath(import.meta.url);
|
|
18
|
+
const SRC_ROOT = dirname(SELF);
|
|
19
|
+
|
|
20
|
+
const TEST_FILE_RE = /\.test\.[cm]?js$/;
|
|
21
|
+
const FORGE_HOST_RE = /\bgit\.tokenoftrust\.com\b/i;
|
|
22
|
+
const SKIP_DIRS = new Set(["node_modules", "dist", ".git"]);
|
|
23
|
+
|
|
24
|
+
function walk(dir, out = []) {
|
|
25
|
+
for (const entry of readdirSync(dir)) {
|
|
26
|
+
if (SKIP_DIRS.has(entry)) continue;
|
|
27
|
+
const full = join(dir, entry);
|
|
28
|
+
const st = statSync(full);
|
|
29
|
+
if (st.isDirectory()) {
|
|
30
|
+
walk(full, out);
|
|
31
|
+
} else if (/\.[cm]?js$/.test(entry)) {
|
|
32
|
+
out.push(full);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
test("no Gitea forge links in the CLI source outside test fixtures", () => {
|
|
39
|
+
const offenders = [];
|
|
40
|
+
for (const file of walk(SRC_ROOT)) {
|
|
41
|
+
if (TEST_FILE_RE.test(file)) continue;
|
|
42
|
+
if (file === SELF) continue;
|
|
43
|
+
const content = readFileSync(file, "utf8");
|
|
44
|
+
content.split("\n").forEach((line, i) => {
|
|
45
|
+
if (FORGE_HOST_RE.test(line)) {
|
|
46
|
+
offenders.push(`${relative(SRC_ROOT, file)}:${i + 1}: ${line.trim()}`);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
assert.deepEqual(
|
|
51
|
+
offenders,
|
|
52
|
+
[],
|
|
53
|
+
`Found Gitea forge links in non-test CLI source:\n${offenders.join("\n")}`,
|
|
54
|
+
);
|
|
55
|
+
});
|