@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.
@@ -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
+ }