@tokenoftrust/cli 1.4.0-rc.0 → 1.4.0-rc.10
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 +19 -13
- package/bin/tot.mjs +51 -11
- package/package.json +2 -2
- package/src/candidate-state.mjs +97 -0
- package/src/commands/{checkout.mjs → clone.mjs} +129 -28
- package/src/commands/dev.mjs +110 -15
- package/src/commands/doctor.mjs +4 -3
- package/src/commands/grants.mjs +8 -3
- package/src/commands/link.mjs +225 -0
- package/src/commands/login.mjs +19 -12
- package/src/commands/pr.mjs +200 -0
- package/src/commands/preview.mjs +71 -0
- package/src/commands/ship.mjs +53 -0
- package/src/commands/start.mjs +34 -14
- package/src/commands/submit.mjs +239 -23
- package/src/commands/validate.mjs +2 -2
- package/src/commands/whoami.mjs +6 -2
- package/src/context.mjs +2 -2
- package/src/oauth.mjs +92 -42
- package/src/obstacle-beacon.cjs +1 -1
- package/src/token-store.mjs +38 -15
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `tot preview` — submit your store for PREVIEW (the deliberate, gated step).
|
|
3
|
+
*
|
|
4
|
+
* This is the first-class verb for the dev → preview → ship loop:
|
|
5
|
+
*
|
|
6
|
+
* tot dev run your store locally with save→reload
|
|
7
|
+
* tot preview push it to a reviewable preview (validate → reconcile → compliance) ← you are here
|
|
8
|
+
* tot ship promote a reconciled preview live ← unit u3
|
|
9
|
+
*
|
|
10
|
+
* The whole preview flow (validate, push the preview ref, open/update the PR-backed
|
|
11
|
+
* candidate, stream the reconcile/compliance/preview result) lives in submit.mjs — this
|
|
12
|
+
* module is a thin wrapper that runs that SAME flow and, on success, teaches the next
|
|
13
|
+
* verb (`tot ship`). Keeping the flow in one place means `tot preview` and its teaching
|
|
14
|
+
* aliases (`tot submit` / `tot deploy`, below) share exactly one implementation — every
|
|
15
|
+
* submit flag works identically through preview, with no chance of drift.
|
|
16
|
+
*
|
|
17
|
+
* TEACHING ALIASES: `tot submit` and `tot deploy` are the same preview flow. They run it
|
|
18
|
+
* unchanged and then print a one-line hint teaching the rename (and `tot ship`), so a
|
|
19
|
+
* developer who reaches for the old verb is nudged onto the new model without losing a
|
|
20
|
+
* step. Pass `{ alias }` (the verb the developer typed) to get that hint.
|
|
21
|
+
*
|
|
22
|
+
* Dependency-free (delegates to submit.mjs, which uses global fetch + `git`).
|
|
23
|
+
*/
|
|
24
|
+
import { run as runPreviewFlow, parseArgs, renderUsage } from "./submit.mjs";
|
|
25
|
+
|
|
26
|
+
/** The ship hint printed after a successful `tot preview` — teaches the next verb.
|
|
27
|
+
* Pure — unit-tested. */
|
|
28
|
+
export const shipHint = () =>
|
|
29
|
+
"\n → next: once this preview reconciles cleanly, `tot ship` promotes it live.";
|
|
30
|
+
|
|
31
|
+
/** The teaching hint printed when the preview flow is reached via an old verb
|
|
32
|
+
* (`tot submit` / `tot deploy`). Names the rename AND the ship verb in one line.
|
|
33
|
+
* Pure — unit-tested. */
|
|
34
|
+
export const aliasHint = (alias) =>
|
|
35
|
+
`\n ℹ \`tot ${alias}\` is now \`tot preview\` — and \`tot ship\` promotes it live.`;
|
|
36
|
+
|
|
37
|
+
/** The hint to print after the flow returns, or null when there's nothing to teach
|
|
38
|
+
* (the flow failed). An alias gets the rename+ship hint; the first-class verb gets
|
|
39
|
+
* the ship hint; a non-zero exit teaches nothing (the push didn't land). Pure. */
|
|
40
|
+
export function postRunHint(code, alias) {
|
|
41
|
+
if (code !== 0) return null;
|
|
42
|
+
return alias ? aliasHint(alias) : shipHint();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @param {string[]} argv
|
|
47
|
+
* @param {any} ctx
|
|
48
|
+
* @param {{ alias?: string|null }} [opts] — `alias` is the old verb the developer typed
|
|
49
|
+
* (`"submit"` / `"deploy"`) when this flow is reached as a teaching alias; null/omitted
|
|
50
|
+
* for the first-class `tot preview`.
|
|
51
|
+
*/
|
|
52
|
+
export async function run(argv, ctx, { alias = null } = {}) {
|
|
53
|
+
const verb = alias || "preview";
|
|
54
|
+
const args = parseArgs(argv);
|
|
55
|
+
|
|
56
|
+
// Handle --help here so we can brand the usage with the typed verb and, for an
|
|
57
|
+
// alias, teach the rename — without also printing the post-success ship hint.
|
|
58
|
+
if (args.help) {
|
|
59
|
+
console.log(renderUsage(verb));
|
|
60
|
+
if (alias) console.log(aliasHint(alias));
|
|
61
|
+
return 0;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const code = await runPreviewFlow(argv, ctx, { verb });
|
|
65
|
+
|
|
66
|
+
// Teach the next step only on a successful preview (the push landed). An alias
|
|
67
|
+
// gets the rename+ship hint; the first-class verb just gets the ship hint.
|
|
68
|
+
const hint = postRunHint(code, alias);
|
|
69
|
+
if (hint) console.log(hint);
|
|
70
|
+
return code;
|
|
71
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `tot ship` — promote a reconciled PREVIEW live (the ship gate).
|
|
3
|
+
*
|
|
4
|
+
* This is the second half of the dev → preview → ship loop:
|
|
5
|
+
*
|
|
6
|
+
* tot dev run your store locally with save→reload
|
|
7
|
+
* tot preview push it to a reviewable preview (validate → reconcile → compliance)
|
|
8
|
+
* tot ship promote a reconciled preview live ← you are here
|
|
9
|
+
*
|
|
10
|
+
* STATUS: STUB — the command is registered and named so the loop reads end-to-end,
|
|
11
|
+
* but the real ship logic (resolve the current candidate/change for this checkout,
|
|
12
|
+
* show the diff about to go live, confirm, then `change_accept` / `candidate_accept`
|
|
13
|
+
* via the MCP, and report the shipped result) lands in a later unit.
|
|
14
|
+
*
|
|
15
|
+
* TODO(u3): implement the real ship flow. Symmetric with submit.mjs's preview flow —
|
|
16
|
+
* it should reuse the same candidate handle (deriveChangeId / the active-candidate
|
|
17
|
+
* state in candidate-state.mjs) so `tot ship` promotes the exact preview `tot preview`
|
|
18
|
+
* last pushed, gate on a clean reconcile, and diff-confirm before accepting.
|
|
19
|
+
*
|
|
20
|
+
* Until then this prints a clear "not yet wired" message pointing at the reviewer
|
|
21
|
+
* path that ships a change today, and exits non-zero so nothing mistakes it for a
|
|
22
|
+
* completed ship. Additive + non-breaking: it performs no action.
|
|
23
|
+
*
|
|
24
|
+
* Dependency-free.
|
|
25
|
+
*/
|
|
26
|
+
import { fail } from "../errors.mjs";
|
|
27
|
+
|
|
28
|
+
const USAGE = `tot ship — promote a reconciled preview live
|
|
29
|
+
|
|
30
|
+
tot ship (coming soon) promote the preview you last pushed with \`tot preview\`
|
|
31
|
+
|
|
32
|
+
Ship the preview live once it has reconciled cleanly. This is the deliberate
|
|
33
|
+
step AFTER \`tot preview\` — preview makes it reviewable; ship makes it live.`;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @param {string[]} argv
|
|
37
|
+
* @param {any} _ctx
|
|
38
|
+
*/
|
|
39
|
+
export async function run(argv, _ctx) {
|
|
40
|
+
if (argv.includes("--help") || argv.includes("-h")) {
|
|
41
|
+
console.log(USAGE);
|
|
42
|
+
return 0;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
console.error(
|
|
46
|
+
fail(
|
|
47
|
+
"`tot ship` isn't wired up yet",
|
|
48
|
+
"push your work for review with `tot preview`; a reviewer accepts it to ship it live " +
|
|
49
|
+
"(change_accept / `tot pr`). Self-serve `tot ship` is coming soon.",
|
|
50
|
+
),
|
|
51
|
+
);
|
|
52
|
+
return 2;
|
|
53
|
+
}
|
package/src/commands/start.mjs
CHANGED
|
@@ -57,8 +57,9 @@ import { CliError, fail, formatError, exitCodeFor } from "../errors.mjs";
|
|
|
57
57
|
import { openBrowser, waitForServer, firstFreePort } from "../open.mjs";
|
|
58
58
|
import { startProgress } from "../progress.mjs";
|
|
59
59
|
import { defaultLastTenantPath, readLastTenant, writeLastTenant } from "../last-tenant.mjs";
|
|
60
|
+
import { readCredentials, defaultCredentialsPath } from "../token-store.mjs";
|
|
60
61
|
import { collectChecks } from "./doctor.mjs";
|
|
61
|
-
import { normalizeStores, storeListError, checkoutTenant } from "./
|
|
62
|
+
import { normalizeStores, storeListError, checkoutTenant, noStoresGuidance } from "./clone.mjs";
|
|
62
63
|
import {
|
|
63
64
|
buildContainerPlan, spawnDevContainer, dockerAvailable, tryStartDocker,
|
|
64
65
|
resolveDevImage, isPrivateRegistryImage, ensureRegistryLogin,
|
|
@@ -161,6 +162,24 @@ export function decideStartMode({ sampleFlag, hasSession }) {
|
|
|
161
162
|
return hasSession ? "authed" : "sample";
|
|
162
163
|
}
|
|
163
164
|
|
|
165
|
+
/**
|
|
166
|
+
* The MCP base URL the cached session was minted on — what `tot login` stored in the
|
|
167
|
+
* credentials file (honoring TOT_PROFILE via defaultCredentialsPath). Lets `tot start`
|
|
168
|
+
* (and callers) FOLLOW wherever the developer signed in rather than defaulting to prod,
|
|
169
|
+
* which would look up a session on the wrong MCP and report "not signed in". Returns
|
|
170
|
+
* null when there's no cached session or it can't be read (falls through to the default).
|
|
171
|
+
* @param {NodeJS.ProcessEnv} env
|
|
172
|
+
* @returns {string|null}
|
|
173
|
+
*/
|
|
174
|
+
export function cachedMcpUrl(env) {
|
|
175
|
+
try {
|
|
176
|
+
const creds = readCredentials(defaultCredentialsPath(env));
|
|
177
|
+
return creds && typeof creds.mcpUrl === "string" && creds.mcpUrl ? creds.mcpUrl : null;
|
|
178
|
+
} catch {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
164
183
|
/** @param {string[]} argv @param {any} ctx */
|
|
165
184
|
export async function run(argv, ctx) {
|
|
166
185
|
const env = process.env;
|
|
@@ -178,7 +197,11 @@ export async function run(argv, ctx) {
|
|
|
178
197
|
}
|
|
179
198
|
|
|
180
199
|
try {
|
|
181
|
-
|
|
200
|
+
// Resolve the MCP: an explicit flag / env wins, then the MCP the cached session was
|
|
201
|
+
// minted on (what `tot login` stored — so `start` FOLLOWS wherever you signed in
|
|
202
|
+
// instead of defaulting to prod and reporting "not signed in"), then the prod default.
|
|
203
|
+
const baseUrl =
|
|
204
|
+
args.mcp || env.MCP_BASE_URL || env.TOT_MCP_URL || cachedMcpUrl(env) || DEFAULT_MCP_URL;
|
|
182
205
|
const client = createMcpClient(baseUrl);
|
|
183
206
|
|
|
184
207
|
// Resolve a session, tolerating a no-session / no-network condition so we can
|
|
@@ -223,6 +246,7 @@ export async function run(argv, ctx) {
|
|
|
223
246
|
tenant = await resolveTenant(stores, args, env, baseUrl, {
|
|
224
247
|
session,
|
|
225
248
|
listErr: storeListError(listResp),
|
|
249
|
+
list: listResp,
|
|
226
250
|
});
|
|
227
251
|
} catch (e) {
|
|
228
252
|
stopEarlyHeartbeat();
|
|
@@ -537,7 +561,7 @@ function mcpOrigin(baseUrl) {
|
|
|
537
561
|
* (surface the reason — likely an auth/entitlement problem) from a genuinely empty
|
|
538
562
|
* result (invite may still be propagating, or you need one). Points at `tot whoami`.
|
|
539
563
|
*/
|
|
540
|
-
function noStoresError({ session, baseUrl, listErr }) {
|
|
564
|
+
function noStoresError({ session, baseUrl, listErr, list = null }) {
|
|
541
565
|
const who = describeIdentity(session);
|
|
542
566
|
const origin = mcpOrigin(baseUrl);
|
|
543
567
|
if (listErr) {
|
|
@@ -546,14 +570,10 @@ function noStoresError({ session, baseUrl, listErr }) {
|
|
|
546
570
|
{ next: "run `tot whoami` to check your session, or `tot login` again — then re-run `tot start`" },
|
|
547
571
|
);
|
|
548
572
|
}
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
"if you were just invited, it may still be propagating — try again in a minute; " +
|
|
554
|
-
"otherwise ask your Token of Trust contact for a store invite (see `tot whoami`)",
|
|
555
|
-
},
|
|
556
|
-
);
|
|
573
|
+
// Status-aware (card c2): an UNLINKED identity is pointed at `tot link`, not the
|
|
574
|
+
// misleading "ask for a store invite" copy; the genuine zero-grants case keeps it.
|
|
575
|
+
const g = noStoresGuidance(list);
|
|
576
|
+
return new CliError(`signed in as ${who} via ${origin}, but ${g.headline}`, { next: g.next });
|
|
557
577
|
}
|
|
558
578
|
|
|
559
579
|
/**
|
|
@@ -561,7 +581,7 @@ function noStoresError({ session, baseUrl, listErr }) {
|
|
|
561
581
|
* the remembered last tenant (A4) — then remember whatever was decided so the
|
|
562
582
|
* next bare `tot start` doesn't have to ask again.
|
|
563
583
|
*/
|
|
564
|
-
async function resolveTenant(stores, args, env, baseUrl, { session = null, listErr = null } = {}) {
|
|
584
|
+
async function resolveTenant(stores, args, env, baseUrl, { session = null, listErr = null, list = null } = {}) {
|
|
565
585
|
const lastTenantPath = defaultLastTenantPath(env);
|
|
566
586
|
const pick = pickTenant(stores, {
|
|
567
587
|
explicit: args.tenant || null,
|
|
@@ -570,7 +590,7 @@ async function resolveTenant(stores, args, env, baseUrl, { session = null, listE
|
|
|
570
590
|
|
|
571
591
|
let tenant;
|
|
572
592
|
if (pick.kind === "none") {
|
|
573
|
-
throw noStoresError({ session, baseUrl, listErr });
|
|
593
|
+
throw noStoresError({ session, baseUrl, listErr, list });
|
|
574
594
|
} else if (pick.kind === "explicit") {
|
|
575
595
|
tenant = pick.tenant;
|
|
576
596
|
console.log(` → your store: ${tenant} (--tenant)`);
|
|
@@ -613,7 +633,7 @@ async function ensureCheckout(client, tenant, dir, env) {
|
|
|
613
633
|
console.log(` ✓ reusing existing checkout ./${tenant}`);
|
|
614
634
|
return;
|
|
615
635
|
}
|
|
616
|
-
throw new CliError(`./${tenant} already exists and isn't a
|
|
636
|
+
throw new CliError(`./${tenant} already exists and isn't a store checkout`, {
|
|
617
637
|
next: `remove it (or run \`tot start\` from an empty directory)`,
|
|
618
638
|
exitCode: 2,
|
|
619
639
|
});
|