magic-spells 0.0.0-stage → 0.2.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 +21 -0
- package/README.md +90 -2
- package/bin/magic.js +12 -0
- package/dist/chunk-JZ7XESF4.js +1829 -0
- package/dist/chunk-PQFOO5A7.js +1057 -0
- package/dist/main.js +1111 -0
- package/dist/operations-GGMLIYEW.js +9 -0
- package/dist/src-DJYPPJFM.js +16 -0
- package/dist/src-KJGFPRQP.js +17 -0
- package/dist/src-L2I546M7.js +1071 -0
- package/package.json +33 -5
package/dist/main.js
ADDED
|
@@ -0,0 +1,1111 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AccountClient,
|
|
3
|
+
GRANT_ITEM,
|
|
4
|
+
MagicError,
|
|
5
|
+
authorizeUrl,
|
|
6
|
+
buildContext,
|
|
7
|
+
cell,
|
|
8
|
+
configFilePath,
|
|
9
|
+
contextFor,
|
|
10
|
+
createCredentialStore,
|
|
11
|
+
createMcpServer,
|
|
12
|
+
createPkce,
|
|
13
|
+
exitCodeFor,
|
|
14
|
+
forgetProductKey,
|
|
15
|
+
forgetSessions,
|
|
16
|
+
grantExpired,
|
|
17
|
+
grantFromToken,
|
|
18
|
+
grantIdleExpiry,
|
|
19
|
+
isMagicError,
|
|
20
|
+
jsonMode,
|
|
21
|
+
loadCliConfig,
|
|
22
|
+
manualItem,
|
|
23
|
+
maskKey,
|
|
24
|
+
mintSessions,
|
|
25
|
+
parseGlobals,
|
|
26
|
+
processIO,
|
|
27
|
+
productConfig,
|
|
28
|
+
productEnvVar,
|
|
29
|
+
productSessions,
|
|
30
|
+
randomState,
|
|
31
|
+
readConfigFile,
|
|
32
|
+
readField,
|
|
33
|
+
readGrant,
|
|
34
|
+
readIndex,
|
|
35
|
+
readPackageVersion,
|
|
36
|
+
readStr,
|
|
37
|
+
rec,
|
|
38
|
+
remintFor,
|
|
39
|
+
render,
|
|
40
|
+
renderError,
|
|
41
|
+
resolveCredential,
|
|
42
|
+
revokeSessions,
|
|
43
|
+
runDoctorChecks,
|
|
44
|
+
runProductCli,
|
|
45
|
+
runWithRemint,
|
|
46
|
+
saveGrant,
|
|
47
|
+
saveProductKey,
|
|
48
|
+
sessionItem,
|
|
49
|
+
startLoopback,
|
|
50
|
+
storeSessions,
|
|
51
|
+
toMagicError,
|
|
52
|
+
usage,
|
|
53
|
+
waitForDevice
|
|
54
|
+
} from "./chunk-JZ7XESF4.js";
|
|
55
|
+
|
|
56
|
+
// src/browser.ts
|
|
57
|
+
import { spawn } from "node:child_process";
|
|
58
|
+
import { createInterface } from "node:readline/promises";
|
|
59
|
+
function browserIsLocal(env, platform) {
|
|
60
|
+
if (env.SSH_CONNECTION) return false;
|
|
61
|
+
if (platform === "linux") return Boolean(env.DISPLAY || env.WAYLAND_DISPLAY);
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
function openBrowser(url) {
|
|
65
|
+
return new Promise((resolve) => {
|
|
66
|
+
const platform = process.platform;
|
|
67
|
+
const command = platform === "darwin" ? "open" : platform === "win32" ? "rundll32" : "xdg-open";
|
|
68
|
+
const args = platform === "win32" ? ["url.dll,FileProtocolHandler", url] : [url];
|
|
69
|
+
const child = spawn(command, args, { detached: true, stdio: "ignore" });
|
|
70
|
+
child.once("error", () => resolve(false));
|
|
71
|
+
child.once("close", (code) => resolve(code === 0));
|
|
72
|
+
child.unref();
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
async function confirmOnTty(question) {
|
|
76
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
77
|
+
try {
|
|
78
|
+
return /^y(es)?$/i.test((await rl.question(question)).trim());
|
|
79
|
+
} finally {
|
|
80
|
+
rl.close();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// src/version.ts
|
|
85
|
+
var cached;
|
|
86
|
+
function version() {
|
|
87
|
+
return cached ??= readPackageVersion(import.meta.url);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// src/shell.ts
|
|
91
|
+
function chooseEntries(shell, ids) {
|
|
92
|
+
const known = shell.entries.map((e) => e.id);
|
|
93
|
+
for (const id of ids ?? []) {
|
|
94
|
+
if (!known.includes(id)) {
|
|
95
|
+
throw usage(`unknown product "${id}"`, `Products: ${known.join(", ")}.`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return ids ? shell.entries.filter((e) => ids.includes(e.id)) : shell.entries;
|
|
99
|
+
}
|
|
100
|
+
async function loadProducts(shell, ids) {
|
|
101
|
+
return Promise.all(chooseEntries(shell, ids).map((e) => e.load()));
|
|
102
|
+
}
|
|
103
|
+
function takeValue(args, name) {
|
|
104
|
+
for (let i = 0; i < args.length; i++) {
|
|
105
|
+
const tok = args[i];
|
|
106
|
+
if (tok === `--${name}`) {
|
|
107
|
+
const v = args[i + 1];
|
|
108
|
+
if (v === void 0 || v.startsWith("--")) throw usage(`flag --${name} expects a value`);
|
|
109
|
+
args.splice(i, 2);
|
|
110
|
+
return v;
|
|
111
|
+
}
|
|
112
|
+
if (tok.startsWith(`--${name}=`)) {
|
|
113
|
+
args.splice(i, 1);
|
|
114
|
+
return tok.slice(name.length + 3);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return void 0;
|
|
118
|
+
}
|
|
119
|
+
function wantsHelp(args) {
|
|
120
|
+
return args.includes("--help") || args.includes("-h");
|
|
121
|
+
}
|
|
122
|
+
function takeSwitch(args, name) {
|
|
123
|
+
const i = args.indexOf(`--${name}`);
|
|
124
|
+
if (i < 0) return false;
|
|
125
|
+
args.splice(i, 1);
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
function noMoreArgs(command, args) {
|
|
129
|
+
if (args.length > 0) throw usage(`${command}: unexpected argument "${args[0]}"`);
|
|
130
|
+
}
|
|
131
|
+
function rejectWorkspace(command, opts) {
|
|
132
|
+
if (opts.workspace) throw usage(`--workspace is not supported by ${command}`);
|
|
133
|
+
}
|
|
134
|
+
function storeLabel(where) {
|
|
135
|
+
return where === "keyring" ? "keychain" : where ?? "none";
|
|
136
|
+
}
|
|
137
|
+
function cancelled() {
|
|
138
|
+
return new MagicError("usage", "Cancelled.");
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// src/commands/doctor.ts
|
|
142
|
+
var REACH_TIMEOUT_MS = 5e3;
|
|
143
|
+
async function doctor(shell, argv) {
|
|
144
|
+
if (wantsHelp(argv)) {
|
|
145
|
+
shell.io.err("Usage: magic doctor [--json]\n\nCheck the install, the keys and each product.\n");
|
|
146
|
+
return 0;
|
|
147
|
+
}
|
|
148
|
+
rejectWorkspace("doctor", shell.opts);
|
|
149
|
+
noMoreArgs("doctor", argv);
|
|
150
|
+
const products = await loadProducts(shell);
|
|
151
|
+
const checks = [...shellChecks(shell), ...await accountChecks(shell)];
|
|
152
|
+
const signedIn = await anyCredential(shell, products);
|
|
153
|
+
for (const product of products) checks.push(...await productChecks(shell, product, signedIn));
|
|
154
|
+
const ok = checks.every((c) => c.ok);
|
|
155
|
+
if (jsonMode(shell.opts, shell.io)) render({ ok, checks }, shell.opts, shell.io);
|
|
156
|
+
else printHuman(shell, checks);
|
|
157
|
+
const failed = checks.find((c) => !c.ok);
|
|
158
|
+
return failed ? exitCodeFor(failed.code ?? "unknown") : 0;
|
|
159
|
+
}
|
|
160
|
+
function shellChecks(shell) {
|
|
161
|
+
const fileChosen = shell.env.MAGIC_CREDENTIALS_FILE === "1";
|
|
162
|
+
return [
|
|
163
|
+
{
|
|
164
|
+
name: "cli",
|
|
165
|
+
product: "magic",
|
|
166
|
+
ok: true,
|
|
167
|
+
detail: `magic ${version()}`,
|
|
168
|
+
data: { version: version() }
|
|
169
|
+
},
|
|
170
|
+
// Information only: bin/magic.js exits before this runs on Node < 24.
|
|
171
|
+
{ name: "node", product: "magic", ok: true, detail: `Node ${process.versions.node}` },
|
|
172
|
+
shell.store.backend === "keyring" ? { name: "credential store", product: "magic", ok: true, detail: "OS keychain" } : {
|
|
173
|
+
name: "credential store",
|
|
174
|
+
product: "magic",
|
|
175
|
+
ok: true,
|
|
176
|
+
...fileChosen ? {} : { warn: true },
|
|
177
|
+
detail: fileChosen ? "file (MAGIC_CREDENTIALS_FILE=1), mode 0600" : "file, mode 0600 \u2014 no OS keychain on this machine"
|
|
178
|
+
}
|
|
179
|
+
];
|
|
180
|
+
}
|
|
181
|
+
async function accountChecks(shell) {
|
|
182
|
+
const grant = await readGrant(shell.store);
|
|
183
|
+
let accountUrl;
|
|
184
|
+
try {
|
|
185
|
+
accountUrl = loadCliConfig({
|
|
186
|
+
products: [],
|
|
187
|
+
env: shell.env,
|
|
188
|
+
file: shell.readConfig()
|
|
189
|
+
}).accountUrl;
|
|
190
|
+
} catch (err) {
|
|
191
|
+
const e = toMagicError(err);
|
|
192
|
+
return [{ name: "account", product: "account", ok: false, code: e.code, detail: e.message }];
|
|
193
|
+
}
|
|
194
|
+
const results = [];
|
|
195
|
+
try {
|
|
196
|
+
const res = await fetch(accountUrl, {
|
|
197
|
+
method: "HEAD",
|
|
198
|
+
signal: AbortSignal.timeout(REACH_TIMEOUT_MS)
|
|
199
|
+
});
|
|
200
|
+
results.push({
|
|
201
|
+
name: "account",
|
|
202
|
+
product: "account",
|
|
203
|
+
ok: true,
|
|
204
|
+
detail: `${accountUrl} answers (HTTP ${res.status})`
|
|
205
|
+
});
|
|
206
|
+
} catch (err) {
|
|
207
|
+
const e = toMagicError(networkish(err), {
|
|
208
|
+
product: "account",
|
|
209
|
+
networkHint: "Check MAGIC_ACCOUNT_URL and your network connection."
|
|
210
|
+
});
|
|
211
|
+
const detail = e.hint ? `${e.message} \u2014 ${e.hint}` : e.message;
|
|
212
|
+
results.push(
|
|
213
|
+
grant ? { name: "account", product: "account", ok: false, code: e.code, detail } : { name: "account", product: "account", ok: true, warn: true, detail }
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
if (!grant) {
|
|
217
|
+
results.push({
|
|
218
|
+
name: "account grant",
|
|
219
|
+
product: "account",
|
|
220
|
+
ok: true,
|
|
221
|
+
warn: true,
|
|
222
|
+
detail: "Not signed in (run `magic login`); product keys only."
|
|
223
|
+
});
|
|
224
|
+
} else if (grantExpired(grant)) {
|
|
225
|
+
results.push({
|
|
226
|
+
name: "account grant",
|
|
227
|
+
product: "account",
|
|
228
|
+
ok: false,
|
|
229
|
+
code: "not_logged_in",
|
|
230
|
+
detail: "Your sign-in has ended \u2014 run `magic login`."
|
|
231
|
+
});
|
|
232
|
+
} else {
|
|
233
|
+
const who = grant.user ? `${grant.user.display_name} <${grant.user.email}>` : "signed in";
|
|
234
|
+
results.push({
|
|
235
|
+
name: "account grant",
|
|
236
|
+
product: "account",
|
|
237
|
+
ok: true,
|
|
238
|
+
detail: `${who} until ${grant.expires_at.slice(0, 10)} (idle sign-out after ${grantIdleExpiry(grant).slice(0, 10)})`,
|
|
239
|
+
data: { user: grant.user ?? null, expires_at: grant.expires_at }
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
return results;
|
|
243
|
+
}
|
|
244
|
+
async function anyCredential(shell, products) {
|
|
245
|
+
if (await readGrant(shell.store)) return true;
|
|
246
|
+
if (Object.keys(await readIndex(shell.store)).length > 0) return true;
|
|
247
|
+
for (const p of products) {
|
|
248
|
+
if (shell.env[productEnvVar(p.id, "KEY")]?.trim()) return true;
|
|
249
|
+
if (await shell.store.locate(manualItem(p.id)) !== null) return true;
|
|
250
|
+
}
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
async function productChecks(shell, product, signedIn) {
|
|
254
|
+
const id = product.id;
|
|
255
|
+
const networkHint = `Check ${productEnvVar(id, "URL")} and your network connection.`;
|
|
256
|
+
const fail = (name, err) => {
|
|
257
|
+
const e = toMagicError(err, { product: id, networkHint });
|
|
258
|
+
return {
|
|
259
|
+
name,
|
|
260
|
+
product: id,
|
|
261
|
+
ok: false,
|
|
262
|
+
code: e.code,
|
|
263
|
+
detail: e.hint ? `${e.message} \u2014 ${e.hint}` : e.message
|
|
264
|
+
};
|
|
265
|
+
};
|
|
266
|
+
let config;
|
|
267
|
+
try {
|
|
268
|
+
config = productConfig(product, shell);
|
|
269
|
+
} catch (err) {
|
|
270
|
+
return [fail(`${id} config`, err)];
|
|
271
|
+
}
|
|
272
|
+
const apiUrl = config.products[id].apiUrl;
|
|
273
|
+
const results = [];
|
|
274
|
+
try {
|
|
275
|
+
const res = await fetch(apiUrl, {
|
|
276
|
+
method: "HEAD",
|
|
277
|
+
signal: AbortSignal.timeout(REACH_TIMEOUT_MS)
|
|
278
|
+
});
|
|
279
|
+
results.push({
|
|
280
|
+
name: `${id} api`,
|
|
281
|
+
product: id,
|
|
282
|
+
ok: true,
|
|
283
|
+
detail: `${apiUrl} answers (HTTP ${res.status})`
|
|
284
|
+
});
|
|
285
|
+
} catch (err) {
|
|
286
|
+
results.push(fail(`${id} api`, networkish(err)));
|
|
287
|
+
}
|
|
288
|
+
let session;
|
|
289
|
+
try {
|
|
290
|
+
const workspace = config.defaultWorkspace[id];
|
|
291
|
+
session = await resolveCredential(product, {
|
|
292
|
+
env: shell.env,
|
|
293
|
+
store: shell.store,
|
|
294
|
+
...workspace ? { workspace } : {}
|
|
295
|
+
});
|
|
296
|
+
} catch (err) {
|
|
297
|
+
const failed = fail(`${id} key`, err);
|
|
298
|
+
const soft = failed.code === "usage" || failed.code === "not_logged_in" && signedIn;
|
|
299
|
+
results.push(soft ? { ...failed, ok: true, warn: true } : failed);
|
|
300
|
+
return results;
|
|
301
|
+
}
|
|
302
|
+
let ctx = contextFor(product, config, session);
|
|
303
|
+
if (session.source === "account" && Date.parse(session.expires_at ?? "") <= Date.now()) {
|
|
304
|
+
try {
|
|
305
|
+
ctx = await remintFor(product, shell.store)(ctx) ?? ctx;
|
|
306
|
+
} catch (err) {
|
|
307
|
+
results.push(fail(`${id} key`, err));
|
|
308
|
+
return results;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
results.push({
|
|
312
|
+
name: `${id} key`,
|
|
313
|
+
product: id,
|
|
314
|
+
ok: true,
|
|
315
|
+
detail: `${maskKey(ctx.session.key, product.credential)} from ${keySource(session.source)}${session.workspace ? ` (workspace ${session.workspace.handle})` : ""}`
|
|
316
|
+
});
|
|
317
|
+
results.push(...await runDoctorChecks(product.doctor ?? [], ctx, id));
|
|
318
|
+
return results;
|
|
319
|
+
}
|
|
320
|
+
function keySource(source) {
|
|
321
|
+
return source === "env" ? "the environment" : source === "account" ? "your sign-in" : "a pasted key";
|
|
322
|
+
}
|
|
323
|
+
function networkish(err) {
|
|
324
|
+
if (err instanceof Error && err.cause instanceof Error) {
|
|
325
|
+
return new Error(`${err.message}: ${err.cause.message}`);
|
|
326
|
+
}
|
|
327
|
+
return err;
|
|
328
|
+
}
|
|
329
|
+
function printHuman(shell, checks) {
|
|
330
|
+
const width = Math.max(...checks.map((c) => c.name.length));
|
|
331
|
+
const lines = checks.map((c) => {
|
|
332
|
+
const mark = !c.ok ? "FAIL" : c.warn ? "warn" : "ok ";
|
|
333
|
+
return `${mark} ${c.name.padEnd(width)} ${c.detail}`;
|
|
334
|
+
});
|
|
335
|
+
shell.io.out(`${lines.join("\n")}
|
|
336
|
+
`);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// src/commands/login.ts
|
|
340
|
+
var LOGIN_HELP = `Usage: magic login [--device] [--product <product>] [--yes]
|
|
341
|
+
magic login --product-key [<product>]
|
|
342
|
+
|
|
343
|
+
Signs in to your Magic Spells account, then mints a short-lived key in each product
|
|
344
|
+
for every workspace you belong to. Opens your browser when one is on this machine;
|
|
345
|
+
otherwise (or with --device) prints a code to enter on any device.
|
|
346
|
+
|
|
347
|
+
--device sign in with a code instead of a browser on this machine
|
|
348
|
+
--product <product> mint (or re-mint) one product's keys
|
|
349
|
+
--yes, -y replace an existing sign-in without asking
|
|
350
|
+
--product-key store a pasted product key instead (from a hidden prompt or
|
|
351
|
+
stdin; for CI). Without <product>, the key's prefix picks it.
|
|
352
|
+
`;
|
|
353
|
+
var LOGOUT_HELP = `Usage: magic logout [--product <product>]
|
|
354
|
+
|
|
355
|
+
Revokes your product keys and your sign-in, and deletes every stored credential.
|
|
356
|
+
With --product, only that product's keys (you stay signed in).
|
|
357
|
+
`;
|
|
358
|
+
async function login(shell, argv) {
|
|
359
|
+
const args = [...argv];
|
|
360
|
+
if (wantsHelp(args)) {
|
|
361
|
+
shell.io.err(LOGIN_HELP);
|
|
362
|
+
return 0;
|
|
363
|
+
}
|
|
364
|
+
rejectWorkspace("login", shell.opts);
|
|
365
|
+
if (takeSwitch(args, "product-key")) return productKeyLogin(shell, args);
|
|
366
|
+
const device = takeSwitch(args, "device");
|
|
367
|
+
const only = takeValue(args, "product");
|
|
368
|
+
noMoreArgs("login", args);
|
|
369
|
+
if (only) chooseEntries(shell, [only]);
|
|
370
|
+
const all = await loadProducts(shell);
|
|
371
|
+
const targets = all.filter((p) => p.sessions && (!only || p.id === only));
|
|
372
|
+
if (only && targets.length === 0) {
|
|
373
|
+
throw usage(
|
|
374
|
+
`${only} does not mint keys from your account.`,
|
|
375
|
+
`Run \`magic login --product-key ${only}\` to store a key.`
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
const config = loadCliConfig({
|
|
379
|
+
products: all.map((p) => ({ id: p.id, apiUrl: p.apiUrl })),
|
|
380
|
+
env: shell.env,
|
|
381
|
+
file: shell.readConfig()
|
|
382
|
+
});
|
|
383
|
+
const interrupt = interruptSignal(shell);
|
|
384
|
+
try {
|
|
385
|
+
const old = await readGrant(shell.store);
|
|
386
|
+
const live = old && !grantExpired(old) ? old : null;
|
|
387
|
+
let grant;
|
|
388
|
+
let previous = null;
|
|
389
|
+
if (only && live) {
|
|
390
|
+
grant = live;
|
|
391
|
+
} else {
|
|
392
|
+
if (live) await confirmReplace(shell, live);
|
|
393
|
+
previous = old ? await snapshot(shell, all, old) : null;
|
|
394
|
+
const token = await signIn(shell, config, device, interrupt.signal);
|
|
395
|
+
grant = grantFromToken(token, config.accountUrl);
|
|
396
|
+
await saveGrant(shell.store, grant);
|
|
397
|
+
}
|
|
398
|
+
const minted = await mintAll(shell, config, targets, grant, interrupt.signal);
|
|
399
|
+
grant = await rememberUser(shell, config, minted, grant);
|
|
400
|
+
if (previous) await retire(shell, config, previous, minted);
|
|
401
|
+
const result = loginResult(grant, minted);
|
|
402
|
+
report(shell, result);
|
|
403
|
+
if (jsonMode(shell.opts, shell.io)) render(result, shell.opts, shell.io);
|
|
404
|
+
return 0;
|
|
405
|
+
} finally {
|
|
406
|
+
interrupt.dispose();
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
function interruptSignal(shell) {
|
|
410
|
+
if (shell.login.interrupt) return { signal: shell.login.interrupt, dispose: () => {
|
|
411
|
+
} };
|
|
412
|
+
const controller = new AbortController();
|
|
413
|
+
const onSigint = () => controller.abort();
|
|
414
|
+
process.once("SIGINT", onSigint);
|
|
415
|
+
return { signal: controller.signal, dispose: () => process.off("SIGINT", onSigint) };
|
|
416
|
+
}
|
|
417
|
+
async function confirmReplace(shell, grant) {
|
|
418
|
+
if (shell.opts.yes) return;
|
|
419
|
+
const who = grant.user ? `${grant.user.display_name} (${grant.user.email})` : "another person";
|
|
420
|
+
if (!shell.confirm) {
|
|
421
|
+
throw usage(`Already logged in as ${who}.`, "Re-run with --yes to replace that sign-in.");
|
|
422
|
+
}
|
|
423
|
+
if (!await shell.confirm(`Already logged in as ${who}. Replace that sign-in? [y/N] `)) {
|
|
424
|
+
throw cancelled();
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
async function signIn(shell, config, forceDevice, signal) {
|
|
428
|
+
const client = new AccountClient({
|
|
429
|
+
accountUrl: config.accountUrl,
|
|
430
|
+
clientId: config.accountClientId
|
|
431
|
+
});
|
|
432
|
+
void client.clientId;
|
|
433
|
+
const local = browserIsLocal(shell.env, shell.platform);
|
|
434
|
+
return !forceDevice && local ? loopbackSignIn(shell, client, signal) : deviceSignIn(shell, client, local, signal);
|
|
435
|
+
}
|
|
436
|
+
async function loopbackSignIn(shell, client, signal) {
|
|
437
|
+
const pkce = createPkce();
|
|
438
|
+
const state = randomState();
|
|
439
|
+
const loopback = await startLoopback({
|
|
440
|
+
state,
|
|
441
|
+
signal,
|
|
442
|
+
...shell.login.loopbackTimeoutMs !== void 0 ? { timeoutMs: shell.login.loopbackTimeoutMs } : {},
|
|
443
|
+
exchange: (code, redirectUri, exchangeSignal) => client.exchangeCode({ code, verifier: pkce.verifier, redirectUri, signal: exchangeSignal })
|
|
444
|
+
});
|
|
445
|
+
const url = authorizeUrl({
|
|
446
|
+
accountUrl: client.accountUrl,
|
|
447
|
+
clientId: client.clientId,
|
|
448
|
+
redirectUri: loopback.redirectUri,
|
|
449
|
+
state,
|
|
450
|
+
challenge: pkce.challenge
|
|
451
|
+
});
|
|
452
|
+
if (await shell.openBrowser(url)) {
|
|
453
|
+
note(shell, "Opened your browser to sign in to Magic Spells. Waiting for you to finish\u2026");
|
|
454
|
+
} else {
|
|
455
|
+
shell.io.err(`Open this page to sign in to Magic Spells:
|
|
456
|
+
|
|
457
|
+
${url}
|
|
458
|
+
|
|
459
|
+
`);
|
|
460
|
+
}
|
|
461
|
+
return loopback.result;
|
|
462
|
+
}
|
|
463
|
+
async function deviceSignIn(shell, client, browserLocal, signal) {
|
|
464
|
+
const start = await client.startDevice(signal);
|
|
465
|
+
const bold = (t) => shell.opts.color === false ? t : `\x1B[1m${t}\x1B[22m`;
|
|
466
|
+
const big = start.user_code.split("").join(" ");
|
|
467
|
+
shell.io.err(
|
|
468
|
+
`To sign in, open this page on any device:
|
|
469
|
+
|
|
470
|
+
${start.verification_uri}
|
|
471
|
+
|
|
472
|
+
and enter the code:
|
|
473
|
+
|
|
474
|
+
${bold(big)}
|
|
475
|
+
|
|
476
|
+
`
|
|
477
|
+
);
|
|
478
|
+
if (browserLocal && start.verification_uri_complete) {
|
|
479
|
+
if (await shell.openBrowser(start.verification_uri_complete)) {
|
|
480
|
+
note(shell, "Opened your browser.");
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
const countdown = shell.io.isTTY() && !shell.opts.quiet;
|
|
484
|
+
if (!countdown) {
|
|
485
|
+
note(shell, `Waiting for approval (the code expires in ${minutes(start.expires_in)})\u2026`);
|
|
486
|
+
}
|
|
487
|
+
try {
|
|
488
|
+
return await waitForDevice(client, start, {
|
|
489
|
+
signal,
|
|
490
|
+
...shell.login.sleep ? { sleep: shell.login.sleep } : {},
|
|
491
|
+
...shell.login.now ? { now: shell.login.now } : {},
|
|
492
|
+
...countdown ? { onTick: (s) => shell.io.err(`\rWaiting for approval\u2026 ${clock(s)} left `) } : {}
|
|
493
|
+
});
|
|
494
|
+
} finally {
|
|
495
|
+
if (countdown) shell.io.err("\r\x1B[K");
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
async function mintAll(shell, config, products, grant, signal) {
|
|
499
|
+
const answers = await Promise.all(
|
|
500
|
+
products.map(async (product) => {
|
|
501
|
+
try {
|
|
502
|
+
const res = await mintSessions(product, {
|
|
503
|
+
apiUrl: config.products[product.id].apiUrl,
|
|
504
|
+
grant: grant.access_token,
|
|
505
|
+
signal
|
|
506
|
+
});
|
|
507
|
+
return { product, res };
|
|
508
|
+
} catch (err) {
|
|
509
|
+
return { product, error: toMagicError(err, { product: product.id }) };
|
|
510
|
+
}
|
|
511
|
+
})
|
|
512
|
+
);
|
|
513
|
+
const out = [];
|
|
514
|
+
for (const a of answers) {
|
|
515
|
+
if (!a.res) {
|
|
516
|
+
out.push({ product: a.product, error: a.error });
|
|
517
|
+
} else if (a.res.sessions.length === 0) {
|
|
518
|
+
out.push({
|
|
519
|
+
product: a.product,
|
|
520
|
+
error: new MagicError("permission_denied", `You have no ${a.product.title} workspaces.`, {
|
|
521
|
+
product: a.product.id,
|
|
522
|
+
details: { reason: "no_workspaces" }
|
|
523
|
+
})
|
|
524
|
+
});
|
|
525
|
+
} else {
|
|
526
|
+
const entry = await storeSessions(shell.store, a.product.id, a.res);
|
|
527
|
+
out.push({
|
|
528
|
+
product: a.product,
|
|
529
|
+
entry,
|
|
530
|
+
sessions: a.res.sessions,
|
|
531
|
+
...a.res.user ? { user: a.res.user } : {}
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
return out;
|
|
536
|
+
}
|
|
537
|
+
async function rememberUser(shell, config, minted, grant) {
|
|
538
|
+
let user = minted.find((m) => m.user)?.user ?? grant.user;
|
|
539
|
+
for (const m of minted) {
|
|
540
|
+
if (user || !m.sessions) continue;
|
|
541
|
+
try {
|
|
542
|
+
const op = (await m.product.load()).find((o) => o.name === `${m.product.id}.whoami`);
|
|
543
|
+
if (!op) continue;
|
|
544
|
+
const ctx = contextFor(m.product, config, m.sessions[0]);
|
|
545
|
+
const identity = await runWithRemint(
|
|
546
|
+
op,
|
|
547
|
+
op.input.parse({}),
|
|
548
|
+
ctx,
|
|
549
|
+
remintFor(m.product, shell.store)
|
|
550
|
+
);
|
|
551
|
+
const u = rec(readField(identity, "user"));
|
|
552
|
+
const email = readStr(u.email);
|
|
553
|
+
if (email) {
|
|
554
|
+
user = {
|
|
555
|
+
id: readStr(u.id) ?? "",
|
|
556
|
+
display_name: readStr(u.display_name) ?? readStr(u.name) ?? email,
|
|
557
|
+
email
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
} catch {
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
const touched = {
|
|
564
|
+
...grant,
|
|
565
|
+
...user ? { user } : {},
|
|
566
|
+
...minted.some((m) => m.sessions) ? { last_used_at: (/* @__PURE__ */ new Date()).toISOString() } : {}
|
|
567
|
+
};
|
|
568
|
+
await saveGrant(shell.store, touched);
|
|
569
|
+
return touched;
|
|
570
|
+
}
|
|
571
|
+
async function snapshot(shell, all, grant) {
|
|
572
|
+
const sessions = [];
|
|
573
|
+
for (const product of all) {
|
|
574
|
+
const s = await productSessions(shell.store, product.id);
|
|
575
|
+
if (s.length > 0) sessions.push({ product, sessions: s });
|
|
576
|
+
}
|
|
577
|
+
return { grant, sessions };
|
|
578
|
+
}
|
|
579
|
+
async function retire(shell, config, previous, minted) {
|
|
580
|
+
for (const { product, sessions } of previous.sessions) {
|
|
581
|
+
await revokeProduct(product, sessions, config).catch(() => {
|
|
582
|
+
});
|
|
583
|
+
if (!minted.some((m) => m.product.id === product.id && m.sessions)) {
|
|
584
|
+
await forgetSessions(shell.store, product.id);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
await new AccountClient({ accountUrl: previous.grant.account_url }).revoke(previous.grant.access_token).catch(() => {
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
function loginResult(grant, minted) {
|
|
591
|
+
const products = {};
|
|
592
|
+
for (const m of minted) {
|
|
593
|
+
products[m.product.id] = m.entry ? { workspaces: m.entry.workspaces.map((w) => w.handle), default: m.entry.default ?? null } : { error: m.error.toJSON() };
|
|
594
|
+
}
|
|
595
|
+
return {
|
|
596
|
+
account: {
|
|
597
|
+
user: grant.user ?? null,
|
|
598
|
+
expires_at: grant.expires_at,
|
|
599
|
+
account_url: grant.account_url
|
|
600
|
+
},
|
|
601
|
+
products
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
function report(shell, result) {
|
|
605
|
+
if (shell.opts.quiet) return;
|
|
606
|
+
const u = result.account.user;
|
|
607
|
+
const lines = [u ? `Logged in as ${u.display_name} (${u.email}).` : "Logged in."];
|
|
608
|
+
const width = Math.max(...Object.keys(result.products).map((id) => id.length));
|
|
609
|
+
for (const [id, p] of Object.entries(result.products)) {
|
|
610
|
+
let line;
|
|
611
|
+
if ("error" in p) {
|
|
612
|
+
line = `skipped: ${p.error.message}${p.error.hint ? ` ${p.error.hint}` : ""}`;
|
|
613
|
+
} else {
|
|
614
|
+
const n = p.workspaces.length;
|
|
615
|
+
const which = `${n} workspace${n === 1 ? "" : "s"}: ${p.workspaces.join(", ")}`;
|
|
616
|
+
line = p.default ? `${which} (default)` : `${which} (use --workspace to pick one)`;
|
|
617
|
+
}
|
|
618
|
+
lines.push(` ${id.padEnd(width)} ${line}`);
|
|
619
|
+
}
|
|
620
|
+
shell.io.err(`${lines.join("\n")}
|
|
621
|
+
`);
|
|
622
|
+
}
|
|
623
|
+
function note(shell, line) {
|
|
624
|
+
if (!shell.opts.quiet) shell.io.err(`${line}
|
|
625
|
+
`);
|
|
626
|
+
}
|
|
627
|
+
function minutes(seconds) {
|
|
628
|
+
const m = Math.round(seconds / 60);
|
|
629
|
+
return m === 1 ? "1 minute" : `${m} minutes`;
|
|
630
|
+
}
|
|
631
|
+
function clock(seconds) {
|
|
632
|
+
return `${Math.floor(seconds / 60)}:${String(seconds % 60).padStart(2, "0")}`;
|
|
633
|
+
}
|
|
634
|
+
async function productKeyLogin(shell, args) {
|
|
635
|
+
if (args.some((a) => a.includes("_"))) {
|
|
636
|
+
throw usage(
|
|
637
|
+
"Keys are never read from arguments.",
|
|
638
|
+
"Run `magic login --product-key` and paste the key at the prompt, or pipe it on stdin."
|
|
639
|
+
);
|
|
640
|
+
}
|
|
641
|
+
const named = args.length > 0 && !args[0].startsWith("--") ? args.shift() : void 0;
|
|
642
|
+
if (args.length > 0) {
|
|
643
|
+
throw usage("login: unexpected arguments.", "Usage: magic login --product-key [<product>]");
|
|
644
|
+
}
|
|
645
|
+
const products = await loadProducts(shell, named ? [named] : void 0);
|
|
646
|
+
const label = named ? `${products[0].title} key` : "Product key";
|
|
647
|
+
const key = await shell.readSecret(`${label} (input hidden): `);
|
|
648
|
+
if (!key) throw usage("No key given.", "Paste the key at the prompt, or pipe it on stdin.");
|
|
649
|
+
const product = named ? products[0] : byPrefix(products, key);
|
|
650
|
+
await saveProductKey(product, shell.store, key);
|
|
651
|
+
const where = storeLabel(await shell.store.locate(manualItem(product.id)));
|
|
652
|
+
envShadowNote(shell, product);
|
|
653
|
+
render(
|
|
654
|
+
{ product: product.id, key: maskKey(key, product.credential), stored_in: where },
|
|
655
|
+
shell.opts,
|
|
656
|
+
shell.io
|
|
657
|
+
);
|
|
658
|
+
return 0;
|
|
659
|
+
}
|
|
660
|
+
function byPrefix(products, key) {
|
|
661
|
+
const match = products.find((p) => key.startsWith(p.credential));
|
|
662
|
+
if (match) return match;
|
|
663
|
+
throw usage(
|
|
664
|
+
"The key prefix matches no product.",
|
|
665
|
+
`Known prefixes: ${products.map((p) => `${p.credential} (${p.id})`).join(", ")}.`
|
|
666
|
+
);
|
|
667
|
+
}
|
|
668
|
+
function envShadowNote(shell, product) {
|
|
669
|
+
const envVar = productEnvVar(product.id, "KEY");
|
|
670
|
+
if (shell.env[envVar]?.trim() && !shell.opts.quiet) {
|
|
671
|
+
shell.io.err(`note: ${envVar} is set and takes precedence over the stored key.
|
|
672
|
+
`);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
async function logout(shell, argv) {
|
|
676
|
+
const args = [...argv];
|
|
677
|
+
if (wantsHelp(args)) {
|
|
678
|
+
shell.io.err(LOGOUT_HELP);
|
|
679
|
+
return 0;
|
|
680
|
+
}
|
|
681
|
+
rejectWorkspace("logout", shell.opts);
|
|
682
|
+
const one = takeValue(args, "product");
|
|
683
|
+
noMoreArgs("logout", args);
|
|
684
|
+
const chosen = await loadProducts(shell, one ? [one] : void 0);
|
|
685
|
+
const grant = await readGrant(shell.store);
|
|
686
|
+
const result = { removed: [], revoked: [], account: "none" };
|
|
687
|
+
const errors = {};
|
|
688
|
+
for (const product of chosen) {
|
|
689
|
+
const sessions = await productSessions(shell.store, product.id);
|
|
690
|
+
if (sessions.length > 0 && product.sessions) {
|
|
691
|
+
try {
|
|
692
|
+
await revokeProduct(product, sessions, productConfig(product, shell));
|
|
693
|
+
result.revoked.push(product.id);
|
|
694
|
+
} catch (err) {
|
|
695
|
+
errors[product.id] = toMagicError(err, { product: product.id }).toJSON();
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
const items = await forgetSessions(shell.store, product.id);
|
|
699
|
+
const hadManual = await shell.store.locate(manualItem(product.id)) !== null;
|
|
700
|
+
await forgetProductKey(product, shell.store);
|
|
701
|
+
if (items.length > 0 || hadManual) result.removed.push(product.id);
|
|
702
|
+
envShadowNote(shell, product);
|
|
703
|
+
}
|
|
704
|
+
if (one) {
|
|
705
|
+
result.account = grant ? "kept" : "none";
|
|
706
|
+
} else if (grant) {
|
|
707
|
+
try {
|
|
708
|
+
result.account = await new AccountClient({ accountUrl: grant.account_url }).revoke(
|
|
709
|
+
grant.access_token
|
|
710
|
+
);
|
|
711
|
+
} catch (err) {
|
|
712
|
+
result.account = "failed";
|
|
713
|
+
errors.account = toMagicError(err, { product: "account" }).toJSON();
|
|
714
|
+
}
|
|
715
|
+
await shell.store.delete(GRANT_ITEM);
|
|
716
|
+
}
|
|
717
|
+
if (Object.keys(errors).length > 0) result.errors = errors;
|
|
718
|
+
if (!shell.opts.quiet) {
|
|
719
|
+
if (result.account === "unsupported") {
|
|
720
|
+
shell.io.err(
|
|
721
|
+
"note: Account does not offer grant revocation yet; the sign-in was forgotten here.\n"
|
|
722
|
+
);
|
|
723
|
+
}
|
|
724
|
+
for (const [id, e] of Object.entries(errors)) {
|
|
725
|
+
shell.io.err(`note: ${id}: could not revoke server-side (${e.message}); forgotten here.
|
|
726
|
+
`);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
render(result, shell.opts, shell.io);
|
|
730
|
+
return 0;
|
|
731
|
+
}
|
|
732
|
+
async function revokeProduct(product, sessions, config) {
|
|
733
|
+
for (const s of sessions) {
|
|
734
|
+
try {
|
|
735
|
+
await revokeSessions(product, { apiUrl: config.products[product.id].apiUrl, key: s.key });
|
|
736
|
+
return;
|
|
737
|
+
} catch (err) {
|
|
738
|
+
if (isMagicError(err) && (err.code === "auth_invalid" || err.code === "auth_expired")) {
|
|
739
|
+
continue;
|
|
740
|
+
}
|
|
741
|
+
throw err;
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// src/commands/mcp.ts
|
|
747
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
748
|
+
var PREAMBLE = "Magic MCP \u2014 tools for Magic Spells services. Every tool, resource and prompt is prefixed with its product (`pyramid_\u2026`); each product\u2019s rules follow under its own heading. Errors come back as `{ error: { code, message, hint, product } }`: act on `code`.";
|
|
749
|
+
async function buildMcp(shell, argv, signal) {
|
|
750
|
+
const args = [...argv];
|
|
751
|
+
rejectWorkspace("mcp", shell.opts);
|
|
752
|
+
const list = takeValue(args, "products");
|
|
753
|
+
noMoreArgs("mcp", args);
|
|
754
|
+
const ids = list?.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
755
|
+
const skipped = /* @__PURE__ */ new Set();
|
|
756
|
+
const skip = (id, err) => {
|
|
757
|
+
skipped.add(id);
|
|
758
|
+
shell.io.err(`magic mcp: ${id}: left out (${toMagicError(err, { product: id }).message})
|
|
759
|
+
`);
|
|
760
|
+
};
|
|
761
|
+
const products = [];
|
|
762
|
+
for (const entry of chooseEntries(shell, ids)) {
|
|
763
|
+
try {
|
|
764
|
+
const module = await entry.load();
|
|
765
|
+
const operations = await module.load();
|
|
766
|
+
if (operations.length === 0 && !module.mcp && !ids?.includes(entry.id)) continue;
|
|
767
|
+
products.push({
|
|
768
|
+
module,
|
|
769
|
+
operations,
|
|
770
|
+
context: contextSource(shell, module, signal),
|
|
771
|
+
remint: remintFor(module, shell.store)
|
|
772
|
+
});
|
|
773
|
+
} catch (err) {
|
|
774
|
+
skip(entry.id, err);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
const server = createMcpServer({
|
|
778
|
+
name: "magic",
|
|
779
|
+
version: version(),
|
|
780
|
+
preamble: PREAMBLE,
|
|
781
|
+
products,
|
|
782
|
+
onSkip: skip
|
|
783
|
+
});
|
|
784
|
+
return { server, products: products.filter((p) => !skipped.has(p.module.id)) };
|
|
785
|
+
}
|
|
786
|
+
function contextSource(shell, module, signal) {
|
|
787
|
+
return async () => buildContext(module, productConfig(module, shell), {
|
|
788
|
+
env: shell.env,
|
|
789
|
+
store: shell.store,
|
|
790
|
+
signal
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
async function serveMcp(shell, argv) {
|
|
794
|
+
if (wantsHelp(argv)) {
|
|
795
|
+
shell.io.err(
|
|
796
|
+
"Usage: magic mcp [--products a,b]\n\nServe every product\u2019s tools to an MCP host over stdio.\n"
|
|
797
|
+
);
|
|
798
|
+
return 0;
|
|
799
|
+
}
|
|
800
|
+
guardStdout();
|
|
801
|
+
const err = (line) => shell.io.err(`magic mcp: ${line}
|
|
802
|
+
`);
|
|
803
|
+
const controller = new AbortController();
|
|
804
|
+
const built = await buildMcp(shell, argv, controller.signal);
|
|
805
|
+
const transport = new StdioServerTransport();
|
|
806
|
+
let closing = false;
|
|
807
|
+
const shutdown = async (reason) => {
|
|
808
|
+
if (closing) return;
|
|
809
|
+
closing = true;
|
|
810
|
+
err(`shutting down (${reason}).`);
|
|
811
|
+
controller.abort();
|
|
812
|
+
try {
|
|
813
|
+
await built.server.close();
|
|
814
|
+
} catch (e) {
|
|
815
|
+
err(`error during shutdown: ${toMagicError(e).message}`);
|
|
816
|
+
}
|
|
817
|
+
process.exit(0);
|
|
818
|
+
};
|
|
819
|
+
process.on("SIGINT", () => void shutdown("SIGINT"));
|
|
820
|
+
process.on("SIGTERM", () => void shutdown("SIGTERM"));
|
|
821
|
+
process.stdin.on("close", () => void shutdown("stdin closed"));
|
|
822
|
+
process.stdin.on("end", () => void shutdown("stdin end"));
|
|
823
|
+
await built.server.connect(transport);
|
|
824
|
+
const summary = built.products.map((p) => `${p.module.id} (${p.operations.length} tools)`);
|
|
825
|
+
err(`ready on stdio \xB7 ${summary.join(", ")}`);
|
|
826
|
+
for (const { module } of built.products) {
|
|
827
|
+
try {
|
|
828
|
+
await resolveCredential(module, { env: shell.env, store: shell.store });
|
|
829
|
+
} catch (e) {
|
|
830
|
+
const why = toMagicError(e, { product: module.id });
|
|
831
|
+
err(
|
|
832
|
+
`${module.id}: ${why.message} Its tools answer ${why.code} until that is fixed. ${why.hint ?? ""}`.trim()
|
|
833
|
+
);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
return 0;
|
|
837
|
+
}
|
|
838
|
+
function guardStdout() {
|
|
839
|
+
const toErr = (...args) => {
|
|
840
|
+
process.stderr.write(`${args.map((a) => typeof a === "string" ? a : String(a)).join(" ")}
|
|
841
|
+
`);
|
|
842
|
+
};
|
|
843
|
+
console.log = toErr;
|
|
844
|
+
console.info = toErr;
|
|
845
|
+
console.debug = toErr;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
// src/commands/whoami.ts
|
|
849
|
+
async function whoami(shell, argv) {
|
|
850
|
+
if (wantsHelp(argv)) {
|
|
851
|
+
shell.io.err(
|
|
852
|
+
"Usage: magic whoami [--json]\n\nYour account, and your workspaces and keys in each product.\n"
|
|
853
|
+
);
|
|
854
|
+
return 0;
|
|
855
|
+
}
|
|
856
|
+
rejectWorkspace("whoami", shell.opts);
|
|
857
|
+
noMoreArgs("whoami", argv);
|
|
858
|
+
const grant = await readGrant(shell.store);
|
|
859
|
+
const result = {
|
|
860
|
+
account: grant ? {
|
|
861
|
+
user: grant.user ?? null,
|
|
862
|
+
grant_id: grant.grant_id,
|
|
863
|
+
expires_at: grant.expires_at,
|
|
864
|
+
idle_expires_at: grantIdleExpiry(grant),
|
|
865
|
+
account_url: grant.account_url
|
|
866
|
+
} : null,
|
|
867
|
+
products: {}
|
|
868
|
+
};
|
|
869
|
+
let firstError;
|
|
870
|
+
for (const product of await loadProducts(shell)) {
|
|
871
|
+
const entry = await identify(shell, product);
|
|
872
|
+
result.products[product.id] = entry.identity;
|
|
873
|
+
firstError ??= entry.error;
|
|
874
|
+
}
|
|
875
|
+
if (jsonMode(shell.opts, shell.io)) render(result, shell.opts, shell.io);
|
|
876
|
+
else printHuman2(shell, result);
|
|
877
|
+
if (firstError) return exitCodeFor(firstError.code);
|
|
878
|
+
if (!result.account && Object.values(result.products).every((p) => p.source === "none")) {
|
|
879
|
+
if (!shell.opts.quiet) shell.io.err("Not logged in. Run `magic login`.\n");
|
|
880
|
+
return exitCodeFor("not_logged_in");
|
|
881
|
+
}
|
|
882
|
+
return 0;
|
|
883
|
+
}
|
|
884
|
+
async function identify(shell, product) {
|
|
885
|
+
const id = product.id;
|
|
886
|
+
const sessions = await productSessions(shell.store, id);
|
|
887
|
+
const index = (await readIndex(shell.store))[id];
|
|
888
|
+
const envSet = Boolean(shell.env[productEnvVar(id, "KEY")]?.trim());
|
|
889
|
+
const manualAt = await shell.store.locate(manualItem(id));
|
|
890
|
+
const source = envSet ? "env" : sessions.length > 0 ? "account" : manualAt ? "manual" : "none";
|
|
891
|
+
const stored = source === "account" ? sessions[0].workspace.handle : null;
|
|
892
|
+
const base = {
|
|
893
|
+
source,
|
|
894
|
+
stored_in: source === "account" ? storeLabel(await shell.store.locate(sessionItem(id, stored))) : source === "manual" ? storeLabel(manualAt) : null,
|
|
895
|
+
key: null,
|
|
896
|
+
default: index?.default ?? null,
|
|
897
|
+
workspaces: workspaces(product, sessions, index?.workspaces ?? []),
|
|
898
|
+
identity: null
|
|
899
|
+
};
|
|
900
|
+
if (source === "none") return { identity: base };
|
|
901
|
+
let session;
|
|
902
|
+
const config = productConfig(product, shell);
|
|
903
|
+
try {
|
|
904
|
+
const workspace = config.defaultWorkspace[id];
|
|
905
|
+
session = await resolveCredential(product, {
|
|
906
|
+
env: shell.env,
|
|
907
|
+
store: shell.store,
|
|
908
|
+
...workspace ? { workspace } : {}
|
|
909
|
+
});
|
|
910
|
+
} catch (err) {
|
|
911
|
+
const error = toMagicError(err, { product: id });
|
|
912
|
+
if (error.code === "usage") return { identity: base };
|
|
913
|
+
return { identity: { ...base, error: error.toJSON() }, error };
|
|
914
|
+
}
|
|
915
|
+
const identity = { ...base, key: maskKey(session.key, product.credential) };
|
|
916
|
+
try {
|
|
917
|
+
const op = (await product.load()).find((o) => o.name === `${id}.whoami`);
|
|
918
|
+
if (!op) return { identity };
|
|
919
|
+
const ctx = contextFor(product, config, session);
|
|
920
|
+
const who = await runWithRemint(op, op.input.parse({}), ctx, remintFor(product, shell.store));
|
|
921
|
+
return { identity: { ...identity, identity: who } };
|
|
922
|
+
} catch (err) {
|
|
923
|
+
const error = toMagicError(err, { product: id });
|
|
924
|
+
return { identity: { ...identity, error: error.toJSON() }, error };
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
function workspaces(product, sessions, listed) {
|
|
928
|
+
return listed.map((w) => {
|
|
929
|
+
const s = sessions.find((x) => x.workspace?.handle === w.handle);
|
|
930
|
+
return {
|
|
931
|
+
handle: w.handle,
|
|
932
|
+
name: w.name,
|
|
933
|
+
role: w.role,
|
|
934
|
+
key: s ? maskKey(s.key, product.credential) : null,
|
|
935
|
+
expires_at: s?.expires_at ?? null
|
|
936
|
+
};
|
|
937
|
+
});
|
|
938
|
+
}
|
|
939
|
+
function printHuman2(shell, result) {
|
|
940
|
+
const lines = [];
|
|
941
|
+
const a = result.account;
|
|
942
|
+
if (a) {
|
|
943
|
+
lines.push(a.user ? `${a.user.display_name} <${a.user.email}>` : "Signed in");
|
|
944
|
+
lines.push(
|
|
945
|
+
` signed in until ${day(a.expires_at)}; ends after ${day(a.idle_expires_at)} unused`
|
|
946
|
+
);
|
|
947
|
+
} else {
|
|
948
|
+
lines.push("Not signed in to a Magic Spells account.");
|
|
949
|
+
}
|
|
950
|
+
lines.push("");
|
|
951
|
+
for (const [id, p] of Object.entries(result.products)) {
|
|
952
|
+
const rows = [["source", p.source]];
|
|
953
|
+
if (p.key) rows.push(["key", p.key]);
|
|
954
|
+
for (const w of p.workspaces) {
|
|
955
|
+
const mark = w.handle === p.default ? " (default)" : "";
|
|
956
|
+
const expires = w.expires_at ? `, key expires ${w.expires_at}` : "";
|
|
957
|
+
rows.push([w.handle, `${w.name} \xB7 ${w.role}${w.key ? ` \xB7 ${w.key}` : ""}${mark}${expires}`]);
|
|
958
|
+
}
|
|
959
|
+
if (p.identity && typeof p.identity === "object") {
|
|
960
|
+
for (const [k, v] of Object.entries(p.identity)) rows.push([k, cell(v)]);
|
|
961
|
+
}
|
|
962
|
+
if (p.error) rows.push(["error", `[${p.error.code}] ${p.error.message}`]);
|
|
963
|
+
const width = Math.max(...rows.map(([k]) => k.length));
|
|
964
|
+
lines.push(id, ...rows.map(([k, v]) => ` ${k.padEnd(width)} ${v}`), "");
|
|
965
|
+
}
|
|
966
|
+
shell.io.out(lines.join("\n"));
|
|
967
|
+
}
|
|
968
|
+
function day(iso) {
|
|
969
|
+
return iso.slice(0, 10);
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
// src/products.ts
|
|
973
|
+
var PRODUCTS = [
|
|
974
|
+
{
|
|
975
|
+
id: "pyramid",
|
|
976
|
+
title: "Pyramid \u2014 projects and tasks",
|
|
977
|
+
load: async () => (await import("./src-L2I546M7.js")).default
|
|
978
|
+
},
|
|
979
|
+
{
|
|
980
|
+
id: "sites",
|
|
981
|
+
title: "Sites \u2014 sign-in only until its commands arrive",
|
|
982
|
+
load: async () => (await import("./src-DJYPPJFM.js")).default
|
|
983
|
+
},
|
|
984
|
+
{
|
|
985
|
+
id: "marketing",
|
|
986
|
+
title: "Marketing \u2014 sign-in only until its commands arrive",
|
|
987
|
+
load: async () => (await import("./src-KJGFPRQP.js")).default
|
|
988
|
+
}
|
|
989
|
+
];
|
|
990
|
+
|
|
991
|
+
// src/secret.ts
|
|
992
|
+
async function readSecret(prompt) {
|
|
993
|
+
const stdin = process.stdin;
|
|
994
|
+
if (!stdin.isTTY) {
|
|
995
|
+
process.stderr.write("reading key from stdin\u2026\n");
|
|
996
|
+
return (await readAll(stdin)).trim();
|
|
997
|
+
}
|
|
998
|
+
process.stderr.write(prompt);
|
|
999
|
+
stdin.setRawMode(true);
|
|
1000
|
+
stdin.setEncoding("utf8");
|
|
1001
|
+
stdin.resume();
|
|
1002
|
+
return new Promise((resolve, reject) => {
|
|
1003
|
+
let value = "";
|
|
1004
|
+
const finish = () => {
|
|
1005
|
+
stdin.off("data", onData);
|
|
1006
|
+
stdin.setRawMode(false);
|
|
1007
|
+
stdin.pause();
|
|
1008
|
+
process.stderr.write("\n");
|
|
1009
|
+
};
|
|
1010
|
+
const onData = (chunk) => {
|
|
1011
|
+
for (const ch of chunk) {
|
|
1012
|
+
if (ch === "\r" || ch === "\n" || ch === "") {
|
|
1013
|
+
finish();
|
|
1014
|
+
resolve(value.trim());
|
|
1015
|
+
return;
|
|
1016
|
+
}
|
|
1017
|
+
if (ch === "") {
|
|
1018
|
+
finish();
|
|
1019
|
+
reject(cancelled());
|
|
1020
|
+
return;
|
|
1021
|
+
}
|
|
1022
|
+
if (ch === "\x7F" || ch === "\b") value = value.slice(0, -1);
|
|
1023
|
+
else value += ch;
|
|
1024
|
+
}
|
|
1025
|
+
};
|
|
1026
|
+
stdin.on("data", onData);
|
|
1027
|
+
});
|
|
1028
|
+
}
|
|
1029
|
+
async function readAll(stream) {
|
|
1030
|
+
let text = "";
|
|
1031
|
+
for await (const chunk of stream) text += String(chunk);
|
|
1032
|
+
return text;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
// src/main.ts
|
|
1036
|
+
var COMMANDS = {
|
|
1037
|
+
login: { summary: "sign in and mint product keys: login [--device] [--product <p>]", run: login },
|
|
1038
|
+
logout: { summary: "revoke and forget credentials: logout [--product <p>]", run: logout },
|
|
1039
|
+
whoami: { summary: "your account and your workspaces in each product", run: whoami },
|
|
1040
|
+
doctor: { summary: "check the install, the sign-in and each product", run: doctor },
|
|
1041
|
+
mcp: { summary: "serve every product\u2019s tools over stdio: mcp [--products a,b]", run: serveMcp }
|
|
1042
|
+
};
|
|
1043
|
+
function help(products) {
|
|
1044
|
+
const lines = [
|
|
1045
|
+
"magic \u2014 the command line for Magic Spells services",
|
|
1046
|
+
"",
|
|
1047
|
+
"Usage: magic <command> [args] [--flags]",
|
|
1048
|
+
" magic <product> <noun> <verb> [args] [--flags]",
|
|
1049
|
+
"",
|
|
1050
|
+
"Commands:",
|
|
1051
|
+
...Object.entries(COMMANDS).map(([name, c]) => ` ${name.padEnd(21)}${c.summary}`),
|
|
1052
|
+
` ${"version".padEnd(21)}print the version`,
|
|
1053
|
+
"",
|
|
1054
|
+
"Products:",
|
|
1055
|
+
...products.map((p) => ` ${p.id.padEnd(21)}${p.title}`),
|
|
1056
|
+
"",
|
|
1057
|
+
"Global flags: --json --yes/-y --no-color --quiet/-q --workspace <handle> --version --help",
|
|
1058
|
+
"Run `magic <product> --help` for a product\u2019s commands.",
|
|
1059
|
+
""
|
|
1060
|
+
];
|
|
1061
|
+
return lines.join("\n");
|
|
1062
|
+
}
|
|
1063
|
+
async function run(argv, deps = {}) {
|
|
1064
|
+
const io = deps.io ?? processIO;
|
|
1065
|
+
const env = deps.env ?? process.env;
|
|
1066
|
+
const products = deps.products ?? PRODUCTS;
|
|
1067
|
+
const [cmd, ...rest] = argv;
|
|
1068
|
+
if (cmd === "version" || cmd === "--version" || cmd === "-v") {
|
|
1069
|
+
io.out(`magic ${version()}
|
|
1070
|
+
`);
|
|
1071
|
+
return 0;
|
|
1072
|
+
}
|
|
1073
|
+
if (cmd === void 0 || cmd === "help" || cmd === "--help" || cmd === "-h") {
|
|
1074
|
+
io.err(help(products));
|
|
1075
|
+
return 0;
|
|
1076
|
+
}
|
|
1077
|
+
const store = () => deps.store ?? createCredentialStore({ env });
|
|
1078
|
+
const readConfig = () => readConfigFile(configFilePath({ env }));
|
|
1079
|
+
const entry = products.find((p) => p.id === cmd);
|
|
1080
|
+
if (entry) {
|
|
1081
|
+
const product = await entry.load();
|
|
1082
|
+
return runProductCli(product, rest, { io, env, store: store(), readConfig });
|
|
1083
|
+
}
|
|
1084
|
+
let opts = { json: argv.includes("--json"), yes: false, quiet: false };
|
|
1085
|
+
try {
|
|
1086
|
+
const command = COMMANDS[cmd];
|
|
1087
|
+
if (!command) throw usage(`unknown command "${cmd}"`, "Run `magic --help` for usage.");
|
|
1088
|
+
const parsed = parseGlobals(rest, [], env);
|
|
1089
|
+
opts = parsed.opts;
|
|
1090
|
+
const shell = {
|
|
1091
|
+
io,
|
|
1092
|
+
env,
|
|
1093
|
+
store: store(),
|
|
1094
|
+
entries: products,
|
|
1095
|
+
opts,
|
|
1096
|
+
readConfig,
|
|
1097
|
+
readSecret: deps.readSecret ?? readSecret,
|
|
1098
|
+
openBrowser: deps.openBrowser ?? openBrowser,
|
|
1099
|
+
confirm: deps.confirm !== void 0 ? deps.confirm : process.stdin.isTTY ? confirmOnTty : null,
|
|
1100
|
+
platform: deps.platform ?? process.platform,
|
|
1101
|
+
login: deps.login ?? {}
|
|
1102
|
+
};
|
|
1103
|
+
return await command.run(shell, parsed.rest);
|
|
1104
|
+
} catch (err) {
|
|
1105
|
+
return renderError(toMagicError(err), opts, io);
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
export {
|
|
1109
|
+
run,
|
|
1110
|
+
version
|
|
1111
|
+
};
|