@tokenoftrust/cli 1.4.1 → 1.5.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/accept.mjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `tot accept --tenant <t> --pr <N>` (alias: `tot merge`) — OPERATOR verb: QUEUE a
|
|
3
3
|
* PR's integration into the protected `preview` AGGREGATE. This SUPERSEDES the
|
|
4
|
-
* retired `tot accept` = merge-PR→main semantics
|
|
4
|
+
* retired `tot accept` = merge-PR→main semantics: accepting a
|
|
5
5
|
* change no longer merges it to main — it enqueues it into the tenant's shared
|
|
6
|
-
* `preview` aggregate, where
|
|
6
|
+
* `preview` aggregate, where the tenant-serialized queue merges it (via
|
|
7
7
|
* `candidate_accept`, preview-base only), rebuilds the aggregate, and moves the
|
|
8
8
|
* shared preview pointer ONLY when combined evidence is green.
|
|
9
9
|
*
|
|
@@ -14,20 +14,20 @@
|
|
|
14
14
|
* see.
|
|
15
15
|
*
|
|
16
16
|
* TARGET RESOLUTION — `--pr N --tenant t` names the PR; the server resolves it to its
|
|
17
|
-
* candidate from the tenant's ReviewEnvironment index (
|
|
17
|
+
* candidate from the tenant's ReviewEnvironment index (tenant + PR, NO
|
|
18
18
|
* chg-id). An explicit `--change-id` is still accepted (targets a specific record),
|
|
19
|
-
* but is no longer REQUIRED —
|
|
19
|
+
* but is no longer REQUIRED — a PR number alone is enough.
|
|
20
20
|
*
|
|
21
21
|
* TRANSPORT — the honest accept path is `POST /api/changes/integrate` (the ONE call
|
|
22
|
-
* site of
|
|
23
|
-
* `tot pr list --tenant
|
|
22
|
+
* site of `TenantIntegrationQueue.enqueue`). Like `tot ship --pr` and
|
|
23
|
+
* `tot pr list --tenant`, the CLI reaches it with the OPERATOR-SECRET Bearer
|
|
24
24
|
* transport (`resolveOperatorSecret` + `X-Tot-Owner` + `x-tot-capability`), since the
|
|
25
25
|
* CLI holds no storefront cookie. The response is the honest `IntegrateOutcome` —
|
|
26
26
|
* `queueState` / `runState` / `pointerMoved` / `aggregateSha` / `statusMessage` —
|
|
27
27
|
* which this verb renders VERBATIM, never a bare "merged".
|
|
28
28
|
*
|
|
29
29
|
* HUMAN GATE — integrating into the shared preview is a decision a human makes, so
|
|
30
|
-
* this ALWAYS states the EXACT plan (shared `planForAction
|
|
30
|
+
* this ALWAYS states the EXACT plan (shared `planForAction`: which PR,
|
|
31
31
|
* which tenant, "queue for integration into the preview aggregate — NO merge, NO
|
|
32
32
|
* go-live") and requires an explicit confirm. `--yes` is an explicit affirmative; a
|
|
33
33
|
* non-TTY without `--yes` is refused (mirrors `tot ship`'s non-TTY refusal).
|
|
@@ -39,17 +39,24 @@ import { planForAction, printPlanAndConfirm } from "../plan.mjs";
|
|
|
39
39
|
// Reuse `tot ship`'s operator-secret precedence verbatim so accept + ship + pr-list
|
|
40
40
|
// speak ONE operator-auth contract, not three.
|
|
41
41
|
import { resolveOperatorSecret } from "./ship.mjs";
|
|
42
|
-
// Automate-first conflict recovery
|
|
43
|
-
//
|
|
42
|
+
// Automate-first conflict recovery: the ONE-CLICK `--refresh` path calls the
|
|
43
|
+
// `candidate_refresh` MCP tool —
|
|
44
44
|
// which has NO operator-secret HTTP route, so it is reached over the MCP client the
|
|
45
45
|
// same way `tot pr` reaches `candidate_status`/`candidate_close`.
|
|
46
46
|
import { createMcpClient } from "../mcp.mjs";
|
|
47
47
|
import { establishSession, AuthUnavailableError } from "../auth.mjs";
|
|
48
|
+
// No-operator-secret path: mint a viewer session from the developer's OWN `tot`
|
|
49
|
+
// login and integrate as themselves (server gates on their live ship-on-behalf grant).
|
|
50
|
+
import { resolveViewerTransport } from "../viewer-session.mjs";
|
|
51
|
+
// The doctor PUSH: on a failed accept/integrate, auto-append the COMPACT
|
|
52
|
+
// "here's what's blocking you" summary over the SAME transport we just used, so a
|
|
53
|
+
// developer doesn't have to remember to run `tot preview doctor` themselves.
|
|
54
|
+
import { autoSurfaceDoctor } from "../merge-doctor-report.mjs";
|
|
48
55
|
|
|
49
56
|
const DEFAULT_STOREFRONT_URL = "https://storefront.tokenoftrust.store";
|
|
50
57
|
const DEFAULT_MCP_URL = "https://mcp.tokenoftrust.com";
|
|
51
58
|
|
|
52
|
-
// The three refresh strategies `candidate_refresh`
|
|
59
|
+
// The three refresh strategies `candidate_refresh` accepts, mirroring the admin
|
|
53
60
|
// panel's one-click resolver (keep-mine / keep-current / smart-merge). `merge` is the
|
|
54
61
|
// DEFAULT: a real forge three-way merge that refuses cleanly (with the diverged file
|
|
55
62
|
// list) on a genuine same-line overlap rather than silently clobbering either side —
|
|
@@ -170,7 +177,7 @@ function candidateLabel(pr, changeId) {
|
|
|
170
177
|
|
|
171
178
|
/**
|
|
172
179
|
* Read the forge `mergeable` verdict for a candidate out of a `GET /api/changes`
|
|
173
|
-
* body (
|
|
180
|
+
* body (the operator queue — its BUILT entries carry the full ReviewEnvironment
|
|
174
181
|
* shape, incl. `mergeable`). This is the mergeable PREFLIGHT source: one read over the
|
|
175
182
|
* SAME operator-secret transport accept already uses, so a not-mergeable candidate is
|
|
176
183
|
* caught BEFORE a doomed integrate round-trips to the forge. Best-effort by design —
|
|
@@ -212,7 +219,7 @@ export function readCandidateVerdict(data, { pr, changeId }) {
|
|
|
212
219
|
}
|
|
213
220
|
|
|
214
221
|
/**
|
|
215
|
-
* Normalise a `candidate_refresh`
|
|
222
|
+
* Normalise a `candidate_refresh` result read back over the MCP client.
|
|
216
223
|
* `status:"committed"` is the ONLY success (the candidate was rebuilt and is now
|
|
217
224
|
* mergeable); a `merge_failed` refusal carries `unresolved` (the diverged files);
|
|
218
225
|
* any other status is an honest refusal/error surfaced by its `message`. Reads
|
|
@@ -260,7 +267,7 @@ export function notMergeableOfferLines(tenant, { pr, changeId }) {
|
|
|
260
267
|
}
|
|
261
268
|
|
|
262
269
|
/**
|
|
263
|
-
* Run `candidate_refresh`
|
|
270
|
+
* Run `candidate_refresh` over the MCP client — the ONE-CLICK rebuild of a
|
|
264
271
|
* not-mergeable candidate onto the current preview tip. `candidate_refresh` has no
|
|
265
272
|
* operator-secret HTTP route, so this reaches it exactly as `tot pr` reaches
|
|
266
273
|
* `candidate_status`: an OAuth developer session (`tot login`) + `client_switch` to
|
|
@@ -453,8 +460,10 @@ export function reportIntegrated(result, { tenant, label }) {
|
|
|
453
460
|
* unit-tested with no network/TTY.
|
|
454
461
|
*
|
|
455
462
|
* @param {{ tenant:string, pr:number|null, changeId:string|null, headSha:string|null,
|
|
456
|
-
* secret:string, storefrontUrl?:string|null, yes?:boolean
|
|
457
|
-
*
|
|
463
|
+
* secret:string, storefrontUrl?:string|null, yes?:boolean, refresh?:boolean,
|
|
464
|
+
* strategy?:string, mcpUrl?:string|null, identity?:string|null, env?:NodeJS.ProcessEnv }} params
|
|
465
|
+
* @param {{ fetch?:typeof fetch, confirmPlan?:typeof printPlanAndConfirm,
|
|
466
|
+
* resolveViewerTransport?:typeof resolveViewerTransport }} [deps]
|
|
458
467
|
* @returns {Promise<number>} process exit code
|
|
459
468
|
*/
|
|
460
469
|
export async function runIntegrate(
|
|
@@ -476,10 +485,11 @@ export async function runIntegrate(
|
|
|
476
485
|
) {
|
|
477
486
|
const fetchImpl = deps.fetch || globalThis.fetch;
|
|
478
487
|
const confirmPlan = deps.confirmPlan || printPlanAndConfirm;
|
|
479
|
-
const
|
|
488
|
+
const resolveViewer = deps.resolveViewerTransport || resolveViewerTransport;
|
|
489
|
+
let base = (storefrontUrl || DEFAULT_STOREFRONT_URL).trim().replace(/\/+$/, "");
|
|
480
490
|
const label = pr != null ? `PR #${pr}` : changeId;
|
|
481
491
|
|
|
482
|
-
// 1. State the EXACT plan (shared
|
|
492
|
+
// 1. State the EXACT plan (shared affordance) — queue-integrate-into-preview,
|
|
483
493
|
// NO merge, NO go-live — and gate on an explicit confirm.
|
|
484
494
|
const planLines = planForAction({ action: "accept", tenant, pr, changeId, headSha });
|
|
485
495
|
const { confirmed, reason } = await confirmPlan(planLines, {
|
|
@@ -502,25 +512,39 @@ export async function runIntegrate(
|
|
|
502
512
|
return 1;
|
|
503
513
|
}
|
|
504
514
|
|
|
505
|
-
// 2.
|
|
506
|
-
// Bearer + X-Tot-Owner
|
|
507
|
-
//
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
515
|
+
// 2. Resolve the transport. Two routes to the SAME `/api/changes/integrate`:
|
|
516
|
+
// - OPERATOR SECRET (operators/CI): Bearer + X-Tot-Owner on the generic host.
|
|
517
|
+
// - VIEWER SESSION (an invited developer, no secret): mint a `tot_session` from
|
|
518
|
+
// their OWN `tot` login on the TENANT'S host and send it as a cookie. The
|
|
519
|
+
// server authorizes on their live ship-on-behalf grant either way.
|
|
520
|
+
// No `content-type` here: it's added per-POST; the mergeable preflight GET wants none.
|
|
521
|
+
let authHeaders;
|
|
522
|
+
if (secret) {
|
|
523
|
+
authHeaders = {
|
|
524
|
+
authorization: `Bearer ${secret}`,
|
|
525
|
+
"x-tot-owner": tenant,
|
|
526
|
+
"x-tot-capability": "ship-on-behalf",
|
|
527
|
+
};
|
|
528
|
+
} else {
|
|
529
|
+
const viewer = await resolveViewer({ tenant, env, fetchImpl });
|
|
530
|
+
if (!viewer.ok) {
|
|
531
|
+
console.error(fail(viewer.message, viewer.hint));
|
|
532
|
+
return 2;
|
|
533
|
+
}
|
|
534
|
+
base = viewer.base; // the tenant's own host — the dev-viewer admission is host-scoped
|
|
535
|
+
authHeaders = viewer.authHeaders;
|
|
516
536
|
}
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
537
|
+
|
|
538
|
+
// Auto-surface the merge-doctor on a failed accept. Best-effort: fetches the
|
|
539
|
+
// hosted doctor over the SAME transport we just resolved and prints the COMPACT
|
|
540
|
+
// summary; never throws and never touches the exit code (it's called AFTER the
|
|
541
|
+
// outcome is decided, purely to append diagnostic context to where the failure
|
|
542
|
+
// already surfaced). A clean/unreachable doctor prints nothing.
|
|
543
|
+
const surfaceDoctor = async () => {
|
|
544
|
+
for (const line of await autoSurfaceDoctor({ base, authHeaders, fetchImpl })) console.error(line);
|
|
521
545
|
};
|
|
522
546
|
|
|
523
|
-
// 2.5. MERGEABLE PREFLIGHT — read the candidate's forge verdict from
|
|
547
|
+
// 2.5. MERGEABLE PREFLIGHT — read the candidate's forge verdict from the queue
|
|
524
548
|
// (`GET /api/changes`, SAME transport) so a doomed accept never round-trips to
|
|
525
549
|
// the forge. Best-effort: on any listing gap it's `known:false` → proceed as
|
|
526
550
|
// normal. When it KNOWS the candidate isn't mergeable, take the automate-first
|
|
@@ -543,13 +567,13 @@ export async function runIntegrate(
|
|
|
543
567
|
},
|
|
544
568
|
deps,
|
|
545
569
|
);
|
|
546
|
-
if (!handled.integrate) return handled.code;
|
|
570
|
+
if (!handled.integrate) return /** @type {number} */ (handled.code);
|
|
547
571
|
curPr = handled.pr ?? curPr;
|
|
548
572
|
curChangeId = handled.changeId ?? curChangeId;
|
|
549
573
|
curHead = null; // the rebuilt PR has a fresh head; let the server re-resolve.
|
|
550
574
|
}
|
|
551
575
|
|
|
552
|
-
// 3. POST the honest accept path (
|
|
576
|
+
// 3. POST the honest accept path (queue enqueue) and render the terminal state.
|
|
553
577
|
let res;
|
|
554
578
|
let result;
|
|
555
579
|
try {
|
|
@@ -579,7 +603,7 @@ export async function runIntegrate(
|
|
|
579
603
|
{ tenant, pr: curPr, changeId: result.changeId ?? curChangeId, strategy, refresh, mcpUrl, identity, env },
|
|
580
604
|
deps,
|
|
581
605
|
);
|
|
582
|
-
if (!handled.integrate) return handled.code;
|
|
606
|
+
if (!handled.integrate) return /** @type {number} */ (handled.code);
|
|
583
607
|
try {
|
|
584
608
|
({ res, result } = await postIntegrate(
|
|
585
609
|
{
|
|
@@ -614,10 +638,13 @@ export async function runIntegrate(
|
|
|
614
638
|
for (const line of notMergeableOfferLines(tenant, { pr: curPr, changeId: curChangeId })) {
|
|
615
639
|
console.log(line);
|
|
616
640
|
}
|
|
641
|
+
await surfaceDoctor();
|
|
617
642
|
return 1;
|
|
618
643
|
}
|
|
619
644
|
|
|
620
|
-
|
|
645
|
+
const code = reportIntegrated(result, { tenant, label: /** @type {string} */ (label) });
|
|
646
|
+
if (code !== 0) await surfaceDoctor();
|
|
647
|
+
return code;
|
|
621
648
|
}
|
|
622
649
|
|
|
623
650
|
/** The mergeable PREFLIGHT read (best-effort). See readCandidateVerdict. */
|
|
@@ -646,7 +673,7 @@ function reportIntegrateHttpError(result, res, { tenant, label }) {
|
|
|
646
673
|
fail(
|
|
647
674
|
`the integration queue refused the request: ${msg}`,
|
|
648
675
|
res.status === 401 || res.status === 403
|
|
649
|
-
? "
|
|
676
|
+
? "you need a live ship-on-behalf grant on this tenant (ask the store owner) — or an operator secret authorised for it"
|
|
650
677
|
: res.status === 404
|
|
651
678
|
? `check that ${label} has a built candidate in ${tenant}'s queue (\`tot pr list --tenant ${tenant}\`)`
|
|
652
679
|
: "check --tenant / --url / --pr, then re-run",
|
package/src/commands/app/dev.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `tot app dev` — the local Private App harness (PrivateApps epic
|
|
2
|
+
* `tot app dev` — the local Private App harness (PrivateApps epic).
|
|
3
3
|
* Everything here is OFFLINE: no network beyond the localhost URL you point
|
|
4
4
|
* it at, no MCP, no real ToT credentials. It generates and reuses its own
|
|
5
5
|
* throwaway RS256 keypair per app directory (`.tot/dev-keys.json`) so signing
|
|
@@ -49,7 +49,10 @@ function randHex(bytes) {
|
|
|
49
49
|
return Buffer.from(crypto.getRandomValues(new Uint8Array(bytes))).toString("hex");
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
/**
|
|
52
|
+
/**
|
|
53
|
+
* Load or create the app's throwaway RS256 keypair at `<appDir>/.tot/dev-keys.json`.
|
|
54
|
+
* @param {string} appDir @param {{ kid?: string }} [opts]
|
|
55
|
+
*/
|
|
53
56
|
export async function ensureDevKeys(appDir, { kid } = {}) {
|
|
54
57
|
const keysPath = join(appDir, ".tot", "dev-keys.json");
|
|
55
58
|
const alg = { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" };
|
|
@@ -107,8 +110,9 @@ function runValidate(argv, { appDir }) {
|
|
|
107
110
|
console.log(`✔ ${manifestPath} is a valid tot-app.json (contract v${result.manifest.contractVersion})`);
|
|
108
111
|
return 0;
|
|
109
112
|
}
|
|
110
|
-
|
|
111
|
-
|
|
113
|
+
const errs = result.errors || [];
|
|
114
|
+
console.log(`✖ ${manifestPath} — ${errs.length} error(s):`);
|
|
115
|
+
for (const e of errs) console.log(` - ${e}`);
|
|
112
116
|
return 1;
|
|
113
117
|
}
|
|
114
118
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `tot app` — the Storefront Private App developer subcommand group
|
|
3
|
-
* (PrivateApps epic
|
|
3
|
+
* (PrivateApps epic): scaffold a new app, then exercise its
|
|
4
4
|
* webhook/manifest/widget-launch loop entirely offline.
|
|
5
5
|
*
|
|
6
6
|
* tot app scaffold <name> materialize a runnable Private App skeleton
|
|
@@ -23,11 +23,11 @@ export async function run(argv, ctx) {
|
|
|
23
23
|
|
|
24
24
|
if (sub === "scaffold") {
|
|
25
25
|
const { run: runScaffold } = await import("./scaffold.mjs");
|
|
26
|
-
return runScaffold(rest, ctx);
|
|
26
|
+
return /** @type {any} */ (runScaffold)(rest, ctx);
|
|
27
27
|
}
|
|
28
28
|
if (sub === "dev") {
|
|
29
29
|
const { run: runDev } = await import("./dev.mjs");
|
|
30
|
-
return runDev(rest, ctx);
|
|
30
|
+
return /** @type {any} */ (runDev)(rest, ctx);
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
console.error(fail(`unknown \`tot app\` subcommand: ${sub}`, "tot app --help"));
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `tot app scaffold <name>` — materialize a runnable Storefront Private App
|
|
3
|
-
* skeleton on disk (PrivateApps epic
|
|
3
|
+
* skeleton on disk (PrivateApps epic). See ../../app-scaffold.mjs
|
|
4
4
|
* for the offline, idempotent copy logic this wraps.
|
|
5
5
|
*
|
|
6
6
|
* tot app scaffold <name> scaffold ./<name>
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `tot branches` — the full branch-cleanup REPORT for one tenant's repo (
|
|
3
|
-
*
|
|
2
|
+
* `tot branches` — the full branch-cleanup REPORT for one tenant's repo (per
|
|
3
|
+
* the branch-lifecycle-and-integration-preview contract: "join
|
|
4
4
|
* branch, PR, candidate, head SHA, actor, full zoned timestamp, age, evidence,
|
|
5
5
|
* aggregate status, and cleanup eligibility"). Distinct from `tot pr list`,
|
|
6
6
|
* which only shows OPEN PRs — this shows EVERY branch on the repo (protected,
|
|
7
7
|
* active, stale, integrated, closed, orphaned), because that is what a
|
|
8
8
|
* cleanup decision needs.
|
|
9
9
|
*
|
|
10
|
-
* The report itself is server-side classification (
|
|
10
|
+
* The report itself is server-side classification (the `candidate_list` MCP
|
|
11
11
|
* tool → `classifyBranches`); this command is presentation only — it never
|
|
12
12
|
* re-derives eligibility, an "aggregate" verdict, or a classification bucket
|
|
13
13
|
* client-side. The contract's complaint about a "truncated human table" (see
|
|
@@ -58,6 +58,7 @@ Options:
|
|
|
58
58
|
|
|
59
59
|
/** Parse `tot branches` argv. Pure — unit-testable. */
|
|
60
60
|
export function parseBranchesArgs(argv) {
|
|
61
|
+
/** @type {{ tenant: string|null, repo: string|null, staleAfterDays: number|null, mcp: string|null, identity: string|null, json: boolean, help: boolean }} */
|
|
61
62
|
const a = {
|
|
62
63
|
tenant: null,
|
|
63
64
|
repo: null,
|
package/src/commands/cleanup.mjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `tot cleanup` — owner-confirmed branch GC
|
|
3
|
-
* branch-lifecycle-and-integration-preview contract's "Current gaps"
|
|
2
|
+
* `tot cleanup` — owner-confirmed branch GC, per the
|
|
3
|
+
* branch-lifecycle-and-integration-preview contract's "Current gaps".
|
|
4
4
|
*
|
|
5
5
|
* `tot cleanup --dry-run` runs the SAME fresh server-side classification
|
|
6
|
-
* `tot branches` shows (
|
|
6
|
+
* `tot branches` shows (the `candidate_list` MCP tool) and prints exactly
|
|
7
7
|
* which refs are eligible to delete and WHY — no writes, ever, no matter what
|
|
8
8
|
* else is passed. An owner-confirmed execution mode (`--yes`, or an
|
|
9
9
|
* interactive y/N via the shared plan affordance) then deletes ONLY that
|
|
@@ -12,10 +12,6 @@
|
|
|
12
12
|
* — this CLI's dry-run read is a convenience for the human, never the source
|
|
13
13
|
* of truth the delete trusts.
|
|
14
14
|
*
|
|
15
|
-
* Distinct from `tot retire` (evicts a HOSTED PREVIEW artifact, rebuildable,
|
|
16
|
-
* touches no git ref) — `tot cleanup` is branch GC: an irreversible ref
|
|
17
|
-
* delete. See retire.mjs's header for that distinction.
|
|
18
|
-
*
|
|
19
15
|
* THE CONTRACT'S HARD RULES, ENFORCED SERVER-SIDE AND NEVER RELAXED HERE:
|
|
20
16
|
* - age alone NEVER makes an open candidate deletable (`stale-open` is a
|
|
21
17
|
* WARN bucket only);
|
|
@@ -45,9 +41,8 @@ const USAGE = `tot cleanup — owner-confirmed branch GC (deletes terminal candi
|
|
|
45
41
|
Deletes ONLY branches the server just (re-)classified as "integrated" (PR
|
|
46
42
|
merged) or "closed-or-rejected" (PR closed without merging). NEVER deletes
|
|
47
43
|
by age alone, NEVER touches \`main\`/\`preview\`, and NEVER deletes an orphan
|
|
48
|
-
(no PR record) — orphans are reported, not removed.
|
|
49
|
-
|
|
50
|
-
git branch.
|
|
44
|
+
(no PR record) — orphans are reported, not removed. Reject closes the PR and
|
|
45
|
+
removes its hosted candidate artifact; cleanup deletes the terminal git ref.
|
|
51
46
|
|
|
52
47
|
Options:
|
|
53
48
|
--tenant <appDomain> Target tenant/repo. Defaults to the current
|
|
@@ -65,6 +60,7 @@ Options:
|
|
|
65
60
|
|
|
66
61
|
/** Parse `tot cleanup` argv. Pure — unit-testable. */
|
|
67
62
|
export function parseCleanupArgs(argv) {
|
|
63
|
+
/** @type {{ tenant: string|null, repo: string|null, staleAfterDays: number|null, ref: string|null, dryRun: boolean, yes: boolean, mcp: string|null, identity: string|null, help: boolean }} */
|
|
68
64
|
const a = {
|
|
69
65
|
tenant: null,
|
|
70
66
|
repo: null,
|
|
@@ -206,7 +202,7 @@ export async function run(argv, ctx) {
|
|
|
206
202
|
|
|
207
203
|
const planLines = planForAction({
|
|
208
204
|
action: "cleanup",
|
|
209
|
-
tenant: tenant || repo,
|
|
205
|
+
tenant: /** @type {string} */ (tenant || repo),
|
|
210
206
|
refs: eligible.map((b) => ({ ref: b.ref, sha: b.sha, reason: b.reason })),
|
|
211
207
|
});
|
|
212
208
|
const { confirmed, reason } = await printPlanAndConfirm(planLines, {
|
package/src/commands/clone.mjs
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* resolved via src/auth.mjs). When there's no session yet and we're on a TTY, we
|
|
24
24
|
* offer to sign in right here and retry — no "run tot login, then re-run".
|
|
25
25
|
*
|
|
26
|
-
* OPERATOR CROSS-TENANT PR CLONE
|
|
26
|
+
* OPERATOR CROSS-TENANT PR CLONE. `tot clone <tenant> --pr <N> [dir]`
|
|
27
27
|
* materializes a PARTICULAR pull request's head — including on a tenant the
|
|
28
28
|
* operator does NOT own/isn't a member of (e.g. inspecting someone else's PR
|
|
29
29
|
* #7). This does NOT go through `tenant_checkout` — that tool mints a PUSH
|
|
@@ -61,6 +61,7 @@ const execFileP = promisify(execFile);
|
|
|
61
61
|
const DEFAULT_MCP_URL = "https://mcp.tokenoftrust.com";
|
|
62
62
|
|
|
63
63
|
function parseArgs(argv) {
|
|
64
|
+
/** @type {{ tenant: string|null, dir: string|null, tag: string, pr: string|null, remoteOnly: boolean, mcp: string|null, printRemote: boolean, help: boolean }} */
|
|
64
65
|
const a = {
|
|
65
66
|
tenant: null,
|
|
66
67
|
dir: null,
|
|
@@ -325,10 +326,10 @@ export async function checkoutTenant(client, { tenant, tag = "main", cloneDir =
|
|
|
325
326
|
|
|
326
327
|
/**
|
|
327
328
|
* git clone the authenticated remote into `dir` — WITHOUT ever writing the
|
|
328
|
-
* live token to `.git/config
|
|
329
|
+
* live token to `.git/config`. The MCP mints `gitRemote` as a
|
|
329
330
|
* basic-auth URL (`user:token@host`); rather than passing that straight to
|
|
330
331
|
* `git clone` (which records exactly the URL it was given as `origin`, token
|
|
331
|
-
* and all
|
|
332
|
+
* and all), the token is handed to git
|
|
332
333
|
* EPHEMERALLY via a one-shot `http.extraheader` (same mechanism
|
|
333
334
|
* pushPreviewRef in submit.mjs uses for a push) while the clone SOURCE is
|
|
334
335
|
* already the tokenless public URL — so `origin` comes out tokenless from
|
|
@@ -377,7 +378,7 @@ async function cloneRepo(gitRemote, dir, redact) {
|
|
|
377
378
|
}
|
|
378
379
|
|
|
379
380
|
/**
|
|
380
|
-
* The cross-tenant PR-clone core
|
|
381
|
+
* The cross-tenant PR-clone core, composable in-process exactly like
|
|
381
382
|
* `checkoutTenant`: mint a READ-ONLY credential via `repo_read_credential`, then
|
|
382
383
|
* (optionally) clone it and materialize PR #<pr>'s head. This is the path `--pr`
|
|
383
384
|
* ALWAYS takes — own tenant or foreign — because tenant_checkout has no PR-ref
|
|
@@ -474,7 +475,7 @@ export async function cloneRepoAtPrRef(gitRemote, dir, pr, redact, deps = {}) {
|
|
|
474
475
|
* @returns {{ message: string, next: string }|null}
|
|
475
476
|
*/
|
|
476
477
|
export function readCredentialError(minted) {
|
|
477
|
-
const c = minted && typeof minted === "object" && !Array.isArray(minted) ? minted : null;
|
|
478
|
+
const c = /** @type {any} */ (minted && typeof minted === "object" && !Array.isArray(minted) ? minted : null);
|
|
478
479
|
if (c && Array.isArray(c.repos) && c.repos.some((r) => r && r.gitRemote)) return null;
|
|
479
480
|
const msg =
|
|
480
481
|
(c && (c.message || (typeof c.error === "string" ? c.error : c.error?.message))) ||
|
|
@@ -521,7 +522,7 @@ export function readCredentialError(minted) {
|
|
|
521
522
|
* @returns {{ message: string, next: string }|null}
|
|
522
523
|
*/
|
|
523
524
|
export function checkoutError(checkout) {
|
|
524
|
-
const c = checkout && typeof checkout === "object" && !Array.isArray(checkout) ? checkout : null;
|
|
525
|
+
const c = /** @type {any} */ (checkout && typeof checkout === "object" && !Array.isArray(checkout) ? checkout : null);
|
|
525
526
|
if (c && c.gitRemote) return null; // a usable checkout — never an error
|
|
526
527
|
const msg =
|
|
527
528
|
(c && (c.message || (typeof c.error === "string" ? c.error : c.error?.message))) ||
|
|
@@ -564,7 +565,8 @@ export function checkoutError(checkout) {
|
|
|
564
565
|
* @returns {Array<{ id: string, name: string, raw: any }>}
|
|
565
566
|
*/
|
|
566
567
|
export function normalizeStores(list) {
|
|
567
|
-
const
|
|
568
|
+
const l = /** @type {any} */ (list);
|
|
569
|
+
const rows = Array.isArray(l) ? l : l?.clients || l?.tenants || [];
|
|
568
570
|
if (!Array.isArray(rows)) return [];
|
|
569
571
|
return rows
|
|
570
572
|
.map((r) => ({
|
|
@@ -590,23 +592,24 @@ export function normalizeStores(list) {
|
|
|
590
592
|
*/
|
|
591
593
|
export function storeListError(list) {
|
|
592
594
|
if (list == null || typeof list !== "object" || Array.isArray(list)) return null;
|
|
595
|
+
const l = /** @type {any} */ (list);
|
|
593
596
|
// A resolvable store array present → it succeeded, never an error.
|
|
594
|
-
if (Array.isArray(
|
|
597
|
+
if (Array.isArray(l.clients) || Array.isArray(l.tenants)) return null;
|
|
595
598
|
const msg =
|
|
596
|
-
|
|
597
|
-
(typeof
|
|
599
|
+
l.message ||
|
|
600
|
+
(typeof l.error === "string" ? l.error : l.error?.message) ||
|
|
598
601
|
null;
|
|
599
|
-
if (
|
|
600
|
-
if (typeof
|
|
601
|
-
return msg || `the store list request returned status "${
|
|
602
|
+
if (l.isError) return msg || "the store list request returned an error";
|
|
603
|
+
if (typeof l.status === "string" && !/^(ok|success)$/i.test(l.status)) {
|
|
604
|
+
return msg || `the store list request returned status "${l.status}"`;
|
|
602
605
|
}
|
|
603
|
-
if (
|
|
604
|
-
if (typeof
|
|
606
|
+
if (l.error) return msg || "the store list request returned an error";
|
|
607
|
+
if (typeof l.raw === "string" && l.raw.trim()) return l.raw.trim();
|
|
605
608
|
return null;
|
|
606
609
|
}
|
|
607
610
|
|
|
608
611
|
/**
|
|
609
|
-
* Extract
|
|
612
|
+
* Extract the broker-identity remediation signal from a `client_list`
|
|
610
613
|
* result. When the identity resolved ZERO stores, the server MAY carry:
|
|
611
614
|
* - `brokerStatus`: 'unlinked' | 'unconfigured' | 'broker_error' — present only
|
|
612
615
|
* when the empty scope is a BROKER-IDENTITY problem (not an entitlement one);
|
|
@@ -620,7 +623,7 @@ export function storeListError(list) {
|
|
|
620
623
|
* @returns {{ brokerStatus: string|null, nextAction: string|null }}
|
|
621
624
|
*/
|
|
622
625
|
export function brokerRemediation(list) {
|
|
623
|
-
const c = list && typeof list === "object" && !Array.isArray(list) ? list : null;
|
|
626
|
+
const c = /** @type {any} */ (list && typeof list === "object" && !Array.isArray(list) ? list : null);
|
|
624
627
|
const brokerStatus = c && typeof c.brokerStatus === "string" ? c.brokerStatus : null;
|
|
625
628
|
const nextAction =
|
|
626
629
|
c && typeof c.nextAction === "string" && c.nextAction.trim() ? c.nextAction.trim() : null;
|
|
@@ -629,8 +632,8 @@ export function brokerRemediation(list) {
|
|
|
629
632
|
|
|
630
633
|
/**
|
|
631
634
|
* Status-aware human guidance for an authenticated identity that resolved ZERO
|
|
632
|
-
* stores (
|
|
633
|
-
* a store invite" copy that dead-ended an UNLINKED identity). Driven by
|
|
635
|
+
* stores (fixes the misleading "may still be propagating / ask for
|
|
636
|
+
* a store invite" copy that dead-ended an UNLINKED identity). Driven by
|
|
634
637
|
* `brokerStatus`/`nextAction`:
|
|
635
638
|
* - `unlinked` → the identity isn't linked to the ToT broker yet, so no scope can
|
|
636
639
|
* resolve. This is NOT an entitlement problem — point at `tot link` (the
|
|
@@ -655,7 +658,7 @@ export function noStoresGuidance(list, { linkHint = "tot link" } = {}) {
|
|
|
655
658
|
|
|
656
659
|
if (brokerStatus === "unlinked") {
|
|
657
660
|
// The actionable terminal step beats the server's identity_link_begin/poll
|
|
658
|
-
// MCP-tool wording for a CLI user, so lead with `tot link` (
|
|
661
|
+
// MCP-tool wording for a CLI user, so lead with `tot link` (the optional-arm
|
|
659
662
|
// directive). Fall back to the server string only if there's no link action.
|
|
660
663
|
return {
|
|
661
664
|
brokerStatus,
|