@tokenoftrust/cli 1.4.0-rc.10 → 1.4.0-rc.12
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 +7 -4
- package/bin/tot.mjs +35 -2
- package/package.json +1 -1
- package/src/candidate-state.mjs +56 -16
- package/src/commands/accept.mjs +247 -0
- package/src/commands/go-live.mjs +482 -0
- package/src/commands/pr.mjs +21 -7
- package/src/commands/preview-build.mjs +225 -0
- package/src/commands/preview.mjs +9 -0
- package/src/commands/retire.mjs +203 -0
- package/src/commands/rollback.mjs +401 -0
- package/src/commands/ship.mjs +990 -28
- package/src/commands/submit.mjs +259 -28
- package/src/plan.mjs +160 -0
- package/src/sample.mjs +27 -1
package/README.md
CHANGED
|
@@ -9,7 +9,8 @@ tot clone # list the stores you can build on
|
|
|
9
9
|
tot clone <tenant> my-store # mirrors `git clone`; dir defaults to <tenant>
|
|
10
10
|
cd my-store
|
|
11
11
|
tot dev # run it locally with save→reload — no Docker needed
|
|
12
|
-
tot
|
|
12
|
+
tot preview # bundle your edits into a compliance-reviewed preview
|
|
13
|
+
tot ship # promote a reconciled preview live (diff + one confirm)
|
|
13
14
|
```
|
|
14
15
|
|
|
15
16
|
**Prerequisites: Node.js and an invite. Nothing else.** `tot dev` downloads the
|
|
@@ -23,9 +24,11 @@ be fetched.)
|
|
|
23
24
|
| Command | Status | What it does |
|
|
24
25
|
| --- | --- | --- |
|
|
25
26
|
| `tot clone [<tenant>] [<dir>]` | **built** | Clone a store you're entitled to build on (mirrors `git clone`), with an authenticated remote configured. Dir defaults to `<tenant>`. No arg → list your stores. |
|
|
26
|
-
| `tot validate` | next | Lint your store before you
|
|
27
|
+
| `tot validate` | next | Lint your store before you preview. |
|
|
27
28
|
| `tot dev` | **built** | Run your store locally with save→reload — NATIVELY (no Docker; falls back to it with `--docker` or automatically if the native artifact isn't available). |
|
|
28
|
-
| `tot
|
|
29
|
+
| `tot preview` | **built** | Bundle your edits into a compliance-reviewed preview (validates, auto-commits the known content trees, pushes the preview ref, opens/updates a candidate PR, reports the reconcile/compliance verdict + preview URL). `tot submit` / `tot deploy` still work as teaching aliases for this same flow. |
|
|
30
|
+
| `tot ship` | **built** | Promote a reconciled preview live: always shows a diff-vs-live and asks for one `[y/N]` confirm (no `--yes`, refuses outside a terminal); records an approval request if you're not authorised to ship yourself. |
|
|
31
|
+
| `tot pr [list\|view\|close]` | **built** | See and manage the candidate PRs `tot preview` opens (`gh pr`-shaped). |
|
|
29
32
|
| `tot doctor` | built | Check this machine is ready and show which context `tot` detected. |
|
|
30
33
|
|
|
31
34
|
## Context-aware
|
|
@@ -42,7 +45,7 @@ The same `tot` does the right thing wherever you run it (walks up like `git`):
|
|
|
42
45
|
|
|
43
46
|
`tot` talks to the Token of Trust MCP (default `https://mcp.tokenoftrust.com`, override with `--mcp` or `MCP_BASE_URL`). It is **single-plane**: the only identity is **you**, signed in against the MCP over OAuth.
|
|
44
47
|
|
|
45
|
-
- Run `tot login` once — it opens your browser (or falls back to a device code on a headless box), you sign in as yourself, and the session is cached at `~/.tot/credentials.json` and refreshed silently. Every later command (`tot clone`, `tot start`, `tot
|
|
48
|
+
- Run `tot login` once — it opens your browser (or falls back to a device code on a headless box), you sign in as yourself, and the session is cached at `~/.tot/credentials.json` and refreshed silently. Every later command (`tot clone`, `tot start`, `tot preview`, `tot ship`, …) runs as you, with no re-auth. Entitlement is derived server-side from your ToT memberships.
|
|
46
49
|
- Not signed in? On a terminal, `tot start` / `tot clone` **offer to sign you in right there** and continue in-flow — no "run `tot login`, then re-run".
|
|
47
50
|
- The old operator env-triple (`TOT_API_KEY` / `TOT_SECRET_KEY` / `TOT_APP_DOMAIN`) **no longer signs the CLI in** — tot-mcp went OAuth-first on 2026-07-23. If those vars are set, `tot` prints a one-line advisory and uses your `tot login` session anyway; it never reads them for auth.
|
|
48
51
|
|
package/bin/tot.mjs
CHANGED
|
@@ -14,7 +14,9 @@
|
|
|
14
14
|
* tot validate lint your store before you submit ← built
|
|
15
15
|
* tot dev run your store locally with save→reload ← built (monorepo: host astro; standalone: runs the published runner image)
|
|
16
16
|
* tot preview push your store to a reviewable preview ← built (validate + push preview ref; MCP preview_status read-back). `submit`/`deploy` are teaching aliases.
|
|
17
|
-
* tot ship promote a reconciled preview live ←
|
|
17
|
+
* tot ship promote a reconciled preview live ← built (diff-vs-live + y/N confirm → change_accept; refuses non-TTY / unreconciled)
|
|
18
|
+
* tot accept / tot merge merge a PR into main (operator verb) ← built (plan + y/N confirm → the SAME change_accept ship uses; no deploy; refuses non-TTY without --yes)
|
|
19
|
+
* tot rollback [<version>] instant re-point to a prior live version ← built (u3 promotion_status/promotion_rollback seam; diff + y/N confirm; refuses non-TTY / ineligible)
|
|
18
20
|
* tot pr list / view / close your candidate PRs ← built (candidate_status/candidate_close; gh-pr-shaped)
|
|
19
21
|
* tot doctor check this machine is ready
|
|
20
22
|
* tot ideas copy-paste AI prompts that reliably wow
|
|
@@ -62,7 +64,11 @@ tot — Token of Trust developer CLI
|
|
|
62
64
|
tot validate lint your store before you submit
|
|
63
65
|
tot dev run your store locally with save→reload
|
|
64
66
|
tot preview push your store to a reviewable preview
|
|
65
|
-
tot ship promote a reconciled preview live (
|
|
67
|
+
tot ship promote a reconciled preview live (diff → confirm → ship)
|
|
68
|
+
tot accept / tot merge merge a PR into main — operator verb, no deploy (plan → confirm → merge)
|
|
69
|
+
tot rollback [<version>] instant re-point to a prior live version (list → confirm → rollback)
|
|
70
|
+
tot retire evict a candidate PR's preview to reclaim space — operator verb, rebuildable (plan → confirm → evict)
|
|
71
|
+
tot go-live cut the apex domain over to the storefront (readiness → confirm → cutover)
|
|
66
72
|
tot pr list / view / close your candidate PRs
|
|
67
73
|
tot doctor check this machine is ready
|
|
68
74
|
tot ideas copy-paste AI prompts that reliably wow
|
|
@@ -168,6 +174,33 @@ async function dispatch(cmd, rest, ctx) {
|
|
|
168
174
|
return run(rest, ctx);
|
|
169
175
|
}
|
|
170
176
|
|
|
177
|
+
// `accept` merges a PR into main (no deploy) — a DISTINCT operator verb from
|
|
178
|
+
// `ship`, backed by the SAME change_accept pipeline (see accept.mjs's header).
|
|
179
|
+
// `merge` is a first-class alias, not a teaching nudge: both dispatch straight
|
|
180
|
+
// to the same command.
|
|
181
|
+
if (cmd === "accept" || cmd === "merge") {
|
|
182
|
+
const { run } = await import("../src/commands/accept.mjs");
|
|
183
|
+
return run(rest, ctx);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (cmd === "rollback") {
|
|
187
|
+
const { run } = await import("../src/commands/rollback.mjs");
|
|
188
|
+
return run(rest, ctx);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// `retire` evicts a candidate PR's hosted preview to reclaim space (unit U7) —
|
|
192
|
+
// a DISTINCT operator verb from `accept`/reject: retire touches no change
|
|
193
|
+
// lifecycle and is reversible-by-rebuild (`tot preview build`). See retire.mjs.
|
|
194
|
+
if (cmd === "retire") {
|
|
195
|
+
const { run } = await import("../src/commands/retire.mjs");
|
|
196
|
+
return run(rest, ctx);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (cmd === "go-live") {
|
|
200
|
+
const { run } = await import("../src/commands/go-live.mjs");
|
|
201
|
+
return run(rest, ctx);
|
|
202
|
+
}
|
|
203
|
+
|
|
171
204
|
if (cmd === "pr") {
|
|
172
205
|
const { run } = await import("../src/commands/pr.mjs");
|
|
173
206
|
return run(rest, ctx);
|
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.12",
|
|
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/candidate-state.mjs
CHANGED
|
@@ -13,8 +13,14 @@
|
|
|
13
13
|
* - a terminal-roll (the active candidate was merged/closed) records the fresh
|
|
14
14
|
* candidate it rolled to, so you're never wedged submitting to a dead PR.
|
|
15
15
|
*
|
|
16
|
-
* ONE file, `~/.tot/candidates.json`, a map keyed by `<mcpUrl>::<repo>`
|
|
17
|
-
*
|
|
16
|
+
* ONE file, `~/.tot/candidates.json`, a map keyed by `<mcpUrl>::<repo>` on the
|
|
17
|
+
* DEFAULT branch and `<mcpUrl>::<repo>::<branch>` on any other (u4 — branch-bound
|
|
18
|
+
* candidates): a different MCP, repo, OR non-default git branch is a different
|
|
19
|
+
* candidate namespace, so a feature branch gets its OWN candidate PR instead of
|
|
20
|
+
* fighting main's over the same handle. The default branch deliberately keeps the
|
|
21
|
+
* OLD branch-less key so existing devs' state is byte-identical (zero migration),
|
|
22
|
+
* and a branch-scoped read that misses FALLS BACK to that old key so state written
|
|
23
|
+
* before the rekey (or by the default branch) is never orphaned. Same atomic-write
|
|
18
24
|
* discipline as last-tenant.mjs (0600 in a 0700 dir, write-tmp-then-rename).
|
|
19
25
|
* Dependency-free (node:fs/os/path). `TOT_HOME` overrides home (tests).
|
|
20
26
|
*/
|
|
@@ -31,11 +37,38 @@ export function defaultCandidateStatePath(env = process.env) {
|
|
|
31
37
|
return join(home, ".tot", "candidates.json");
|
|
32
38
|
}
|
|
33
39
|
|
|
34
|
-
/**
|
|
35
|
-
|
|
40
|
+
/**
|
|
41
|
+
* Branch names that are treated as the repo's DEFAULT — their candidates keep the
|
|
42
|
+
* OLD branch-less key (zero migration). A null/empty/detached ("HEAD") branch is
|
|
43
|
+
* treated as default too, so an environment where the branch can't be resolved
|
|
44
|
+
* degrades to exactly today's behavior rather than minting a spurious namespace.
|
|
45
|
+
*/
|
|
46
|
+
export const DEFAULT_BRANCHES = new Set(["main", "master"]);
|
|
47
|
+
|
|
48
|
+
/** Whether `branch` should use the OLD branch-less candidate key. Pure. */
|
|
49
|
+
export function isDefaultBranch(branch) {
|
|
50
|
+
return !branch || branch === "HEAD" || DEFAULT_BRANCHES.has(branch);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** The legacy (branch-less) namespace key — today's exact `<mcpUrl>::<repo>`. */
|
|
54
|
+
function legacyStateKey(mcpUrl, repo) {
|
|
36
55
|
return `${mcpUrl}::${repo}`;
|
|
37
56
|
}
|
|
38
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Namespace key for one (MCP, repo, branch) candidate pointer. The DEFAULT branch
|
|
60
|
+
* keeps the legacy `<mcpUrl>::<repo>` key byte-for-byte (zero migration); any other
|
|
61
|
+
* branch gets its own `<mcpUrl>::<repo>::<branch>` namespace. Pure.
|
|
62
|
+
*/
|
|
63
|
+
function stateKey(mcpUrl, repo, branch) {
|
|
64
|
+
return isDefaultBranch(branch) ? legacyStateKey(mcpUrl, repo) : `${mcpUrl}::${repo}::${branch}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Extract a usable changeId from a stored record, or null. Pure. */
|
|
68
|
+
function recordChangeId(rec) {
|
|
69
|
+
return rec && typeof rec.changeId === "string" && rec.changeId ? rec.changeId : null;
|
|
70
|
+
}
|
|
71
|
+
|
|
39
72
|
function readMap(filePath) {
|
|
40
73
|
try {
|
|
41
74
|
const parsed = JSON.parse(readFileSync(filePath, "utf8"));
|
|
@@ -54,26 +87,33 @@ function writeMap(filePath, map) {
|
|
|
54
87
|
}
|
|
55
88
|
|
|
56
89
|
/**
|
|
57
|
-
* The remembered active changeId for `(mcpUrl, repo)`, or null when there
|
|
58
|
-
* one (absent/unreadable/malformed) — a miss means "use the stable default".
|
|
59
|
-
*
|
|
90
|
+
* The remembered active changeId for `(mcpUrl, repo, branch)`, or null when there
|
|
91
|
+
* isn't one (absent/unreadable/malformed) — a miss means "use the stable default".
|
|
92
|
+
* On a non-default branch whose branch-scoped key misses, FALLS BACK to the legacy
|
|
93
|
+
* branch-less key so state written before the rekey (or by the default branch)
|
|
94
|
+
* isn't orphaned. Never throws.
|
|
60
95
|
*/
|
|
61
|
-
export function readActiveChangeId(filePath, { mcpUrl, repo }) {
|
|
62
|
-
const
|
|
63
|
-
|
|
96
|
+
export function readActiveChangeId(filePath, { mcpUrl, repo, branch }) {
|
|
97
|
+
const map = readMap(filePath);
|
|
98
|
+
const primary = recordChangeId(map[stateKey(mcpUrl, repo, branch)]);
|
|
99
|
+
if (primary) return primary;
|
|
100
|
+
// Legacy fallback: a branch-scoped miss reads the old branch-less key (a no-op
|
|
101
|
+
// when we're already on the default branch, which IS the legacy key).
|
|
102
|
+
if (!isDefaultBranch(branch)) return recordChangeId(map[legacyStateKey(mcpUrl, repo)]);
|
|
103
|
+
return null;
|
|
64
104
|
}
|
|
65
105
|
|
|
66
|
-
/** Remember `changeId` as the active candidate for `(mcpUrl, repo)`, atomically. */
|
|
67
|
-
export function writeActiveChangeId(filePath, { mcpUrl, repo, changeId }) {
|
|
106
|
+
/** Remember `changeId` as the active candidate for `(mcpUrl, repo, branch)`, atomically. */
|
|
107
|
+
export function writeActiveChangeId(filePath, { mcpUrl, repo, branch, changeId }) {
|
|
68
108
|
const map = readMap(filePath);
|
|
69
|
-
map[stateKey(mcpUrl, repo)] = { changeId, updatedAt: Date.now() };
|
|
109
|
+
map[stateKey(mcpUrl, repo, branch)] = { changeId, updatedAt: Date.now() };
|
|
70
110
|
writeMap(filePath, map);
|
|
71
111
|
}
|
|
72
112
|
|
|
73
|
-
/** Forget the active candidate for `(mcpUrl, repo)` (e.g. after closing it). */
|
|
74
|
-
export function clearActiveChangeId(filePath, { mcpUrl, repo }) {
|
|
113
|
+
/** Forget the active candidate for `(mcpUrl, repo, branch)` (e.g. after closing it). */
|
|
114
|
+
export function clearActiveChangeId(filePath, { mcpUrl, repo, branch }) {
|
|
75
115
|
const map = readMap(filePath);
|
|
76
|
-
const key = stateKey(mcpUrl, repo);
|
|
116
|
+
const key = stateKey(mcpUrl, repo, branch);
|
|
77
117
|
if (key in map) {
|
|
78
118
|
delete map[key];
|
|
79
119
|
writeMap(filePath, map);
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `tot accept --tenant <t> --pr <N>` (alias: `tot merge`) — OPERATOR verb: MERGE
|
|
3
|
+
* a PR into main. DISTINCT from `tot ship`: per board decision
|
|
4
|
+
* `operator-verb-and-hosting-model`, accepting a PR merges it into main; main
|
|
5
|
+
* becomes default-hosted on preview + live only after the NEXT `tot ship` /
|
|
6
|
+
* deploy — accept itself does not deploy. (`tot ship` run by the developer in
|
|
7
|
+
* their own checkout stays accept-THEN-deploy in one gated step — see
|
|
8
|
+
* `ship-context-dependent-semantics` in `../plan.mjs`; this verb is the
|
|
9
|
+
* explicit, deploy-free half of that, for an operator who just wants the merge.)
|
|
10
|
+
*
|
|
11
|
+
* TRANSPORT — reuses the EXISTING accept pipeline `tot ship` already drives over
|
|
12
|
+
* MCP (merge → reconcile → promote → verify): the SAME `change_accept` tool,
|
|
13
|
+
* the SAME session/auth pattern (`createMcpClient` + `establishSession`), and
|
|
14
|
+
* the SAME result normaliser/poll (`normalizeChangeResult` / `pollChangeShipped`,
|
|
15
|
+
* imported straight from `./ship.mjs` rather than re-implemented). No new
|
|
16
|
+
* transport, no new wire contract.
|
|
17
|
+
*
|
|
18
|
+
* TARGET RESOLUTION — `--pr N --tenant t` names the PR, but there is no
|
|
19
|
+
* PR→change-record lookup on the wire, and even if there were, this CLI's own
|
|
20
|
+
* identity may not have cross-tenant forge read access to resolve one for a PR
|
|
21
|
+
* that isn't the developer's own (`resolveChangeRecordId` in ship.mjs only works
|
|
22
|
+
* because it reads the DEVELOPER'S OWN local git HEAD — an operator targeting an
|
|
23
|
+
* arbitrary PR has no local checkout of it to read). So this verb requires the
|
|
24
|
+
* change-RECORD id explicitly via `--change-id` (the `chg_<uuid>` a prior
|
|
25
|
+
* change_open/change_ready minted — e.g. from the PR author's own `tot ship`
|
|
26
|
+
* review step, or the `/admin` confirm flow) and an optional `--head-sha`
|
|
27
|
+
* (passed through as `expectedHeadSha` — an optimistic-concurrency guard so
|
|
28
|
+
* accept refuses if the PR moved since the operator looked at it).
|
|
29
|
+
*
|
|
30
|
+
* // u9: auto-resolve PR→changeId via forge PR-read (candidate_status), so an
|
|
31
|
+
* // operator who DOES have forge access can omit --change-id and pass --pr
|
|
32
|
+
* // alone. Not implemented here — deliberately out of scope for this unit.
|
|
33
|
+
*
|
|
34
|
+
* HUMAN GATE — merging to main is a decision a human makes at the keyboard, so
|
|
35
|
+
* this verb ALWAYS states the EXACT plan (shared `planForAction`, unit U10:
|
|
36
|
+
* which PR, which tenant, "merge into main", plus a note that live/preview
|
|
37
|
+
* don't move until the next ship/deploy) and requires an explicit confirm.
|
|
38
|
+
* `--yes` is still an explicit affirmative supplied on the command line — there
|
|
39
|
+
* is deliberately NO default-yes, and a non-TTY without `--yes` is refused
|
|
40
|
+
* rather than silently proceeding (mirrors `tot ship`'s non-TTY refusal).
|
|
41
|
+
*
|
|
42
|
+
* Dependency-free (the MCP client + the shared plan module).
|
|
43
|
+
*/
|
|
44
|
+
import { createMcpClient } from "../mcp.mjs";
|
|
45
|
+
import { establishSession, AuthUnavailableError } from "../auth.mjs";
|
|
46
|
+
import { fail } from "../errors.mjs";
|
|
47
|
+
import { planForAction, printPlanAndConfirm } from "../plan.mjs";
|
|
48
|
+
import { normalizeChangeResult, pollChangeShipped } from "./ship.mjs";
|
|
49
|
+
|
|
50
|
+
const DEFAULT_MCP_URL = "https://mcp.tokenoftrust.com";
|
|
51
|
+
|
|
52
|
+
const USAGE = `tot accept — merge a PR into main (alias: tot merge)
|
|
53
|
+
|
|
54
|
+
tot accept --tenant <t> --pr <N> --change-id <chg_...>
|
|
55
|
+
tot merge --tenant <t> --pr <N> --change-id <chg_...> (same command)
|
|
56
|
+
|
|
57
|
+
Merges the given PR into main via the existing accept pipeline (merge →
|
|
58
|
+
reconcile → promote → verify) — the SAME gate \`tot ship\` uses. Distinct
|
|
59
|
+
from \`tot ship\`: accepting does NOT deploy. Main becomes default-hosted on
|
|
60
|
+
preview + live only after the next \`tot ship\` / deploy.
|
|
61
|
+
|
|
62
|
+
Merge-to-main is a human decision: this ALWAYS prints the exact plan and asks
|
|
63
|
+
for an explicit confirm. There is no default-yes; a non-TTY without --yes is
|
|
64
|
+
refused rather than silently proceeding.
|
|
65
|
+
|
|
66
|
+
Options:
|
|
67
|
+
--tenant <appDomain> Target tenant (e.g. tokenoftrust.com). Defaults to the
|
|
68
|
+
current checkout's tenant when run inside one.
|
|
69
|
+
--pr <N> PR number to merge (for the plan text / labeling).
|
|
70
|
+
--change-id <id> The change-record id (chg_...) to accept. REQUIRED —
|
|
71
|
+
there is no PR→changeId auto-resolution yet (u9); pass
|
|
72
|
+
the id a prior change_open/change_ready minted (e.g.
|
|
73
|
+
from the author's own \`tot ship\` review step, or the
|
|
74
|
+
/admin confirm flow).
|
|
75
|
+
--head-sha <sha> Optional expected PR head sha (expectedHeadSha) — an
|
|
76
|
+
optimistic-concurrency guard against a PR that moved.
|
|
77
|
+
--mcp <url> MCP base URL (default: env MCP_BASE_URL / TOT_MCP_URL)
|
|
78
|
+
--identity <id> sign in as a specific identity for this accept
|
|
79
|
+
--yes, -y Skip the interactive confirm (still an explicit human
|
|
80
|
+
affirmative — there is no default-yes).
|
|
81
|
+
--help, -h Show this help.`;
|
|
82
|
+
|
|
83
|
+
/** Parse `tot accept` / `tot merge` argv. Pure — unit-testable. */
|
|
84
|
+
export function parseAcceptArgs(argv) {
|
|
85
|
+
const a = {
|
|
86
|
+
tenant: null,
|
|
87
|
+
pr: null,
|
|
88
|
+
changeId: null,
|
|
89
|
+
headSha: null,
|
|
90
|
+
mcp: null,
|
|
91
|
+
identity: null,
|
|
92
|
+
yes: false,
|
|
93
|
+
help: false,
|
|
94
|
+
};
|
|
95
|
+
for (let i = 0; i < argv.length; i++) {
|
|
96
|
+
const t = argv[i];
|
|
97
|
+
if (t === "--tenant") a.tenant = argv[++i];
|
|
98
|
+
else if (t === "--pr") a.pr = argv[++i];
|
|
99
|
+
else if (t === "--change-id") a.changeId = argv[++i];
|
|
100
|
+
else if (t === "--head-sha" || t === "--head") a.headSha = argv[++i];
|
|
101
|
+
else if (t === "--mcp") a.mcp = argv[++i];
|
|
102
|
+
else if (t === "--identity") a.identity = argv[++i];
|
|
103
|
+
else if (t === "--yes" || t === "-y") a.yes = true;
|
|
104
|
+
else if (t === "--help" || t === "-h") a.help = true;
|
|
105
|
+
}
|
|
106
|
+
return a;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Report the accept result in house style. Pure given its inputs. */
|
|
110
|
+
function reportAccepted(accept, status, { tenant, pr, changeId }) {
|
|
111
|
+
const label = pr != null ? `PR #${pr}` : changeId;
|
|
112
|
+
const merged = Boolean(status?.shipped || accept?.shipped);
|
|
113
|
+
if (merged) {
|
|
114
|
+
console.log(`\n ✓ merged ${label} into main for ${tenant}.`);
|
|
115
|
+
console.log(
|
|
116
|
+
" → next: `tot ship` (or an operator `tot ship`/deploy) to promote main to preview + live.",
|
|
117
|
+
);
|
|
118
|
+
return 0;
|
|
119
|
+
}
|
|
120
|
+
// The accept committed but the terminal state hasn't been observed yet (the
|
|
121
|
+
// pipeline may still be landing reconcile/promote/verify) — report honestly.
|
|
122
|
+
console.log(`\n ~ accept committed for ${label} on ${tenant}; the merge is landing now.`);
|
|
123
|
+
console.log(" → next: re-check shortly, then `tot ship` / deploy to go live.");
|
|
124
|
+
return 0;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* @param {string[]} argv
|
|
129
|
+
* @param {any} ctx
|
|
130
|
+
*/
|
|
131
|
+
export async function run(argv, ctx) {
|
|
132
|
+
const env = process.env;
|
|
133
|
+
const args = parseAcceptArgs(argv);
|
|
134
|
+
if (args.help) {
|
|
135
|
+
console.log(USAGE);
|
|
136
|
+
return 0;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const tenant = (args.tenant || ctx?.tenant || "").trim();
|
|
140
|
+
if (!tenant) {
|
|
141
|
+
console.error(
|
|
142
|
+
fail(
|
|
143
|
+
"no target tenant.",
|
|
144
|
+
"pass --tenant <appDomain> (e.g. --tenant tokenoftrust.com), or run inside a store checkout.",
|
|
145
|
+
),
|
|
146
|
+
);
|
|
147
|
+
return 2;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const prRaw = args.pr;
|
|
151
|
+
const pr = prRaw != null && `${prRaw}`.trim() && Number.isFinite(Number(prRaw)) ? Number(prRaw) : null;
|
|
152
|
+
if (pr == null && !args.changeId) {
|
|
153
|
+
console.error(
|
|
154
|
+
fail("no PR or change to accept.", "pass --pr <N> (the PR number to merge) and --change-id <chg_...>."),
|
|
155
|
+
);
|
|
156
|
+
return 2;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const changeId = (args.changeId || "").trim();
|
|
160
|
+
if (!changeId) {
|
|
161
|
+
// u9: auto-resolve PR→changeId via forge PR-read (candidate_status) — until
|
|
162
|
+
// then an operator targeting a specific PR must supply the change-record id
|
|
163
|
+
// directly (there's no cross-tenant forge lookup this CLI can do for a PR
|
|
164
|
+
// that isn't the caller's own checkout).
|
|
165
|
+
console.error(
|
|
166
|
+
fail(
|
|
167
|
+
`no --change-id for PR #${pr} — can't auto-resolve it yet.`,
|
|
168
|
+
"pass --change-id <chg_...> (the change record a prior review step opened/readied), " +
|
|
169
|
+
"or run `tot ship` from the PR author's own checkout instead.",
|
|
170
|
+
),
|
|
171
|
+
);
|
|
172
|
+
return 2;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const headSha = (args.headSha || "").trim() || null;
|
|
176
|
+
|
|
177
|
+
// Self-declaring: state the EXACT plan before acting (the shared plan module,
|
|
178
|
+
// unit U10) plus the accept-specific caveat that main doesn't go live/preview
|
|
179
|
+
// until the next ship/deploy.
|
|
180
|
+
const planLines = [
|
|
181
|
+
...planForAction({ action: "accept", tenant, pr, changeId, headSha }),
|
|
182
|
+
" note: main becomes default-hosted on preview + live only after the next `tot ship` / deploy.",
|
|
183
|
+
];
|
|
184
|
+
const { confirmed, reason } = await printPlanAndConfirm(planLines, {
|
|
185
|
+
yes: args.yes,
|
|
186
|
+
question: "Merge this into main?",
|
|
187
|
+
});
|
|
188
|
+
if (!confirmed) {
|
|
189
|
+
if (reason === "non-tty") {
|
|
190
|
+
console.error(
|
|
191
|
+
fail(
|
|
192
|
+
"refusing to merge without confirmation on a non-TTY.",
|
|
193
|
+
"re-run with --yes (an explicit human affirmative), or from an interactive terminal.",
|
|
194
|
+
),
|
|
195
|
+
);
|
|
196
|
+
return 2;
|
|
197
|
+
}
|
|
198
|
+
console.log("Aborted — nothing was merged.");
|
|
199
|
+
return 1;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const baseUrl = args.mcp || env.MCP_BASE_URL || env.TOT_MCP_URL || DEFAULT_MCP_URL;
|
|
203
|
+
const client = createMcpClient(baseUrl);
|
|
204
|
+
try {
|
|
205
|
+
await establishSession(client, { env, prefer: args.identity || undefined });
|
|
206
|
+
// Bind the active tenant so change_accept/change_status read the right scope.
|
|
207
|
+
await client.callTool("client_switch", { tenant });
|
|
208
|
+
|
|
209
|
+
let accept;
|
|
210
|
+
try {
|
|
211
|
+
accept = normalizeChangeResult(
|
|
212
|
+
await client.callTool("change_accept", {
|
|
213
|
+
id: changeId,
|
|
214
|
+
tenant,
|
|
215
|
+
dryRun: false,
|
|
216
|
+
...(headSha ? { expectedHeadSha: headSha } : {}),
|
|
217
|
+
// Stable per (changeId, head): a re-run after a blip returns the
|
|
218
|
+
// original accept instead of double-merging.
|
|
219
|
+
idempotencyKey: `accept-${changeId}-${headSha || pr || "pr"}`,
|
|
220
|
+
}),
|
|
221
|
+
);
|
|
222
|
+
} catch (e) {
|
|
223
|
+
console.error(
|
|
224
|
+
fail(
|
|
225
|
+
`the accept pipeline refused to merge this change: ${String(e?.message || e)}`,
|
|
226
|
+
"check its review/reconcile status, then re-run `tot accept`",
|
|
227
|
+
),
|
|
228
|
+
);
|
|
229
|
+
return 1;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const status = await pollChangeShipped(client, { id: changeId, tenant });
|
|
233
|
+
return reportAccepted(accept, status, { tenant, pr, changeId });
|
|
234
|
+
} catch (e) {
|
|
235
|
+
if (e instanceof AuthUnavailableError) {
|
|
236
|
+
console.error(fail("sign in to accept a change", e.hint || "run `tot login`, then re-run `tot accept`"));
|
|
237
|
+
return 1;
|
|
238
|
+
}
|
|
239
|
+
console.error(
|
|
240
|
+
fail(
|
|
241
|
+
`couldn't reach the accept service: ${String(e?.message || e)}`,
|
|
242
|
+
"check your connection and that you're signed in, then re-run",
|
|
243
|
+
),
|
|
244
|
+
);
|
|
245
|
+
return 1;
|
|
246
|
+
}
|
|
247
|
+
}
|