@tokenoftrust/cli 1.4.0-rc.7 → 1.4.0-rc.9
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 +6 -6
- package/bin/tot.mjs +25 -7
- package/package.json +2 -2
- package/src/candidate-state.mjs +97 -0
- package/src/commands/{checkout.mjs → clone.mjs} +43 -25
- package/src/commands/dev.mjs +4 -4
- package/src/commands/doctor.mjs +3 -3
- package/src/commands/grants.mjs +1 -1
- package/src/commands/link.mjs +3 -3
- package/src/commands/login.mjs +4 -4
- package/src/commands/pr.mjs +200 -0
- package/src/commands/start.mjs +2 -2
- package/src/commands/submit.mjs +51 -9
- package/src/commands/validate.mjs +2 -2
- package/src/commands/whoami.mjs +1 -1
- package/src/context.mjs +2 -2
- package/src/obstacle-beacon.cjs +1 -1
package/README.md
CHANGED
|
@@ -5,8 +5,8 @@ One command to go from a Token of Trust invite to a running store.
|
|
|
5
5
|
```sh
|
|
6
6
|
npm i -g @tokenoftrust/cli
|
|
7
7
|
|
|
8
|
-
tot
|
|
9
|
-
tot
|
|
8
|
+
tot clone # list the stores you can build on
|
|
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
12
|
tot submit # (coming) submit it for preview
|
|
@@ -22,7 +22,7 @@ be fetched.)
|
|
|
22
22
|
|
|
23
23
|
| Command | Status | What it does |
|
|
24
24
|
| --- | --- | --- |
|
|
25
|
-
| `tot
|
|
25
|
+
| `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
26
|
| `tot validate` | next | Lint your store before you submit. |
|
|
27
27
|
| `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
28
|
| `tot submit` | next | Submit your store for preview (pushes the preview ref, reports the reconcile/compliance verdict + preview URL). |
|
|
@@ -34,7 +34,7 @@ The same `tot` does the right thing wherever you run it (walks up like `git`):
|
|
|
34
34
|
|
|
35
35
|
- **storefront monorepo** — a full platform checkout;
|
|
36
36
|
- **tenant checkout** — a standalone `content/ public/ theme.json .tot/config.json` clone (tenant read from `.tot/config.json`);
|
|
37
|
-
- **loose** — anywhere else; `tot
|
|
37
|
+
- **loose** — anywhere else; `tot clone <tenant>` gets you a checkout.
|
|
38
38
|
|
|
39
39
|
`tot doctor` prints the detected context.
|
|
40
40
|
|
|
@@ -42,8 +42,8 @@ The same `tot` does the right thing wherever you run it (walks up like `git`):
|
|
|
42
42
|
|
|
43
43
|
`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
44
|
|
|
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
|
|
46
|
-
- Not signed in? On a terminal, `tot start` / `tot
|
|
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 submit`, …) runs as you, with no re-auth. Entitlement is derived server-side from your ToT memberships.
|
|
46
|
+
- 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
47
|
- 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
48
|
|
|
49
49
|
### Optional: multiple identities at once (`TOT_PROFILE`)
|
package/bin/tot.mjs
CHANGED
|
@@ -10,10 +10,11 @@
|
|
|
10
10
|
* tot whoami who you're signed in as ← built
|
|
11
11
|
* tot link link your identity to the ToT broker (resolve store scope) ← built
|
|
12
12
|
* tot grants capability/tier/expiry per store you can act on ← built (introspection diagnostics)
|
|
13
|
-
* tot
|
|
13
|
+
* tot clone [<tenant>] clone a store you can build on ← built
|
|
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 submit submit your store for preview ← built (validate + push preview ref; MCP preview_status read-back)
|
|
17
|
+
* tot pr list / view / close your candidate PRs ← built (candidate_status/candidate_close; gh-pr-shaped)
|
|
17
18
|
* tot doctor check this machine is ready
|
|
18
19
|
* tot ideas copy-paste AI prompts that reliably wow
|
|
19
20
|
* tot feedback send a note to ToT + your recent CLI activity ← built (activity-log.mjs → feedback_submit MCP tool)
|
|
@@ -25,7 +26,7 @@
|
|
|
25
26
|
* from anywhere else. Every command receives the detected context.
|
|
26
27
|
*
|
|
27
28
|
* This package is `@tokenoftrust/cli` (bin `tot`). Its heaviest command, `tot dev`,
|
|
28
|
-
* fetches the published storefront runner; `tot
|
|
29
|
+
* fetches the published storefront runner; `tot clone/validate/submit` are
|
|
29
30
|
* pure Node. Dependency-free by design so `npm i -g @tokenoftrust/cli` stays light.
|
|
30
31
|
*/
|
|
31
32
|
import "../src/ensure-node.mjs"; // hard Node-version gate — must stay first (see the module doc)
|
|
@@ -35,7 +36,7 @@ import { printError } from "../src/errors.mjs";
|
|
|
35
36
|
import { recordActivity, redactArgs } from "../src/activity-log.mjs";
|
|
36
37
|
import { maybeNotifyUpdate } from "../src/update-check.mjs";
|
|
37
38
|
|
|
38
|
-
const BUILD_ORDER = ["
|
|
39
|
+
const BUILD_ORDER = ["clone", "validate", "dev", "submit"];
|
|
39
40
|
|
|
40
41
|
// CLI version (stamped into the activity log). Read from our own package.json; best-effort.
|
|
41
42
|
const VERSION = (() => {
|
|
@@ -56,10 +57,11 @@ tot — Token of Trust developer CLI
|
|
|
56
57
|
tot whoami show who you're signed in as
|
|
57
58
|
tot link link your identity so your stores resolve
|
|
58
59
|
tot grants capability/tier/expiry per store you can act on
|
|
59
|
-
tot
|
|
60
|
+
tot clone [<tenant>] clone a store you can build on
|
|
60
61
|
tot validate lint your store before you submit
|
|
61
62
|
tot dev run your store locally with save→reload
|
|
62
63
|
tot submit submit your store for preview
|
|
64
|
+
tot pr list / view / close your candidate PRs
|
|
63
65
|
tot doctor check this machine is ready
|
|
64
66
|
tot ideas copy-paste AI prompts that reliably wow
|
|
65
67
|
tot feedback "<msg>" send feedback to Token of Trust (attaches recent activity)
|
|
@@ -131,8 +133,8 @@ async function dispatch(cmd, rest, ctx) {
|
|
|
131
133
|
return run(rest, ctx);
|
|
132
134
|
}
|
|
133
135
|
|
|
134
|
-
if (cmd === "
|
|
135
|
-
const { run } = await import("../src/commands/
|
|
136
|
+
if (cmd === "clone") {
|
|
137
|
+
const { run } = await import("../src/commands/clone.mjs");
|
|
136
138
|
return run(rest, ctx);
|
|
137
139
|
}
|
|
138
140
|
|
|
@@ -151,15 +153,31 @@ async function dispatch(cmd, rest, ctx) {
|
|
|
151
153
|
return run(rest, ctx);
|
|
152
154
|
}
|
|
153
155
|
|
|
156
|
+
if (cmd === "pr") {
|
|
157
|
+
const { run } = await import("../src/commands/pr.mjs");
|
|
158
|
+
return run(rest, ctx);
|
|
159
|
+
}
|
|
160
|
+
|
|
154
161
|
if (cmd === "app") {
|
|
155
162
|
const { run } = await import("../src/commands/app/index.mjs");
|
|
156
163
|
return run(rest, ctx);
|
|
157
164
|
}
|
|
158
165
|
|
|
166
|
+
// `checkout` was deliberately freed up: it's no longer a tot verb precisely so it
|
|
167
|
+
// can mean what it means in git. Point both ways rather than a bare "unknown".
|
|
168
|
+
if (cmd === "checkout") {
|
|
169
|
+
console.error(
|
|
170
|
+
"`tot checkout` isn't a tot command — `checkout` now means what it does in git.\n\n" +
|
|
171
|
+
" • switch branches / restore files in your store: git checkout <ref>\n" +
|
|
172
|
+
" • clone a store you can build on: tot clone <tenant>\n",
|
|
173
|
+
);
|
|
174
|
+
return 2;
|
|
175
|
+
}
|
|
176
|
+
|
|
159
177
|
if (BUILD_ORDER.includes(cmd)) {
|
|
160
178
|
console.error(
|
|
161
179
|
`\`tot ${cmd}\` isn't built yet.\n\n` +
|
|
162
|
-
`Build order: ${BUILD_ORDER.map((c) => (c === "
|
|
180
|
+
`Build order: ${BUILD_ORDER.map((c) => (c === "clone" ? `${c}✓` : c)).join(" → ")}.\n` +
|
|
163
181
|
`Inside a storefront checkout you can use the in-repo scripts today ` +
|
|
164
182
|
`(e.g. \`pnpm tot ${cmd}\`).`,
|
|
165
183
|
);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tokenoftrust/cli",
|
|
3
|
-
"version": "1.4.0-rc.
|
|
4
|
-
"description": "Token of Trust developer CLI —
|
|
3
|
+
"version": "1.4.0-rc.9",
|
|
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",
|
|
7
7
|
"homepage": "https://www.tokenoftrust.com",
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The "active candidate" pointer — which open candidate PR a plain `tot submit`
|
|
3
|
+
* updates, per forge repo.
|
|
4
|
+
*
|
|
5
|
+
* `tot submit` is idempotent on a STABLE changeId (`deriveChangeId`) so a re-submit
|
|
6
|
+
* updates the same PR by default — the common case needs NO state and writes
|
|
7
|
+
* nothing here (backward-compatible with the stateless original). This file only
|
|
8
|
+
* records a DIVERGENCE from that stable default:
|
|
9
|
+
*
|
|
10
|
+
* - `tot submit --new` forks a fresh candidate and remembers it here, so the
|
|
11
|
+
* NEXT plain `tot submit` keeps updating the NEW PR (like pushing more commits
|
|
12
|
+
* to a `gh pr` branch), not the old one; and
|
|
13
|
+
* - a terminal-roll (the active candidate was merged/closed) records the fresh
|
|
14
|
+
* candidate it rolled to, so you're never wedged submitting to a dead PR.
|
|
15
|
+
*
|
|
16
|
+
* ONE file, `~/.tot/candidates.json`, a map keyed by `<mcpUrl>::<repo>` — a
|
|
17
|
+
* different MCP or repo is a different candidate namespace. Same atomic-write
|
|
18
|
+
* discipline as last-tenant.mjs (0600 in a 0700 dir, write-tmp-then-rename).
|
|
19
|
+
* Dependency-free (node:fs/os/path). `TOT_HOME` overrides home (tests).
|
|
20
|
+
*/
|
|
21
|
+
import {
|
|
22
|
+
readFileSync, writeFileSync, mkdirSync, renameSync, chmodSync,
|
|
23
|
+
} from "node:fs";
|
|
24
|
+
import { homedir } from "node:os";
|
|
25
|
+
import { join, dirname } from "node:path";
|
|
26
|
+
import { randomBytes } from "node:crypto";
|
|
27
|
+
|
|
28
|
+
/** Absolute path to the active-candidate map for this machine. */
|
|
29
|
+
export function defaultCandidateStatePath(env = process.env) {
|
|
30
|
+
const home = env.TOT_HOME || homedir();
|
|
31
|
+
return join(home, ".tot", "candidates.json");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Namespace key for one (MCP, repo) candidate pointer. */
|
|
35
|
+
function stateKey(mcpUrl, repo) {
|
|
36
|
+
return `${mcpUrl}::${repo}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function readMap(filePath) {
|
|
40
|
+
try {
|
|
41
|
+
const parsed = JSON.parse(readFileSync(filePath, "utf8"));
|
|
42
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
43
|
+
} catch {
|
|
44
|
+
return {};
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function writeMap(filePath, map) {
|
|
49
|
+
mkdirSync(dirname(filePath), { recursive: true, mode: 0o700 });
|
|
50
|
+
const tmp = `${filePath}.tmp`;
|
|
51
|
+
writeFileSync(tmp, `${JSON.stringify(map, null, 2)}\n`, { mode: 0o600 });
|
|
52
|
+
renameSync(tmp, filePath);
|
|
53
|
+
chmodSync(filePath, 0o600);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The remembered active changeId for `(mcpUrl, repo)`, or null when there isn't
|
|
58
|
+
* one (absent/unreadable/malformed) — a miss means "use the stable default".
|
|
59
|
+
* Never throws.
|
|
60
|
+
*/
|
|
61
|
+
export function readActiveChangeId(filePath, { mcpUrl, repo }) {
|
|
62
|
+
const rec = readMap(filePath)[stateKey(mcpUrl, repo)];
|
|
63
|
+
return rec && typeof rec.changeId === "string" && rec.changeId ? rec.changeId : null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Remember `changeId` as the active candidate for `(mcpUrl, repo)`, atomically. */
|
|
67
|
+
export function writeActiveChangeId(filePath, { mcpUrl, repo, changeId }) {
|
|
68
|
+
const map = readMap(filePath);
|
|
69
|
+
map[stateKey(mcpUrl, repo)] = { changeId, updatedAt: Date.now() };
|
|
70
|
+
writeMap(filePath, map);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Forget the active candidate for `(mcpUrl, repo)` (e.g. after closing it). */
|
|
74
|
+
export function clearActiveChangeId(filePath, { mcpUrl, repo }) {
|
|
75
|
+
const map = readMap(filePath);
|
|
76
|
+
const key = stateKey(mcpUrl, repo);
|
|
77
|
+
if (key in map) {
|
|
78
|
+
delete map[key];
|
|
79
|
+
writeMap(filePath, map);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* A fresh candidate handle forked from a stable base — `<baseId>-<suffix>`, still
|
|
85
|
+
* matching candidate_open's `[a-z0-9._-]` handle grammar. The suffix defaults to
|
|
86
|
+
* 6 random hex chars (so two `--new` runs never collide); tests inject a fixed
|
|
87
|
+
* suffix. Pure given `suffix`.
|
|
88
|
+
*/
|
|
89
|
+
export function mintFreshChangeId(baseId, suffix = randomBytes(3).toString("hex")) {
|
|
90
|
+
const safe = String(suffix).toLowerCase().replace(/[^a-z0-9]/g, "").slice(0, 12) || "new";
|
|
91
|
+
return `${baseId}-${safe}`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Whether a forge candidate state means "no open PR to update" (rolled past). */
|
|
95
|
+
export function isTerminalCandidateState(state) {
|
|
96
|
+
return state === "merged" || state === "closed";
|
|
97
|
+
}
|
|
@@ -1,16 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `tot
|
|
3
|
-
* authenticated remote already configured, ready for the local loop.
|
|
2
|
+
* `tot clone` — clone the tenant store you're entitled to build on, with an
|
|
3
|
+
* authenticated remote already configured, ready for the local loop. Mirrors
|
|
4
|
+
* `git clone <repo> [dir]`: the tenant is the repo, the dir defaults to the
|
|
5
|
+
* tenant name. (Named `clone`, not `checkout`, so it means what `git clone`
|
|
6
|
+
* means — materialize a local working copy — and never collides with the
|
|
7
|
+
* unrelated `git checkout` = switch-refs verb.)
|
|
4
8
|
*
|
|
5
|
-
* tot
|
|
6
|
-
* tot
|
|
7
|
-
* tot
|
|
9
|
+
* tot clone list the stores you can build on
|
|
10
|
+
* tot clone <tenant> clone into ./<tenant> (authenticated remote configured)
|
|
11
|
+
* tot clone <tenant> <dir> clone into <dir>
|
|
12
|
+
* tot clone <tenant> --remote-only just mint + print the remote, don't materialize
|
|
8
13
|
*
|
|
9
14
|
* This is the SAME MCP code path a developer gets when they switch to a tenant:
|
|
10
15
|
* sign-in → client_switch(tenant) → tenant_checkout, where the MCP derives your
|
|
11
16
|
* per-tenant Git user and mints a FRESH, single-active, repo-scoped push
|
|
12
|
-
* credential (a later
|
|
13
|
-
* privileged forge work itself — the MCP owns that.
|
|
17
|
+
* credential (a later clone for the same tenant rotates it). `tot` performs NO
|
|
18
|
+
* privileged forge work itself — the MCP owns that. ("checkout" survives only as
|
|
19
|
+
* the domain/lease term: the `tenant_checkout` MCP tool + the on-disk noun for a
|
|
20
|
+
* materialized store dir — never as a developer-facing verb.)
|
|
14
21
|
*
|
|
15
22
|
* Auth is the developer's own ToT identity (the cached `tot login` session,
|
|
16
23
|
* resolved via src/auth.mjs). When there's no session yet and we're on a TTY, we
|
|
@@ -34,8 +41,9 @@ const DEFAULT_MCP_URL = "https://mcp.tokenoftrust.com";
|
|
|
34
41
|
function parseArgs(argv) {
|
|
35
42
|
const a = {
|
|
36
43
|
tenant: null,
|
|
44
|
+
dir: null,
|
|
37
45
|
tag: "main",
|
|
38
|
-
|
|
46
|
+
remoteOnly: false,
|
|
39
47
|
mcp: null,
|
|
40
48
|
printRemote: false,
|
|
41
49
|
help: false,
|
|
@@ -43,27 +51,32 @@ function parseArgs(argv) {
|
|
|
43
51
|
for (let i = 0; i < argv.length; i++) {
|
|
44
52
|
const t = argv[i];
|
|
45
53
|
if (t === "--tag") a.tag = argv[++i];
|
|
46
|
-
else if (t === "--
|
|
54
|
+
else if (t === "--remote-only") a.remoteOnly = true;
|
|
47
55
|
else if (t === "--mcp") a.mcp = argv[++i];
|
|
48
56
|
else if (t === "--print-remote") a.printRemote = true;
|
|
49
57
|
else if (t === "--help" || t === "-h") a.help = true;
|
|
50
|
-
|
|
58
|
+
// Positional, git-clone style: `tot clone <tenant> [<dir>]`.
|
|
59
|
+
else if (!t.startsWith("--")) {
|
|
60
|
+
if (!a.tenant) a.tenant = t;
|
|
61
|
+
else if (!a.dir) a.dir = t;
|
|
62
|
+
}
|
|
51
63
|
}
|
|
52
64
|
return a;
|
|
53
65
|
}
|
|
54
66
|
|
|
55
|
-
const USAGE = `tot
|
|
67
|
+
const USAGE = `tot clone — clone a tenant store you can build on (mirrors \`git clone\`)
|
|
56
68
|
|
|
57
|
-
tot
|
|
58
|
-
tot
|
|
59
|
-
tot
|
|
69
|
+
tot clone list the stores you can build on
|
|
70
|
+
tot clone <tenant> clone into ./<tenant> (authenticated remote configured)
|
|
71
|
+
tot clone <tenant> <dir> clone into <dir>
|
|
60
72
|
|
|
61
73
|
Options:
|
|
62
74
|
--tag <tag> which repo (repo = "<tenant>-<tag>"). Default: main.
|
|
63
|
-
--
|
|
75
|
+
--remote-only don't clone; just mint + print the clone URL for <tenant>.
|
|
64
76
|
--mcp <url> MCP base URL. Default: env MCP_BASE_URL / TOT_MCP_URL, else
|
|
65
77
|
${DEFAULT_MCP_URL}.
|
|
66
|
-
--print-remote also print the authenticated remote
|
|
78
|
+
--print-remote with --remote-only, also print the authenticated remote
|
|
79
|
+
(contains a live token!).`;
|
|
67
80
|
|
|
68
81
|
/** Redact known secrets from any string before it hits the terminal. */
|
|
69
82
|
function makeRedactor(env) {
|
|
@@ -92,6 +105,11 @@ export async function run(argv, ctx) {
|
|
|
92
105
|
args.tenant = ctx.tenant;
|
|
93
106
|
}
|
|
94
107
|
|
|
108
|
+
// git-clone semantics: unless you explicitly asked for --remote-only, cloning
|
|
109
|
+
// is the default, and the target dir defaults to the tenant name (like
|
|
110
|
+
// `git clone <url>` deriving the dir from the repo basename).
|
|
111
|
+
const cloneDir = args.remoteOnly ? null : args.dir || args.tenant;
|
|
112
|
+
|
|
95
113
|
const baseUrl = args.mcp || env.MCP_BASE_URL || env.TOT_MCP_URL || DEFAULT_MCP_URL;
|
|
96
114
|
const redact = makeRedactor(env);
|
|
97
115
|
const client = createMcpClient(baseUrl);
|
|
@@ -128,21 +146,21 @@ export async function run(argv, ctx) {
|
|
|
128
146
|
const res = await checkoutTenant(client, {
|
|
129
147
|
tenant: args.tenant,
|
|
130
148
|
tag: args.tag,
|
|
131
|
-
cloneDir
|
|
149
|
+
cloneDir,
|
|
132
150
|
redact,
|
|
133
151
|
});
|
|
134
|
-
console.log(`\n+
|
|
152
|
+
console.log(`\n+ ready. repo: ${res.cloneUrl || res.publicUrl}`);
|
|
135
153
|
|
|
136
154
|
if (res.cloned) {
|
|
137
155
|
console.log(`+ cloned. HEAD: ${res.head}`);
|
|
138
156
|
console.log(`\nYour local working clone is at ${res.dir} with an authenticated remote.`);
|
|
139
157
|
console.log(` cd ${res.dir} && tot dev # run it locally with save→reload`);
|
|
140
|
-
console.log(` (the minted token lives in .git/config; a later
|
|
158
|
+
console.log(` (the minted token lives in .git/config; a later clone rotates it)`);
|
|
141
159
|
return 0;
|
|
142
160
|
}
|
|
143
161
|
|
|
144
162
|
console.log(`\nPublic clone URL (no credential): ${res.publicUrl}`);
|
|
145
|
-
console.log(`
|
|
163
|
+
console.log(`Drop --remote-only to clone with the authenticated remote configured.`);
|
|
146
164
|
if (args.printRemote) {
|
|
147
165
|
console.log(
|
|
148
166
|
`\nAUTHENTICATED remote (contains a live token — handle carefully):\n${res.gitRemote}`,
|
|
@@ -164,7 +182,7 @@ export async function run(argv, ctx) {
|
|
|
164
182
|
}
|
|
165
183
|
|
|
166
184
|
/**
|
|
167
|
-
* The privileged checkout core, composable in-process (used by `tot
|
|
185
|
+
* The privileged checkout core, composable in-process (used by `tot clone`
|
|
168
186
|
* and by `tot start`): client_switch → tenant_checkout → optionally clone. The
|
|
169
187
|
* MCP mints a fresh, repo-scoped push credential each time. Assumes `client` is
|
|
170
188
|
* already initialized + has a validated session.
|
|
@@ -211,7 +229,7 @@ async function cloneRepo(gitRemote, dir, redact) {
|
|
|
211
229
|
await git(["clone", gitRemote, dir]);
|
|
212
230
|
} catch (e) {
|
|
213
231
|
// Beacon the cockpit before we surface the error — covers this path for both
|
|
214
|
-
// `tot
|
|
232
|
+
// `tot clone` and `tot start` (which clones through here). Awaited so the
|
|
215
233
|
// packet lands before the process prints + exits; swallowed either way.
|
|
216
234
|
await emitObstacle("clone-failed");
|
|
217
235
|
throw new CliError(`clone failed: ${redact(String(e.stderr || e.message || e))}`, {
|
|
@@ -255,13 +273,13 @@ export function checkoutError(checkout) {
|
|
|
255
273
|
}
|
|
256
274
|
return {
|
|
257
275
|
message: msg || "the store checkout couldn't be completed",
|
|
258
|
-
next: "confirm you're entitled to this store — `tot
|
|
276
|
+
next: "confirm you're entitled to this store — `tot clone` (lists your stores)",
|
|
259
277
|
};
|
|
260
278
|
}
|
|
261
279
|
|
|
262
280
|
/**
|
|
263
281
|
* Normalize the (shape-varying) `client_list` response into a plain, sorted
|
|
264
|
-
* list of the stores this identity can build on. Shared by `tot
|
|
282
|
+
* list of the stores this identity can build on. Shared by `tot clone`'s
|
|
265
283
|
* listing and `tot start`'s auto-pick so both read the same fields.
|
|
266
284
|
* @param {unknown} list
|
|
267
285
|
* @returns {Array<{ id: string, name: string, raw: any }>}
|
|
@@ -409,5 +427,5 @@ function printClientList(list) {
|
|
|
409
427
|
for (const s of stores) {
|
|
410
428
|
console.log(` ${s.id}${s.name ? ` — ${s.name}` : ""}`);
|
|
411
429
|
}
|
|
412
|
-
console.log(`\nNext: tot
|
|
430
|
+
console.log(`\nNext: tot clone <tenant>`);
|
|
413
431
|
}
|
package/src/commands/dev.mjs
CHANGED
|
@@ -138,7 +138,7 @@ export async function run(argv, ctx) {
|
|
|
138
138
|
console.error(
|
|
139
139
|
fail(
|
|
140
140
|
"nothing to run — you're not inside a tenant checkout",
|
|
141
|
-
"tot
|
|
141
|
+
"tot clone <tenant> (then `cd` in and re-run), pass --workspace <dir>, or try `tot dev --sample`",
|
|
142
142
|
),
|
|
143
143
|
);
|
|
144
144
|
return 2;
|
|
@@ -194,7 +194,7 @@ async function runNativePublic(workspace, args, ctx) {
|
|
|
194
194
|
const cfg = readWorkspaceConfig(workspace);
|
|
195
195
|
if (!cfg) {
|
|
196
196
|
throw new CliError(`${workspace} isn't a tenant checkout (no readable .tot/config.json)`, {
|
|
197
|
-
next: "tot
|
|
197
|
+
next: "tot clone <tenant> (produces a runnable checkout)",
|
|
198
198
|
exitCode: 2,
|
|
199
199
|
});
|
|
200
200
|
}
|
|
@@ -280,7 +280,7 @@ async function runNative(workspace, args, ctx) {
|
|
|
280
280
|
const cfg = readWorkspaceConfig(workspace);
|
|
281
281
|
if (!cfg) {
|
|
282
282
|
throw new CliError(`${workspace} isn't a tenant checkout (no readable .tot/config.json)`, {
|
|
283
|
-
next: "tot
|
|
283
|
+
next: "tot clone <tenant> (produces a runnable checkout)",
|
|
284
284
|
exitCode: 2,
|
|
285
285
|
});
|
|
286
286
|
}
|
|
@@ -1240,7 +1240,7 @@ export function buildContainerPlan(workspace, args, _ctx) {
|
|
|
1240
1240
|
const cfg = readWorkspaceConfig(workspace);
|
|
1241
1241
|
if (!cfg) {
|
|
1242
1242
|
throw new CliError(`${workspace} isn't a tenant checkout (no readable .tot/config.json)`, {
|
|
1243
|
-
next: "tot
|
|
1243
|
+
next: "tot clone <tenant> (produces a runnable checkout)",
|
|
1244
1244
|
exitCode: 2,
|
|
1245
1245
|
});
|
|
1246
1246
|
}
|
package/src/commands/doctor.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `tot doctor` — is this machine ready to run the loop?
|
|
3
3
|
*
|
|
4
|
-
* Standalone (unlike the in-monorepo doctor): checks the things `tot
|
|
4
|
+
* Standalone (unlike the in-monorepo doctor): checks the things `tot clone`
|
|
5
5
|
* and `tot dev`/`tot start` actually need — Node, git, an MCP URL, whether an
|
|
6
6
|
* auth identity is available, and Docker — plus reports the detected context
|
|
7
7
|
* so a developer knows which mode `tot` will use here.
|
|
@@ -187,7 +187,7 @@ export async function run(argv, ctx) {
|
|
|
187
187
|
}
|
|
188
188
|
console.log(
|
|
189
189
|
failed === 0
|
|
190
|
-
? "\n✔ Ready. Try: tot start (checks out your store and runs it), or tot
|
|
190
|
+
? "\n✔ Ready. Try: tot start (checks out your store and runs it), or tot clone\n"
|
|
191
191
|
: args.fix
|
|
192
192
|
? `\n✖ ${failed} check(s) still failing — see the exact next command(s) above, then re-run \`tot doctor\`.\n`
|
|
193
193
|
: `\n✖ ${failed} check(s) failed — try \`tot doctor --fix\`, or fix the above, then re-run \`tot doctor\`.\n`,
|
|
@@ -199,5 +199,5 @@ function describeContext(ctx) {
|
|
|
199
199
|
if (ctx.mode === "monorepo") return `storefront monorepo (${ctx.repoRoot})`;
|
|
200
200
|
if (ctx.mode === "checkout")
|
|
201
201
|
return `tenant checkout${ctx.tenant ? ` for "${ctx.tenant}"` : ""} (${ctx.workspacePath})`;
|
|
202
|
-
return "loose (not inside a checkout — `tot
|
|
202
|
+
return "loose (not inside a checkout — `tot clone <tenant>` to get one)";
|
|
203
203
|
}
|
package/src/commands/grants.mjs
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
import { defaultCredentialsPath, readCredentials } from "../token-store.mjs";
|
|
27
27
|
import { establishSession, AuthUnavailableError } from "../auth.mjs";
|
|
28
28
|
import { createMcpClient } from "../mcp.mjs";
|
|
29
|
-
import { storeListError, noStoresGuidance } from "./
|
|
29
|
+
import { storeListError, noStoresGuidance } from "./clone.mjs";
|
|
30
30
|
import { offerSignIn } from "./login.mjs";
|
|
31
31
|
import { recordServerPolicy } from "../update-check.mjs";
|
|
32
32
|
|
package/src/commands/link.mjs
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Some developers sign in fine (a valid ToT OAuth session) but their identity
|
|
6
6
|
* isn't yet linked to the broker that maps a person → the tenants they can build
|
|
7
|
-
* on — so `tot start` / `tot
|
|
7
|
+
* on — so `tot start` / `tot clone` / `tot whoami` resolve ZERO stores. That's
|
|
8
8
|
* NOT a "you weren't invited" problem and NOT "the invite is still propagating";
|
|
9
9
|
* it's a one-time link step. This command drives it end-to-end from the terminal:
|
|
10
10
|
*
|
|
@@ -24,7 +24,7 @@ import { establishSession, AuthUnavailableError } from "../auth.mjs";
|
|
|
24
24
|
import { offerSignIn } from "./login.mjs";
|
|
25
25
|
import { openBrowser } from "../open.mjs";
|
|
26
26
|
import { CliError, fail, formatError } from "../errors.mjs";
|
|
27
|
-
import { normalizeStores } from "./
|
|
27
|
+
import { normalizeStores } from "./clone.mjs";
|
|
28
28
|
|
|
29
29
|
const DEFAULT_MCP_URL = "https://mcp.tokenoftrust.com";
|
|
30
30
|
|
|
@@ -168,7 +168,7 @@ export async function run(argv, _ctx) {
|
|
|
168
168
|
const stores = normalizeStores(list);
|
|
169
169
|
if (stores.length) {
|
|
170
170
|
console.log(` stores you can build on: ${stores.map((s) => s.id).join(", ")}`);
|
|
171
|
-
console.log(" Next: `tot start` (or `tot
|
|
171
|
+
console.log(" Next: `tot start` (or `tot clone <tenant>`).");
|
|
172
172
|
} else {
|
|
173
173
|
console.log(" Next: `tot start` — if it still shows no stores, ask your ToT contact for a store invite.");
|
|
174
174
|
}
|
package/src/commands/login.mjs
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Runs the MCP OAuth 2.1 PKCE loopback (the same ceremony `claude mcp add` runs):
|
|
5
5
|
* opens the browser to the MCP's authorize page, the developer signs in with their
|
|
6
6
|
* ToT identity + approves, and the loopback catches the code and exchanges it for a
|
|
7
|
-
* token cached at ~/.tot/credentials.json. Every later command (`tot
|
|
7
|
+
* token cached at ~/.tot/credentials.json. Every later command (`tot clone`,
|
|
8
8
|
* `tot submit`, …) then runs as that developer with NO re-auth.
|
|
9
9
|
*
|
|
10
10
|
* The MCP defaults to the same target `tot submit` talks to (env MCP_BASE_URL /
|
|
@@ -109,7 +109,7 @@ const USAGE = `tot login — sign in to Token of Trust
|
|
|
109
109
|
link a later \`tot feedback\` report to your invite + session) — not meant to be
|
|
110
110
|
typed by hand.
|
|
111
111
|
|
|
112
|
-
After signing in, run \`tot whoami\` to confirm, then \`tot
|
|
112
|
+
After signing in, run \`tot whoami\` to confirm, then \`tot clone\` / \`tot submit\`.`;
|
|
113
113
|
|
|
114
114
|
/**
|
|
115
115
|
* The core of `tot login`: run the OAuth ceremony (browser loopback, or the
|
|
@@ -270,7 +270,7 @@ export async function run(argv, _ctx) {
|
|
|
270
270
|
cacheTraceId(env, args.traceId);
|
|
271
271
|
cacheEmailHint(env, args.emailHint);
|
|
272
272
|
console.log(`\n+ signed in. Session cached to ${defaultCredentialsPath(env)}.`);
|
|
273
|
-
console.log(" Next: `tot whoami` to confirm, or `tot
|
|
273
|
+
console.log(" Next: `tot whoami` to confirm, or `tot clone` / `tot submit` to build.");
|
|
274
274
|
return 0;
|
|
275
275
|
} catch (e) {
|
|
276
276
|
console.error(
|
|
@@ -288,7 +288,7 @@ export async function run(argv, _ctx) {
|
|
|
288
288
|
await loginAndCache(mcpUrl, env, { log: (m) => console.error(m), device: args.device });
|
|
289
289
|
cacheTraceId(env, args.traceId);
|
|
290
290
|
console.log(`\n+ signed in. Session cached to ${defaultCredentialsPath(env)}.`);
|
|
291
|
-
console.log(" Next: `tot whoami` to confirm, or `tot
|
|
291
|
+
console.log(" Next: `tot whoami` to confirm, or `tot clone` / `tot submit` to build.");
|
|
292
292
|
return 0;
|
|
293
293
|
} catch (e) {
|
|
294
294
|
console.error(
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `tot pr` — see and manage the candidate PRs `tot submit` opens, mirroring
|
|
3
|
+
* `gh pr`.
|
|
4
|
+
*
|
|
5
|
+
* tot pr [list] list your OPEN candidate PRs for this store
|
|
6
|
+
* tot pr view <N|id> show one candidate (by PR number or changeId)
|
|
7
|
+
* tot pr close <N|id> close (reject) a candidate without merging
|
|
8
|
+
*
|
|
9
|
+
* A candidate PR is the reviewable unit `tot submit` creates. By default a
|
|
10
|
+
* re-submit UPDATES your open candidate; `tot submit --new` forks another. This
|
|
11
|
+
* command fills the gap the raw submit loop left — a first-party way to list your
|
|
12
|
+
* open candidates and to close one (candidate close was otherwise gated to
|
|
13
|
+
* version-control apps only).
|
|
14
|
+
*
|
|
15
|
+
* Runs from inside a tenant checkout (it derives the forge repo from the
|
|
16
|
+
* checkout's origin remote, exactly as `tot submit` does) and reads over the MCP
|
|
17
|
+
* `candidate_status` / `candidate_close` tools. Dependency-free (global fetch +
|
|
18
|
+
* `git`).
|
|
19
|
+
*/
|
|
20
|
+
import { execFileSync } from "node:child_process";
|
|
21
|
+
import { createMcpClient } from "../mcp.mjs";
|
|
22
|
+
import { establishSession, AuthUnavailableError } from "../auth.mjs";
|
|
23
|
+
import { fail } from "../errors.mjs";
|
|
24
|
+
import { repoNameFromRemote } from "./submit.mjs";
|
|
25
|
+
import {
|
|
26
|
+
defaultCandidateStatePath,
|
|
27
|
+
readActiveChangeId,
|
|
28
|
+
clearActiveChangeId,
|
|
29
|
+
} from "../candidate-state.mjs";
|
|
30
|
+
|
|
31
|
+
const DEFAULT_MCP_URL = "https://mcp.tokenoftrust.com";
|
|
32
|
+
const SUBCOMMANDS = ["list", "view", "close"];
|
|
33
|
+
|
|
34
|
+
const USAGE = `tot pr — see and manage your candidate PRs
|
|
35
|
+
|
|
36
|
+
tot pr [list] list your open candidate PRs for this store
|
|
37
|
+
tot pr view <N|id> show one candidate PR (by PR number or changeId)
|
|
38
|
+
tot pr close <N|id> close (reject) a candidate PR without merging
|
|
39
|
+
tot pr close <N|id> --reason "<why>" record why it was closed (audit note)
|
|
40
|
+
tot pr --mcp <url> MCP base URL (default: env MCP_BASE_URL / TOT_MCP_URL)
|
|
41
|
+
|
|
42
|
+
A candidate PR is what \`tot submit\` opens for review. A re-submit updates your
|
|
43
|
+
open one by default; \`tot submit --new\` forks another. Use these to see and
|
|
44
|
+
manage them.`;
|
|
45
|
+
|
|
46
|
+
/** Parse `tot pr` argv into { sub, target, reason, mcp, identity, help }. Pure. */
|
|
47
|
+
export function parsePrArgs(argv) {
|
|
48
|
+
const a = { sub: null, target: null, reason: null, mcp: null, identity: null, help: false };
|
|
49
|
+
const positional = [];
|
|
50
|
+
for (let i = 0; i < argv.length; i++) {
|
|
51
|
+
const t = argv[i];
|
|
52
|
+
if (t === "--mcp") a.mcp = argv[++i];
|
|
53
|
+
else if (t === "--identity") a.identity = argv[++i];
|
|
54
|
+
else if (t === "--reason") a.reason = argv[++i];
|
|
55
|
+
else if (t === "--help" || t === "-h") a.help = true;
|
|
56
|
+
else positional.push(t);
|
|
57
|
+
}
|
|
58
|
+
a.sub = positional[0] || "list";
|
|
59
|
+
a.target = positional[1] ?? null;
|
|
60
|
+
return a;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Normalize the `candidate_status` list response to a flat candidate array. Pure. */
|
|
64
|
+
export function extractCandidates(result) {
|
|
65
|
+
if (Array.isArray(result)) return result;
|
|
66
|
+
if (result && Array.isArray(result.candidates)) return result.candidates;
|
|
67
|
+
if (result && Array.isArray(result.environments)) return result.environments;
|
|
68
|
+
if (result && typeof result.changeId === "string") return [result];
|
|
69
|
+
return [];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Find a candidate by PR number (all-digits target) or exact changeId. Pure. */
|
|
73
|
+
export function matchCandidate(candidates, target) {
|
|
74
|
+
if (target == null) return null;
|
|
75
|
+
if (/^\d+$/.test(target)) {
|
|
76
|
+
const n = Number(target);
|
|
77
|
+
return candidates.find((c) => c.prNumber === n) ?? null;
|
|
78
|
+
}
|
|
79
|
+
return candidates.find((c) => c.changeId === target) ?? null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** One-line candidate summary for `tot pr list`. */
|
|
83
|
+
function line(c) {
|
|
84
|
+
const pr = typeof c.prNumber === "number" ? `#${c.prNumber}` : "#—";
|
|
85
|
+
const url = c.url ? ` ${c.url}` : "";
|
|
86
|
+
return ` PR ${pr} ${c.changeId} [${c.state ?? "?"}]${url}`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** @param {string[]} argv @param {any} ctx */
|
|
90
|
+
export async function run(argv, ctx) {
|
|
91
|
+
const env = process.env;
|
|
92
|
+
const args = parsePrArgs(argv);
|
|
93
|
+
if (args.help) {
|
|
94
|
+
console.log(USAGE);
|
|
95
|
+
return 0;
|
|
96
|
+
}
|
|
97
|
+
if (!SUBCOMMANDS.includes(args.sub)) {
|
|
98
|
+
console.error(fail(`unknown subcommand: \`tot pr ${args.sub}\``, "tot pr list | view <N> | close <N>"));
|
|
99
|
+
return 2;
|
|
100
|
+
}
|
|
101
|
+
if (ctx.mode !== "checkout") {
|
|
102
|
+
console.error(
|
|
103
|
+
fail("`tot pr` runs from inside a tenant checkout", "tot clone <tenant> <dir> (then `cd` in and re-run)"),
|
|
104
|
+
);
|
|
105
|
+
return 2;
|
|
106
|
+
}
|
|
107
|
+
if ((args.sub === "view" || args.sub === "close") && !args.target) {
|
|
108
|
+
console.error(fail(`\`tot pr ${args.sub}\` needs a PR number or changeId`, `tot pr ${args.sub} <N>`));
|
|
109
|
+
return 2;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const workspace = ctx.workspacePath;
|
|
113
|
+
const tenant = ctx.tenant;
|
|
114
|
+
const gitSafe = (cargs) => {
|
|
115
|
+
try {
|
|
116
|
+
return execFileSync("git", ["-C", workspace, ...cargs], { stdio: ["ignore", "pipe", "pipe"] }).toString();
|
|
117
|
+
} catch {
|
|
118
|
+
return "";
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
const repo = repoNameFromRemote(gitSafe(["remote", "get-url", "origin"]).trim());
|
|
122
|
+
if (!repo) {
|
|
123
|
+
console.error(
|
|
124
|
+
fail("couldn't derive the forge repo from this checkout's remote", "run this from a `tot clone`d store"),
|
|
125
|
+
);
|
|
126
|
+
return 1;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const baseUrl = args.mcp || env.MCP_BASE_URL || env.TOT_MCP_URL || DEFAULT_MCP_URL;
|
|
130
|
+
const statePath = defaultCandidateStatePath(env);
|
|
131
|
+
const scope = { mcpUrl: baseUrl, repo };
|
|
132
|
+
const client = createMcpClient(baseUrl);
|
|
133
|
+
try {
|
|
134
|
+
const session = await establishSession(client, { env, prefer: args.identity || undefined });
|
|
135
|
+
void session;
|
|
136
|
+
// Bind the active tenant so candidate_status/close read the right scope.
|
|
137
|
+
await client.callTool("client_switch", { tenant });
|
|
138
|
+
|
|
139
|
+
const candidates = extractCandidates(await client.callTool("candidate_status", { repo }));
|
|
140
|
+
|
|
141
|
+
if (args.sub === "list") {
|
|
142
|
+
if (!candidates.length) {
|
|
143
|
+
console.log(`No open candidate PRs for ${repo}. Run \`tot submit\` to open one.`);
|
|
144
|
+
return 0;
|
|
145
|
+
}
|
|
146
|
+
const active = readActiveChangeId(statePath, scope);
|
|
147
|
+
console.log(`Open candidate PRs for ${repo}:`);
|
|
148
|
+
for (const c of candidates) {
|
|
149
|
+
console.log(line(c) + (active && c.changeId === active ? " ← active" : ""));
|
|
150
|
+
}
|
|
151
|
+
return 0;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const match = matchCandidate(candidates, args.target);
|
|
155
|
+
if (!match) {
|
|
156
|
+
console.error(fail(`no open candidate matches "${args.target}"`, "tot pr list (to see your open candidates)"));
|
|
157
|
+
return 1;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (args.sub === "view") {
|
|
161
|
+
const active = readActiveChangeId(statePath, scope);
|
|
162
|
+
console.log(`PR ${typeof match.prNumber === "number" ? `#${match.prNumber}` : "#—"} — ${match.changeId}${match.changeId === active ? " (active)" : ""}`);
|
|
163
|
+
console.log(` state: ${match.state ?? "?"}`);
|
|
164
|
+
if (match.branch) console.log(` branch: ${match.branch}`);
|
|
165
|
+
if (match.headSha) console.log(` head: ${match.headSha}`);
|
|
166
|
+
if (match.baseSha) console.log(` base: ${match.baseSha}`);
|
|
167
|
+
console.log(` mergeable (forge): ${match.mergeable ?? "?"}`);
|
|
168
|
+
if (match.url) console.log(` ${match.url}`);
|
|
169
|
+
return 0;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// close
|
|
173
|
+
await client.callTool("candidate_close", {
|
|
174
|
+
repo,
|
|
175
|
+
changeId: match.changeId,
|
|
176
|
+
...(args.reason ? { reason: args.reason } : {}),
|
|
177
|
+
});
|
|
178
|
+
console.log(`✓ closed candidate ${match.changeId}${typeof match.prNumber === "number" ? ` (PR #${match.prNumber})` : ""}.`);
|
|
179
|
+
// If we just closed the remembered active candidate, forget it so the next
|
|
180
|
+
// plain `tot submit` starts a fresh one rather than resurrecting this handle.
|
|
181
|
+
if (readActiveChangeId(statePath, scope) === match.changeId) {
|
|
182
|
+
try {
|
|
183
|
+
clearActiveChangeId(statePath, scope);
|
|
184
|
+
} catch { /* best-effort */ }
|
|
185
|
+
}
|
|
186
|
+
return 0;
|
|
187
|
+
} catch (e) {
|
|
188
|
+
if (e instanceof AuthUnavailableError) {
|
|
189
|
+
console.error(fail("sign in to manage candidate PRs", e.hint || "run `tot login`, then re-run"));
|
|
190
|
+
return 1;
|
|
191
|
+
}
|
|
192
|
+
console.error(
|
|
193
|
+
fail(
|
|
194
|
+
`couldn't reach the candidate service: ${String(e?.message || e)}`,
|
|
195
|
+
"check your connection and that you're signed in, then re-run",
|
|
196
|
+
),
|
|
197
|
+
);
|
|
198
|
+
return 1;
|
|
199
|
+
}
|
|
200
|
+
}
|
package/src/commands/start.mjs
CHANGED
|
@@ -59,7 +59,7 @@ import { startProgress } from "../progress.mjs";
|
|
|
59
59
|
import { defaultLastTenantPath, readLastTenant, writeLastTenant } from "../last-tenant.mjs";
|
|
60
60
|
import { readCredentials, defaultCredentialsPath } from "../token-store.mjs";
|
|
61
61
|
import { collectChecks } from "./doctor.mjs";
|
|
62
|
-
import { normalizeStores, storeListError, checkoutTenant, noStoresGuidance } from "./
|
|
62
|
+
import { normalizeStores, storeListError, checkoutTenant, noStoresGuidance } from "./clone.mjs";
|
|
63
63
|
import {
|
|
64
64
|
buildContainerPlan, spawnDevContainer, dockerAvailable, tryStartDocker,
|
|
65
65
|
resolveDevImage, isPrivateRegistryImage, ensureRegistryLogin,
|
|
@@ -633,7 +633,7 @@ async function ensureCheckout(client, tenant, dir, env) {
|
|
|
633
633
|
console.log(` ✓ reusing existing checkout ./${tenant}`);
|
|
634
634
|
return;
|
|
635
635
|
}
|
|
636
|
-
throw new CliError(`./${tenant} already exists and isn't a
|
|
636
|
+
throw new CliError(`./${tenant} already exists and isn't a store checkout`, {
|
|
637
637
|
next: `remove it (or run \`tot start\` from an empty directory)`,
|
|
638
638
|
exitCode: 2,
|
|
639
639
|
});
|
package/src/commands/submit.mjs
CHANGED
|
@@ -46,12 +46,19 @@ import { validateTenant, ERROR } from "../validate.mjs";
|
|
|
46
46
|
import { openBrowser } from "../open.mjs";
|
|
47
47
|
import { startProgress } from "../progress.mjs";
|
|
48
48
|
import { fail } from "../errors.mjs";
|
|
49
|
+
import {
|
|
50
|
+
defaultCandidateStatePath,
|
|
51
|
+
readActiveChangeId,
|
|
52
|
+
writeActiveChangeId,
|
|
53
|
+
mintFreshChangeId,
|
|
54
|
+
isTerminalCandidateState,
|
|
55
|
+
} from "../candidate-state.mjs";
|
|
49
56
|
|
|
50
57
|
const DEFAULT_MCP_URL = "https://mcp.tokenoftrust.com";
|
|
51
58
|
const DEFAULT_REF = "preview";
|
|
52
59
|
|
|
53
60
|
export function parseArgs(argv) {
|
|
54
|
-
const a = { mcp: null, identity: null, ref: DEFAULT_REF, skipValidate: false, noWait: false, watch: false, noOpen: false, message: null, summary: null, help: false };
|
|
61
|
+
const a = { mcp: null, identity: null, ref: DEFAULT_REF, skipValidate: false, noWait: false, watch: false, noOpen: false, message: null, summary: null, new: false, help: false };
|
|
55
62
|
for (let i = 0; i < argv.length; i++) {
|
|
56
63
|
const t = argv[i];
|
|
57
64
|
if (t === "--mcp") a.mcp = argv[++i];
|
|
@@ -63,6 +70,7 @@ export function parseArgs(argv) {
|
|
|
63
70
|
else if (t === "--no-wait") a.noWait = true;
|
|
64
71
|
else if (t === "--watch") a.watch = true;
|
|
65
72
|
else if (t === "--no-open") a.noOpen = true;
|
|
73
|
+
else if (t === "--new") a.new = true;
|
|
66
74
|
else if (t === "--help" || t === "-h") a.help = true;
|
|
67
75
|
}
|
|
68
76
|
return a;
|
|
@@ -71,6 +79,7 @@ export function parseArgs(argv) {
|
|
|
71
79
|
const USAGE = `tot submit — submit your store for preview
|
|
72
80
|
|
|
73
81
|
tot submit validate → push the preview ref → stream the result
|
|
82
|
+
tot submit --new open a NEW candidate PR instead of updating your open one
|
|
74
83
|
tot submit --watch stay attached through reconcile + compliance + accept (long-poll)
|
|
75
84
|
tot submit --skip-validate push without the local lint (not recommended)
|
|
76
85
|
tot submit --ref <name> push ref (default: ${DEFAULT_REF})
|
|
@@ -80,6 +89,12 @@ const USAGE = `tot submit — submit your store for preview
|
|
|
80
89
|
tot submit --no-open don't open the preview URL in the browser on success
|
|
81
90
|
tot submit --mcp <url> MCP base URL (default: env MCP_BASE_URL / TOT_MCP_URL)
|
|
82
91
|
|
|
92
|
+
By default a re-submit UPDATES your open candidate PR (like pushing more commits
|
|
93
|
+
to a GitHub PR), rather than opening a new one each time. Use --new to fork a
|
|
94
|
+
fresh candidate PR; the next plain \`tot submit\` then updates THAT one. Manage your
|
|
95
|
+
open candidates with \`tot pr\` (list / view / close). If your candidate was already
|
|
96
|
+
merged or closed, a re-submit automatically opens a fresh one.
|
|
97
|
+
|
|
83
98
|
If you omit -m, a summary is generated from git (commit subject + the diff vs
|
|
84
99
|
what's live in preview) so the change record the approver reviews is never blank.`;
|
|
85
100
|
|
|
@@ -289,7 +304,7 @@ export async function run(argv, ctx) {
|
|
|
289
304
|
console.error(
|
|
290
305
|
fail(
|
|
291
306
|
"`tot submit` runs from inside a tenant checkout",
|
|
292
|
-
"tot
|
|
307
|
+
"tot clone <tenant> <dir> (then `cd` in, commit your work, and re-run)",
|
|
293
308
|
),
|
|
294
309
|
);
|
|
295
310
|
return 2;
|
|
@@ -381,13 +396,40 @@ export async function run(argv, ctx) {
|
|
|
381
396
|
// 2b. PR-backed candidate (g1b candidate_open, unit c1) — best-effort: a
|
|
382
397
|
// failure here (older MCP, VC not configured, preview-access capability) is
|
|
383
398
|
// reported and swallowed, never blocking the preview push that already landed.
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
399
|
+
//
|
|
400
|
+
// Which candidate this submit lands on (gh-pr-like):
|
|
401
|
+
// --new → fork a FRESH candidate and remember it as active;
|
|
402
|
+
// otherwise → the remembered active candidate (from a prior --new /
|
|
403
|
+
// roll), else the STABLE per-dev-per-tenant default.
|
|
404
|
+
// If the chosen candidate turns out to be merged/closed, roll to a fresh one
|
|
405
|
+
// so a re-submit is never wedged on a dead PR.
|
|
406
|
+
const repo = repoNameFromRemote(gitSafe(["remote", "get-url", "origin"]).trim());
|
|
407
|
+
const readBlob = (path) => execFileSync("git", ["-C", workspace, "show", `HEAD:${path}`], { stdio: ["ignore", "pipe", "pipe"] });
|
|
408
|
+
const statePath = defaultCandidateStatePath(env);
|
|
409
|
+
const stableId = deriveChangeId(tenant, actorKeyFor(session));
|
|
410
|
+
const active = repo ? readActiveChangeId(statePath, { mcpUrl: baseUrl, repo }) : null;
|
|
411
|
+
let changeId = args.new ? mintFreshChangeId(stableId) : (active || stableId);
|
|
412
|
+
// Persist when we diverge from the stable default (a --new fork, or a
|
|
413
|
+
// previously-remembered active pointer) so the next plain submit follows it.
|
|
414
|
+
let persist = args.new || (!!active && active !== stableId);
|
|
415
|
+
|
|
416
|
+
let candidate = await submitCandidate(client, { repo, changeId, changeSummary, patchEntries, readBlob });
|
|
417
|
+
|
|
418
|
+
if (candidate && isTerminalCandidateState(candidate.state)) {
|
|
419
|
+
const rolled = mintFreshChangeId(stableId);
|
|
420
|
+
console.log(` ~ candidate ${changeId} is ${candidate.state} — opening a fresh candidate PR instead.`);
|
|
421
|
+
changeId = rolled;
|
|
422
|
+
persist = true;
|
|
423
|
+
candidate = await submitCandidate(client, { repo, changeId, changeSummary, patchEntries, readBlob });
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Remember the active candidate only on a real, non-terminal open (best-effort;
|
|
427
|
+
// never let a state-write failure break the submit).
|
|
428
|
+
if (persist && repo && candidate && !isTerminalCandidateState(candidate.state)) {
|
|
429
|
+
try {
|
|
430
|
+
writeActiveChangeId(statePath, { mcpUrl: baseUrl, repo, changeId });
|
|
431
|
+
} catch { /* best-effort local hint — a miss just re-derives the stable id */ }
|
|
432
|
+
}
|
|
391
433
|
|
|
392
434
|
let status;
|
|
393
435
|
if (args.noWait) {
|
|
@@ -68,11 +68,11 @@ export function run(argv, ctx) {
|
|
|
68
68
|
|
|
69
69
|
const target = resolveTarget(args, ctx);
|
|
70
70
|
if (target.error) {
|
|
71
|
-
console.error(fail(target.error, "tot
|
|
71
|
+
console.error(fail(target.error, "tot clone <tenant>, or pass --workspace <dir>"));
|
|
72
72
|
return 2;
|
|
73
73
|
}
|
|
74
74
|
if (!existsSync(target.dir)) {
|
|
75
|
-
console.error(fail(`no tenant directory at ${target.dir}`, "confirm the path, or `tot
|
|
75
|
+
console.error(fail(`no tenant directory at ${target.dir}`, "confirm the path, or `tot clone <tenant>`"));
|
|
76
76
|
return 2;
|
|
77
77
|
}
|
|
78
78
|
|
package/src/commands/whoami.mjs
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import { defaultCredentialsPath, readCredentials, isExpired, activeProfile } from "../token-store.mjs";
|
|
12
12
|
import { establishSession } from "../auth.mjs";
|
|
13
13
|
import { createMcpClient } from "../mcp.mjs";
|
|
14
|
-
import { normalizeStores, storeListError, noStoresGuidance } from "./
|
|
14
|
+
import { normalizeStores, storeListError, noStoresGuidance } from "./clone.mjs";
|
|
15
15
|
import { recordServerPolicy } from "../update-check.mjs";
|
|
16
16
|
|
|
17
17
|
/**
|
package/src/context.mjs
CHANGED
|
@@ -6,13 +6,13 @@
|
|
|
6
6
|
*
|
|
7
7
|
* monorepo — inside a full storefront checkout (pnpm-workspace.yaml +
|
|
8
8
|
* apps/storefront + tenants/). This is us / a platform dev.
|
|
9
|
-
* `tot dev` here runs the in-tree astro dev; `tot
|
|
9
|
+
* `tot dev` here runs the in-tree astro dev; `tot clone`
|
|
10
10
|
* can suggest cloning a sibling dir.
|
|
11
11
|
* checkout — inside a STANDALONE tenant checkout: the flat, content-only
|
|
12
12
|
* shape `content/ public/ theme.json .tot/config.json` a
|
|
13
13
|
* developer clones. The tenant is read from .tot/config.json.
|
|
14
14
|
* `tot dev` here boots the bundled runner against this dir.
|
|
15
|
-
* loose — anywhere else. `tot
|
|
15
|
+
* loose — anywhere else. `tot clone <tenant>` still works (it's how
|
|
16
16
|
* you GET a checkout); commands that need a workspace say so.
|
|
17
17
|
*
|
|
18
18
|
* Detection walks UP from the cwd so `tot` works from any subdirectory of a
|
package/src/obstacle-beacon.cjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The obstacle beacon — a fire-and-forget POST that tells the hosted cockpit a
|
|
3
|
-
* `tot start` / `tot
|
|
3
|
+
* `tot start` / `tot clone` failed, so it can show the exact fix in the bridge
|
|
4
4
|
* strip (obstacle lane, server side already shipped). Best-effort telemetry that
|
|
5
5
|
* rides ALONGSIDE the house-style `✗ … → next:` error; it must NEVER change,
|
|
6
6
|
* delay past its timeout, or fail that error path.
|