magic-spells 0.0.0-stage → 0.1.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 +66 -2
- package/bin/magic.js +12 -0
- package/dist/chunk-S5IUP7G2.js +1057 -0
- package/dist/chunk-YT2Z756Z.js +1128 -0
- package/dist/main.js +557 -0
- package/dist/operations-P3DUAVPP.js +9 -0
- package/dist/src-4S3XHPWY.js +1298 -0
- package/package.json +33 -5
|
@@ -0,0 +1,1128 @@
|
|
|
1
|
+
// ../kit/src/errors.ts
|
|
2
|
+
var SHELL_PRODUCT = "magic";
|
|
3
|
+
var MagicError = class _MagicError extends Error {
|
|
4
|
+
code;
|
|
5
|
+
product;
|
|
6
|
+
hint;
|
|
7
|
+
candidates;
|
|
8
|
+
details;
|
|
9
|
+
constructor(code, message, opts = {}) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = "MagicError";
|
|
12
|
+
this.code = code;
|
|
13
|
+
this.product = opts.product ?? SHELL_PRODUCT;
|
|
14
|
+
if (opts.hint !== void 0) this.hint = opts.hint;
|
|
15
|
+
if (opts.candidates !== void 0) this.candidates = opts.candidates;
|
|
16
|
+
if (opts.details !== void 0) this.details = opts.details;
|
|
17
|
+
Object.setPrototypeOf(this, _MagicError.prototype);
|
|
18
|
+
}
|
|
19
|
+
/** The inner envelope: what `{ error: … }` wraps on stderr and in MCP results. */
|
|
20
|
+
toJSON() {
|
|
21
|
+
const out = { code: this.code, message: this.message, product: this.product };
|
|
22
|
+
if (this.hint !== void 0) out.hint = this.hint;
|
|
23
|
+
if (this.candidates !== void 0) out.candidates = this.candidates;
|
|
24
|
+
if (this.details !== void 0) out.details = this.details;
|
|
25
|
+
return out;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
function exitCodeFor(code) {
|
|
29
|
+
switch (code) {
|
|
30
|
+
case "usage":
|
|
31
|
+
return 2;
|
|
32
|
+
case "not_logged_in":
|
|
33
|
+
case "auth_invalid":
|
|
34
|
+
case "auth_expired":
|
|
35
|
+
return 3;
|
|
36
|
+
case "permission_denied":
|
|
37
|
+
return 4;
|
|
38
|
+
case "not_found":
|
|
39
|
+
return 5;
|
|
40
|
+
case "ambiguous_name":
|
|
41
|
+
return 6;
|
|
42
|
+
case "validation_failed":
|
|
43
|
+
case "conflict":
|
|
44
|
+
case "diagnostics":
|
|
45
|
+
return 7;
|
|
46
|
+
case "destructive_action_disabled":
|
|
47
|
+
return 8;
|
|
48
|
+
case "rate_limited":
|
|
49
|
+
return 9;
|
|
50
|
+
case "network":
|
|
51
|
+
return 10;
|
|
52
|
+
case "product_unavailable":
|
|
53
|
+
return 11;
|
|
54
|
+
case "unknown":
|
|
55
|
+
return 1;
|
|
56
|
+
default: {
|
|
57
|
+
const never = code;
|
|
58
|
+
void never;
|
|
59
|
+
return 1;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function toToolError(e) {
|
|
64
|
+
return {
|
|
65
|
+
content: [{ type: "text", text: JSON.stringify({ error: e.toJSON() }) }],
|
|
66
|
+
isError: true
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function readEnvelope(body) {
|
|
70
|
+
if (!body || typeof body !== "object" || !("error" in body)) return {};
|
|
71
|
+
const err = body.error;
|
|
72
|
+
if (!err || typeof err !== "object") return {};
|
|
73
|
+
const e = err;
|
|
74
|
+
const rawField = e.details && typeof e.details === "object" ? e.details.field : void 0;
|
|
75
|
+
return {
|
|
76
|
+
code: typeof e.code === "string" ? e.code : void 0,
|
|
77
|
+
message: typeof e.message === "string" ? e.message : void 0,
|
|
78
|
+
field: typeof rawField === "string" && rawField.length > 0 ? rawField : void 0
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function mapHttpError(status, body, opts) {
|
|
82
|
+
const env = readEnvelope(body);
|
|
83
|
+
const product = opts.product;
|
|
84
|
+
const message = env.message ?? `${opts.title ?? product} request failed (HTTP ${status}).`;
|
|
85
|
+
if (status === 401) {
|
|
86
|
+
const expired = `${env.code ?? ""} ${env.message ?? ""}`.toLowerCase().includes("expired");
|
|
87
|
+
return new MagicError(expired ? "auth_expired" : "auth_invalid", message, {
|
|
88
|
+
product,
|
|
89
|
+
...opts.authHint ? { hint: opts.authHint } : {}
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
if (status === 403) return new MagicError("permission_denied", message, { product });
|
|
93
|
+
if (status === 404) {
|
|
94
|
+
const hay = `${env.code ?? ""} ${env.message ?? ""}`.toLowerCase();
|
|
95
|
+
const kind = opts.notFoundKinds?.find((k) => hay.includes(k));
|
|
96
|
+
return new MagicError("not_found", message, {
|
|
97
|
+
product,
|
|
98
|
+
...kind ? { details: { kind } } : {}
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
if (status === 400 || status === 422) {
|
|
102
|
+
return env.field === void 0 ? new MagicError("validation_failed", message, { product }) : new MagicError("validation_failed", message, {
|
|
103
|
+
product,
|
|
104
|
+
hint: `The server rejected the \`${env.field}\` field.`,
|
|
105
|
+
details: { field: env.field }
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
if (status === 409) {
|
|
109
|
+
return new MagicError("conflict", message, {
|
|
110
|
+
product,
|
|
111
|
+
hint: "The resource changed since you last read it \u2014 reread it and try again."
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
if (status === 429) {
|
|
115
|
+
return new MagicError("rate_limited", message, { product, hint: "Slow down and retry." });
|
|
116
|
+
}
|
|
117
|
+
return new MagicError("unknown", message, { product });
|
|
118
|
+
}
|
|
119
|
+
function toMagicError(e, opts = {}) {
|
|
120
|
+
if (e instanceof MagicError) return e;
|
|
121
|
+
const product = opts.product ?? SHELL_PRODUCT;
|
|
122
|
+
if (e instanceof Error) {
|
|
123
|
+
if (isNetworkError(e)) {
|
|
124
|
+
return new MagicError("network", e.message, {
|
|
125
|
+
product,
|
|
126
|
+
hint: opts.networkHint ?? "Check your network connection."
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
return new MagicError("unknown", e.message, { product });
|
|
130
|
+
}
|
|
131
|
+
return new MagicError("unknown", typeof e === "string" ? e : "Unknown error.", { product });
|
|
132
|
+
}
|
|
133
|
+
function isNetworkError(e) {
|
|
134
|
+
const code = e.code ?? "";
|
|
135
|
+
if ([
|
|
136
|
+
"ECONNREFUSED",
|
|
137
|
+
"ENOTFOUND",
|
|
138
|
+
"ECONNRESET",
|
|
139
|
+
"ETIMEDOUT",
|
|
140
|
+
"EAI_AGAIN",
|
|
141
|
+
"UND_ERR_CONNECT_TIMEOUT",
|
|
142
|
+
"UND_ERR_SOCKET",
|
|
143
|
+
"UND_ERR_HEADERS_TIMEOUT"
|
|
144
|
+
].includes(code)) {
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
if (e.name === "AbortError" || e.name === "TimeoutError") return true;
|
|
148
|
+
return /fetch failed|network|socket|connect/i.test(e.message ?? "");
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ../kit/src/config.ts
|
|
152
|
+
import { readFileSync } from "node:fs";
|
|
153
|
+
import { join as join2 } from "node:path";
|
|
154
|
+
|
|
155
|
+
// ../kit/src/paths.ts
|
|
156
|
+
import { homedir } from "node:os";
|
|
157
|
+
import { join } from "node:path";
|
|
158
|
+
function configDir(opts = {}) {
|
|
159
|
+
const platform = opts.platform ?? process.platform;
|
|
160
|
+
const env = opts.env ?? process.env;
|
|
161
|
+
const home = opts.home ?? homedir();
|
|
162
|
+
if (platform === "win32") return join(env.APPDATA ?? join(home, "AppData", "Roaming"), "magic");
|
|
163
|
+
return join(env.XDG_CONFIG_HOME || join(home, ".config"), "magic");
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ../kit/src/config.ts
|
|
167
|
+
var DEFAULT_ACCOUNT_URL = "https://account.magicspells.io";
|
|
168
|
+
function productEnvVar(id, suffix) {
|
|
169
|
+
return `MAGIC_${id.toUpperCase().replace(/-/g, "_")}_${suffix}`;
|
|
170
|
+
}
|
|
171
|
+
function configFilePath(opts = {}) {
|
|
172
|
+
return join2(configDir(opts), "config.json");
|
|
173
|
+
}
|
|
174
|
+
function readConfigFile(path = configFilePath(), read = (p) => readFileSync(p, "utf8")) {
|
|
175
|
+
try {
|
|
176
|
+
const parsed = JSON.parse(read(path));
|
|
177
|
+
return parsed && typeof parsed === "object" ? parsed : null;
|
|
178
|
+
} catch {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
function loadCliConfig(opts) {
|
|
183
|
+
const env = opts.env ?? process.env;
|
|
184
|
+
const file = opts.file ?? null;
|
|
185
|
+
const products = {};
|
|
186
|
+
const defaultWorkspace = {};
|
|
187
|
+
for (const p of opts.products) {
|
|
188
|
+
const envVar = productEnvVar(p.id, "URL");
|
|
189
|
+
products[p.id] = {
|
|
190
|
+
apiUrl: normalizeUrl(env[envVar] || file?.products?.[p.id]?.apiUrl || p.apiUrl, envVar)
|
|
191
|
+
};
|
|
192
|
+
const ws = opts.workspace ?? file?.defaultWorkspace?.[p.id];
|
|
193
|
+
if (ws) defaultWorkspace[p.id] = ws;
|
|
194
|
+
}
|
|
195
|
+
const fileTelemetry = file?.telemetry;
|
|
196
|
+
return {
|
|
197
|
+
accountUrl: normalizeUrl(
|
|
198
|
+
env.MAGIC_ACCOUNT_URL || file?.accountUrl || DEFAULT_ACCOUNT_URL,
|
|
199
|
+
"MAGIC_ACCOUNT_URL"
|
|
200
|
+
),
|
|
201
|
+
products,
|
|
202
|
+
defaultWorkspace,
|
|
203
|
+
allowDestructive: env.MAGIC_ALLOW_DESTRUCTIVE === "1",
|
|
204
|
+
telemetry: fileTelemetry === "on" || fileTelemetry === "off" ? fileTelemetry : "unset",
|
|
205
|
+
updateCheck: !env.CI && !env.MAGIC_NO_UPDATE_CHECK,
|
|
206
|
+
output: {
|
|
207
|
+
json: opts.output?.json ?? false,
|
|
208
|
+
color: opts.output?.color ?? !env.NO_COLOR,
|
|
209
|
+
quiet: opts.output?.quiet ?? false
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
function normalizeUrl(value, source) {
|
|
214
|
+
const trimmed = value.endsWith("/") ? value.slice(0, -1) : value;
|
|
215
|
+
try {
|
|
216
|
+
new URL(trimmed);
|
|
217
|
+
} catch {
|
|
218
|
+
throw new MagicError("usage", `${source} is not a valid URL: "${value}".`);
|
|
219
|
+
}
|
|
220
|
+
return trimmed;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// ../kit/src/keychain.ts
|
|
224
|
+
import { execFileSync } from "node:child_process";
|
|
225
|
+
import { chmodSync, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "node:fs";
|
|
226
|
+
import { dirname, join as join3 } from "node:path";
|
|
227
|
+
var KEYCHAIN_SERVICE = "io.magicspells.cli";
|
|
228
|
+
var SECURITY = "/usr/bin/security";
|
|
229
|
+
function credentialsFilePath(opts = {}) {
|
|
230
|
+
return join3(configDir(opts), "credentials.json");
|
|
231
|
+
}
|
|
232
|
+
var realExec = (file, args) => execFileSync(file, args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
233
|
+
function createCredentialStore(opts = {}) {
|
|
234
|
+
const platform = opts.platform ?? process.platform;
|
|
235
|
+
const env = opts.env ?? process.env;
|
|
236
|
+
const chosen = env.MAGIC_CREDENTIALS_FILE === "1";
|
|
237
|
+
const warn = chosen ? () => {
|
|
238
|
+
} : opts.warn ?? defaultWarn;
|
|
239
|
+
const file = createFileStore(credentialsFilePath(opts), warn);
|
|
240
|
+
if (platform !== "darwin" || chosen) return file;
|
|
241
|
+
return createMacStore(opts.exec ?? realExec, file);
|
|
242
|
+
}
|
|
243
|
+
function defaultWarn(line2) {
|
|
244
|
+
process.stderr.write(`${line2}
|
|
245
|
+
`);
|
|
246
|
+
}
|
|
247
|
+
function createFileStore(path, warn) {
|
|
248
|
+
let warned = false;
|
|
249
|
+
const read = () => {
|
|
250
|
+
try {
|
|
251
|
+
const parsed = JSON.parse(readFileSync2(path, "utf8"));
|
|
252
|
+
return { items: parsed.items && typeof parsed.items === "object" ? parsed.items : {} };
|
|
253
|
+
} catch {
|
|
254
|
+
return { items: {} };
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
const write = (data) => {
|
|
258
|
+
mkdirSync(dirname(path), { recursive: true, mode: 448 });
|
|
259
|
+
writeFileSync(path, `${JSON.stringify(data, null, 2)}
|
|
260
|
+
`, { mode: 384 });
|
|
261
|
+
chmodSync(path, 384);
|
|
262
|
+
};
|
|
263
|
+
const value = (item) => {
|
|
264
|
+
const v = read().items[item];
|
|
265
|
+
return typeof v === "string" && v.length > 0 ? v : null;
|
|
266
|
+
};
|
|
267
|
+
return {
|
|
268
|
+
backend: "file",
|
|
269
|
+
async get(item) {
|
|
270
|
+
return value(item);
|
|
271
|
+
},
|
|
272
|
+
async locate(item) {
|
|
273
|
+
return value(item) === null ? null : "file";
|
|
274
|
+
},
|
|
275
|
+
async set(item, secret) {
|
|
276
|
+
if (!warned) {
|
|
277
|
+
warn(`warning: storing credentials in ${path} (mode 0600), not the OS keychain.`);
|
|
278
|
+
warned = true;
|
|
279
|
+
}
|
|
280
|
+
const data = read();
|
|
281
|
+
data.items[item] = secret;
|
|
282
|
+
write(data);
|
|
283
|
+
},
|
|
284
|
+
async delete(item) {
|
|
285
|
+
const data = read();
|
|
286
|
+
if (!(item in data.items)) return;
|
|
287
|
+
delete data.items[item];
|
|
288
|
+
write(data);
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
function createMacStore(exec, file) {
|
|
293
|
+
const fromKeychain = (item) => {
|
|
294
|
+
try {
|
|
295
|
+
const v = exec(SECURITY, [
|
|
296
|
+
"find-generic-password",
|
|
297
|
+
"-s",
|
|
298
|
+
KEYCHAIN_SERVICE,
|
|
299
|
+
"-a",
|
|
300
|
+
item,
|
|
301
|
+
"-w"
|
|
302
|
+
]).trim();
|
|
303
|
+
return v.length > 0 ? v : null;
|
|
304
|
+
} catch {
|
|
305
|
+
return null;
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
return {
|
|
309
|
+
backend: "keyring",
|
|
310
|
+
async get(item) {
|
|
311
|
+
return fromKeychain(item) ?? file.get(item);
|
|
312
|
+
},
|
|
313
|
+
async locate(item) {
|
|
314
|
+
return fromKeychain(item) !== null ? "keyring" : file.locate(item);
|
|
315
|
+
},
|
|
316
|
+
async set(item, secret) {
|
|
317
|
+
try {
|
|
318
|
+
exec(SECURITY, [
|
|
319
|
+
"add-generic-password",
|
|
320
|
+
"-U",
|
|
321
|
+
"-s",
|
|
322
|
+
KEYCHAIN_SERVICE,
|
|
323
|
+
"-a",
|
|
324
|
+
item,
|
|
325
|
+
"-w",
|
|
326
|
+
secret
|
|
327
|
+
]);
|
|
328
|
+
} catch {
|
|
329
|
+
await file.set(item, secret);
|
|
330
|
+
}
|
|
331
|
+
},
|
|
332
|
+
async delete(item) {
|
|
333
|
+
try {
|
|
334
|
+
exec(SECURITY, ["delete-generic-password", "-s", KEYCHAIN_SERVICE, "-a", item]);
|
|
335
|
+
} catch {
|
|
336
|
+
}
|
|
337
|
+
await file.delete(item);
|
|
338
|
+
}
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// ../kit/src/render.ts
|
|
343
|
+
var processIO = {
|
|
344
|
+
out: (text) => void process.stdout.write(text),
|
|
345
|
+
err: (text) => void process.stderr.write(text),
|
|
346
|
+
isTTY: () => process.stdout.isTTY === true
|
|
347
|
+
};
|
|
348
|
+
function line(write, text) {
|
|
349
|
+
write(text.endsWith("\n") ? text : `${text}
|
|
350
|
+
`);
|
|
351
|
+
}
|
|
352
|
+
function jsonMode(opts, io = processIO) {
|
|
353
|
+
return opts.json || !io.isTTY();
|
|
354
|
+
}
|
|
355
|
+
function render(result, opts, io = processIO) {
|
|
356
|
+
if (jsonMode(opts, io)) {
|
|
357
|
+
line(io.out, JSON.stringify(result, null, 2));
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
renderHuman(result, opts, io);
|
|
361
|
+
}
|
|
362
|
+
function isPage(v) {
|
|
363
|
+
return typeof v === "object" && v !== null && Array.isArray(v.items) && "next_cursor" in v;
|
|
364
|
+
}
|
|
365
|
+
function renderHuman(result, opts, io) {
|
|
366
|
+
const out = (t) => line(io.out, t);
|
|
367
|
+
if (result === null || result === void 0) {
|
|
368
|
+
out("(none)");
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
if (isPage(result)) {
|
|
372
|
+
renderRows(result.items, out);
|
|
373
|
+
if (result.next_cursor && !opts.quiet) line(io.err, `more: --cursor ${result.next_cursor}`);
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
if (Array.isArray(result)) {
|
|
377
|
+
renderRows(result, out);
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
if (typeof result !== "object") {
|
|
381
|
+
out(String(result));
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
const rec2 = result;
|
|
385
|
+
const keys = Object.keys(rec2);
|
|
386
|
+
if (keys.length === 0) {
|
|
387
|
+
out("(empty)");
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
const width = Math.max(...keys.map((k) => k.length));
|
|
391
|
+
for (const k of keys) out(`${k.padEnd(width)} ${cell(rec2[k])}`);
|
|
392
|
+
}
|
|
393
|
+
function renderRows(rows, out) {
|
|
394
|
+
if (rows.length === 0) {
|
|
395
|
+
out("(no results)");
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
if (rows.every((r) => typeof r !== "object" || r === null)) {
|
|
399
|
+
for (const r of rows) out(cell(r));
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
const objs = rows.map((r) => r && typeof r === "object" ? r : {});
|
|
403
|
+
const cols = [];
|
|
404
|
+
for (const o of objs) for (const k of Object.keys(o)) if (!cols.includes(k)) cols.push(k);
|
|
405
|
+
const matrix = objs.map((o) => cols.map((c) => cell(o[c])));
|
|
406
|
+
const widths = cols.map((c, i) => Math.max(c.length, ...matrix.map((row) => row[i].length)));
|
|
407
|
+
out(cols.map((c, i) => c.padEnd(widths[i])).join(" "));
|
|
408
|
+
for (const row of matrix) out(row.map((v, i) => v.padEnd(widths[i])).join(" "));
|
|
409
|
+
}
|
|
410
|
+
function cell(v) {
|
|
411
|
+
if (v === null || v === void 0) return "";
|
|
412
|
+
if (typeof v === "string") return v;
|
|
413
|
+
if (typeof v === "number" || typeof v === "boolean") return String(v);
|
|
414
|
+
if (Array.isArray(v)) return v.map(cell).join(", ");
|
|
415
|
+
if (typeof v === "object") {
|
|
416
|
+
const o = v;
|
|
417
|
+
if (typeof o.name === "string") return o.name;
|
|
418
|
+
if (typeof o.display_name === "string") {
|
|
419
|
+
return typeof o.job_title === "string" && o.job_title.length > 0 ? `${o.display_name} (${o.job_title})` : o.display_name;
|
|
420
|
+
}
|
|
421
|
+
if (typeof o.key === "string") return o.key;
|
|
422
|
+
if (typeof o.id === "string") return o.id;
|
|
423
|
+
return JSON.stringify(v);
|
|
424
|
+
}
|
|
425
|
+
return String(v);
|
|
426
|
+
}
|
|
427
|
+
function renderError(e, opts, io = processIO) {
|
|
428
|
+
const shape = e.toJSON();
|
|
429
|
+
if (jsonMode(opts, io)) {
|
|
430
|
+
line(io.err, JSON.stringify({ error: shape }, null, 2));
|
|
431
|
+
} else {
|
|
432
|
+
line(io.err, `error [${shape.code}]: ${shape.message}`);
|
|
433
|
+
if (shape.hint) line(io.err, ` hint: ${shape.hint}`);
|
|
434
|
+
if (shape.candidates && shape.candidates.length > 0) {
|
|
435
|
+
line(io.err, ` candidates: ${shape.candidates.join(", ")}`);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
return exitCodeFor(e.code);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// ../kit/src/operation.ts
|
|
442
|
+
function toolName(op) {
|
|
443
|
+
return op.name.replace(/[.-]/g, "_");
|
|
444
|
+
}
|
|
445
|
+
function commandWords(op) {
|
|
446
|
+
return op.name.split(".").slice(1);
|
|
447
|
+
}
|
|
448
|
+
function onSurface(op, surface) {
|
|
449
|
+
return op.meta?.surfaces?.includes(surface) ?? true;
|
|
450
|
+
}
|
|
451
|
+
function assertRegistry(product, ops) {
|
|
452
|
+
const names = /* @__PURE__ */ new Set();
|
|
453
|
+
const tools = /* @__PURE__ */ new Set();
|
|
454
|
+
for (const op of ops) {
|
|
455
|
+
const parts = op.name.split(".");
|
|
456
|
+
if (parts[0] !== product || parts.length < 2 || parts.length > 3) {
|
|
457
|
+
throw new Error(`operation "${op.name}" must be named ${product}.<noun>.<verb>`);
|
|
458
|
+
}
|
|
459
|
+
if (!parts.every((p) => /^[a-z][a-z0-9-]*$/.test(p))) {
|
|
460
|
+
throw new Error(`operation "${op.name}" must be lower case words`);
|
|
461
|
+
}
|
|
462
|
+
if (names.has(op.name)) throw new Error(`duplicate operation "${op.name}"`);
|
|
463
|
+
if (tools.has(toolName(op))) throw new Error(`duplicate MCP tool "${toolName(op)}"`);
|
|
464
|
+
names.add(op.name);
|
|
465
|
+
tools.add(toolName(op));
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// ../kit/src/argv.ts
|
|
470
|
+
function parseGlobals(argv, productFlags = [], env = process.env) {
|
|
471
|
+
const opts = {
|
|
472
|
+
json: false,
|
|
473
|
+
yes: false,
|
|
474
|
+
quiet: false,
|
|
475
|
+
...env.NO_COLOR ? { color: false } : {}
|
|
476
|
+
};
|
|
477
|
+
const flags = {};
|
|
478
|
+
const rest = [];
|
|
479
|
+
const valueFlag = (name, set, i) => {
|
|
480
|
+
const v = argv[i + 1];
|
|
481
|
+
if (v === void 0 || v.startsWith("--")) {
|
|
482
|
+
throw new MagicError("usage", `flag --${name} expects a value`);
|
|
483
|
+
}
|
|
484
|
+
set(v);
|
|
485
|
+
return i + 1;
|
|
486
|
+
};
|
|
487
|
+
for (let i = 0; i < argv.length; i++) {
|
|
488
|
+
const tok = argv[i];
|
|
489
|
+
switch (tok) {
|
|
490
|
+
case "--json":
|
|
491
|
+
opts.json = true;
|
|
492
|
+
continue;
|
|
493
|
+
case "--yes":
|
|
494
|
+
case "-y":
|
|
495
|
+
opts.yes = true;
|
|
496
|
+
continue;
|
|
497
|
+
case "--quiet":
|
|
498
|
+
case "-q":
|
|
499
|
+
opts.quiet = true;
|
|
500
|
+
continue;
|
|
501
|
+
case "--no-color":
|
|
502
|
+
opts.color = false;
|
|
503
|
+
continue;
|
|
504
|
+
case "--workspace":
|
|
505
|
+
i = valueFlag("workspace", (v) => opts.workspace = v, i);
|
|
506
|
+
continue;
|
|
507
|
+
}
|
|
508
|
+
if (tok.startsWith("--workspace=")) {
|
|
509
|
+
opts.workspace = tok.slice("--workspace=".length);
|
|
510
|
+
continue;
|
|
511
|
+
}
|
|
512
|
+
const pf = productFlags.find((f) => tok === `--${f}` || tok.startsWith(`--${f}=`));
|
|
513
|
+
if (pf) {
|
|
514
|
+
const key = pf.replace(/-/g, "_");
|
|
515
|
+
if (tok.includes("=")) flags[key] = tok.slice(tok.indexOf("=") + 1);
|
|
516
|
+
else i = valueFlag(pf, (v) => flags[key] = v, i);
|
|
517
|
+
continue;
|
|
518
|
+
}
|
|
519
|
+
rest.push(tok);
|
|
520
|
+
}
|
|
521
|
+
return { opts, productFlags: flags, rest };
|
|
522
|
+
}
|
|
523
|
+
function buildCommands(ops) {
|
|
524
|
+
return ops.map((op) => ({
|
|
525
|
+
op,
|
|
526
|
+
words: commandWords(op),
|
|
527
|
+
verbAliases: op.meta?.cli?.verbAliases ?? [],
|
|
528
|
+
positionals: op.meta?.cli?.positionals ?? []
|
|
529
|
+
}));
|
|
530
|
+
}
|
|
531
|
+
function matchCommand(cmds, argv) {
|
|
532
|
+
const sorted = [...cmds].sort((a, b) => b.words.length - a.words.length);
|
|
533
|
+
for (const cmd of sorted) {
|
|
534
|
+
const n = cmd.words.length;
|
|
535
|
+
if (argv.length < n) continue;
|
|
536
|
+
const head = argv.slice(0, n - 1);
|
|
537
|
+
const last = argv[n - 1];
|
|
538
|
+
const lastOk = last === cmd.words[n - 1] || cmd.verbAliases.includes(last);
|
|
539
|
+
if (lastOk && head.every((w, i) => w === cmd.words[i])) {
|
|
540
|
+
return { cmd, args: argv.slice(n) };
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
return void 0;
|
|
544
|
+
}
|
|
545
|
+
function baseType(schema) {
|
|
546
|
+
let cur = schema;
|
|
547
|
+
let optional = false;
|
|
548
|
+
for (let guard = 0; guard < 12; guard++) {
|
|
549
|
+
const def = cur._def;
|
|
550
|
+
const name = def?.typeName;
|
|
551
|
+
if ((name === "ZodOptional" || name === "ZodNullable" || name === "ZodDefault") && def?.innerType) {
|
|
552
|
+
optional = true;
|
|
553
|
+
cur = def.innerType;
|
|
554
|
+
continue;
|
|
555
|
+
}
|
|
556
|
+
break;
|
|
557
|
+
}
|
|
558
|
+
return { type: cur, optional };
|
|
559
|
+
}
|
|
560
|
+
function describeFields(input) {
|
|
561
|
+
const shape = input.shape;
|
|
562
|
+
if (!shape) return [];
|
|
563
|
+
return Object.entries(shape).map(([key, raw]) => {
|
|
564
|
+
const { type, optional } = baseType(raw);
|
|
565
|
+
const def = type._def;
|
|
566
|
+
let kind = "string";
|
|
567
|
+
let values;
|
|
568
|
+
switch (def?.typeName) {
|
|
569
|
+
case "ZodBoolean":
|
|
570
|
+
kind = "boolean";
|
|
571
|
+
break;
|
|
572
|
+
case "ZodNumber":
|
|
573
|
+
kind = "number";
|
|
574
|
+
break;
|
|
575
|
+
case "ZodArray": {
|
|
576
|
+
const el = def.type ? baseType(def.type).type : void 0;
|
|
577
|
+
const elName = el?._def?.typeName;
|
|
578
|
+
kind = elName === "ZodObject" ? "json" : "string[]";
|
|
579
|
+
break;
|
|
580
|
+
}
|
|
581
|
+
case "ZodEnum":
|
|
582
|
+
kind = "enum";
|
|
583
|
+
if (Array.isArray(def.values)) values = def.values;
|
|
584
|
+
break;
|
|
585
|
+
case "ZodNativeEnum":
|
|
586
|
+
kind = "enum";
|
|
587
|
+
break;
|
|
588
|
+
case "ZodObject":
|
|
589
|
+
case "ZodRecord":
|
|
590
|
+
kind = "json";
|
|
591
|
+
break;
|
|
592
|
+
}
|
|
593
|
+
const spec = { key, kind, required: !optional };
|
|
594
|
+
if (values) spec.values = values;
|
|
595
|
+
return spec;
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
function keyToFlag(key) {
|
|
599
|
+
return key.replace(/_/g, "-");
|
|
600
|
+
}
|
|
601
|
+
function flagToKey(flag, fields, aliases = {}) {
|
|
602
|
+
if (aliases[flag]) return aliases[flag];
|
|
603
|
+
const snake = flag.replace(/-/g, "_");
|
|
604
|
+
const direct = fields.find((f) => f.key === snake || keyToFlag(f.key) === flag);
|
|
605
|
+
if (direct) return direct.key;
|
|
606
|
+
const plural = fields.find((f) => f.kind === "string[]" && f.key === `${snake}s`);
|
|
607
|
+
if (plural) return plural.key;
|
|
608
|
+
const seg = fields.filter((f) => f.key.split("_")[0] === snake);
|
|
609
|
+
if (seg.length === 1) return seg[0].key;
|
|
610
|
+
return void 0;
|
|
611
|
+
}
|
|
612
|
+
function parseCommandArgs(cmd, args) {
|
|
613
|
+
const cli = cmd.op.meta?.cli ?? {};
|
|
614
|
+
const fields = describeFields(cmd.op.input);
|
|
615
|
+
const byKey = new Map(fields.map((f) => [f.key, f]));
|
|
616
|
+
const switchNames = /* @__PURE__ */ new Set([
|
|
617
|
+
...Object.keys(cli.presets ?? {}),
|
|
618
|
+
...Object.keys(cli.routes ?? {})
|
|
619
|
+
]);
|
|
620
|
+
const raw = {};
|
|
621
|
+
const parsed = { raw, switches: [] };
|
|
622
|
+
const queue = [...cmd.positionals];
|
|
623
|
+
const label = cmd.words.join(" ");
|
|
624
|
+
const pushArray = (key, value) => {
|
|
625
|
+
const parts = value.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
626
|
+
raw[key] = [...Array.isArray(raw[key]) ? raw[key] : [], ...parts];
|
|
627
|
+
};
|
|
628
|
+
for (let i = 0; i < args.length; i++) {
|
|
629
|
+
const tok = args[i];
|
|
630
|
+
if (tok.startsWith("--")) {
|
|
631
|
+
let name = tok.slice(2);
|
|
632
|
+
let inline;
|
|
633
|
+
const eq = name.indexOf("=");
|
|
634
|
+
if (eq >= 0) {
|
|
635
|
+
inline = name.slice(eq + 1);
|
|
636
|
+
name = name.slice(0, eq);
|
|
637
|
+
}
|
|
638
|
+
if (switchNames.has(name)) {
|
|
639
|
+
parsed.switches.push(name);
|
|
640
|
+
continue;
|
|
641
|
+
}
|
|
642
|
+
if (cli.fileInput && name === "file") {
|
|
643
|
+
const v = inline ?? args[++i];
|
|
644
|
+
if (v === void 0) throw usage(`${label}: flag --file expects a path`);
|
|
645
|
+
parsed.file = v;
|
|
646
|
+
continue;
|
|
647
|
+
}
|
|
648
|
+
const key = flagToKey(name, fields, cli.aliases);
|
|
649
|
+
if (!key) throw usage(`${label}: unknown flag --${name}`);
|
|
650
|
+
const spec = byKey.get(key);
|
|
651
|
+
if (spec.kind === "boolean") {
|
|
652
|
+
raw[key] = inline === void 0 ? true : inline !== "false";
|
|
653
|
+
continue;
|
|
654
|
+
}
|
|
655
|
+
const value = inline ?? args[++i];
|
|
656
|
+
if (value === void 0) throw usage(`${label}: flag --${name} expects a value`);
|
|
657
|
+
if (spec.kind === "string[]") pushArray(key, value);
|
|
658
|
+
else if (spec.kind === "json") raw[key] = parseJsonFlag(label, name, value);
|
|
659
|
+
else raw[key] = value;
|
|
660
|
+
continue;
|
|
661
|
+
}
|
|
662
|
+
const posKey = queue.shift();
|
|
663
|
+
if (!posKey) throw usage(`${label}: unexpected argument "${tok}"`);
|
|
664
|
+
if (byKey.get(posKey)?.kind === "string[]") pushArray(posKey, tok);
|
|
665
|
+
else raw[posKey] = tok;
|
|
666
|
+
}
|
|
667
|
+
return parsed;
|
|
668
|
+
}
|
|
669
|
+
function parseJsonFlag(label, flag, value) {
|
|
670
|
+
try {
|
|
671
|
+
return JSON.parse(value);
|
|
672
|
+
} catch {
|
|
673
|
+
throw usage(`${label}: flag --${flag} expects JSON`);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
function coerce(raw, fields) {
|
|
677
|
+
const out = {};
|
|
678
|
+
for (const f of fields) {
|
|
679
|
+
const v = raw[f.key];
|
|
680
|
+
if (v === void 0) continue;
|
|
681
|
+
if (f.kind === "number" && typeof v === "string") {
|
|
682
|
+
const n = Number(v);
|
|
683
|
+
out[f.key] = Number.isNaN(n) ? v : n;
|
|
684
|
+
} else {
|
|
685
|
+
out[f.key] = v;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
return out;
|
|
689
|
+
}
|
|
690
|
+
function usage(message, hint) {
|
|
691
|
+
return new MagicError("usage", message, hint ? { hint } : {});
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
// ../kit/src/cli.ts
|
|
695
|
+
import { readFileSync as readFileSync3 } from "node:fs";
|
|
696
|
+
|
|
697
|
+
// ../kit/src/credentials.ts
|
|
698
|
+
function manualItem(product) {
|
|
699
|
+
return `${product}:manual`;
|
|
700
|
+
}
|
|
701
|
+
function maskKey(key, prefix) {
|
|
702
|
+
const m = new RegExp(`^${escapeRegExp(prefix)}([^_]+)_(.+)$`).exec(key);
|
|
703
|
+
if (!m) return `${prefix}\u2026`;
|
|
704
|
+
return `${prefix}${m[1]}_\u2026${m[2].slice(-4)}`;
|
|
705
|
+
}
|
|
706
|
+
function escapeRegExp(s) {
|
|
707
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
708
|
+
}
|
|
709
|
+
function parseStoredSession(product, value) {
|
|
710
|
+
try {
|
|
711
|
+
const parsed = JSON.parse(value);
|
|
712
|
+
if (parsed && typeof parsed.key === "string" && parsed.key.length > 0) {
|
|
713
|
+
return { ...parsed, product, key: parsed.key, source: parsed.source ?? "manual" };
|
|
714
|
+
}
|
|
715
|
+
} catch {
|
|
716
|
+
if (value.trim().length > 0) return { product, key: value.trim(), source: "manual" };
|
|
717
|
+
}
|
|
718
|
+
return null;
|
|
719
|
+
}
|
|
720
|
+
function loginHint(spec) {
|
|
721
|
+
const where = spec.keyHelp ? ` (${spec.keyHelp})` : "";
|
|
722
|
+
return `Set ${productEnvVar(spec.id, "KEY")} or run \`magic login --product-key\` with a ${spec.credential}\u2026 key${where}.`;
|
|
723
|
+
}
|
|
724
|
+
function checkPrefix(spec, key) {
|
|
725
|
+
if (!key.startsWith(spec.credential)) {
|
|
726
|
+
throw new MagicError(
|
|
727
|
+
"auth_invalid",
|
|
728
|
+
`${spec.title} key must start with "${spec.credential}".`,
|
|
729
|
+
{
|
|
730
|
+
product: spec.id,
|
|
731
|
+
hint: loginHint(spec)
|
|
732
|
+
}
|
|
733
|
+
);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
async function resolveCredential(spec, opts) {
|
|
737
|
+
const env = opts.env ?? process.env;
|
|
738
|
+
const envKey = env[productEnvVar(spec.id, "KEY")]?.trim();
|
|
739
|
+
if (envKey) {
|
|
740
|
+
checkPrefix(spec, envKey);
|
|
741
|
+
return { product: spec.id, key: envKey, source: "env" };
|
|
742
|
+
}
|
|
743
|
+
const stored = await opts.store.get(manualItem(spec.id));
|
|
744
|
+
const session = stored ? parseStoredSession(spec.id, stored) : null;
|
|
745
|
+
if (!session) {
|
|
746
|
+
throw new MagicError("not_logged_in", `No ${spec.title} key found.`, {
|
|
747
|
+
product: spec.id,
|
|
748
|
+
hint: loginHint(spec)
|
|
749
|
+
});
|
|
750
|
+
}
|
|
751
|
+
checkPrefix(spec, session.key);
|
|
752
|
+
return session;
|
|
753
|
+
}
|
|
754
|
+
async function saveProductKey(spec, store, key) {
|
|
755
|
+
const trimmed = key.trim();
|
|
756
|
+
checkPrefix(spec, trimmed);
|
|
757
|
+
const session = { product: spec.id, key: trimmed, source: "manual" };
|
|
758
|
+
await store.set(manualItem(spec.id), JSON.stringify(session));
|
|
759
|
+
return session;
|
|
760
|
+
}
|
|
761
|
+
async function forgetProductKey(spec, store) {
|
|
762
|
+
await store.delete(manualItem(spec.id));
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// ../kit/src/context.ts
|
|
766
|
+
function productConfig(product, deps, extra = {}) {
|
|
767
|
+
return loadCliConfig({
|
|
768
|
+
products: [{ id: product.id, apiUrl: product.apiUrl }],
|
|
769
|
+
env: deps.env ?? process.env,
|
|
770
|
+
file: (deps.readConfig ?? (() => readConfigFile()))(),
|
|
771
|
+
...extra
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
async function buildContext(product, config, deps, requiresSession = true) {
|
|
775
|
+
const session = requiresSession ? await resolveCredential(product, { env: deps.env ?? process.env, store: deps.store }) : null;
|
|
776
|
+
return contextFor(product, config, session, deps.signal);
|
|
777
|
+
}
|
|
778
|
+
function contextFor(product, config, session, signal = new AbortController().signal) {
|
|
779
|
+
return {
|
|
780
|
+
config,
|
|
781
|
+
session,
|
|
782
|
+
client: product.client(session ?? { product: product.id, key: "", source: "env" }, config),
|
|
783
|
+
signal
|
|
784
|
+
};
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
// ../kit/src/cli.ts
|
|
788
|
+
async function runProductCli(product, argv, deps) {
|
|
789
|
+
const io = deps.io ?? processIO;
|
|
790
|
+
let opts = { json: false, yes: false, quiet: false };
|
|
791
|
+
try {
|
|
792
|
+
const parsed = parseGlobals(argv, product.globalFlags, deps.env ?? process.env);
|
|
793
|
+
opts = parsed.opts;
|
|
794
|
+
const { rest, productFlags } = parsed;
|
|
795
|
+
const extra = rest[0] ? product.commands?.[rest[0]] : void 0;
|
|
796
|
+
if (extra) {
|
|
797
|
+
if (opts.workspace) throw usage(`--workspace is not supported by ${rest[0]}`);
|
|
798
|
+
return await extra.run(rest.slice(1));
|
|
799
|
+
}
|
|
800
|
+
const ops = await product.load();
|
|
801
|
+
assertRegistry(product.id, ops);
|
|
802
|
+
const cmds = buildCommands(ops.filter((op2) => onSurface(op2, "cli")));
|
|
803
|
+
if (rest.length === 0 || ["help", "--help", "-h"].includes(rest[0])) {
|
|
804
|
+
io.err(helpText(product, cmds));
|
|
805
|
+
return 0;
|
|
806
|
+
}
|
|
807
|
+
const matched = matchCommand(cmds, rest);
|
|
808
|
+
if (!matched) {
|
|
809
|
+
throw usage(
|
|
810
|
+
`unknown command: ${product.id} ${rest.join(" ")}`,
|
|
811
|
+
`Run \`magic ${product.id} --help\` for usage.`
|
|
812
|
+
);
|
|
813
|
+
}
|
|
814
|
+
let { cmd } = matched;
|
|
815
|
+
let args = matched.args;
|
|
816
|
+
let parsedArgs = parseCommandArgs(cmd, args);
|
|
817
|
+
const routes = cmd.op.meta?.cli?.routes ?? {};
|
|
818
|
+
const route = parsedArgs.switches.find((s) => routes[s]);
|
|
819
|
+
if (route) {
|
|
820
|
+
const target = cmds.find((c) => c.op.name === routes[route]);
|
|
821
|
+
if (!target) throw new Error(`route --${route} names a missing operation`);
|
|
822
|
+
cmd = target;
|
|
823
|
+
args = args.filter((a) => a !== `--${route}`);
|
|
824
|
+
parsedArgs = parseCommandArgs(cmd, args);
|
|
825
|
+
}
|
|
826
|
+
const input = buildInput(cmd, parsedArgs, productFlags, opts);
|
|
827
|
+
const op = cmd.op;
|
|
828
|
+
const config = productConfig(product, deps, {
|
|
829
|
+
output: {
|
|
830
|
+
json: opts.json,
|
|
831
|
+
quiet: opts.quiet,
|
|
832
|
+
...opts.color === false ? { color: false } : {}
|
|
833
|
+
},
|
|
834
|
+
...opts.workspace ? { workspace: opts.workspace } : {}
|
|
835
|
+
});
|
|
836
|
+
if (op.meta?.destructive) {
|
|
837
|
+
if (!opts.yes) {
|
|
838
|
+
throw usage(`${cmd.words.join(" ")} is destructive \u2014 re-run with --yes to confirm.`);
|
|
839
|
+
}
|
|
840
|
+
if (!config.allowDestructive) throw destructiveDisabled(product.id);
|
|
841
|
+
}
|
|
842
|
+
const ctx = await buildContext(product, config, deps, op.meta?.requiresSession !== false);
|
|
843
|
+
const out = await op.run(input, ctx);
|
|
844
|
+
render(out, opts, io);
|
|
845
|
+
return 0;
|
|
846
|
+
} catch (err) {
|
|
847
|
+
return renderError(toMagicError(err, { product: product.id }), opts, io);
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
function destructiveDisabled(product) {
|
|
851
|
+
return new MagicError("destructive_action_disabled", "This action is destructive and disabled.", {
|
|
852
|
+
product,
|
|
853
|
+
hint: "Set MAGIC_ALLOW_DESTRUCTIVE=1 to enable destructive operations."
|
|
854
|
+
});
|
|
855
|
+
}
|
|
856
|
+
function buildInput(cmd, parsed, productFlags, opts) {
|
|
857
|
+
const cli = cmd.op.meta?.cli ?? {};
|
|
858
|
+
const fields = describeFields(cmd.op.input);
|
|
859
|
+
const has = (key) => fields.some((f) => f.key === key);
|
|
860
|
+
const raw = parsed.raw;
|
|
861
|
+
for (const s of parsed.switches) Object.assign(raw, cli.presets?.[s] ?? {});
|
|
862
|
+
if (cli.fileInput && parsed.file !== void 0)
|
|
863
|
+
raw[cli.fileInput] = loadFileInput(parsed.file, cli.fileInput);
|
|
864
|
+
for (const [key, value] of Object.entries(productFlags)) {
|
|
865
|
+
if (has(key) && raw[key] === void 0) raw[key] = value;
|
|
866
|
+
}
|
|
867
|
+
if (opts.workspace) {
|
|
868
|
+
if (!has("workspace")) {
|
|
869
|
+
throw usage(`--workspace is not supported by ${cmd.words.join(" ")}`);
|
|
870
|
+
}
|
|
871
|
+
if (raw.workspace === void 0) raw.workspace = opts.workspace;
|
|
872
|
+
}
|
|
873
|
+
const result = cmd.op.input.safeParse(coerce(raw, fields));
|
|
874
|
+
if (!result.success) {
|
|
875
|
+
const first = result.error.issues[0];
|
|
876
|
+
const where = first?.path.length ? ` (${first.path.join(".")})` : "";
|
|
877
|
+
throw usage(
|
|
878
|
+
`${cmd.words.join(" ")}: invalid arguments${where}: ${first?.message ?? "validation failed"}`
|
|
879
|
+
);
|
|
880
|
+
}
|
|
881
|
+
return result.data;
|
|
882
|
+
}
|
|
883
|
+
function loadFileInput(path, key) {
|
|
884
|
+
let text;
|
|
885
|
+
try {
|
|
886
|
+
text = readFileSync3(path, "utf8");
|
|
887
|
+
} catch (err) {
|
|
888
|
+
throw usage(`cannot read --file ${path}: ${err.message}`);
|
|
889
|
+
}
|
|
890
|
+
let parsed;
|
|
891
|
+
try {
|
|
892
|
+
parsed = JSON.parse(text);
|
|
893
|
+
} catch (err) {
|
|
894
|
+
throw usage(`--file ${path} is not valid JSON: ${err.message}`);
|
|
895
|
+
}
|
|
896
|
+
if (Array.isArray(parsed)) return parsed;
|
|
897
|
+
const inner = parsed && typeof parsed === "object" ? parsed[key] : void 0;
|
|
898
|
+
if (Array.isArray(inner)) return inner;
|
|
899
|
+
throw usage(`--file ${path} must be a JSON array (or { "${key}": [...] }).`);
|
|
900
|
+
}
|
|
901
|
+
function helpText(product, cmds) {
|
|
902
|
+
const lines = [
|
|
903
|
+
`magic ${product.id} \u2014 ${product.title}`,
|
|
904
|
+
"",
|
|
905
|
+
`Usage: magic ${product.id} <command> [args] [--flags]`,
|
|
906
|
+
"",
|
|
907
|
+
"Commands:"
|
|
908
|
+
];
|
|
909
|
+
const rows = cmds.map((c) => {
|
|
910
|
+
const last = c.words[c.words.length - 1];
|
|
911
|
+
const verb = c.verbAliases.length ? `${last} (${c.verbAliases.join(", ")})` : last;
|
|
912
|
+
const name = [...c.words.slice(0, -1), verb].join(" ");
|
|
913
|
+
const pos = c.positionals.map((p) => `<${p}>`).join(" ");
|
|
914
|
+
return [`${name} ${pos}`.trim(), summaryOf(c.op)];
|
|
915
|
+
});
|
|
916
|
+
for (const [name, cmd] of Object.entries(product.commands ?? {})) rows.push([name, cmd.summary]);
|
|
917
|
+
const width = Math.min(40, Math.max(...rows.map(([n]) => n.length)) + 2);
|
|
918
|
+
for (const [name, summary] of rows) lines.push(` ${name.padEnd(width)}${summary}`);
|
|
919
|
+
lines.push("");
|
|
920
|
+
const pf = (product.globalFlags ?? []).map((f) => ` --${f} <${f}>`).join("");
|
|
921
|
+
lines.push(`Flags: --json --yes/-y --no-color --quiet/-q --workspace <handle>${pf}`);
|
|
922
|
+
return `${lines.join("\n")}
|
|
923
|
+
`;
|
|
924
|
+
}
|
|
925
|
+
function summaryOf(op) {
|
|
926
|
+
return op.meta?.destructive ? `${op.summary} [destructive]` : op.summary;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
// ../kit/src/read.ts
|
|
930
|
+
function rec(v) {
|
|
931
|
+
return v && typeof v === "object" ? v : {};
|
|
932
|
+
}
|
|
933
|
+
function readStr(v) {
|
|
934
|
+
return typeof v === "string" && v.length > 0 ? v : void 0;
|
|
935
|
+
}
|
|
936
|
+
function readField(obj, key) {
|
|
937
|
+
return obj && typeof obj === "object" && key in obj ? obj[key] : void 0;
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
// ../kit/src/doctor.ts
|
|
941
|
+
async function runDoctorChecks(checks, ctx, product = "magic") {
|
|
942
|
+
const results = [];
|
|
943
|
+
for (const check of checks) {
|
|
944
|
+
try {
|
|
945
|
+
results.push({ name: check.name, product, ...await check.run(ctx) });
|
|
946
|
+
} catch (err) {
|
|
947
|
+
const e = toMagicError(err, { product });
|
|
948
|
+
results.push({
|
|
949
|
+
name: check.name,
|
|
950
|
+
product,
|
|
951
|
+
ok: false,
|
|
952
|
+
detail: e.hint ? `${e.message} \u2014 ${e.hint}` : e.message,
|
|
953
|
+
code: e.code
|
|
954
|
+
});
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
return results;
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
// ../kit/src/mcp.ts
|
|
961
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
962
|
+
import { z } from "zod";
|
|
963
|
+
function createMcpServer(opts) {
|
|
964
|
+
const products = opts.products.filter((p) => {
|
|
965
|
+
try {
|
|
966
|
+
registerProduct(new McpServer({ name: opts.name, version: opts.version }), p);
|
|
967
|
+
return true;
|
|
968
|
+
} catch (err) {
|
|
969
|
+
if (!opts.onSkip) throw err;
|
|
970
|
+
opts.onSkip(p.module.id, err);
|
|
971
|
+
return false;
|
|
972
|
+
}
|
|
973
|
+
});
|
|
974
|
+
const sections = products.flatMap((p) => {
|
|
975
|
+
const text = p.module.mcp?.instructions?.(opts.version);
|
|
976
|
+
if (!text) return [];
|
|
977
|
+
return [opts.preamble ? `## ${p.module.title}
|
|
978
|
+
|
|
979
|
+
${text}` : text];
|
|
980
|
+
});
|
|
981
|
+
const instructions = [...opts.preamble ? [opts.preamble] : [], ...sections].join("\n\n");
|
|
982
|
+
const server = new McpServer(
|
|
983
|
+
{ name: opts.name, version: opts.version },
|
|
984
|
+
instructions ? { instructions } : {}
|
|
985
|
+
);
|
|
986
|
+
for (const p of products) registerProduct(server, p);
|
|
987
|
+
return server;
|
|
988
|
+
}
|
|
989
|
+
function registerProduct(server, p) {
|
|
990
|
+
for (const op of p.operations) {
|
|
991
|
+
if (onSurface(op, "mcp")) registerTool(server, p.module.id, op, p.context);
|
|
992
|
+
}
|
|
993
|
+
p.module.mcp?.register?.(server, p.context);
|
|
994
|
+
}
|
|
995
|
+
function toolInputShape(op) {
|
|
996
|
+
const shape = op.input.shape ?? {};
|
|
997
|
+
if (!op.meta?.destructive) return shape;
|
|
998
|
+
return {
|
|
999
|
+
...shape,
|
|
1000
|
+
yes: z.boolean().optional().describe("Confirm this destructive action (also needs MAGIC_ALLOW_DESTRUCTIVE=1).")
|
|
1001
|
+
};
|
|
1002
|
+
}
|
|
1003
|
+
async function callTool(product, op, context, args) {
|
|
1004
|
+
try {
|
|
1005
|
+
const ctx = await context();
|
|
1006
|
+
const { yes, ...rest } = args ?? {};
|
|
1007
|
+
if (op.meta?.destructive) {
|
|
1008
|
+
if (!ctx.config.allowDestructive) throw destructiveDisabled(product);
|
|
1009
|
+
if (yes !== true) {
|
|
1010
|
+
throw new MagicError("destructive_action_disabled", "This action is destructive.", {
|
|
1011
|
+
product,
|
|
1012
|
+
hint: "Pass yes: true to confirm it."
|
|
1013
|
+
});
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
const parsed = op.input.safeParse(op.meta?.destructive ? rest : args ?? {});
|
|
1017
|
+
if (!parsed.success) throw invalidInput(product, parsed.error);
|
|
1018
|
+
const result = await op.run(parsed.data, ctx);
|
|
1019
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
1020
|
+
} catch (err) {
|
|
1021
|
+
return toToolError(toMagicError(err, { product }));
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
function invalidInput(product, error) {
|
|
1025
|
+
const first = error.issues[0];
|
|
1026
|
+
const field = first?.path.join(".") ?? "";
|
|
1027
|
+
const message = first?.message ?? "Invalid input.";
|
|
1028
|
+
return new MagicError("validation_failed", field ? `${field}: ${message}` : message, {
|
|
1029
|
+
product,
|
|
1030
|
+
...field ? { details: { field } } : {}
|
|
1031
|
+
});
|
|
1032
|
+
}
|
|
1033
|
+
function registerTool(server, product, op, context) {
|
|
1034
|
+
const description = op.detail ? `${op.summary}
|
|
1035
|
+
|
|
1036
|
+
${op.detail}` : op.summary;
|
|
1037
|
+
server.registerTool(
|
|
1038
|
+
toolName(op),
|
|
1039
|
+
{ description, inputSchema: toolInputShape(op) },
|
|
1040
|
+
async (args) => callTool(product, op, context, args)
|
|
1041
|
+
);
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
// ../kit/src/cache.ts
|
|
1045
|
+
var TtlCache = class {
|
|
1046
|
+
constructor(ttlMs, now = () => Date.now()) {
|
|
1047
|
+
this.ttlMs = ttlMs;
|
|
1048
|
+
this.now = now;
|
|
1049
|
+
}
|
|
1050
|
+
entries = /* @__PURE__ */ new Map();
|
|
1051
|
+
get(key) {
|
|
1052
|
+
const hit = this.entries.get(key);
|
|
1053
|
+
if (!hit) return void 0;
|
|
1054
|
+
if (hit.expiresAt > this.now()) return hit.value;
|
|
1055
|
+
this.entries.delete(key);
|
|
1056
|
+
return void 0;
|
|
1057
|
+
}
|
|
1058
|
+
set(key, value) {
|
|
1059
|
+
this.entries.set(key, { value, expiresAt: this.now() + this.ttlMs });
|
|
1060
|
+
}
|
|
1061
|
+
delete(key) {
|
|
1062
|
+
this.entries.delete(key);
|
|
1063
|
+
}
|
|
1064
|
+
clear() {
|
|
1065
|
+
this.entries.clear();
|
|
1066
|
+
}
|
|
1067
|
+
/** Return the live entry, or load, store and return a fresh one. */
|
|
1068
|
+
async getOrLoad(key, load) {
|
|
1069
|
+
const hit = this.get(key);
|
|
1070
|
+
if (hit !== void 0) return hit;
|
|
1071
|
+
const value = await load();
|
|
1072
|
+
this.set(key, value);
|
|
1073
|
+
return value;
|
|
1074
|
+
}
|
|
1075
|
+
};
|
|
1076
|
+
|
|
1077
|
+
// ../kit/src/version.ts
|
|
1078
|
+
import { readFileSync as readFileSync4 } from "node:fs";
|
|
1079
|
+
import { dirname as dirname2, join as join4 } from "node:path";
|
|
1080
|
+
import { fileURLToPath } from "node:url";
|
|
1081
|
+
function readPackageVersion(moduleUrl) {
|
|
1082
|
+
let dir = dirname2(fileURLToPath(moduleUrl));
|
|
1083
|
+
for (let i = 0; i < 8; i++) {
|
|
1084
|
+
try {
|
|
1085
|
+
const pkg = JSON.parse(readFileSync4(join4(dir, "package.json"), "utf8"));
|
|
1086
|
+
if (typeof pkg.version === "string") return pkg.version;
|
|
1087
|
+
} catch {
|
|
1088
|
+
}
|
|
1089
|
+
const parent = dirname2(dir);
|
|
1090
|
+
if (parent === dir) break;
|
|
1091
|
+
dir = parent;
|
|
1092
|
+
}
|
|
1093
|
+
return "0.0.0";
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
export {
|
|
1097
|
+
MagicError,
|
|
1098
|
+
exitCodeFor,
|
|
1099
|
+
mapHttpError,
|
|
1100
|
+
toMagicError,
|
|
1101
|
+
productEnvVar,
|
|
1102
|
+
configFilePath,
|
|
1103
|
+
readConfigFile,
|
|
1104
|
+
manualItem,
|
|
1105
|
+
maskKey,
|
|
1106
|
+
resolveCredential,
|
|
1107
|
+
saveProductKey,
|
|
1108
|
+
forgetProductKey,
|
|
1109
|
+
createCredentialStore,
|
|
1110
|
+
processIO,
|
|
1111
|
+
jsonMode,
|
|
1112
|
+
render,
|
|
1113
|
+
cell,
|
|
1114
|
+
renderError,
|
|
1115
|
+
parseGlobals,
|
|
1116
|
+
usage,
|
|
1117
|
+
productConfig,
|
|
1118
|
+
buildContext,
|
|
1119
|
+
contextFor,
|
|
1120
|
+
runDoctorChecks,
|
|
1121
|
+
runProductCli,
|
|
1122
|
+
createMcpServer,
|
|
1123
|
+
TtlCache,
|
|
1124
|
+
rec,
|
|
1125
|
+
readStr,
|
|
1126
|
+
readField,
|
|
1127
|
+
readPackageVersion
|
|
1128
|
+
};
|