@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,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `tot checkout` — clone the tenant store you're entitled to build on, with an
|
|
3
|
+
* authenticated remote already configured, ready for the local loop.
|
|
4
|
+
*
|
|
5
|
+
* tot checkout list the stores you can build on
|
|
6
|
+
* tot checkout <tenant> show the checkout for <tenant>
|
|
7
|
+
* tot checkout <tenant> --clone DIR clone it to DIR with an authed remote
|
|
8
|
+
*
|
|
9
|
+
* This is the SAME MCP code path a developer/operator gets when they switch to a
|
|
10
|
+
* tenant: credential/sign-in → client_switch(tenant) → tenant_checkout, where
|
|
11
|
+
* the MCP derives your per-tenant Git user and mints a FRESH, single-active,
|
|
12
|
+
* repo-scoped push credential (a later checkout for the same tenant rotates it).
|
|
13
|
+
* `tot` performs NO privileged forge work itself — the MCP owns that.
|
|
14
|
+
*
|
|
15
|
+
* Auth is resolved through src/auth.mjs: operator creds (our dogfooding / CI) or
|
|
16
|
+
* the developer's own ToT identity (workstream #2). This command doesn't care
|
|
17
|
+
* which — it just needs a validated session.
|
|
18
|
+
*
|
|
19
|
+
* Dependency-free (global fetch + `git` via child_process).
|
|
20
|
+
*/
|
|
21
|
+
import { execFileSync } from "node:child_process";
|
|
22
|
+
import { createMcpClient } from "../mcp.mjs";
|
|
23
|
+
import { resolveSession, AuthUnavailableError } from "../auth.mjs";
|
|
24
|
+
import { CliError, fail, formatError } from "../errors.mjs";
|
|
25
|
+
|
|
26
|
+
const DEFAULT_MCP_URL = "https://mcp.tokenoftrust.com";
|
|
27
|
+
|
|
28
|
+
function parseArgs(argv) {
|
|
29
|
+
const a = {
|
|
30
|
+
tenant: null,
|
|
31
|
+
tag: "main",
|
|
32
|
+
clone: null,
|
|
33
|
+
mcp: null,
|
|
34
|
+
identity: null, // "operator" | "developer" — override auto-selection
|
|
35
|
+
printRemote: false,
|
|
36
|
+
help: false,
|
|
37
|
+
};
|
|
38
|
+
for (let i = 0; i < argv.length; i++) {
|
|
39
|
+
const t = argv[i];
|
|
40
|
+
if (t === "--tag") a.tag = argv[++i];
|
|
41
|
+
else if (t === "--clone") a.clone = argv[++i];
|
|
42
|
+
else if (t === "--mcp") a.mcp = argv[++i];
|
|
43
|
+
else if (t === "--identity") a.identity = argv[++i];
|
|
44
|
+
else if (t === "--print-remote") a.printRemote = true;
|
|
45
|
+
else if (t === "--help" || t === "-h") a.help = true;
|
|
46
|
+
else if (!t.startsWith("--") && !a.tenant) a.tenant = t;
|
|
47
|
+
}
|
|
48
|
+
return a;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const USAGE = `tot checkout — clone a tenant store you can build on
|
|
52
|
+
|
|
53
|
+
tot checkout list the stores you can build on
|
|
54
|
+
tot checkout <tenant> show the checkout for <tenant>
|
|
55
|
+
tot checkout <tenant> --clone <dir> clone it locally (authenticated remote configured)
|
|
56
|
+
|
|
57
|
+
Options:
|
|
58
|
+
--tag <tag> which repo (repo = "<tenant>-<tag>"). Default: main.
|
|
59
|
+
--clone <dir> git clone the authenticated remote into <dir>.
|
|
60
|
+
--mcp <url> MCP base URL. Default: env MCP_BASE_URL / TOT_MCP_URL, else
|
|
61
|
+
${DEFAULT_MCP_URL}.
|
|
62
|
+
--identity <who> force "operator" or "developer" auth (default: auto).
|
|
63
|
+
--print-remote also print the authenticated remote (contains a live token!).`;
|
|
64
|
+
|
|
65
|
+
/** Redact known secrets from any string before it hits the terminal. */
|
|
66
|
+
function makeRedactor(env) {
|
|
67
|
+
const secrets = [env.TOT_API_KEY, env.TOT_SECRET_KEY].filter(Boolean);
|
|
68
|
+
return (s) => {
|
|
69
|
+
let o = String(s);
|
|
70
|
+
for (const x of secrets) o = o.split(x).join("***");
|
|
71
|
+
return o;
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* @param {string[]} argv - args AFTER `checkout`
|
|
77
|
+
* @param {import("../context.mjs").detectContext extends (...a:any)=>infer R ? R : any} ctx
|
|
78
|
+
*/
|
|
79
|
+
export async function run(argv, ctx) {
|
|
80
|
+
const env = process.env;
|
|
81
|
+
const args = parseArgs(argv);
|
|
82
|
+
if (args.help) {
|
|
83
|
+
console.log(USAGE);
|
|
84
|
+
return 0;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Infer the tenant from a standalone checkout when run without one.
|
|
88
|
+
if (!args.tenant && ctx.mode === "checkout" && ctx.tenant) {
|
|
89
|
+
args.tenant = ctx.tenant;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const baseUrl = args.mcp || env.MCP_BASE_URL || env.TOT_MCP_URL || DEFAULT_MCP_URL;
|
|
93
|
+
const redact = makeRedactor(env);
|
|
94
|
+
const client = createMcpClient(baseUrl);
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
await client.initialize();
|
|
98
|
+
const session = await resolveSession(client, {
|
|
99
|
+
env,
|
|
100
|
+
prefer: args.identity || undefined,
|
|
101
|
+
});
|
|
102
|
+
console.error(`~ signed in (${session.identity}) → ${client.mcpUrl}`);
|
|
103
|
+
|
|
104
|
+
// No tenant → list the stores this identity can build on and stop.
|
|
105
|
+
if (!args.tenant) {
|
|
106
|
+
const list = await client.callTool("client_list", {});
|
|
107
|
+
printClientList(list);
|
|
108
|
+
return 0;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
console.error(`~ client_switch → ${args.tenant}`);
|
|
112
|
+
console.error(
|
|
113
|
+
`~ tenant_checkout (tag=${args.tag}) — MCP mints a fresh repo-scoped credential`,
|
|
114
|
+
);
|
|
115
|
+
const res = await checkoutTenant(client, {
|
|
116
|
+
tenant: args.tenant,
|
|
117
|
+
tag: args.tag,
|
|
118
|
+
cloneDir: args.clone || null,
|
|
119
|
+
redact,
|
|
120
|
+
});
|
|
121
|
+
console.log(`\n+ checkout ready. repo: ${res.cloneUrl || res.publicUrl}`);
|
|
122
|
+
|
|
123
|
+
if (res.cloned) {
|
|
124
|
+
console.log(`+ cloned. HEAD: ${res.head}`);
|
|
125
|
+
console.log(`\nYour local working clone is at ${res.dir} with an authenticated remote.`);
|
|
126
|
+
console.log(` cd ${res.dir} && tot dev # run it locally with save→reload`);
|
|
127
|
+
console.log(` (the minted token lives in .git/config; a later checkout rotates it)`);
|
|
128
|
+
return 0;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
console.log(`\nPublic clone URL (no credential): ${res.publicUrl}`);
|
|
132
|
+
console.log(`Re-run with --clone <dir> to clone with the authenticated remote configured.`);
|
|
133
|
+
if (args.printRemote) {
|
|
134
|
+
console.log(
|
|
135
|
+
`\nAUTHENTICATED remote (contains a live token — handle carefully):\n${res.gitRemote}`,
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
return 0;
|
|
139
|
+
} catch (e) {
|
|
140
|
+
if (e instanceof AuthUnavailableError) {
|
|
141
|
+
console.error(formatError(e));
|
|
142
|
+
return 1;
|
|
143
|
+
}
|
|
144
|
+
if (e instanceof CliError) {
|
|
145
|
+
console.error(formatError(e));
|
|
146
|
+
return e.exitCode ?? 1;
|
|
147
|
+
}
|
|
148
|
+
console.error(fail(`checkout failed: ${redact(String(e?.message || e))}`));
|
|
149
|
+
return 1;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* The privileged checkout core, composable in-process (used by `tot checkout`
|
|
155
|
+
* and by `tot start`): client_switch → tenant_checkout → optionally clone. The
|
|
156
|
+
* MCP mints a fresh, repo-scoped push credential each time. Assumes `client` is
|
|
157
|
+
* already initialized + has a validated session.
|
|
158
|
+
*
|
|
159
|
+
* @param {ReturnType<import("../mcp.mjs").createMcpClient>} client
|
|
160
|
+
* @param {{ tenant: string, tag?: string, cloneDir?: string|null, redact?: (s:string)=>string }} opts
|
|
161
|
+
* @returns {Promise<{ gitRemote: string, cloneUrl: string|null, publicUrl: string,
|
|
162
|
+
* cloned: boolean, dir: string|null, head: string|null }>}
|
|
163
|
+
*/
|
|
164
|
+
export async function checkoutTenant(client, { tenant, tag = "main", cloneDir = null, redact = (s) => s }) {
|
|
165
|
+
await client.callTool("client_switch", { tenant });
|
|
166
|
+
const checkout = await client.callTool("tenant_checkout", { tenant, tag });
|
|
167
|
+
|
|
168
|
+
const gitRemote = checkout?.gitRemote;
|
|
169
|
+
const cloneUrl = checkout?.cloneUrl ?? null;
|
|
170
|
+
if (!gitRemote) {
|
|
171
|
+
throw new CliError(
|
|
172
|
+
`tenant_checkout returned no gitRemote for ${tenant}: ${redact(JSON.stringify(checkout))}`,
|
|
173
|
+
{ next: "confirm you're entitled to this store — `tot checkout` (lists your stores)" },
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
const u = new URL(gitRemote);
|
|
177
|
+
const publicUrl = `${u.protocol}//${u.host}${u.pathname}`;
|
|
178
|
+
|
|
179
|
+
if (!cloneDir) {
|
|
180
|
+
return { gitRemote, cloneUrl, publicUrl, cloned: false, dir: null, head: null };
|
|
181
|
+
}
|
|
182
|
+
const { dir, head } = cloneRepo(gitRemote, cloneDir, redact);
|
|
183
|
+
return { gitRemote, cloneUrl, publicUrl, cloned: true, dir, head };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** git clone the authenticated remote into `dir`. Throws CliError on failure. */
|
|
187
|
+
function cloneRepo(gitRemote, dir, redact) {
|
|
188
|
+
const git = (cargs) =>
|
|
189
|
+
execFileSync("git", cargs, { stdio: ["ignore", "pipe", "pipe"] }).toString();
|
|
190
|
+
console.log(`+ git clone → ${dir}`);
|
|
191
|
+
try {
|
|
192
|
+
git(["clone", gitRemote, dir]);
|
|
193
|
+
} catch (e) {
|
|
194
|
+
throw new CliError(`clone failed: ${redact(String(e.stderr || e.message || e))}`, {
|
|
195
|
+
next: `check the target dir is empty and you can reach the remote, then re-run`,
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
const head = git(["-C", dir, "log", "-1", "--oneline"]).trim();
|
|
199
|
+
return { dir, head };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Normalize the (shape-varying) `client_list` response into a plain, sorted
|
|
204
|
+
* list of the stores this identity can build on. Shared by `tot checkout`'s
|
|
205
|
+
* listing and `tot start`'s auto-pick so both read the same fields.
|
|
206
|
+
* @param {unknown} list
|
|
207
|
+
* @returns {Array<{ id: string, name: string, raw: any }>}
|
|
208
|
+
*/
|
|
209
|
+
export function normalizeStores(list) {
|
|
210
|
+
const rows = Array.isArray(list) ? list : list?.clients || list?.tenants || [];
|
|
211
|
+
if (!Array.isArray(rows)) return [];
|
|
212
|
+
return rows
|
|
213
|
+
.map((r) => ({
|
|
214
|
+
id: r?.tenant || r?.id || r?.clientId || r?.appDomain || null,
|
|
215
|
+
name: r?.displayName || r?.name || "",
|
|
216
|
+
raw: r,
|
|
217
|
+
}))
|
|
218
|
+
.filter((r) => r.id);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function printClientList(list) {
|
|
222
|
+
const stores = normalizeStores(list);
|
|
223
|
+
if (stores.length === 0) {
|
|
224
|
+
console.log("\nNo stores available to build on for this identity yet.");
|
|
225
|
+
if (list && !Array.isArray(list)) console.log(JSON.stringify(list, null, 2));
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
console.log("\nStores you can build on:\n");
|
|
229
|
+
for (const s of stores) {
|
|
230
|
+
console.log(` ${s.id}${s.name ? ` — ${s.name}` : ""}`);
|
|
231
|
+
}
|
|
232
|
+
console.log(`\nNext: tot checkout <tenant> --clone <dir>`);
|
|
233
|
+
}
|