@tokenoftrust/cli 1.4.0-rc.13 → 1.4.0-rc.14
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/pr.mjs +180 -7
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.14",
|
|
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/pr.mjs
CHANGED
|
@@ -16,12 +16,22 @@
|
|
|
16
16
|
* checkout's origin remote, exactly as `tot submit` does) and reads over the MCP
|
|
17
17
|
* `candidate_status` / `candidate_close` tools. Dependency-free (global fetch +
|
|
18
18
|
* `git`).
|
|
19
|
+
*
|
|
20
|
+
* OPERATOR MODE (unit U17). `tot pr list --tenant <t>` lists a tenant's OPEN
|
|
21
|
+
* candidate queue WITHOUT a checkout — the read-only companion to `tot ship --pr <N>
|
|
22
|
+
* --tenant <t>` (U16). It reuses U16's exact transport: the SAME `GET /api/changes`
|
|
23
|
+
* HTTP endpoint + operator-secret Bearer auth (`resolveOperatorSecret`,
|
|
24
|
+
* `normalizeChangesQueue` from ship.mjs). `view`/`close` stay developer-only.
|
|
19
25
|
*/
|
|
20
26
|
import { execFileSync } from "node:child_process";
|
|
21
27
|
import { createMcpClient } from "../mcp.mjs";
|
|
22
28
|
import { establishSession, AuthUnavailableError } from "../auth.mjs";
|
|
23
29
|
import { fail } from "../errors.mjs";
|
|
24
30
|
import { repoNameFromRemote, currentBranch } from "./submit.mjs";
|
|
31
|
+
// U17 reuses U16's operator-secret transport helpers verbatim (same env precedence,
|
|
32
|
+
// same queue normalisation) so the operator `tot pr list` and `tot ship --pr` speak
|
|
33
|
+
// one wire, not two.
|
|
34
|
+
import { resolveOperatorSecret, normalizeChangesQueue } from "./ship.mjs";
|
|
25
35
|
import {
|
|
26
36
|
defaultCandidateStatePath,
|
|
27
37
|
readActiveChangeId,
|
|
@@ -29,29 +39,55 @@ import {
|
|
|
29
39
|
} from "../candidate-state.mjs";
|
|
30
40
|
|
|
31
41
|
const DEFAULT_MCP_URL = "https://mcp.tokenoftrust.com";
|
|
42
|
+
const DEFAULT_STOREFRONT_URL = "https://storefront.tokenoftrust.store";
|
|
32
43
|
const SUBCOMMANDS = ["list", "view", "close"];
|
|
33
44
|
|
|
34
|
-
const USAGE = `tot pr — see and manage
|
|
45
|
+
const USAGE = `tot pr — see and manage candidate PRs
|
|
35
46
|
|
|
36
47
|
tot pr [list] list your open candidate PRs for this store
|
|
48
|
+
tot pr list --tenant <t> OPERATOR: list a tenant's open candidate PRs (no checkout)
|
|
37
49
|
tot pr view <N|id> show one candidate PR (by PR number or changeId)
|
|
38
50
|
tot pr close <N|id> close (reject) a candidate PR without merging
|
|
39
51
|
tot pr close <N|id> --reason "<why>" record why it was closed (audit note)
|
|
40
52
|
tot pr --mcp <url> MCP base URL (default: env MCP_BASE_URL / TOT_MCP_URL)
|
|
41
53
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
54
|
+
DEVELOPER (default) — run inside your OWN store checkout: lists/manages the
|
|
55
|
+
candidates \`tot submit\` opens. A re-submit updates your open one by default;
|
|
56
|
+
\`tot submit --new\` forks another.
|
|
57
|
+
|
|
58
|
+
OPERATOR — \`tot pr list --tenant <appDomain>\` lists ANY tenant's open candidate
|
|
59
|
+
queue WITHOUT a checkout (the read-only companion to \`tot ship --pr <N> --tenant\`).
|
|
60
|
+
It reads \`GET /api/changes\` with an operator secret:
|
|
61
|
+
|
|
62
|
+
tot pr list --tenant <t> list <t>'s open candidate PRs
|
|
63
|
+
tot pr list --url <origin> storefront origin (default: env TOT_STOREFRONT_URL)
|
|
64
|
+
tot pr list --secret <s> operator secret (prefer the env vars below)
|
|
65
|
+
|
|
66
|
+
Operator secret (from env, first found): PREVIEW_RECONCILE_SECRET,
|
|
67
|
+
GRANTS_ADMIN_SECRET, TOT_OPERATOR_SECRET (or pass --secret).`;
|
|
45
68
|
|
|
46
|
-
/** Parse `tot pr` argv into { sub, target, reason, mcp, identity, help }. Pure. */
|
|
69
|
+
/** Parse `tot pr` argv into { sub, target, reason, mcp, identity, tenant, url, secret, help }. Pure. */
|
|
47
70
|
export function parsePrArgs(argv) {
|
|
48
|
-
const a = {
|
|
71
|
+
const a = {
|
|
72
|
+
sub: null,
|
|
73
|
+
target: null,
|
|
74
|
+
reason: null,
|
|
75
|
+
mcp: null,
|
|
76
|
+
identity: null,
|
|
77
|
+
tenant: null,
|
|
78
|
+
url: null,
|
|
79
|
+
secret: null,
|
|
80
|
+
help: false,
|
|
81
|
+
};
|
|
49
82
|
const positional = [];
|
|
50
83
|
for (let i = 0; i < argv.length; i++) {
|
|
51
84
|
const t = argv[i];
|
|
52
85
|
if (t === "--mcp") a.mcp = argv[++i];
|
|
53
86
|
else if (t === "--identity") a.identity = argv[++i];
|
|
54
87
|
else if (t === "--reason") a.reason = argv[++i];
|
|
88
|
+
else if (t === "--tenant") a.tenant = argv[++i];
|
|
89
|
+
else if (t === "--url") a.url = argv[++i];
|
|
90
|
+
else if (t === "--secret") a.secret = argv[++i];
|
|
55
91
|
else if (t === "--help" || t === "-h") a.help = true;
|
|
56
92
|
else positional.push(t);
|
|
57
93
|
}
|
|
@@ -98,6 +134,118 @@ export function formatCandidateLine(c, { active = false } = {}) {
|
|
|
98
134
|
return ` PR ${pr} ${branch} ${c.changeId} [${c.state ?? "?"}]${urlPart}${activePart}`;
|
|
99
135
|
}
|
|
100
136
|
|
|
137
|
+
// ─── Operator queue listing (unit U17) ──────────────────────────────────────────────
|
|
138
|
+
//
|
|
139
|
+
// The read-only companion to `tot ship --pr <N> --tenant <t>` (U16): list a tenant's
|
|
140
|
+
// OPEN candidate queue WITHOUT a checkout. It reuses U16's exact transport — the SAME
|
|
141
|
+
// `GET /api/changes` endpoint (operator accept queue) + Bearer operator-secret auth
|
|
142
|
+
// (`resolveOperatorSecret` / `normalizeChangesQueue` from ship.mjs) — so a listing and
|
|
143
|
+
// a ship read one wire. NO merge/deploy/side-effect; pure listing.
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* One-line summary of an OPEN operator-queue candidate for `tot pr list --tenant`.
|
|
147
|
+
* Surfaces PR # (or `#—`), the review-environment status, the changeId, the short
|
|
148
|
+
* head sha, and the preview URL — the fields an operator needs to pick a PR to ship.
|
|
149
|
+
* Adapts `formatCandidateLine`'s house style to the `GET /api/changes` shape (`status`
|
|
150
|
+
* not `state`; `previewUrl`; a `headSha` to short-render). Pure — unit-tested.
|
|
151
|
+
* @param {{prNumber?:number|null, status?:string|null, changeId:string,
|
|
152
|
+
* headSha?:string|null, previewUrl?:string|null}} c
|
|
153
|
+
* @returns {string}
|
|
154
|
+
*/
|
|
155
|
+
export function formatOperatorCandidateLine(c) {
|
|
156
|
+
const pr = typeof c.prNumber === "number" ? `#${c.prNumber}` : "#—";
|
|
157
|
+
const status = c.status ?? "?";
|
|
158
|
+
const head = c.headSha ? c.headSha.slice(0, 8) : "(no head)";
|
|
159
|
+
const urlPart = c.previewUrl ? ` ${c.previewUrl}` : "";
|
|
160
|
+
return ` PR ${pr} [${status}] ${c.changeId} ${head}${urlPart}`;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Sort an OPEN candidate queue by PR number DESC (newest PR first); candidates
|
|
165
|
+
* without a PR number sort last, then stably by changeId. Pure — unit-tested.
|
|
166
|
+
* @param {ReturnType<typeof normalizeChangesQueue>} changes
|
|
167
|
+
*/
|
|
168
|
+
export function sortQueueByPrDesc(changes) {
|
|
169
|
+
return [...(changes || [])].sort((a, b) => {
|
|
170
|
+
const ap = typeof a.prNumber === "number" ? a.prNumber : -Infinity;
|
|
171
|
+
const bp = typeof b.prNumber === "number" ? b.prNumber : -Infinity;
|
|
172
|
+
if (ap !== bp) return bp - ap;
|
|
173
|
+
return String(a.changeId).localeCompare(String(b.changeId));
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* The OPERATOR `tot pr list --tenant <t>` flow: fetch the tenant's OPEN candidate
|
|
179
|
+
* queue over `GET /api/changes` (Bearer operator secret + `X-Tot-Owner` +
|
|
180
|
+
* `x-tot-capability: ship-on-behalf`, mirroring U16) and print each candidate one per
|
|
181
|
+
* line, sorted by PR number desc. Fail-closed: no secret → honest refusal (exit 2)
|
|
182
|
+
* BEFORE any network. `fetch` is injected so it's unit-tested with no live network.
|
|
183
|
+
*
|
|
184
|
+
* @param {{ tenant:string, secret:string, storefrontUrl?:string|null }} params
|
|
185
|
+
* @param {{ fetch?:typeof fetch }} [deps]
|
|
186
|
+
* @returns {Promise<number>} process exit code
|
|
187
|
+
*/
|
|
188
|
+
export async function runPrListOperator({ tenant, secret, storefrontUrl = null }, deps = {}) {
|
|
189
|
+
const fetchImpl = deps.fetch || globalThis.fetch;
|
|
190
|
+
const base = (storefrontUrl || DEFAULT_STOREFRONT_URL).trim().replace(/\/+$/, "");
|
|
191
|
+
|
|
192
|
+
if (!secret) {
|
|
193
|
+
console.error(
|
|
194
|
+
fail(
|
|
195
|
+
"listing a tenant's queue is an OPERATOR action — it needs an operator secret",
|
|
196
|
+
"set PREVIEW_RECONCILE_SECRET (or GRANTS_ADMIN_SECRET / TOT_OPERATOR_SECRET), or pass --secret",
|
|
197
|
+
),
|
|
198
|
+
);
|
|
199
|
+
return 2;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const authHeaders = {
|
|
203
|
+
authorization: `Bearer ${secret}`,
|
|
204
|
+
"x-tot-owner": tenant,
|
|
205
|
+
"x-tot-capability": "ship-on-behalf",
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
let res;
|
|
209
|
+
try {
|
|
210
|
+
res = await fetchImpl(`${base}/api/changes`, { method: "GET", headers: authHeaders });
|
|
211
|
+
} catch (e) {
|
|
212
|
+
console.error(
|
|
213
|
+
fail(`couldn't reach the candidate queue at ${base}: ${String(e?.message || e)}`, "check --url / your network, then re-run"),
|
|
214
|
+
);
|
|
215
|
+
return 1;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
let data = {};
|
|
219
|
+
try {
|
|
220
|
+
data = await res.json();
|
|
221
|
+
} catch {
|
|
222
|
+
/* non-JSON / empty body */
|
|
223
|
+
}
|
|
224
|
+
if (!res.ok) {
|
|
225
|
+
const msg = data?.error || `HTTP ${res.status}`;
|
|
226
|
+
console.error(
|
|
227
|
+
fail(
|
|
228
|
+
`the candidate queue refused the request: ${msg}`,
|
|
229
|
+
res.status === 401 || res.status === 403
|
|
230
|
+
? "check the operator secret and that it's authorised for this tenant"
|
|
231
|
+
: "check --tenant / --url, then re-run",
|
|
232
|
+
),
|
|
233
|
+
);
|
|
234
|
+
return 1;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const changes = sortQueueByPrDesc(normalizeChangesQueue(data));
|
|
238
|
+
if (!changes.length) {
|
|
239
|
+
console.log(`No open candidate PRs for ${tenant}.`);
|
|
240
|
+
return 0;
|
|
241
|
+
}
|
|
242
|
+
console.log(`Open PRs for ${tenant}:`);
|
|
243
|
+
for (const c of changes) {
|
|
244
|
+
console.log(formatOperatorCandidateLine(c));
|
|
245
|
+
}
|
|
246
|
+
return 0;
|
|
247
|
+
}
|
|
248
|
+
|
|
101
249
|
/** @param {string[]} argv @param {any} ctx */
|
|
102
250
|
export async function run(argv, ctx) {
|
|
103
251
|
const env = process.env;
|
|
@@ -110,9 +258,34 @@ export async function run(argv, ctx) {
|
|
|
110
258
|
console.error(fail(`unknown subcommand: \`tot pr ${args.sub}\``, "tot pr list | view <N> | close <N>"));
|
|
111
259
|
return 2;
|
|
112
260
|
}
|
|
261
|
+
|
|
262
|
+
// OPERATOR MODE (U17): `--tenant <t>` lists that tenant's OPEN candidate queue with
|
|
263
|
+
// NO checkout, over the same HTTP transport `tot ship --pr` uses. Only `list` has an
|
|
264
|
+
// operator path today — `view`/`close` stay developer-only (checkout-bound).
|
|
265
|
+
if (args.tenant && `${args.tenant}`.trim()) {
|
|
266
|
+
if (args.sub !== "list") {
|
|
267
|
+
console.error(
|
|
268
|
+
fail(
|
|
269
|
+
`\`tot pr ${args.sub} --tenant\` isn't wired — only \`tot pr list --tenant\` has an operator path`,
|
|
270
|
+
"use `tot pr list --tenant <t>` to list, then act from a checkout",
|
|
271
|
+
),
|
|
272
|
+
);
|
|
273
|
+
return 2;
|
|
274
|
+
}
|
|
275
|
+
const storefrontUrl = args.url || env.TOT_STOREFRONT_URL || env.STOREFRONT_BASE_URL || DEFAULT_STOREFRONT_URL;
|
|
276
|
+
return await runPrListOperator({
|
|
277
|
+
tenant: `${args.tenant}`.trim(),
|
|
278
|
+
secret: resolveOperatorSecret(args.secret, env),
|
|
279
|
+
storefrontUrl,
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
113
283
|
if (ctx.mode !== "checkout") {
|
|
114
284
|
console.error(
|
|
115
|
-
fail(
|
|
285
|
+
fail(
|
|
286
|
+
"`tot pr` runs from inside a tenant checkout",
|
|
287
|
+
"tot clone <tenant> <dir> (then `cd` in and re-run), or `tot pr list --tenant <t>` for operator mode",
|
|
288
|
+
),
|
|
116
289
|
);
|
|
117
290
|
return 2;
|
|
118
291
|
}
|