@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
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `tot preview doctor --tenant <t>` — the merge self-serve DIAGNOSIS in the terminal.
|
|
3
|
+
*
|
|
4
|
+
* Calls the hosted merge-doctor seam `GET /<tenant>/api/preview/merge-doctor`
|
|
5
|
+
* — which composes the SAME two server-side reads the admin Publish tab builds
|
|
6
|
+
* (`GET /api/changes` + `GET /api/changes/branches`) through the PURE `analyzeMerge`
|
|
7
|
+
* taxonomy — and prints its `{ scopeKnown, verdict, findings, counts }` as the compact,
|
|
8
|
+
* ordered (blocker → warn → info) report, each finding carrying who-owns-it and its
|
|
9
|
+
* remedy/action. It composes NO per-PR forge calls of its own: one GET, one answer.
|
|
10
|
+
*
|
|
11
|
+
* READ-ONLY — it mutates nothing, so there is NO plan/confirm gate (unlike `tot
|
|
12
|
+
* accept` / `tot preview build`). It just answers "what's blocking my merges, and
|
|
13
|
+
* what do I do about it?" in seconds.
|
|
14
|
+
*
|
|
15
|
+
* AUTH — the endpoint is session-gated exactly like `GET /api/changes`, so this
|
|
16
|
+
* reaches it by EITHER of the two transports `tot accept` already speaks:
|
|
17
|
+
* - OPERATOR SECRET (operators / headless CI): Bearer + `X-Tot-Owner` on the
|
|
18
|
+
* generic storefront origin (from PREVIEW_RECONCILE_SECRET / GRANTS_ADMIN_SECRET
|
|
19
|
+
* / TOT_OPERATOR_SECRET, or `--secret`).
|
|
20
|
+
* - VIEWER SESSION (an invited developer with no secret): mint a `tot_session`
|
|
21
|
+
* from their own `tot login` on the tenant's own host (`resolveViewerTransport`).
|
|
22
|
+
* A caller who cannot see the accept queue cannot get a diagnosis of it — an auth
|
|
23
|
+
* refusal (401/403) is relayed as a clean house-style failure, not a wrapped verdict.
|
|
24
|
+
*
|
|
25
|
+
* EXIT CODE — mirrors the `scripts/tenant/gitea-merge-doctor.mjs` driver: a report
|
|
26
|
+
* with any blocker exits 1 (so a CI gate fails on a blocked merge), otherwise 0.
|
|
27
|
+
*
|
|
28
|
+
* The rendering helpers are the SHARED port in `../merge-doctor-report.mjs` (a
|
|
29
|
+
* faithful PORT of the analyzer's own `formatReport`/`attentionBanner`/
|
|
30
|
+
* `supportContext` — mergeDoctor.ts §render / the gitea-merge-doctor.mjs mirror);
|
|
31
|
+
* this package is the dependency-free published `@tokenoftrust/cli`, so it cannot
|
|
32
|
+
* import the app/scripts source, and the port is kept in sync with that taxonomy by
|
|
33
|
+
* hand. They live in their own module so `tot accept` / `tot ship` can reuse the same
|
|
34
|
+
* fetch + compact render without a cycle back through this command's transports.
|
|
35
|
+
*
|
|
36
|
+
* Dependency-free (global fetch + the shared viewer/operator transports).
|
|
37
|
+
*/
|
|
38
|
+
import { fail } from "../errors.mjs";
|
|
39
|
+
import { resolveOperatorSecret } from "./ship.mjs";
|
|
40
|
+
import { resolveViewerTransport } from "../viewer-session.mjs";
|
|
41
|
+
import {
|
|
42
|
+
DOCTOR_PATH,
|
|
43
|
+
normalizeAnalysis,
|
|
44
|
+
attentionBanner,
|
|
45
|
+
supportContext,
|
|
46
|
+
formatReport,
|
|
47
|
+
} from "../merge-doctor-report.mjs";
|
|
48
|
+
|
|
49
|
+
const DEFAULT_STOREFRONT_URL = "https://storefront.tokenoftrust.store";
|
|
50
|
+
|
|
51
|
+
const USAGE = `tot preview doctor — diagnose what's blocking this tenant's merges
|
|
52
|
+
|
|
53
|
+
tot preview doctor diagnose the current checkout's tenant
|
|
54
|
+
tot preview doctor --tenant <t> diagnose <t> (works without a checkout)
|
|
55
|
+
tot preview doctor --json emit the raw { scopeKnown, verdict, findings, counts }
|
|
56
|
+
|
|
57
|
+
Calls the hosted merge-doctor (GET /api/preview/merge-doctor), which composes the
|
|
58
|
+
SAME reads the admin Publish tab builds and runs them through the shared analyzer,
|
|
59
|
+
and prints the ordered report: blockers first, then warnings, then info — each
|
|
60
|
+
finding names who resolves it (you / on-us / housekeeping) and the exact remedy.
|
|
61
|
+
Read-only: it mutates nothing and needs no confirmation.
|
|
62
|
+
|
|
63
|
+
Exit code: 1 if the report has any blocker (so it can gate CI), else 0.
|
|
64
|
+
|
|
65
|
+
Options:
|
|
66
|
+
--tenant <appDomain> Target tenant (e.g. tokenoftrust.com). Defaults to the
|
|
67
|
+
current checkout's tenant when run inside one.
|
|
68
|
+
--url <origin> Storefront origin for the operator-secret path
|
|
69
|
+
(default: env TOT_STOREFRONT_URL or ${DEFAULT_STOREFRONT_URL}).
|
|
70
|
+
--secret <s> Operator secret (prefer the env vars below).
|
|
71
|
+
--json Print the raw analysis JSON instead of the report.
|
|
72
|
+
--help, -h Show this help.
|
|
73
|
+
|
|
74
|
+
Operator secret (from env, first found): PREVIEW_RECONCILE_SECRET,
|
|
75
|
+
GRANTS_ADMIN_SECRET, TOT_OPERATOR_SECRET (or pass --secret). Without one, an
|
|
76
|
+
invited developer's own \`tot login\` session is used.`;
|
|
77
|
+
|
|
78
|
+
/** Parse `tot preview doctor` argv. Pure — unit-testable. */
|
|
79
|
+
export function parsePreviewDoctorArgs(argv) {
|
|
80
|
+
const a = { tenant: null, url: null, secret: null, json: false, help: false };
|
|
81
|
+
for (let i = 0; i < argv.length; i++) {
|
|
82
|
+
const t = argv[i];
|
|
83
|
+
if (t === "--tenant") a.tenant = argv[++i];
|
|
84
|
+
else if (t === "--url") a.url = argv[++i];
|
|
85
|
+
else if (t === "--secret") a.secret = argv[++i];
|
|
86
|
+
else if (t === "--json") a.json = true;
|
|
87
|
+
else if (t === "--help" || t === "-h") a.help = true;
|
|
88
|
+
}
|
|
89
|
+
return a;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// The render/normalize helpers (normalizeAnalysis / attentionBanner /
|
|
93
|
+
// supportContext / formatReport) are the shared port in ../merge-doctor-report.mjs,
|
|
94
|
+
// imported at the top of this file.
|
|
95
|
+
|
|
96
|
+
// ── run ────────────────────────────────────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Fetch + render the merge-doctor report for one tenant. Resolves the SAME two
|
|
100
|
+
* transports `tot accept` uses (operator secret → generic origin + X-Tot-Owner;
|
|
101
|
+
* else viewer session → tenant host + cookie), GETs the hosted seam, and prints the
|
|
102
|
+
* ordered report (or raw JSON). `fetch`/`resolveViewerTransport` are injected so it
|
|
103
|
+
* is unit-tested with no network. Returns the process exit code (blocker → 1).
|
|
104
|
+
*
|
|
105
|
+
* @param {{ tenant:string, secret:string, storefrontUrl?:string|null,
|
|
106
|
+
* json?:boolean, env?:NodeJS.ProcessEnv }} params
|
|
107
|
+
* @param {{ fetch?:typeof fetch, resolveViewerTransport?:typeof resolveViewerTransport }} [deps]
|
|
108
|
+
* @returns {Promise<number>}
|
|
109
|
+
*/
|
|
110
|
+
export async function runDoctor(
|
|
111
|
+
{ tenant, secret, storefrontUrl = null, json = false, env = process.env },
|
|
112
|
+
deps = {},
|
|
113
|
+
) {
|
|
114
|
+
const fetchImpl = deps.fetch || globalThis.fetch;
|
|
115
|
+
const resolveViewer = deps.resolveViewerTransport || resolveViewerTransport;
|
|
116
|
+
|
|
117
|
+
// Resolve the transport — operator secret preferred, else the developer's own
|
|
118
|
+
// viewer session on the tenant host.
|
|
119
|
+
let base;
|
|
120
|
+
let authHeaders;
|
|
121
|
+
if (secret) {
|
|
122
|
+
base = (storefrontUrl || DEFAULT_STOREFRONT_URL).trim().replace(/\/+$/, "");
|
|
123
|
+
authHeaders = {
|
|
124
|
+
authorization: `Bearer ${secret}`,
|
|
125
|
+
"x-tot-owner": tenant,
|
|
126
|
+
"x-tot-capability": "ship-on-behalf",
|
|
127
|
+
};
|
|
128
|
+
} else {
|
|
129
|
+
const viewer = await resolveViewer({ tenant, env, fetchImpl });
|
|
130
|
+
if (!viewer.ok) {
|
|
131
|
+
console.error(fail(viewer.message, viewer.hint));
|
|
132
|
+
return 2;
|
|
133
|
+
}
|
|
134
|
+
base = viewer.base;
|
|
135
|
+
authHeaders = viewer.authHeaders;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
let res;
|
|
139
|
+
try {
|
|
140
|
+
res = await fetchImpl(`${base}${DOCTOR_PATH}`, { method: "GET", headers: authHeaders });
|
|
141
|
+
} catch (e) {
|
|
142
|
+
console.error(
|
|
143
|
+
fail(
|
|
144
|
+
`couldn't reach the merge-doctor at ${base}: ${String(e?.message || e)}`,
|
|
145
|
+
"check --url / your network, then re-run",
|
|
146
|
+
),
|
|
147
|
+
);
|
|
148
|
+
return 1;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
let data = {};
|
|
152
|
+
try {
|
|
153
|
+
data = await res.json();
|
|
154
|
+
} catch {
|
|
155
|
+
/* non-JSON / empty body */
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Session gating: the endpoint relays the queue read's auth refusal verbatim, so a
|
|
159
|
+
// caller who cannot see the accept queue cannot diagnose it.
|
|
160
|
+
if (res.status === 401 || res.status === 403) {
|
|
161
|
+
console.error(
|
|
162
|
+
fail(
|
|
163
|
+
`not authorized to diagnose ${tenant}'s merges: ${data?.error || `HTTP ${res.status}`}`,
|
|
164
|
+
"you need a live ship-on-behalf grant on this tenant (ask the store owner), or an operator secret authorised for it — then re-run",
|
|
165
|
+
),
|
|
166
|
+
);
|
|
167
|
+
return 2;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (!res.ok) {
|
|
171
|
+
console.error(
|
|
172
|
+
fail(
|
|
173
|
+
`the merge-doctor did not answer for ${tenant}: ${data?.error || `HTTP ${res.status}`}`,
|
|
174
|
+
"check --tenant / --url, then re-run",
|
|
175
|
+
),
|
|
176
|
+
);
|
|
177
|
+
return 1;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const analysis = normalizeAnalysis(data);
|
|
181
|
+
|
|
182
|
+
if (json) {
|
|
183
|
+
console.log(JSON.stringify(analysis, null, 2));
|
|
184
|
+
} else {
|
|
185
|
+
console.log(formatReport(analysis));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Mirror the scripts driver: any blocker fails the run so a CI gate catches it.
|
|
189
|
+
return analysis.counts.blocker > 0 ? 1 : 0;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* @param {string[]} argv
|
|
194
|
+
* @param {any} ctx — detected CLI context (ctx.tenant when in a checkout)
|
|
195
|
+
*/
|
|
196
|
+
export async function run(argv, ctx) {
|
|
197
|
+
const env = process.env;
|
|
198
|
+
const args = parsePreviewDoctorArgs(argv);
|
|
199
|
+
if (args.help) {
|
|
200
|
+
console.log(USAGE);
|
|
201
|
+
return 0;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const tenant = (args.tenant || ctx?.tenant || "").trim();
|
|
205
|
+
if (!tenant) {
|
|
206
|
+
console.error(
|
|
207
|
+
fail(
|
|
208
|
+
"no target tenant.",
|
|
209
|
+
"pass --tenant <appDomain> (e.g. --tenant tokenoftrust.com), or run inside a store checkout.",
|
|
210
|
+
),
|
|
211
|
+
);
|
|
212
|
+
return 2;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const storefrontUrl =
|
|
216
|
+
args.url || env.TOT_STOREFRONT_URL || env.STOREFRONT_BASE_URL || DEFAULT_STOREFRONT_URL;
|
|
217
|
+
|
|
218
|
+
return await runDoctor({
|
|
219
|
+
tenant,
|
|
220
|
+
secret: resolveOperatorSecret(args.secret, env),
|
|
221
|
+
storefrontUrl,
|
|
222
|
+
json: args.json,
|
|
223
|
+
env,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `tot preview retry-evidence --tenant <t> --pr <N>` — OPERATOR recovery verb.
|
|
3
|
+
*
|
|
4
|
+
* Re-push evidence for an ALREADY-BUILT candidate whose `change_report_evidence`
|
|
5
|
+
* push to MCP failed (surfaced as `evidence.pushFailed` on the candidate — see
|
|
6
|
+
* `formatOperatorCandidateLine`'s `[evidence push failed]` tag in `tot pr list
|
|
7
|
+
* --tenant`). Calls the session-authenticated `POST
|
|
8
|
+
* /api/changes/candidate-evidence-retry`, which recomputes + re-pushes evidence
|
|
9
|
+
* for the candidate's EXISTING version — no rebuild, no new content, safe to
|
|
10
|
+
* run repeatedly.
|
|
11
|
+
*
|
|
12
|
+
* DISTINCT FROM `tot preview build`: build re-runs the full reconcile against a
|
|
13
|
+
* head sha (for an orphaned/never-built PR). This targets an ALREADY-ready
|
|
14
|
+
* candidate whose content is fine but whose evidence never reached MCP — a
|
|
15
|
+
* narrower, cheaper fix for fb-1787938036559-ppmu4e.
|
|
16
|
+
*
|
|
17
|
+
* AUTH — same headless Bearer-operator-secret path as `tot preview build`:
|
|
18
|
+
* `Authorization: Bearer <secret>`, `X-Tot-Owner: <tenant>`,
|
|
19
|
+
* `X-Tot-Capability: ship-on-behalf`.
|
|
20
|
+
*
|
|
21
|
+
* Non-destructive + idempotent — no confirmation prompt.
|
|
22
|
+
*
|
|
23
|
+
* Dependency-free (global fetch).
|
|
24
|
+
*/
|
|
25
|
+
const DEFAULT_STOREFRONT_URL = "https://storefront.tokenoftrust.store";
|
|
26
|
+
|
|
27
|
+
/** Parse `tot preview retry-evidence` argv. Pure — unit-testable. */
|
|
28
|
+
export function parseRetryEvidenceArgs(argv) {
|
|
29
|
+
const a = { tenant: null, pr: null, changeId: null, url: null, secret: null, help: false };
|
|
30
|
+
for (let i = 0; i < argv.length; i++) {
|
|
31
|
+
const t = argv[i];
|
|
32
|
+
if (t === "--tenant") a.tenant = argv[++i];
|
|
33
|
+
else if (t === "--pr") a.pr = argv[++i];
|
|
34
|
+
else if (t === "--change-id") a.changeId = argv[++i];
|
|
35
|
+
else if (t === "--url") a.url = argv[++i];
|
|
36
|
+
else if (t === "--secret") a.secret = argv[++i];
|
|
37
|
+
else if (t === "--help" || t === "-h") a.help = true;
|
|
38
|
+
}
|
|
39
|
+
return a;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function renderUsage() {
|
|
43
|
+
return `tot preview retry-evidence — operator recovery: re-push a candidate's evidence to MCP
|
|
44
|
+
|
|
45
|
+
Usage:
|
|
46
|
+
tot preview retry-evidence --tenant <appDomain> --pr <N> [options]
|
|
47
|
+
tot preview retry-evidence --tenant <appDomain> --change-id <id> [options]
|
|
48
|
+
|
|
49
|
+
Re-computes and re-pushes evidence for an ALREADY-BUILT candidate whose MCP
|
|
50
|
+
push failed (shown as "[evidence push failed]" in \`tot pr list --tenant\`).
|
|
51
|
+
Does NOT rebuild or change content — safe to run repeatedly.
|
|
52
|
+
|
|
53
|
+
Options:
|
|
54
|
+
--tenant <appDomain> Target tenant (e.g. tokenoftrust.com). Defaults to the
|
|
55
|
+
current checkout's tenant when run inside one.
|
|
56
|
+
--pr <N> PR number to retry evidence for.
|
|
57
|
+
--change-id <id> Optional explicit candidate id (defaults to pr-<N>).
|
|
58
|
+
--url <origin> Storefront origin. Defaults to $TOT_STOREFRONT_URL or
|
|
59
|
+
${DEFAULT_STOREFRONT_URL}.
|
|
60
|
+
--secret <s> Operator secret. Prefer the env vars below.
|
|
61
|
+
--help, -h Show this help.
|
|
62
|
+
|
|
63
|
+
Auth (operator secret, from env, first found):
|
|
64
|
+
PREVIEW_RECONCILE_SECRET, GRANTS_ADMIN_SECRET, TOT_OPERATOR_SECRET`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* @param {string[]} argv
|
|
69
|
+
* @param {any} ctx — detected CLI context (ctx.tenant when in a checkout)
|
|
70
|
+
*/
|
|
71
|
+
export async function run(argv, ctx) {
|
|
72
|
+
const args = parseRetryEvidenceArgs(argv);
|
|
73
|
+
if (args.help) {
|
|
74
|
+
console.log(renderUsage());
|
|
75
|
+
return 0;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const base = (args.url || process.env.TOT_STOREFRONT_URL || process.env.STOREFRONT_BASE_URL || DEFAULT_STOREFRONT_URL)
|
|
79
|
+
.trim()
|
|
80
|
+
.replace(/\/+$/, "");
|
|
81
|
+
|
|
82
|
+
const tenant = (args.tenant || ctx?.tenant || "").trim();
|
|
83
|
+
if (!tenant) {
|
|
84
|
+
console.error(
|
|
85
|
+
"✗ no target tenant.\n\n → next: pass --tenant <appDomain> (e.g. --tenant tokenoftrust.com), " +
|
|
86
|
+
"or run inside a store checkout.",
|
|
87
|
+
);
|
|
88
|
+
return 2;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const prRaw = args.pr;
|
|
92
|
+
const pr = prRaw != null && `${prRaw}`.trim() && Number.isFinite(Number(prRaw)) ? Number(prRaw) : null;
|
|
93
|
+
const changeId = (args.changeId || "").trim() || null;
|
|
94
|
+
if (pr == null && !changeId) {
|
|
95
|
+
console.error(
|
|
96
|
+
"✗ no candidate to retry evidence for.\n\n → next: pass --pr <N> (the PR number), or --change-id <id>.",
|
|
97
|
+
);
|
|
98
|
+
return 2;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const secret = (args.secret || process.env.PREVIEW_RECONCILE_SECRET || process.env.GRANTS_ADMIN_SECRET || process.env.TOT_OPERATOR_SECRET || "").trim();
|
|
102
|
+
if (!secret) {
|
|
103
|
+
console.error(
|
|
104
|
+
"✗ no operator secret.\n\n → next: set PREVIEW_RECONCILE_SECRET (or GRANTS_ADMIN_SECRET / " +
|
|
105
|
+
"TOT_OPERATOR_SECRET) in the environment.",
|
|
106
|
+
);
|
|
107
|
+
return 2;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const body = {
|
|
111
|
+
...(pr != null ? { pr } : {}),
|
|
112
|
+
...(changeId ? { changeId } : {}),
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
let res;
|
|
116
|
+
try {
|
|
117
|
+
res = await fetch(`${base}/api/changes/candidate-evidence-retry`, {
|
|
118
|
+
method: "POST",
|
|
119
|
+
headers: {
|
|
120
|
+
"content-type": "application/json",
|
|
121
|
+
authorization: `Bearer ${secret}`,
|
|
122
|
+
"x-tot-owner": tenant,
|
|
123
|
+
"x-tot-capability": "ship-on-behalf",
|
|
124
|
+
},
|
|
125
|
+
body: JSON.stringify(body),
|
|
126
|
+
});
|
|
127
|
+
} catch (e) {
|
|
128
|
+
console.error(`✗ could not reach ${base}: ${e?.message || e}\n\n → next: check --url / your network.`);
|
|
129
|
+
return 1;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
let data = {};
|
|
133
|
+
try {
|
|
134
|
+
data = await res.json();
|
|
135
|
+
} catch {
|
|
136
|
+
/* non-JSON error body */
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const label = pr != null ? `PR #${pr}` : changeId;
|
|
140
|
+
|
|
141
|
+
if (res.ok && data?.ok && data?.pushed) {
|
|
142
|
+
console.log(`✓ Evidence re-pushed for ${label} on ${tenant} (promotable: ${data.promotable}).`);
|
|
143
|
+
return 0;
|
|
144
|
+
}
|
|
145
|
+
if (res.ok && data?.ok && !data?.pushed) {
|
|
146
|
+
console.error(`✗ Evidence push still failed for ${label} on ${tenant}.`);
|
|
147
|
+
if (data.error) console.error(` • ${data.error}`);
|
|
148
|
+
console.error("\n → next: check MCP reachability, then re-run this command.");
|
|
149
|
+
return 1;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const message = data?.error || `HTTP ${res.status}`;
|
|
153
|
+
console.error(`✗ Retry did not succeed (HTTP ${res.status}).`);
|
|
154
|
+
console.error(` • ${message}`);
|
|
155
|
+
return 1;
|
|
156
|
+
}
|
package/src/commands/preview.mjs
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*
|
|
6
6
|
* tot dev run your store locally with save→reload
|
|
7
7
|
* tot preview push it to a reviewable preview (validate → reconcile → compliance) ← you are here
|
|
8
|
-
* tot ship promote a reconciled preview live
|
|
8
|
+
* tot ship promote a reconciled preview live
|
|
9
9
|
*
|
|
10
10
|
* The whole preview flow (validate, push the preview ref, open/update the PR-backed
|
|
11
11
|
* candidate, stream the reconcile/compliance/preview result) lives in submit.mjs — this
|
|
@@ -45,12 +45,12 @@ export function postRunHint(code, alias) {
|
|
|
45
45
|
/**
|
|
46
46
|
* @param {string[]} argv
|
|
47
47
|
* @param {any} ctx
|
|
48
|
-
* @param {{ alias?: string|null }} [opts]
|
|
48
|
+
* @param {{ alias?: string|null }} [opts] - `alias` is the old verb the developer typed
|
|
49
49
|
* (`"submit"` / `"deploy"`) when this flow is reached as a teaching alias; null/omitted
|
|
50
50
|
* for the first-class `tot preview`.
|
|
51
51
|
*/
|
|
52
52
|
export async function run(argv, ctx, { alias = null } = {}) {
|
|
53
|
-
// `tot preview build …` is the OPERATOR build-on-demand subcommand
|
|
53
|
+
// `tot preview build …` is the OPERATOR build-on-demand subcommand —
|
|
54
54
|
// a distinct verb from the developer preview flow, so it's dispatched BEFORE the
|
|
55
55
|
// submit-flow arg parse. Only the first-class `tot preview` carries it (not the
|
|
56
56
|
// `submit`/`deploy` teaching aliases, which are the push-your-checkout flow).
|
|
@@ -59,6 +59,22 @@ export async function run(argv, ctx, { alias = null } = {}) {
|
|
|
59
59
|
return runBuild(argv.slice(1), ctx);
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
// `tot preview doctor …` is the read-only merge DIAGNOSIS subcommand —
|
|
63
|
+
// a distinct verb from the developer preview flow, dispatched before the submit-
|
|
64
|
+
// flow arg parse (like `build`). Only the first-class `tot preview` carries it.
|
|
65
|
+
if (!alias && argv[0] === "doctor") {
|
|
66
|
+
const { run: runDoctor } = await import("./preview-doctor.mjs");
|
|
67
|
+
return runDoctor(argv.slice(1), ctx);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// `tot preview retry-evidence …` is the OPERATOR recovery subcommand for a
|
|
71
|
+
// candidate whose evidence push to MCP failed (fb-1787938036559-ppmu4e) —
|
|
72
|
+
// dispatched before the submit-flow arg parse, like `build`/`doctor`.
|
|
73
|
+
if (!alias && argv[0] === "retry-evidence") {
|
|
74
|
+
const { run: runRetryEvidence } = await import("./preview-retry-evidence.mjs");
|
|
75
|
+
return runRetryEvidence(argv.slice(1), ctx);
|
|
76
|
+
}
|
|
77
|
+
|
|
62
78
|
const verb = alias || "preview";
|
|
63
79
|
const args = parseArgs(argv);
|
|
64
80
|
|
package/src/commands/revert.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `tot revert --preview <PR|integration-sha>`
|
|
2
|
+
* `tot revert --preview <PR|integration-sha>` — OPERATOR verb: REMOVE
|
|
3
3
|
* already-integrated content from the protected `preview` AGGREGATE by creating a
|
|
4
4
|
* NEW auditable revert commit, then rebuilding the aggregate. It is the explicit,
|
|
5
5
|
* safe answer to "several candidates are accepted, but one must be removed"
|
|
@@ -15,10 +15,10 @@
|
|
|
15
15
|
* TARGET — `--preview <target>` names EITHER a PR number (`42` or `#42`) OR an exact
|
|
16
16
|
* `preview` integration SHA. A numeric target is sent as `prNumber`; anything else as
|
|
17
17
|
* `integrationSha`. The server resolves either to the exact integration commit to
|
|
18
|
-
* undo (
|
|
18
|
+
* undo (the resolver, inside the tenant-serialized queue lock), and REFUSES a
|
|
19
19
|
* target that was never integrated or has already been reverted.
|
|
20
20
|
*
|
|
21
|
-
* TRANSPORT — `POST /api/changes/revert` (the ONE call site of
|
|
21
|
+
* TRANSPORT — `POST /api/changes/revert` (the ONE call site of
|
|
22
22
|
* `TenantIntegrationQueue.enqueueRevert`). Like `tot accept` / `tot ship`, the CLI
|
|
23
23
|
* reaches it with the OPERATOR-SECRET Bearer transport (`resolveOperatorSecret` +
|
|
24
24
|
* `X-Tot-Owner` + `x-tot-capability`). The response is the honest terminal aggregate
|
|
@@ -183,7 +183,7 @@ export async function runRevert(
|
|
|
183
183
|
const base = (storefrontUrl || DEFAULT_STOREFRONT_URL).trim().replace(/\/+$/, "");
|
|
184
184
|
const label = prNumber != null ? `PR #${prNumber}` : `integration ${integrationSha}`;
|
|
185
185
|
|
|
186
|
-
// 1. State the EXACT plan (shared
|
|
186
|
+
// 1. State the EXACT plan (shared affordance) — a NEW revert commit on
|
|
187
187
|
// preview, NO force-reset, NO touch to main/live — and gate on a confirm.
|
|
188
188
|
const planLines = planForAction({
|
|
189
189
|
action: "revert",
|
|
@@ -232,7 +232,7 @@ export async function runRevert(
|
|
|
232
232
|
...(message ? { message } : {}),
|
|
233
233
|
};
|
|
234
234
|
|
|
235
|
-
// 3. POST the honest revert path (
|
|
235
|
+
// 3. POST the honest revert path (queue enqueueRevert) and render terminal state.
|
|
236
236
|
let res;
|
|
237
237
|
try {
|
|
238
238
|
res = await fetchImpl(`${base}/api/changes/revert`, {
|
|
@@ -7,17 +7,17 @@
|
|
|
7
7
|
* tot rollback <versionId> re-point the live channel back to that version
|
|
8
8
|
*
|
|
9
9
|
* This does NOT reimplement pointer moves. Every selection and every move goes
|
|
10
|
-
* through
|
|
10
|
+
* through the MCP-side live-pointer seam (`promotion_status` /
|
|
11
11
|
* `promotion_rollback`, tot-mcp `src/modules/mcp/change/promotion-pointer-tools.ts`
|
|
12
12
|
* + `promotion-pointer-store.ts`) — the SAME primitive `tot ship`'s eventual
|
|
13
|
-
* orchestrator composes
|
|
14
|
-
* There is no
|
|
13
|
+
* orchestrator composes.
|
|
14
|
+
* There is no ship-orchestrator service in-tree yet, so this calls that seam
|
|
15
15
|
* DIRECTLY; when the orchestrator lands it should absorb the physical
|
|
16
16
|
* publish+verify this command currently leaves to it (see the seam's own
|
|
17
17
|
* "NOT a live claim" caveat, echoed in `reportRolledBack` below) — a follow-on can
|
|
18
18
|
* route through it instead without changing this file's UX.
|
|
19
19
|
*
|
|
20
|
-
* Fail-closed
|
|
20
|
+
* Fail-closed: rollback selects
|
|
21
21
|
* an immutable prior digest/versionId recorded by a PRIOR promote; a missing or
|
|
22
22
|
* ineligible target is refused with a clear next step, never a silent no-op dressed
|
|
23
23
|
* up as success. The eligibility check and the pointer move both come from the
|
|
@@ -41,7 +41,7 @@ import { startProgress } from "../progress.mjs";
|
|
|
41
41
|
|
|
42
42
|
const DEFAULT_MCP_URL = "https://mcp.tokenoftrust.com";
|
|
43
43
|
|
|
44
|
-
// The only publish channel the seam is exercised against today (see the
|
|
44
|
+
// The only publish channel the seam is exercised against today (see the tests
|
|
45
45
|
// and `promotion_set`'s own default). A future multi-target store can override with
|
|
46
46
|
// `--target`; nothing here assumes there's only ever one.
|
|
47
47
|
const DEFAULT_TARGET = "production";
|
|
@@ -63,7 +63,9 @@ const USAGE = `tot rollback [<versionId>] — instant re-point to a prior live v
|
|
|
63
63
|
|
|
64
64
|
/** Parse `tot rollback` argv. Pure. Deliberately NO --yes/--force (see the header). */
|
|
65
65
|
export function parseRollbackArgs(argv) {
|
|
66
|
+
/** @type {{ versionId: string|null, target: string|null, mcp: string|null, identity: string|null, help: boolean }} */
|
|
66
67
|
const a = { versionId: null, target: null, mcp: null, identity: null, help: false };
|
|
68
|
+
/** @type {string[]} */
|
|
67
69
|
const positional = [];
|
|
68
70
|
for (let i = 0; i < argv.length; i++) {
|
|
69
71
|
const t = argv[i];
|
|
@@ -138,11 +140,11 @@ export function normalizeRollbackResult(r) {
|
|
|
138
140
|
};
|
|
139
141
|
}
|
|
140
142
|
|
|
141
|
-
// ─── Revision links (pure
|
|
143
|
+
// ─── Revision links (pure) ───────────────────────────────────────────────
|
|
142
144
|
|
|
143
145
|
/**
|
|
144
146
|
* The immutable `/preview/<tenant>/rev/<sha>/` deep-link for a promoted
|
|
145
|
-
* `versionId` —
|
|
147
|
+
* `versionId` — this route, and per `gitea-only-tenant-content-authority` the
|
|
146
148
|
* `versionId` recorded by a promote IS the tenant Gitea sha, so no extra lookup
|
|
147
149
|
* is needed to build it. `null` when there's no `base` (storefront origin) or no
|
|
148
150
|
* `versionId` to link, so a caller can render the history line either way.
|
|
@@ -177,10 +179,10 @@ export function rollbackCandidates(history, currentVersionId) {
|
|
|
177
179
|
}
|
|
178
180
|
|
|
179
181
|
/** Render the `tot rollback` (no args) history listing — the rollback-target
|
|
180
|
-
* picker
|
|
181
|
-
*
|
|
182
|
-
* @param {{ tenant:string, target:string, current:
|
|
183
|
-
* candidates:
|
|
182
|
+
* picker, each entry annotated with its immutable `/rev/<sha>` link
|
|
183
|
+
* when a storefront origin is available. Pure — unit-tested.
|
|
184
|
+
* @param {{ tenant:string, target:string, current:any,
|
|
185
|
+
* candidates:any[], revisionBase?:string|null }} input
|
|
184
186
|
* @returns {string[]}
|
|
185
187
|
*/
|
|
186
188
|
export function renderHistory({ tenant, target, current, candidates, revisionBase }) {
|
|
@@ -209,7 +211,7 @@ export function renderHistory({ tenant, target, current, candidates, revisionBas
|
|
|
209
211
|
*/
|
|
210
212
|
export function renderRollbackPreview({ tenant, target, toVersionId, preview }) {
|
|
211
213
|
const lines = ["", ` This rollback will change the LIVE site for ${tenant}:`];
|
|
212
|
-
const current = preview.pointer?.current;
|
|
214
|
+
const current = /** @type {any} */ (preview.pointer?.current);
|
|
213
215
|
if (current) lines.push(` ${target}: ${current.versionId} → ${toVersionId}`);
|
|
214
216
|
else lines.push(` ${target}: → ${toVersionId}`);
|
|
215
217
|
return lines;
|
|
@@ -225,7 +227,7 @@ export function renderRollbackPreview({ tenant, target, toVersionId, preview })
|
|
|
225
227
|
* @param {{callTool:Function}} client an MCP client (real or mock)
|
|
226
228
|
* @param {{ tenant:string, target:string, toVersionId:string|null,
|
|
227
229
|
* revisionBase?:string|null }} params `revisionBase` is the storefront origin
|
|
228
|
-
* the listing's `/rev/<sha>` links resolve against
|
|
230
|
+
* the listing's `/rev/<sha>` links resolve against; omit to list
|
|
229
231
|
* without links.
|
|
230
232
|
* @param {{ interactive?:()=>boolean, confirm?:(q:string,d:boolean)=>Promise<boolean>,
|
|
231
233
|
* progress?:boolean }} [deps]
|
|
@@ -258,7 +260,7 @@ export async function runRollback(client, { tenant, target, toVersionId, revisio
|
|
|
258
260
|
);
|
|
259
261
|
return 1;
|
|
260
262
|
}
|
|
261
|
-
const candidates = rollbackCandidates(status.history, status.current?.versionId ?? null);
|
|
263
|
+
const candidates = rollbackCandidates(status.history, /** @type {any} */ (status).current?.versionId ?? null);
|
|
262
264
|
for (const line of renderHistory({ tenant, target, current: status.current, candidates, revisionBase })) {
|
|
263
265
|
console.log(line);
|
|
264
266
|
}
|
|
@@ -375,8 +377,8 @@ export async function run(argv, ctx) {
|
|
|
375
377
|
const tenant = ctx.tenant;
|
|
376
378
|
const target = args.target?.trim() || DEFAULT_TARGET;
|
|
377
379
|
const baseUrl = args.mcp || env.MCP_BASE_URL || env.TOT_MCP_URL || DEFAULT_MCP_URL;
|
|
378
|
-
// The storefront origin the listing's /rev/<sha> links resolve against
|
|
379
|
-
//
|
|
380
|
+
// The storefront origin the listing's /rev/<sha> links resolve against
|
|
381
|
+
// — owner == appDomain on this platform, mirroring go-live.mjs's default.
|
|
380
382
|
const revisionBase = env.TOT_STOREFRONT_URL || (tenant ? `https://${tenant}` : null);
|
|
381
383
|
const client = createMcpClient(baseUrl);
|
|
382
384
|
try {
|