@tokenoftrust/cli 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +50 -0
- package/bin/tot.mjs +124 -0
- package/package.json +41 -0
- package/src/auth.mjs +132 -0
- package/src/commands/checkout.mjs +233 -0
- package/src/commands/dev.mjs +564 -0
- package/src/commands/doctor.mjs +172 -0
- package/src/commands/ideas.mjs +45 -0
- package/src/commands/login.mjs +107 -0
- package/src/commands/start.mjs +450 -0
- package/src/commands/submit.mjs +284 -0
- package/src/commands/validate.mjs +99 -0
- package/src/commands/whoami.mjs +69 -0
- package/src/context.mjs +97 -0
- package/src/errors.mjs +64 -0
- package/src/last-tenant.mjs +49 -0
- package/src/mcp.mjs +100 -0
- package/src/oauth.mjs +409 -0
- package/src/open.mjs +63 -0
- package/src/token-store.mjs +65 -0
- package/src/validate.mjs +291 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `tot doctor` — is this machine ready to run the loop?
|
|
3
|
+
*
|
|
4
|
+
* Standalone (unlike the in-monorepo doctor): checks the things `tot checkout`
|
|
5
|
+
* and `tot dev`/`tot start` actually need — Node, git, an MCP URL, whether an
|
|
6
|
+
* auth identity is available, and Docker — plus reports the detected context
|
|
7
|
+
* so a developer knows which mode `tot` will use here.
|
|
8
|
+
*
|
|
9
|
+
* Docker is informational-only (not blocking): WS3/F3 made the native runtime
|
|
10
|
+
* the default for `tot dev`/`tot start`, so a Docker that isn't running no
|
|
11
|
+
* longer stops the loop — it only matters if you pass --docker or the native
|
|
12
|
+
* artifact can't be fetched (see dev.mjs#NativeArtifactUnavailableError).
|
|
13
|
+
*
|
|
14
|
+
* `--fix` (F2) auto-remediates what it safely can, each fix announced as it
|
|
15
|
+
* runs: starts Docker (macOS, opportunistically — harmless even though it's
|
|
16
|
+
* no longer required), creates ~/.tot if it's missing, and signs you in when
|
|
17
|
+
* there's no usable session at all. Anything it can't fix (or won't — a stale
|
|
18
|
+
* sign-in silently refreshes on next use instead, no action needed) is left
|
|
19
|
+
* in the checks list with its usual next command.
|
|
20
|
+
*/
|
|
21
|
+
import { spawnSync } from "node:child_process";
|
|
22
|
+
import { existsSync, mkdirSync } from "node:fs";
|
|
23
|
+
import { homedir } from "node:os";
|
|
24
|
+
import { join } from "node:path";
|
|
25
|
+
import { hasOperatorCreds } from "../auth.mjs";
|
|
26
|
+
import { defaultCredentialsPath, readCredentials, isExpired } from "../token-store.mjs";
|
|
27
|
+
import { dockerAvailable, tryStartDocker } from "./dev.mjs";
|
|
28
|
+
import { loginAndCache } from "./login.mjs";
|
|
29
|
+
|
|
30
|
+
const DEFAULT_MCP_URL = "https://mcp.tokenoftrust.com";
|
|
31
|
+
|
|
32
|
+
function parseArgs(argv) {
|
|
33
|
+
const a = { fix: false, help: false };
|
|
34
|
+
for (const t of argv) {
|
|
35
|
+
if (t === "--fix") a.fix = true;
|
|
36
|
+
else if (t === "--help" || t === "-h") a.help = true;
|
|
37
|
+
}
|
|
38
|
+
return a;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const USAGE = `tot doctor — is this machine ready to run the loop?
|
|
42
|
+
|
|
43
|
+
tot doctor show the readiness checks
|
|
44
|
+
tot doctor --fix auto-remediate what it safely can (Docker, ~/.tot,
|
|
45
|
+
signing in if you aren't at all), then re-check`;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The machine-readiness checks, as data — so both `tot doctor` (which prints
|
|
49
|
+
* them) and `tot start`'s preflight (which acts on the failing ones) read the
|
|
50
|
+
* exact same set. Each check is `{ name, pass, detail, blocking }`; `blocking`
|
|
51
|
+
* marks the ones that actually stop the loop (node, git) vs informational
|
|
52
|
+
* context (MCP endpoint, auth identity, Docker — see the module doc above).
|
|
53
|
+
*
|
|
54
|
+
* @param {any} _ctx @param {NodeJS.ProcessEnv} [env]
|
|
55
|
+
* @returns {Array<{ name: string, pass: boolean, detail: string, blocking: boolean }>}
|
|
56
|
+
*/
|
|
57
|
+
export function collectChecks(_ctx, env = process.env) {
|
|
58
|
+
const checks = [];
|
|
59
|
+
|
|
60
|
+
const nodeMajor = Number(process.versions.node.split(".")[0]);
|
|
61
|
+
checks.push({ name: "node >= 20", pass: nodeMajor >= 20, detail: `have ${process.versions.node}`, blocking: true });
|
|
62
|
+
|
|
63
|
+
const gitRes = spawnSync("git", ["--version"], {
|
|
64
|
+
encoding: "utf8",
|
|
65
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
66
|
+
});
|
|
67
|
+
const git = gitRes.status === 0 ? String(gitRes.stdout).trim() : null;
|
|
68
|
+
checks.push({ name: "git installed", pass: !!git, detail: git || "not found — https://git-scm.com", blocking: true });
|
|
69
|
+
|
|
70
|
+
const dockerOk = dockerAvailable();
|
|
71
|
+
checks.push({
|
|
72
|
+
name: "Docker",
|
|
73
|
+
pass: dockerOk,
|
|
74
|
+
detail: dockerOk
|
|
75
|
+
? "running"
|
|
76
|
+
: "not running — optional (native is the default runtime; only needed for --docker or as its fallback)",
|
|
77
|
+
blocking: false,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const mcpUrl = env.MCP_BASE_URL || env.TOT_MCP_URL || DEFAULT_MCP_URL;
|
|
81
|
+
checks.push({
|
|
82
|
+
name: "MCP endpoint",
|
|
83
|
+
pass: true,
|
|
84
|
+
detail: `${mcpUrl}${env.MCP_BASE_URL || env.TOT_MCP_URL ? " (from env)" : " (default)"}`,
|
|
85
|
+
blocking: false,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const operator = hasOperatorCreds(env);
|
|
89
|
+
const devCreds = operator ? null : readCredentials(defaultCredentialsPath(env));
|
|
90
|
+
const devUsable = !!devCreds?.accessToken && (!isExpired(devCreds) || !!devCreds.refreshToken);
|
|
91
|
+
checks.push({
|
|
92
|
+
name: "auth identity",
|
|
93
|
+
pass: operator || devUsable,
|
|
94
|
+
detail: operator
|
|
95
|
+
? "operator creds present (TOT_API_KEY/TOT_SECRET_KEY/TOT_APP_DOMAIN)"
|
|
96
|
+
: devUsable
|
|
97
|
+
? "signed in as a developer"
|
|
98
|
+
: "not signed in — run `tot login`",
|
|
99
|
+
blocking: false,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
return checks;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Auto-remediate the checks that are safely fixable without human judgment
|
|
107
|
+
* (F2) — announcing each one as it runs. A stale-but-refreshable sign-in isn't
|
|
108
|
+
* handled here because it isn't a failing check: resolveSession() refreshes it
|
|
109
|
+
* silently the next time it's actually used (B4).
|
|
110
|
+
*/
|
|
111
|
+
async function applyFixes(checks, env) {
|
|
112
|
+
const totDir = join(env.TOT_HOME || homedir(), ".tot");
|
|
113
|
+
if (!existsSync(totDir)) {
|
|
114
|
+
console.log(` ~ creating ${totDir}`);
|
|
115
|
+
mkdirSync(totDir, { recursive: true, mode: 0o700 });
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const docker = checks.find((c) => c.name === "Docker");
|
|
119
|
+
if (docker && !docker.pass) await tryStartDocker();
|
|
120
|
+
|
|
121
|
+
const auth = checks.find((c) => c.name === "auth identity");
|
|
122
|
+
if (auth && !auth.pass && !hasOperatorCreds(env)) {
|
|
123
|
+
const mcpUrl = env.MCP_BASE_URL || env.TOT_MCP_URL || DEFAULT_MCP_URL;
|
|
124
|
+
console.log(" ~ not signed in — opening the browser to sign in…");
|
|
125
|
+
try {
|
|
126
|
+
await loginAndCache(mcpUrl, env, { log: (m) => console.log(` ${m}`) });
|
|
127
|
+
console.log(" ✓ signed in");
|
|
128
|
+
} catch (e) {
|
|
129
|
+
console.log(` ~ couldn't sign in automatically (${e?.message || e}) — run \`tot login\``);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** @param {string[]} argv @param {any} ctx */
|
|
135
|
+
export async function run(argv, ctx) {
|
|
136
|
+
const args = parseArgs(argv);
|
|
137
|
+
if (args.help) {
|
|
138
|
+
console.log(USAGE);
|
|
139
|
+
return 0;
|
|
140
|
+
}
|
|
141
|
+
const env = process.env;
|
|
142
|
+
|
|
143
|
+
console.log("\ntot doctor\n");
|
|
144
|
+
console.log(` context: ${describeContext(ctx)}\n`);
|
|
145
|
+
|
|
146
|
+
let checks = collectChecks(ctx, env);
|
|
147
|
+
if (args.fix) {
|
|
148
|
+
await applyFixes(checks, env);
|
|
149
|
+
checks = collectChecks(ctx, env); // re-check after fixing
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
let failed = 0;
|
|
153
|
+
for (const { name, pass, detail } of checks) {
|
|
154
|
+
if (!pass) failed++;
|
|
155
|
+
console.log(` ${pass ? "✓" : "✗"} ${name}${detail ? ` — ${detail}` : ""}`);
|
|
156
|
+
}
|
|
157
|
+
console.log(
|
|
158
|
+
failed === 0
|
|
159
|
+
? "\n✔ Ready. Try: tot start (checks out your store and runs it), or tot checkout\n"
|
|
160
|
+
: args.fix
|
|
161
|
+
? `\n✖ ${failed} check(s) still failing — see the exact next command(s) above, then re-run \`tot doctor\`.\n`
|
|
162
|
+
: `\n✖ ${failed} check(s) failed — try \`tot doctor --fix\`, or fix the above, then re-run \`tot doctor\`.\n`,
|
|
163
|
+
);
|
|
164
|
+
return failed === 0 ? 0 : 1;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function describeContext(ctx) {
|
|
168
|
+
if (ctx.mode === "monorepo") return `storefront monorepo (${ctx.repoRoot})`;
|
|
169
|
+
if (ctx.mode === "checkout")
|
|
170
|
+
return `tenant checkout${ctx.tenant ? ` for "${ctx.tenant}"` : ""} (${ctx.workspacePath})`;
|
|
171
|
+
return "loose (not inside a checkout — `tot checkout <tenant> --clone <dir>` to get one)";
|
|
172
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `tot ideas` — a gallery of copy-paste reskin prompts that reliably wow (G3).
|
|
3
|
+
* Not a script to run — inspiration for the AI moment: paste one into Claude
|
|
4
|
+
* (after `tot start`'s "Connect Claude?" prompt, or `claude mcp add` directly)
|
|
5
|
+
* and watch your store change and reload.
|
|
6
|
+
*
|
|
7
|
+
* IDEAS[0] doubles as the seeded first prompt `tot start` ends on and drops
|
|
8
|
+
* you into (G2, see commands/start.mjs) — one list, no drift between the two.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** Reskin prompts, ordered strongest-first. Pure data — exported for reuse + tests. */
|
|
12
|
+
export const IDEAS = [
|
|
13
|
+
"make it darker but still friendly",
|
|
14
|
+
"give the homepage a bold, oversized headline",
|
|
15
|
+
"swap the accent color for something more premium — think matte gold",
|
|
16
|
+
"make the product grid feel more editorial, like a fashion lookbook",
|
|
17
|
+
"add a subtle scroll-triggered fade-in to the hero section",
|
|
18
|
+
"tighten up the spacing everywhere — it feels a little loose",
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
function parseArgs(argv) {
|
|
22
|
+
const a = { help: false };
|
|
23
|
+
for (const t of argv) if (t === "--help" || t === "-h") a.help = true;
|
|
24
|
+
return a;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const USAGE = `tot ideas — copy-paste prompts that reliably wow
|
|
28
|
+
|
|
29
|
+
tot ideas print a gallery of reskin prompts to try with Claude
|
|
30
|
+
|
|
31
|
+
Paste one into Claude (after \`tot start\`'s "Connect Claude?" prompt, or
|
|
32
|
+
\`claude mcp add\` directly) and watch your store change and reload.`;
|
|
33
|
+
|
|
34
|
+
/** @param {string[]} argv @param {any} _ctx */
|
|
35
|
+
export function run(argv, _ctx) {
|
|
36
|
+
const args = parseArgs(argv);
|
|
37
|
+
if (args.help) {
|
|
38
|
+
console.log(USAGE);
|
|
39
|
+
return 0;
|
|
40
|
+
}
|
|
41
|
+
console.log("\n Ideas to try with Claude:\n");
|
|
42
|
+
for (const idea of IDEAS) console.log(` "${idea}"`);
|
|
43
|
+
console.log("\n Paste one in, save, and watch it reload.\n");
|
|
44
|
+
return 0;
|
|
45
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `tot login` — sign the developer in to Token of Trust.
|
|
3
|
+
*
|
|
4
|
+
* Runs the MCP OAuth 2.1 PKCE loopback (the same ceremony `claude mcp add` runs):
|
|
5
|
+
* opens the browser to the MCP's authorize page, the developer signs in with their
|
|
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 checkout`,
|
|
8
|
+
* `tot submit`, …) then runs as that developer with NO re-auth.
|
|
9
|
+
*
|
|
10
|
+
* The MCP defaults to the same target `tot submit` talks to (env MCP_BASE_URL /
|
|
11
|
+
* TOT_MCP_URL, else the production MCP) so the cached token matches where it's
|
|
12
|
+
* spent — point both at qa with `--mcp https://mcp.qa.tokenoftrust.com` or the env.
|
|
13
|
+
*
|
|
14
|
+
* B3 — `--device` forces the RFC 8628 device-code flow (headless/SSH/no-browser:
|
|
15
|
+
* print a code, poll until it's approved elsewhere). It's also the automatic
|
|
16
|
+
* fallback for a bare `tot login` when there's no browser opener on this box at
|
|
17
|
+
* all (oauth.mjs's loopback flow would otherwise hang forever waiting for a
|
|
18
|
+
* callback nothing can deliver — see NoOpenerError).
|
|
19
|
+
*
|
|
20
|
+
* Dependency-free (node built-ins via oauth.mjs).
|
|
21
|
+
*/
|
|
22
|
+
import { loginFlow, deviceLoginFlow, NoOpenerError } from "../oauth.mjs";
|
|
23
|
+
import { defaultCredentialsPath, readCredentials, writeCredentials } from "../token-store.mjs";
|
|
24
|
+
import { openBrowser } from "../open.mjs";
|
|
25
|
+
import { fail } from "../errors.mjs";
|
|
26
|
+
|
|
27
|
+
const DEFAULT_MCP_URL = "https://mcp.tokenoftrust.com";
|
|
28
|
+
|
|
29
|
+
function parseArgs(argv) {
|
|
30
|
+
const a = { mcp: null, device: false, help: false };
|
|
31
|
+
for (let i = 0; i < argv.length; i++) {
|
|
32
|
+
const t = argv[i];
|
|
33
|
+
if (t === "--mcp") a.mcp = argv[++i];
|
|
34
|
+
else if (t === "--device") a.device = true;
|
|
35
|
+
else if (t === "--help" || t === "-h") a.help = true;
|
|
36
|
+
}
|
|
37
|
+
return a;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const USAGE = `tot login — sign in to Token of Trust
|
|
41
|
+
|
|
42
|
+
tot login open the browser, sign in, cache your session
|
|
43
|
+
tot login --device headless/SSH: print a code, poll until approved
|
|
44
|
+
elsewhere (also the automatic fallback when no
|
|
45
|
+
browser opener exists on this box)
|
|
46
|
+
tot login --mcp <url> MCP base URL (default: env MCP_BASE_URL / TOT_MCP_URL)
|
|
47
|
+
|
|
48
|
+
After signing in, run \`tot whoami\` to confirm, then \`tot checkout\` / \`tot submit\`.`;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The core of `tot login`: run the OAuth ceremony (browser loopback, or the
|
|
52
|
+
* device-code flow if `device` is set or the loopback can't be reached at
|
|
53
|
+
* all) and cache the result, reusing a previously-registered client for THIS
|
|
54
|
+
* MCP so we don't re-register (and burn the DCR rate limit) on every login.
|
|
55
|
+
* Exported so `tot doctor --fix` (F2) can re-login as an auto-remediation
|
|
56
|
+
* without duplicating this.
|
|
57
|
+
* @returns {Promise<object>} the credentials written to disk.
|
|
58
|
+
*/
|
|
59
|
+
export async function loginAndCache(mcpUrl, env = process.env, { log = () => {}, device = false } = {}) {
|
|
60
|
+
const path = defaultCredentialsPath(env);
|
|
61
|
+
const prior = readCredentials(path);
|
|
62
|
+
const clientId = prior && prior.mcpUrl === mcpUrl ? prior.clientId : undefined;
|
|
63
|
+
|
|
64
|
+
let creds;
|
|
65
|
+
if (device) {
|
|
66
|
+
creds = await deviceLoginFlow({ mcpUrl, clientId, log });
|
|
67
|
+
} else {
|
|
68
|
+
try {
|
|
69
|
+
creds = await loginFlow({ mcpUrl, clientId, open: openBrowser, log });
|
|
70
|
+
} catch (e) {
|
|
71
|
+
if (!(e instanceof NoOpenerError)) throw e;
|
|
72
|
+
log(" no browser available here — falling back to device-code sign-in …");
|
|
73
|
+
creds = await deviceLoginFlow({ mcpUrl, clientId, log });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
writeCredentials(path, creds);
|
|
77
|
+
return creds;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** @param {string[]} argv @param {any} _ctx */
|
|
81
|
+
export async function run(argv, _ctx) {
|
|
82
|
+
const env = process.env;
|
|
83
|
+
const args = parseArgs(argv);
|
|
84
|
+
if (args.help) {
|
|
85
|
+
console.log(USAGE);
|
|
86
|
+
return 0;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const mcpUrl = args.mcp || env.MCP_BASE_URL || env.TOT_MCP_URL || DEFAULT_MCP_URL;
|
|
90
|
+
console.error(`~ signing in to Token of Trust (${mcpUrl})`);
|
|
91
|
+
try {
|
|
92
|
+
await loginAndCache(mcpUrl, env, { log: (m) => console.error(m), device: args.device });
|
|
93
|
+
console.log(`\n+ signed in. Session cached to ${defaultCredentialsPath(env)}.`);
|
|
94
|
+
console.log(" Next: `tot whoami` to confirm, or `tot checkout` / `tot submit` to build.");
|
|
95
|
+
return 0;
|
|
96
|
+
} catch (e) {
|
|
97
|
+
console.error(
|
|
98
|
+
fail(
|
|
99
|
+
`sign-in didn't complete: ${e?.message || e}`,
|
|
100
|
+
args.device
|
|
101
|
+
? "re-run `tot login --device` and approve the code."
|
|
102
|
+
: "re-run `tot login` and finish the browser sign-in (approve the request) — or try `tot login --device`.",
|
|
103
|
+
),
|
|
104
|
+
);
|
|
105
|
+
return 1;
|
|
106
|
+
}
|
|
107
|
+
}
|