@tokenoftrust/cli 1.4.0-rc.3 → 1.4.0-rc.5
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/bin/tot.mjs +7 -0
- package/package.json +1 -1
- package/src/commands/checkout.mjs +86 -3
- package/src/commands/link.mjs +217 -0
- package/src/commands/start.mjs +33 -13
- package/src/commands/whoami.mjs +6 -2
package/bin/tot.mjs
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* tot login sign in to Token of Trust (OAuth) ← built (MCP OAuth PKCE loopback; caches ~/.tot/credentials.json)
|
|
9
9
|
* tot logout sign out (clear the cached session) ← built (deletes ~/.tot/credentials.json; local-only, no server revoke)
|
|
10
10
|
* tot whoami who you're signed in as ← built
|
|
11
|
+
* tot link link your identity to the ToT broker (resolve store scope) ← built
|
|
11
12
|
* tot grants capability/tier/expiry per store you can act on ← built (introspection diagnostics)
|
|
12
13
|
* tot checkout [<tenant>] clone a store you can build on ← built
|
|
13
14
|
* tot validate lint your store before you submit ← built
|
|
@@ -53,6 +54,7 @@ tot — Token of Trust developer CLI
|
|
|
53
54
|
tot login sign in to Token of Trust
|
|
54
55
|
tot logout sign out (clear the cached session)
|
|
55
56
|
tot whoami show who you're signed in as
|
|
57
|
+
tot link link your identity so your stores resolve
|
|
56
58
|
tot grants capability/tier/expiry per store you can act on
|
|
57
59
|
tot checkout [<tenant>] clone a store you can build on
|
|
58
60
|
tot validate lint your store before you submit
|
|
@@ -109,6 +111,11 @@ async function dispatch(cmd, rest, ctx) {
|
|
|
109
111
|
return run(rest, ctx);
|
|
110
112
|
}
|
|
111
113
|
|
|
114
|
+
if (cmd === "link") {
|
|
115
|
+
const { run } = await import("../src/commands/link.mjs");
|
|
116
|
+
return run(rest, ctx);
|
|
117
|
+
}
|
|
118
|
+
|
|
112
119
|
if (cmd === "doctor") {
|
|
113
120
|
const { run } = await import("../src/commands/doctor.mjs");
|
|
114
121
|
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.5",
|
|
4
4
|
"description": "Token of Trust developer CLI — check out 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",
|
|
@@ -308,6 +308,86 @@ export function storeListError(list) {
|
|
|
308
308
|
return null;
|
|
309
309
|
}
|
|
310
310
|
|
|
311
|
+
/**
|
|
312
|
+
* Extract card c1's broker-identity remediation signal from a `client_list`
|
|
313
|
+
* result. When the identity resolved ZERO stores, the server MAY carry:
|
|
314
|
+
* - `brokerStatus`: 'unlinked' | 'unconfigured' | 'broker_error' — present only
|
|
315
|
+
* when the empty scope is a BROKER-IDENTITY problem (not an entitlement one);
|
|
316
|
+
* ABSENT for the genuine linked-but-zero-grants case.
|
|
317
|
+
* - `nextAction`: a status-specific human remediation string the server owns
|
|
318
|
+
* (single source of truth; e.g. an unlinked identity is told to finish
|
|
319
|
+
* linking, an operator sees a diagnostic, zero-grants sees NO_TENANTS_MESSAGE).
|
|
320
|
+
* Returns { brokerStatus, nextAction } with nulls when absent. Pure + exported so
|
|
321
|
+
* it's unit-tested without any I/O.
|
|
322
|
+
* @param {unknown} list
|
|
323
|
+
* @returns {{ brokerStatus: string|null, nextAction: string|null }}
|
|
324
|
+
*/
|
|
325
|
+
export function brokerRemediation(list) {
|
|
326
|
+
const c = list && typeof list === "object" && !Array.isArray(list) ? list : null;
|
|
327
|
+
const brokerStatus = c && typeof c.brokerStatus === "string" ? c.brokerStatus : null;
|
|
328
|
+
const nextAction =
|
|
329
|
+
c && typeof c.nextAction === "string" && c.nextAction.trim() ? c.nextAction.trim() : null;
|
|
330
|
+
return { brokerStatus, nextAction };
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Status-aware human guidance for an authenticated identity that resolved ZERO
|
|
335
|
+
* stores (card c2 — the fix for the misleading "may still be propagating / ask for
|
|
336
|
+
* a store invite" copy that dead-ended an UNLINKED identity). Driven by card c1's
|
|
337
|
+
* `brokerStatus`/`nextAction`:
|
|
338
|
+
* - `unlinked` → the identity isn't linked to the ToT broker yet, so no scope can
|
|
339
|
+
* resolve. This is NOT an entitlement problem — point at `tot link` (the
|
|
340
|
+
* terminal action), NOT "ask for a store invite".
|
|
341
|
+
* - `unconfigured` | `broker_error` → an operator/diagnostic condition; surface
|
|
342
|
+
* the server's own `nextAction`, never "ask for an invite".
|
|
343
|
+
* - no brokerStatus → the genuine linked-but-zero-grants case; keep the existing
|
|
344
|
+
* "invite may still be propagating / ask for one" wording (NO_TENANTS_MESSAGE),
|
|
345
|
+
* preferring the server's `nextAction` when present.
|
|
346
|
+
*
|
|
347
|
+
* Prefers the server's `nextAction` as the concrete `next` step (single source of
|
|
348
|
+
* truth) and falls back to sensible local copy when it's absent (older server).
|
|
349
|
+
* `linkHint` names the terminal action an unlinked identity is pointed at — the
|
|
350
|
+
* built-in `tot link`; a caller that didn't build `tot link` passes an MCP-client
|
|
351
|
+
* phrasing instead. Pure + exported so it's unit-tested without any I/O.
|
|
352
|
+
* @param {unknown} list
|
|
353
|
+
* @param {{ linkHint?: string }} [opts]
|
|
354
|
+
* @returns {{ brokerStatus: string|null, headline: string, next: string }}
|
|
355
|
+
*/
|
|
356
|
+
export function noStoresGuidance(list, { linkHint = "tot link" } = {}) {
|
|
357
|
+
const { brokerStatus, nextAction } = brokerRemediation(list);
|
|
358
|
+
|
|
359
|
+
if (brokerStatus === "unlinked") {
|
|
360
|
+
// The actionable terminal step beats the server's identity_link_begin/poll
|
|
361
|
+
// MCP-tool wording for a CLI user, so lead with `tot link` (c2 optional-arm
|
|
362
|
+
// directive). Fall back to the server string only if there's no link action.
|
|
363
|
+
return {
|
|
364
|
+
brokerStatus,
|
|
365
|
+
headline: "your Token of Trust identity isn't linked yet, so no stores could be resolved",
|
|
366
|
+
next: linkHint
|
|
367
|
+
? `run \`${linkHint}\` to finish linking your identity, then re-run`
|
|
368
|
+
: nextAction || "finish linking your identity (identity_link_begin), then re-run",
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
if (brokerStatus === "unconfigured" || brokerStatus === "broker_error") {
|
|
372
|
+
// Operator/diagnostic conditions — surface the server's own words; do NOT tell
|
|
373
|
+
// the developer to "ask for an invite" (it isn't an entitlement problem).
|
|
374
|
+
return {
|
|
375
|
+
brokerStatus,
|
|
376
|
+
headline: "no stores could be resolved — the Token of Trust identity broker had a problem",
|
|
377
|
+
next: nextAction || "run `tot whoami` for details, or contact your Token of Trust operator",
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
// No brokerStatus → the genuine linked-but-zero-grants case.
|
|
381
|
+
return {
|
|
382
|
+
brokerStatus: null,
|
|
383
|
+
headline: "you have no stores to build on yet",
|
|
384
|
+
next:
|
|
385
|
+
nextAction ||
|
|
386
|
+
"if you were just invited, it may still be propagating — try again in a minute; " +
|
|
387
|
+
"otherwise ask your Token of Trust contact for a store invite (see `tot whoami`)",
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
|
|
311
391
|
function printClientList(list) {
|
|
312
392
|
const err = storeListError(list);
|
|
313
393
|
const stores = normalizeStores(list);
|
|
@@ -317,9 +397,12 @@ function printClientList(list) {
|
|
|
317
397
|
return;
|
|
318
398
|
}
|
|
319
399
|
if (stores.length === 0) {
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
400
|
+
// Status-aware, human-readable guidance — NEVER a raw JSON dump (the old
|
|
401
|
+
// behaviour, James's 2026-07-19 failure mode).
|
|
402
|
+
const g = noStoresGuidance(list);
|
|
403
|
+
const line = g.headline.charAt(0).toUpperCase() + g.headline.slice(1);
|
|
404
|
+
console.log(`\n${line}.`);
|
|
405
|
+
console.log(`Next: ${g.next}`);
|
|
323
406
|
return;
|
|
324
407
|
}
|
|
325
408
|
console.log("\nStores you can build on:\n");
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `tot link` — link your signed-in Token of Trust identity to the ToT identity
|
|
3
|
+
* broker so your store scope can be resolved.
|
|
4
|
+
*
|
|
5
|
+
* Some developers sign in fine (a valid ToT OAuth session) but their identity
|
|
6
|
+
* isn't yet linked to the broker that maps a person → the tenants they can build
|
|
7
|
+
* on — so `tot start` / `tot checkout` / `tot whoami` resolve ZERO stores. That's
|
|
8
|
+
* NOT a "you weren't invited" problem and NOT "the invite is still propagating";
|
|
9
|
+
* it's a one-time link step. This command drives it end-to-end from the terminal:
|
|
10
|
+
*
|
|
11
|
+
* identity_link_begin → the MCP returns an authUrl + a poll handle
|
|
12
|
+
* open the authUrl → you approve the link in the browser
|
|
13
|
+
* identity_link_poll(handle) → we poll until it's linked, then confirm scope
|
|
14
|
+
*
|
|
15
|
+
* It reuses the SAME auth ceremony as the rest of the CLI (establishSession — the
|
|
16
|
+
* cached `tot login` session, offering an inline sign-in when there's none and
|
|
17
|
+
* we're on a TTY), so a not-signed-in developer isn't dead-ended.
|
|
18
|
+
*
|
|
19
|
+
* Dependency-free (node built-ins via mcp.mjs / auth.mjs / open.mjs).
|
|
20
|
+
*/
|
|
21
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
22
|
+
import { createMcpClient } from "../mcp.mjs";
|
|
23
|
+
import { establishSession, AuthUnavailableError } from "../auth.mjs";
|
|
24
|
+
import { offerSignIn } from "./login.mjs";
|
|
25
|
+
import { openBrowser } from "../open.mjs";
|
|
26
|
+
import { CliError, fail, formatError } from "../errors.mjs";
|
|
27
|
+
import { normalizeStores } from "./checkout.mjs";
|
|
28
|
+
|
|
29
|
+
const DEFAULT_MCP_URL = "https://mcp.tokenoftrust.com";
|
|
30
|
+
|
|
31
|
+
function parseArgs(argv) {
|
|
32
|
+
const a = { mcp: null, help: false };
|
|
33
|
+
for (let i = 0; i < argv.length; i++) {
|
|
34
|
+
const t = argv[i];
|
|
35
|
+
if (t === "--mcp") a.mcp = argv[++i];
|
|
36
|
+
else if (t === "--help" || t === "-h") a.help = true;
|
|
37
|
+
}
|
|
38
|
+
return a;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const USAGE = `tot link — link your Token of Trust identity so your stores resolve
|
|
42
|
+
|
|
43
|
+
tot link open the browser, approve the link, confirm your scope
|
|
44
|
+
tot link --mcp <url> MCP base URL (default: env MCP_BASE_URL / TOT_MCP_URL)
|
|
45
|
+
|
|
46
|
+
Run this when \`tot whoami\` / \`tot start\` say your identity isn't linked yet.`;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Tolerant extraction of the fields `identity_link_begin` returns. The link URL
|
|
50
|
+
* and poll handle field names can vary by server version, so probe the known
|
|
51
|
+
* candidates (the `pollHandle` name is fixed by the identity_link_poll contract).
|
|
52
|
+
* Pure + exported so it's unit-tested without any I/O.
|
|
53
|
+
* @param {unknown} res
|
|
54
|
+
* @returns {{ authUrl: string|null, pollHandle: string|null }}
|
|
55
|
+
*/
|
|
56
|
+
export function linkBeginFields(res) {
|
|
57
|
+
const c = res && typeof res === "object" && !Array.isArray(res) ? res : {};
|
|
58
|
+
const authUrl =
|
|
59
|
+
c.authUrl ||
|
|
60
|
+
c.url ||
|
|
61
|
+
c.verificationUrl ||
|
|
62
|
+
c.verificationUriComplete ||
|
|
63
|
+
c.verification_uri_complete ||
|
|
64
|
+
null;
|
|
65
|
+
const pollHandle = c.pollHandle || c.handle || c.poll_handle || c.pollHandleId || null;
|
|
66
|
+
return {
|
|
67
|
+
authUrl: typeof authUrl === "string" && authUrl ? authUrl : null,
|
|
68
|
+
pollHandle: typeof pollHandle === "string" && pollHandle ? pollHandle : null,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Classify an `identity_link_poll` result into 'linked' | 'pending' | a raw error
|
|
74
|
+
* status. Tolerant of the shape (a boolean flag, or a status/state string) so a
|
|
75
|
+
* server-version drift doesn't strand the poll. Pure + exported for unit tests.
|
|
76
|
+
* @param {unknown} res
|
|
77
|
+
* @returns {"linked"|"pending"|string}
|
|
78
|
+
*/
|
|
79
|
+
export function linkPollStatus(res) {
|
|
80
|
+
const c = res && typeof res === "object" && !Array.isArray(res) ? res : {};
|
|
81
|
+
if (c.linked === true || c.done === true || c.complete === true) return "linked";
|
|
82
|
+
const raw =
|
|
83
|
+
(typeof c.status === "string" && c.status) || (typeof c.state === "string" && c.state) || "";
|
|
84
|
+
const s = raw.toLowerCase();
|
|
85
|
+
if (!s) return "pending";
|
|
86
|
+
if (/^(linked|complete|completed|done|ok|success|active)$/.test(s)) return "linked";
|
|
87
|
+
if (/^(pending|waiting|in_?progress|processing|started)$/.test(s)) return "pending";
|
|
88
|
+
return s; // an unknown/error status — surfaced to the caller
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** @param {string[]} argv @param {any} _ctx */
|
|
92
|
+
export async function run(argv, _ctx) {
|
|
93
|
+
const env = process.env;
|
|
94
|
+
const args = parseArgs(argv);
|
|
95
|
+
if (args.help) {
|
|
96
|
+
console.log(USAGE);
|
|
97
|
+
return 0;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const baseUrl = args.mcp || env.MCP_BASE_URL || env.TOT_MCP_URL || DEFAULT_MCP_URL;
|
|
101
|
+
const client = createMcpClient(baseUrl);
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
// Same auth ordering as checkout/start — developer bearer BEFORE initialize —
|
|
105
|
+
// and the same inline sign-in offer so a not-signed-in dev isn't dead-ended.
|
|
106
|
+
try {
|
|
107
|
+
await establishSession(client, { env });
|
|
108
|
+
} catch (e) {
|
|
109
|
+
if (e instanceof AuthUnavailableError && e.reason === "missing") {
|
|
110
|
+
const signedIn = await offerSignIn(client.mcpUrl, env, {});
|
|
111
|
+
if (!signedIn) throw e;
|
|
112
|
+
await establishSession(client, { env });
|
|
113
|
+
} else {
|
|
114
|
+
throw e;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
console.error(`~ signed in → ${client.mcpUrl}`);
|
|
118
|
+
|
|
119
|
+
console.error("~ identity_link_begin — asking Token of Trust to start the link");
|
|
120
|
+
let begin;
|
|
121
|
+
try {
|
|
122
|
+
begin = await client.callTool("identity_link_begin", {});
|
|
123
|
+
} catch (e) {
|
|
124
|
+
throw new CliError(`couldn't start the identity link: ${String(e?.message || e)}`, {
|
|
125
|
+
next: "your MCP may not support `tot link` yet — run `tot whoami` for the current guidance",
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
const { authUrl, pollHandle } = linkBeginFields(begin);
|
|
129
|
+
if (!pollHandle) {
|
|
130
|
+
throw new CliError("Token of Trust didn't return a link handle to poll", {
|
|
131
|
+
next: "re-run `tot link`, or run `tot whoami` for the current guidance",
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (authUrl) {
|
|
136
|
+
const opened = openBrowser(authUrl);
|
|
137
|
+
console.log(
|
|
138
|
+
opened
|
|
139
|
+
? `\n+ opening your browser to finish linking:\n ${authUrl}`
|
|
140
|
+
: `\nOpen this URL to finish linking your identity:\n ${authUrl}`,
|
|
141
|
+
);
|
|
142
|
+
} else {
|
|
143
|
+
console.log("\n+ finishing the link — no approval step needed …");
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const linked = await pollLink(client, pollHandle, {
|
|
147
|
+
log: (m) => console.error(m),
|
|
148
|
+
});
|
|
149
|
+
if (!linked) {
|
|
150
|
+
throw new CliError("the identity link didn't complete in time", {
|
|
151
|
+
next: "finish the approval in your browser, then re-run `tot link`",
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
console.log("\n+ your identity is linked.");
|
|
156
|
+
// Confirm scope now resolves — best-effort, so a transient list hiccup doesn't
|
|
157
|
+
// fail an otherwise-successful link.
|
|
158
|
+
try {
|
|
159
|
+
const list = await client.callTool("client_list", {});
|
|
160
|
+
const stores = normalizeStores(list);
|
|
161
|
+
if (stores.length) {
|
|
162
|
+
console.log(` stores you can build on: ${stores.map((s) => s.id).join(", ")}`);
|
|
163
|
+
console.log(" Next: `tot start` (or `tot checkout <tenant>`).");
|
|
164
|
+
} else {
|
|
165
|
+
console.log(" Next: `tot start` — if it still shows no stores, ask your ToT contact for a store invite.");
|
|
166
|
+
}
|
|
167
|
+
} catch {
|
|
168
|
+
console.log(" Next: `tot start` to build your store.");
|
|
169
|
+
}
|
|
170
|
+
return 0;
|
|
171
|
+
} catch (e) {
|
|
172
|
+
if (e instanceof AuthUnavailableError || e instanceof CliError) {
|
|
173
|
+
console.error(formatError(e));
|
|
174
|
+
return e instanceof CliError ? (e.exitCode ?? 1) : 1;
|
|
175
|
+
}
|
|
176
|
+
console.error(fail(`link failed: ${String(e?.message || e)}`));
|
|
177
|
+
return 1;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Poll `identity_link_poll(pollHandle)` until the link is complete, or give up
|
|
183
|
+
* after `timeoutMs`. Returns true on 'linked', false on timeout; throws a CliError
|
|
184
|
+
* on a definite error status the server reports. Injectable clock/interval keep it
|
|
185
|
+
* unit-testable, but the default path is the live poll.
|
|
186
|
+
* @param {ReturnType<import("../mcp.mjs").createMcpClient>} client
|
|
187
|
+
* @param {string} pollHandle
|
|
188
|
+
* @param {{ timeoutMs?: number, intervalMs?: number, log?: (m:string)=>void }} [opts]
|
|
189
|
+
* @returns {Promise<boolean>}
|
|
190
|
+
*/
|
|
191
|
+
export async function pollLink(client, pollHandle, { timeoutMs = 120000, intervalMs = 2500, log = () => {} } = {}) {
|
|
192
|
+
const deadline = Date.now() + timeoutMs;
|
|
193
|
+
let announced = false;
|
|
194
|
+
while (Date.now() < deadline) {
|
|
195
|
+
let res;
|
|
196
|
+
try {
|
|
197
|
+
res = await client.callTool("identity_link_poll", { pollHandle });
|
|
198
|
+
} catch (e) {
|
|
199
|
+
// A transient poll error isn't fatal — keep trying until the deadline.
|
|
200
|
+
await delay(intervalMs);
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
const status = linkPollStatus(res);
|
|
204
|
+
if (status === "linked") return true;
|
|
205
|
+
if (status !== "pending") {
|
|
206
|
+
throw new CliError(`Token of Trust reported the link couldn't complete (${status})`, {
|
|
207
|
+
next: "re-run `tot link`, or run `tot whoami` for the current guidance",
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
if (!announced) {
|
|
211
|
+
log("~ waiting for you to approve the link in your browser …");
|
|
212
|
+
announced = true;
|
|
213
|
+
}
|
|
214
|
+
await delay(intervalMs);
|
|
215
|
+
}
|
|
216
|
+
return false;
|
|
217
|
+
}
|
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 "./checkout.mjs";
|
|
62
|
+
import { normalizeStores, storeListError, checkoutTenant, noStoresGuidance } from "./checkout.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)`);
|
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 } from "./checkout.mjs";
|
|
14
|
+
import { normalizeStores, storeListError, noStoresGuidance } from "./checkout.mjs";
|
|
15
15
|
import { recordServerPolicy } from "../update-check.mjs";
|
|
16
16
|
|
|
17
17
|
/**
|
|
@@ -67,7 +67,11 @@ export async function run(argv, _ctx) {
|
|
|
67
67
|
} else if (listErr) {
|
|
68
68
|
console.log(` (couldn't list your stores: ${listErr} — try \`tot login\` again)`);
|
|
69
69
|
} else {
|
|
70
|
-
|
|
70
|
+
// Status-aware (card c2): an UNLINKED identity is told to link (not "may
|
|
71
|
+
// still be propagating" — that's only the genuine zero-grants case).
|
|
72
|
+
const g = noStoresGuidance(listResp);
|
|
73
|
+
console.log(` (${g.headline})`);
|
|
74
|
+
console.log(` → next: ${g.next}`);
|
|
71
75
|
}
|
|
72
76
|
} catch {
|
|
73
77
|
console.log(" (couldn't reach the MCP to list your stores right now — your cached session is above)");
|