@mushi-mushi/cli 0.22.1 → 0.23.1
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/README.md +28 -2
- package/dist/{chunk-JAUKBLPK.js → chunk-AIZRI665.js} +102 -2
- package/dist/chunk-I4UK3WTN.js +6 -0
- package/dist/{chunk-R2HPR2FV.js → chunk-M6L22NJU.js} +534 -27
- package/dist/chunk-NZ64LBLU.js +437 -0
- package/dist/{connect-JIEIWTF3.js → connect-5VPUYE6H.js} +1 -1
- package/dist/{connect-2RM43XHO.js → connect-IWFKKXGZ.js} +10 -7
- package/dist/detect.d.ts +1 -1
- package/dist/detect.js +1 -1
- package/dist/index.js +269 -423
- package/dist/init.js +106 -255
- package/dist/version.js +1 -1
- package/package.json +12 -14
- package/dist/chunk-XEXYGDQV.js +0 -137
- package/dist/chunk-YK4LF7AM.js +0 -6
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
+
CLOUD_API_ENDPOINT,
|
|
3
4
|
assertEndpoint,
|
|
4
5
|
normalizeEndpoint
|
|
5
6
|
} from "./chunk-LDPFTQLE.js";
|
|
6
7
|
|
|
7
8
|
// src/connect.ts
|
|
8
9
|
import { appendFile, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
|
|
9
|
-
import { join as
|
|
10
|
+
import { join as join4, resolve } from "path";
|
|
10
11
|
|
|
11
12
|
// src/config.ts
|
|
13
|
+
import { randomUUID } from "crypto";
|
|
12
14
|
import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from "fs";
|
|
13
15
|
import { homedir } from "os";
|
|
14
16
|
import { dirname, join } from "path";
|
|
@@ -49,6 +51,18 @@ function loadConfig(path = CONFIG_PATH) {
|
|
|
49
51
|
};
|
|
50
52
|
return { ...file, ...fromEnv };
|
|
51
53
|
}
|
|
54
|
+
function ensureClientId(path = CONFIG_PATH) {
|
|
55
|
+
const existing = loadConfig(path);
|
|
56
|
+
if (existing.clientId && /^[A-Za-z0-9_-]{8,64}$/.test(existing.clientId)) {
|
|
57
|
+
return existing.clientId;
|
|
58
|
+
}
|
|
59
|
+
const clientId = `cli_${randomUUID().replace(/-/g, "")}`;
|
|
60
|
+
try {
|
|
61
|
+
saveConfig({ ...existing, clientId }, path);
|
|
62
|
+
} catch {
|
|
63
|
+
}
|
|
64
|
+
return clientId;
|
|
65
|
+
}
|
|
52
66
|
function saveConfig(config, path = CONFIG_PATH) {
|
|
53
67
|
const dir = dirname(path);
|
|
54
68
|
if (!existsSync(dir)) {
|
|
@@ -101,9 +115,386 @@ function tightenDirPermissions(path) {
|
|
|
101
115
|
}
|
|
102
116
|
}
|
|
103
117
|
|
|
118
|
+
// src/version.ts
|
|
119
|
+
var MUSHI_CLI_VERSION = true ? "0.23.1" : "0.0.0-dev";
|
|
120
|
+
|
|
121
|
+
// src/errors.ts
|
|
122
|
+
var EXIT_CODE_MAP = {
|
|
123
|
+
E_AUTH_MISSING: 2,
|
|
124
|
+
E_AUTH_INVALID: 2,
|
|
125
|
+
E_PROJECT_MISSING: 2,
|
|
126
|
+
E_ENDPOINT_INVALID: 2,
|
|
127
|
+
E_INVALID_INPUT: 2,
|
|
128
|
+
E_NOT_INTERACTIVE: 2,
|
|
129
|
+
E_NETWORK: 3,
|
|
130
|
+
E_TIMEOUT: 3,
|
|
131
|
+
E_API_ERROR: 3,
|
|
132
|
+
E_RATE_LIMITED: 3,
|
|
133
|
+
E_FILE_NOT_FOUND: 1,
|
|
134
|
+
E_FILE_PERMISSION: 1,
|
|
135
|
+
E_FRESHNESS_STALE: 1,
|
|
136
|
+
E_INTERRUPTED: 130,
|
|
137
|
+
// 128 + SIGINT (2). POSIX convention.
|
|
138
|
+
E_INTERNAL: 1
|
|
139
|
+
};
|
|
140
|
+
var MushiCliError = class extends Error {
|
|
141
|
+
code;
|
|
142
|
+
hint;
|
|
143
|
+
cause;
|
|
144
|
+
constructor(code, message, hint, cause) {
|
|
145
|
+
super(message);
|
|
146
|
+
this.name = "MushiCliError";
|
|
147
|
+
this.code = code;
|
|
148
|
+
if (hint) this.hint = hint;
|
|
149
|
+
if (cause !== void 0) this.cause = cause;
|
|
150
|
+
}
|
|
151
|
+
/** Exit code POSIX-aware shell scripts should branch on. */
|
|
152
|
+
get exitCode() {
|
|
153
|
+
return EXIT_CODE_MAP[this.code];
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Format for `--json` output. Used by the JSON writer when a CLI
|
|
157
|
+
* subcommand was invoked with `--json` and an error escapes.
|
|
158
|
+
*/
|
|
159
|
+
toJSON() {
|
|
160
|
+
return {
|
|
161
|
+
error: {
|
|
162
|
+
code: this.code,
|
|
163
|
+
message: this.message,
|
|
164
|
+
...this.hint ? { hint: this.hint } : {}
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
function printAndExit(err, asJson = false) {
|
|
170
|
+
const cliErr = err instanceof MushiCliError ? err : new MushiCliError(
|
|
171
|
+
"E_INTERNAL",
|
|
172
|
+
err instanceof Error ? err.message : String(err),
|
|
173
|
+
"this is unexpected \u2014 re-run with MUSHI_DEBUG=1 and report at https://github.com/kensaurus/mushi-mushi/issues",
|
|
174
|
+
err
|
|
175
|
+
);
|
|
176
|
+
if (asJson) {
|
|
177
|
+
process.stderr.write(JSON.stringify(cliErr.toJSON()) + "\n");
|
|
178
|
+
} else {
|
|
179
|
+
process.stderr.write(`error [${cliErr.code}]: ${cliErr.message}
|
|
180
|
+
`);
|
|
181
|
+
if (cliErr.hint) process.stderr.write(` \u2192 fix: ${cliErr.hint}
|
|
182
|
+
`);
|
|
183
|
+
}
|
|
184
|
+
if (process.env.MUSHI_CLI_JSON_LOG === "1" || process.env.CI === "true") {
|
|
185
|
+
process.stderr.write(
|
|
186
|
+
JSON.stringify({
|
|
187
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
188
|
+
level: "error",
|
|
189
|
+
scope: "mushi:cli",
|
|
190
|
+
...cliErr.toJSON(),
|
|
191
|
+
exitCode: cliErr.exitCode
|
|
192
|
+
}) + "\n"
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
process.exit(cliErr.exitCode);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// src/signals.ts
|
|
199
|
+
var installed = false;
|
|
200
|
+
var activeController = null;
|
|
201
|
+
function installSignalHandlers() {
|
|
202
|
+
if (installed) return;
|
|
203
|
+
installed = true;
|
|
204
|
+
const abortAndExit = (signal) => {
|
|
205
|
+
activeController?.abort(
|
|
206
|
+
new MushiCliError(
|
|
207
|
+
"E_INTERRUPTED",
|
|
208
|
+
`aborted by ${signal}`,
|
|
209
|
+
"partial state on the server is safe to retry \u2014 re-run the same command"
|
|
210
|
+
)
|
|
211
|
+
);
|
|
212
|
+
const code = signal === "SIGINT" ? 130 : signal === "SIGTERM" ? 143 : 1;
|
|
213
|
+
process.nextTick(() => {
|
|
214
|
+
process.exit(code);
|
|
215
|
+
});
|
|
216
|
+
};
|
|
217
|
+
process.on("SIGINT", () => abortAndExit("SIGINT"));
|
|
218
|
+
process.on("SIGTERM", () => abortAndExit("SIGTERM"));
|
|
219
|
+
}
|
|
220
|
+
function getAbortSignal(external) {
|
|
221
|
+
if (external) return external.signal;
|
|
222
|
+
if (!activeController || activeController.signal.aborted) {
|
|
223
|
+
activeController = new AbortController();
|
|
224
|
+
}
|
|
225
|
+
return activeController.signal;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// src/console-url.ts
|
|
229
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
230
|
+
import { dirname as dirname2, join as join2 } from "path";
|
|
231
|
+
var HOSTED_CONSOLE_BASE = "https://kensaur.us/mushi-mushi/admin";
|
|
232
|
+
var LOCAL_ADMIN_URL = "http://localhost:6464";
|
|
233
|
+
var LOCAL_PROBE_MS = 800;
|
|
234
|
+
function normalizeConsoleBase(url) {
|
|
235
|
+
const trimmed = url.trim();
|
|
236
|
+
let end = trimmed.length;
|
|
237
|
+
while (end > 0 && trimmed.charCodeAt(end - 1) === 47) end--;
|
|
238
|
+
return trimmed.slice(0, end);
|
|
239
|
+
}
|
|
240
|
+
function consoleUrl(base, route) {
|
|
241
|
+
const normalizedRoute = route.startsWith("/") ? route : `/${route}`;
|
|
242
|
+
return `${normalizeConsoleBase(base)}${normalizedRoute}`;
|
|
243
|
+
}
|
|
244
|
+
function resolveConsoleUrlFromEnv() {
|
|
245
|
+
const raw = process.env["MUSHI_CONSOLE_URL"]?.trim();
|
|
246
|
+
return raw ? normalizeConsoleBase(raw) : void 0;
|
|
247
|
+
}
|
|
248
|
+
function isInsideMushiMonorepo(cwd) {
|
|
249
|
+
let dir = cwd;
|
|
250
|
+
for (let depth = 0; depth < 14; depth += 1) {
|
|
251
|
+
const pkgPath = join2(dir, "package.json");
|
|
252
|
+
if (existsSync2(pkgPath)) {
|
|
253
|
+
try {
|
|
254
|
+
const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
|
|
255
|
+
if (pkg.name === "mushi-mushi") return true;
|
|
256
|
+
} catch {
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
const parent = dirname2(dir);
|
|
260
|
+
if (parent === dir) break;
|
|
261
|
+
dir = parent;
|
|
262
|
+
}
|
|
263
|
+
return false;
|
|
264
|
+
}
|
|
265
|
+
async function probeLocalAdminConsole() {
|
|
266
|
+
try {
|
|
267
|
+
const controller = new AbortController();
|
|
268
|
+
const timer = setTimeout(() => controller.abort(), LOCAL_PROBE_MS);
|
|
269
|
+
const res = await fetch(LOCAL_ADMIN_URL, {
|
|
270
|
+
signal: controller.signal,
|
|
271
|
+
method: "HEAD"
|
|
272
|
+
});
|
|
273
|
+
clearTimeout(timer);
|
|
274
|
+
if (res.ok || res.status === 404 || res.status === 401) {
|
|
275
|
+
return LOCAL_ADMIN_URL;
|
|
276
|
+
}
|
|
277
|
+
} catch {
|
|
278
|
+
}
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
function resolveConsoleUrlSync(cwd = process.cwd()) {
|
|
282
|
+
const fromEnv = resolveConsoleUrlFromEnv();
|
|
283
|
+
if (fromEnv) return fromEnv;
|
|
284
|
+
const config = loadConfig();
|
|
285
|
+
if (config.consoleUrl) return normalizeConsoleBase(config.consoleUrl);
|
|
286
|
+
if (isInsideMushiMonorepo(cwd)) return LOCAL_ADMIN_URL;
|
|
287
|
+
return HOSTED_CONSOLE_BASE;
|
|
288
|
+
}
|
|
289
|
+
async function resolveConsoleUrl(options) {
|
|
290
|
+
const fromEnv = resolveConsoleUrlFromEnv();
|
|
291
|
+
if (fromEnv) return fromEnv;
|
|
292
|
+
const config = loadConfig();
|
|
293
|
+
if (config.consoleUrl) return normalizeConsoleBase(config.consoleUrl);
|
|
294
|
+
const local = await probeLocalAdminConsole();
|
|
295
|
+
if (local) return local;
|
|
296
|
+
const cwd = options?.cwd ?? process.cwd();
|
|
297
|
+
if (isInsideMushiMonorepo(cwd)) return LOCAL_ADMIN_URL;
|
|
298
|
+
return HOSTED_CONSOLE_BASE;
|
|
299
|
+
}
|
|
300
|
+
function projectIdHint(base) {
|
|
301
|
+
return `Where to find it: ${consoleUrl(base, "/projects")} \u2192 select your project \u2192 click the UUID chip to copy (or copy it from the panel right after you create the project).`;
|
|
302
|
+
}
|
|
303
|
+
function apiKeyHint(base) {
|
|
304
|
+
return `Where to find it: ${consoleUrl(base, "/onboarding?tab=verify")} \u2192 Generate API key (SDK ingest needs report:write \u2014 not Settings \u2192 BYOK keys). Treat it like a password \u2014 env file only, never commit.`;
|
|
305
|
+
}
|
|
306
|
+
function cliSetupDeepLink(base) {
|
|
307
|
+
return consoleUrl(base, "/onboarding?tab=steps&setup=cli");
|
|
308
|
+
}
|
|
309
|
+
function reportsUrl(base, reportId) {
|
|
310
|
+
return reportId ? consoleUrl(base, `/reports/${reportId}`) : consoleUrl(base, "/reports");
|
|
311
|
+
}
|
|
312
|
+
async function openInBrowser(url) {
|
|
313
|
+
let parsed;
|
|
314
|
+
try {
|
|
315
|
+
parsed = new URL(url);
|
|
316
|
+
} catch {
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
const safeUrl = parsed.toString();
|
|
323
|
+
const { spawn } = await import("child_process");
|
|
324
|
+
const [command, args] = process.platform === "win32" ? (
|
|
325
|
+
// Open via rundll32's FileProtocolHandler rather than `cmd /c start`.
|
|
326
|
+
// `start` is a cmd builtin, so the URL would be re-parsed by cmd.exe and
|
|
327
|
+
// characters like `&` / `|` / `^` could still break out — exactly the
|
|
328
|
+
// injection surface CodeQL flags. rundll32 never invokes a shell: the URL
|
|
329
|
+
// is handed to the default browser as one opaque argument.
|
|
330
|
+
["rundll32", ["url.dll,FileProtocolHandler", safeUrl]]
|
|
331
|
+
) : process.platform === "darwin" ? ["open", [safeUrl]] : ["xdg-open", [safeUrl]];
|
|
332
|
+
await new Promise((resolve2) => {
|
|
333
|
+
try {
|
|
334
|
+
const child = spawn(command, [...args], { stdio: "ignore", shell: false });
|
|
335
|
+
child.on("error", () => resolve2());
|
|
336
|
+
child.on("exit", () => resolve2());
|
|
337
|
+
child.unref();
|
|
338
|
+
} catch {
|
|
339
|
+
resolve2();
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// src/cli-shared.ts
|
|
345
|
+
var UUID_RX = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
346
|
+
function requireUuid(value, label = "id") {
|
|
347
|
+
if (UUID_RX.test(value)) return value;
|
|
348
|
+
throw new MushiCliError(
|
|
349
|
+
"E_INVALID_INPUT",
|
|
350
|
+
`${label} must be a valid UUID`,
|
|
351
|
+
"copy the full id from `mushi reports list` or the console URL"
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
var API_TIMEOUT_MS = 15e3;
|
|
355
|
+
async function probeEndpointHealth(endpoint) {
|
|
356
|
+
const t0 = Date.now();
|
|
357
|
+
const controller = new AbortController();
|
|
358
|
+
const timer = setTimeout(() => controller.abort(), API_TIMEOUT_MS);
|
|
359
|
+
try {
|
|
360
|
+
const res = await fetch(`${endpoint}/health`, { signal: controller.signal });
|
|
361
|
+
clearTimeout(timer);
|
|
362
|
+
const body = res.headers.get("content-type")?.includes("json") ? await res.json().catch(() => ({})) : {};
|
|
363
|
+
return { ok: res.ok, status: res.status, latencyMs: Date.now() - t0, body };
|
|
364
|
+
} catch (err) {
|
|
365
|
+
clearTimeout(timer);
|
|
366
|
+
throw err;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
async function apiCall(path, config, options = {}) {
|
|
370
|
+
const endpoint = config.endpoint;
|
|
371
|
+
if (!endpoint) {
|
|
372
|
+
return {
|
|
373
|
+
ok: false,
|
|
374
|
+
error: {
|
|
375
|
+
code: "NO_ENDPOINT",
|
|
376
|
+
message: "No API endpoint configured.\n Run: mushi login --api-key <key> --endpoint <url> --project-id <id>\n Or: export MUSHI_API_ENDPOINT=https://<project-ref>.supabase.co/functions/v1/api"
|
|
377
|
+
}
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
const controller = new AbortController();
|
|
381
|
+
const timer = setTimeout(() => controller.abort(), API_TIMEOUT_MS);
|
|
382
|
+
const signals = [controller.signal, getAbortSignal()];
|
|
383
|
+
const compositeSignal = AbortSignal.any ? AbortSignal.any(signals) : controller.signal;
|
|
384
|
+
try {
|
|
385
|
+
const res = await fetch(`${endpoint}${path}`, {
|
|
386
|
+
...options,
|
|
387
|
+
signal: compositeSignal,
|
|
388
|
+
headers: {
|
|
389
|
+
"Content-Type": "application/json",
|
|
390
|
+
// `apiKeyAuth` reads X-Mushi-Api-Key; the Authorization header is a
|
|
391
|
+
// fallback accepted by some older middleware. Both are sent.
|
|
392
|
+
"Authorization": `Bearer ${config.apiKey ?? ""}`,
|
|
393
|
+
"X-Mushi-Api-Key": config.apiKey ?? "",
|
|
394
|
+
"X-Mushi-Project": config.projectId ?? "",
|
|
395
|
+
"X-Mushi-Cli-Version": MUSHI_CLI_VERSION,
|
|
396
|
+
...options.headers
|
|
397
|
+
}
|
|
398
|
+
});
|
|
399
|
+
clearTimeout(timer);
|
|
400
|
+
let body;
|
|
401
|
+
const contentType = res.headers.get("content-type") ?? "";
|
|
402
|
+
if (contentType.includes("application/json")) {
|
|
403
|
+
try {
|
|
404
|
+
body = await res.json();
|
|
405
|
+
} catch {
|
|
406
|
+
body = null;
|
|
407
|
+
}
|
|
408
|
+
} else {
|
|
409
|
+
const text = await res.text();
|
|
410
|
+
try {
|
|
411
|
+
body = JSON.parse(text);
|
|
412
|
+
} catch {
|
|
413
|
+
body = {
|
|
414
|
+
ok: false,
|
|
415
|
+
error: {
|
|
416
|
+
code: `HTTP_${res.status}`,
|
|
417
|
+
message: text.trim().slice(0, 300) || `HTTP ${res.status}`
|
|
418
|
+
}
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
if (!res.ok && typeof body === "object" && body !== null && !("ok" in body)) {
|
|
423
|
+
const b = body;
|
|
424
|
+
const nested = b["error"];
|
|
425
|
+
const errCode = nested?.["code"] ?? b["code"] ?? `HTTP_${res.status}`;
|
|
426
|
+
const errMsg = nested?.["message"] ?? b["message"] ?? `Request failed (${res.status})`;
|
|
427
|
+
return {
|
|
428
|
+
ok: false,
|
|
429
|
+
httpStatus: res.status,
|
|
430
|
+
error: { code: errCode, message: errMsg }
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
return body;
|
|
434
|
+
} catch (err) {
|
|
435
|
+
clearTimeout(timer);
|
|
436
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
437
|
+
return {
|
|
438
|
+
ok: false,
|
|
439
|
+
error: {
|
|
440
|
+
code: "TIMEOUT",
|
|
441
|
+
message: `Request timed out after ${API_TIMEOUT_MS / 1e3}s. Check your network or endpoint.`
|
|
442
|
+
}
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
return {
|
|
446
|
+
ok: false,
|
|
447
|
+
error: {
|
|
448
|
+
code: "NETWORK_ERROR",
|
|
449
|
+
message: err instanceof Error ? err.message : String(err)
|
|
450
|
+
}
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
function die(result, exitCode = 1) {
|
|
455
|
+
const { code, message } = result.error;
|
|
456
|
+
const status = result.httpStatus ? ` [${result.httpStatus}]` : "";
|
|
457
|
+
process.stderr.write(`error${status}: ${code} \u2014 ${message}
|
|
458
|
+
`);
|
|
459
|
+
if (code === "INSUFFICIENT_SCOPE") {
|
|
460
|
+
process.stderr.write(
|
|
461
|
+
"\n Your current API key does not have the required scope for this command.\n New keys minted by the wizard already include both scopes. Existing keys\n can be upgraded with:\n\n mushi login --upgrade-scope\n\n Or visit your console \u2192 Projects \u2192 API Keys to mint a new key.\n"
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
process.exit(exitCode);
|
|
465
|
+
}
|
|
466
|
+
function requireConfig(opts = {}) {
|
|
467
|
+
const config = loadConfig();
|
|
468
|
+
if (!config.apiKey) {
|
|
469
|
+
printAndExit(new MushiCliError(
|
|
470
|
+
"E_AUTH_MISSING",
|
|
471
|
+
"API key not configured.",
|
|
472
|
+
"Run: mushi login --api-key <key> Or: export MUSHI_API_KEY=<key>"
|
|
473
|
+
));
|
|
474
|
+
}
|
|
475
|
+
if (!config.endpoint) {
|
|
476
|
+
config.endpoint = process.env.MUSHI_API_ENDPOINT?.trim() || CLOUD_API_ENDPOINT;
|
|
477
|
+
}
|
|
478
|
+
if (opts.needsProject && !config.projectId) {
|
|
479
|
+
printAndExit(new MushiCliError(
|
|
480
|
+
"E_PROJECT_MISSING",
|
|
481
|
+
"Project ID not configured.",
|
|
482
|
+
`Run: mushi login --project-id <uuid> Or find your project ID at ${consoleUrl(resolveConsoleUrlSync(), "/projects")}`
|
|
483
|
+
));
|
|
484
|
+
}
|
|
485
|
+
return config;
|
|
486
|
+
}
|
|
487
|
+
function fmtDate(iso) {
|
|
488
|
+
if (!iso) return "\u2014";
|
|
489
|
+
return new Date(iso).toLocaleString(void 0, { dateStyle: "short", timeStyle: "short" });
|
|
490
|
+
}
|
|
491
|
+
function pad(s, width) {
|
|
492
|
+
return s.length >= width ? s : s + " ".repeat(width - s.length);
|
|
493
|
+
}
|
|
494
|
+
|
|
104
495
|
// src/detect.ts
|
|
105
|
-
import { readFileSync as
|
|
106
|
-
import { join as
|
|
496
|
+
import { readFileSync as readFileSync3, existsSync as existsSync3 } from "fs";
|
|
497
|
+
import { join as join3 } from "path";
|
|
107
498
|
var FRAMEWORKS = {
|
|
108
499
|
next: {
|
|
109
500
|
id: "next",
|
|
@@ -141,6 +532,90 @@ createRoot(document.getElementById('root')!).render(
|
|
|
141
532
|
<App />
|
|
142
533
|
</MushiProvider>
|
|
143
534
|
)`
|
|
535
|
+
},
|
|
536
|
+
cra: {
|
|
537
|
+
id: "cra",
|
|
538
|
+
label: "Create React App",
|
|
539
|
+
packageName: "@mushi-mushi/react",
|
|
540
|
+
needsWebPackage: false,
|
|
541
|
+
// CRA inlines only REACT_APP_*-prefixed vars into process.env at build time.
|
|
542
|
+
snippet: () => `// src/index.tsx
|
|
543
|
+
import { MushiProvider } from '@mushi-mushi/react'
|
|
544
|
+
|
|
545
|
+
root.render(
|
|
546
|
+
<MushiProvider config={{
|
|
547
|
+
projectId: process.env.REACT_APP_MUSHI_PROJECT_ID,
|
|
548
|
+
apiKey: process.env.REACT_APP_MUSHI_API_KEY,
|
|
549
|
+
}}>
|
|
550
|
+
<App />
|
|
551
|
+
</MushiProvider>
|
|
552
|
+
)`
|
|
553
|
+
},
|
|
554
|
+
remix: {
|
|
555
|
+
id: "remix",
|
|
556
|
+
label: "Remix",
|
|
557
|
+
packageName: "@mushi-mushi/react",
|
|
558
|
+
needsWebPackage: false,
|
|
559
|
+
// Remix doesn't inline client env. Expose public values at runtime via the
|
|
560
|
+
// root loader + window.ENV (works on both the classic compiler and Vite).
|
|
561
|
+
snippet: () => `// app/root.tsx
|
|
562
|
+
import { MushiProvider } from '@mushi-mushi/react'
|
|
563
|
+
import { useLoaderData } from '@remix-run/react'
|
|
564
|
+
|
|
565
|
+
export async function loader() {
|
|
566
|
+
return { ENV: {
|
|
567
|
+
MUSHI_PROJECT_ID: process.env.MUSHI_PROJECT_ID,
|
|
568
|
+
MUSHI_API_KEY: process.env.MUSHI_API_KEY,
|
|
569
|
+
} }
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
export default function App() {
|
|
573
|
+
const { ENV } = useLoaderData<typeof loader>()
|
|
574
|
+
return (
|
|
575
|
+
<html>
|
|
576
|
+
<body>
|
|
577
|
+
<MushiProvider config={{ projectId: ENV.MUSHI_PROJECT_ID, apiKey: ENV.MUSHI_API_KEY }}>
|
|
578
|
+
<Outlet />
|
|
579
|
+
</MushiProvider>
|
|
580
|
+
<Scripts />
|
|
581
|
+
</body>
|
|
582
|
+
</html>
|
|
583
|
+
)
|
|
584
|
+
}`
|
|
585
|
+
},
|
|
586
|
+
astro: {
|
|
587
|
+
id: "astro",
|
|
588
|
+
label: "Astro",
|
|
589
|
+
packageName: "@mushi-mushi/web",
|
|
590
|
+
needsWebPackage: false,
|
|
591
|
+
// Astro exposes only PUBLIC_*-prefixed vars to client code (Vite-based).
|
|
592
|
+
snippet: () => `---
|
|
593
|
+
// src/layouts/Layout.astro (add this <script> once, e.g. in your base layout)
|
|
594
|
+
---
|
|
595
|
+
<script>
|
|
596
|
+
import { Mushi } from '@mushi-mushi/web'
|
|
597
|
+
Mushi.init({
|
|
598
|
+
projectId: import.meta.env.PUBLIC_MUSHI_PROJECT_ID,
|
|
599
|
+
apiKey: import.meta.env.PUBLIC_MUSHI_API_KEY,
|
|
600
|
+
})
|
|
601
|
+
</script>`
|
|
602
|
+
},
|
|
603
|
+
solid: {
|
|
604
|
+
id: "solid",
|
|
605
|
+
label: "Solid",
|
|
606
|
+
packageName: "@mushi-mushi/web",
|
|
607
|
+
needsWebPackage: false,
|
|
608
|
+
// Solid / SolidStart are Vite-based → VITE_*-prefixed client env.
|
|
609
|
+
snippet: () => `// src/index.tsx (or src/app.tsx for SolidStart)
|
|
610
|
+
import { onMount } from 'solid-js'
|
|
611
|
+
import { Mushi } from '@mushi-mushi/web'
|
|
612
|
+
|
|
613
|
+
onMount(() => {
|
|
614
|
+
Mushi.init({
|
|
615
|
+
projectId: import.meta.env.VITE_MUSHI_PROJECT_ID,
|
|
616
|
+
apiKey: import.meta.env.VITE_MUSHI_API_KEY,
|
|
617
|
+
})
|
|
618
|
+
})`
|
|
144
619
|
},
|
|
145
620
|
vue: {
|
|
146
621
|
id: "vue",
|
|
@@ -355,10 +830,10 @@ Mushi.init({
|
|
|
355
830
|
}
|
|
356
831
|
};
|
|
357
832
|
function readPackageJson(cwd) {
|
|
358
|
-
const path =
|
|
359
|
-
if (!
|
|
833
|
+
const path = join3(cwd, "package.json");
|
|
834
|
+
if (!existsSync3(path)) return null;
|
|
360
835
|
try {
|
|
361
|
-
return JSON.parse(
|
|
836
|
+
return JSON.parse(readFileSync3(path, "utf-8"));
|
|
362
837
|
} catch {
|
|
363
838
|
return null;
|
|
364
839
|
}
|
|
@@ -371,29 +846,41 @@ function detectFramework(cwd, pkg) {
|
|
|
371
846
|
if (deps.has("@angular/core")) return FRAMEWORKS.angular;
|
|
372
847
|
if (deps.has("expo")) return FRAMEWORKS.expo;
|
|
373
848
|
if (deps.has("react-native")) return FRAMEWORKS["react-native"];
|
|
849
|
+
if (deps.has("@remix-run/react") || deps.has("@remix-run/node") || deps.has("@remix-run/serve")) {
|
|
850
|
+
return FRAMEWORKS.remix;
|
|
851
|
+
}
|
|
852
|
+
if (deps.has("astro")) return FRAMEWORKS.astro;
|
|
853
|
+
if (deps.has("@solidjs/start") || deps.has("solid-js")) return FRAMEWORKS.solid;
|
|
374
854
|
if (deps.has("@capacitor/core") && deps.has("react")) return FRAMEWORKS.react;
|
|
375
855
|
if (deps.has("@capacitor/core")) return FRAMEWORKS.capacitor;
|
|
376
856
|
if (deps.has("svelte")) return FRAMEWORKS.svelte;
|
|
377
857
|
if (deps.has("vue")) return FRAMEWORKS.vue;
|
|
858
|
+
if (deps.has("react-scripts")) return FRAMEWORKS.cra;
|
|
378
859
|
if (deps.has("react")) return FRAMEWORKS.react;
|
|
379
860
|
if (deps.has("express")) return FRAMEWORKS.express;
|
|
380
861
|
if (deps.has("fastify")) return FRAMEWORKS.fastify;
|
|
381
862
|
if (deps.has("hono") || deps.has("@hono/hono")) return FRAMEWORKS.hono;
|
|
382
|
-
if (
|
|
863
|
+
if (existsSync3(join3(cwd, "next.config.js")) || existsSync3(join3(cwd, "next.config.ts"))) {
|
|
383
864
|
return FRAMEWORKS.next;
|
|
384
865
|
}
|
|
385
|
-
if (
|
|
866
|
+
if (existsSync3(join3(cwd, "nuxt.config.ts")) || existsSync3(join3(cwd, "nuxt.config.js"))) {
|
|
386
867
|
return FRAMEWORKS.nuxt;
|
|
387
868
|
}
|
|
388
|
-
if (
|
|
389
|
-
if (
|
|
869
|
+
if (existsSync3(join3(cwd, "svelte.config.js"))) return FRAMEWORKS.svelte;
|
|
870
|
+
if (existsSync3(join3(cwd, "angular.json"))) return FRAMEWORKS.angular;
|
|
871
|
+
if (existsSync3(join3(cwd, "remix.config.js")) || existsSync3(join3(cwd, "remix.config.ts"))) {
|
|
872
|
+
return FRAMEWORKS.remix;
|
|
873
|
+
}
|
|
874
|
+
if (existsSync3(join3(cwd, "astro.config.mjs")) || existsSync3(join3(cwd, "astro.config.ts")) || existsSync3(join3(cwd, "astro.config.js"))) {
|
|
875
|
+
return FRAMEWORKS.astro;
|
|
876
|
+
}
|
|
390
877
|
return FRAMEWORKS.vanilla;
|
|
391
878
|
}
|
|
392
879
|
function detectPackageManager(cwd) {
|
|
393
|
-
if (
|
|
394
|
-
if (
|
|
395
|
-
if (
|
|
396
|
-
if (
|
|
880
|
+
if (existsSync3(join3(cwd, "bun.lockb")) || existsSync3(join3(cwd, "bun.lock"))) return "bun";
|
|
881
|
+
if (existsSync3(join3(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
882
|
+
if (existsSync3(join3(cwd, "yarn.lock"))) return "yarn";
|
|
883
|
+
if (existsSync3(join3(cwd, "package-lock.json"))) return "npm";
|
|
397
884
|
const userAgent = process.env.npm_config_user_agent ?? "";
|
|
398
885
|
if (userAgent.startsWith("bun")) return "bun";
|
|
399
886
|
if (userAgent.startsWith("pnpm")) return "pnpm";
|
|
@@ -415,7 +902,7 @@ function envVarsToWrite(apiKey, projectId, framework, endpoint) {
|
|
|
415
902
|
endpointLine
|
|
416
903
|
].filter(Boolean).join("\n");
|
|
417
904
|
}
|
|
418
|
-
if (framework.id === "react-native") {
|
|
905
|
+
if (framework.id === "react-native" || framework.id === "remix") {
|
|
419
906
|
return [
|
|
420
907
|
`MUSHI_PROJECT_ID=${projectId}`,
|
|
421
908
|
`MUSHI_API_KEY=${apiKey}`,
|
|
@@ -429,7 +916,7 @@ function envVarsToWrite(apiKey, projectId, framework, endpoint) {
|
|
|
429
916
|
endpointLine ? `EXPO_PUBLIC_${endpointLine}` : null
|
|
430
917
|
].filter(Boolean).join("\n");
|
|
431
918
|
}
|
|
432
|
-
const prefix = framework.id === "next" ? "NEXT_PUBLIC_" : framework.id === "nuxt" ? "NUXT_PUBLIC_" : "VITE_";
|
|
919
|
+
const prefix = framework.id === "next" ? "NEXT_PUBLIC_" : framework.id === "nuxt" ? "NUXT_PUBLIC_" : framework.id === "cra" ? "REACT_APP_" : framework.id === "astro" ? "PUBLIC_" : "VITE_";
|
|
433
920
|
return [
|
|
434
921
|
`${prefix}MUSHI_PROJECT_ID=${projectId}`,
|
|
435
922
|
`${prefix}MUSHI_API_KEY=${apiKey}`,
|
|
@@ -558,7 +1045,7 @@ async function waitForIngestReady(options) {
|
|
|
558
1045
|
|
|
559
1046
|
// src/mcp-config.ts
|
|
560
1047
|
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
561
|
-
import { dirname as
|
|
1048
|
+
import { dirname as dirname3 } from "path";
|
|
562
1049
|
var DEFAULT_MUSHI_FEATURES = "triage,fixes,inventory,setup,docs";
|
|
563
1050
|
async function writeMcpServerEntry(opts) {
|
|
564
1051
|
const { configPath, serverName, serverBlock, dryRun = false } = opts;
|
|
@@ -580,7 +1067,7 @@ async function writeMcpServerEntry(opts) {
|
|
|
580
1067
|
merged.mcpServers = servers;
|
|
581
1068
|
const output = JSON.stringify(merged, null, 2) + "\n";
|
|
582
1069
|
if (!dryRun) {
|
|
583
|
-
await mkdir(
|
|
1070
|
+
await mkdir(dirname3(configPath), { recursive: true });
|
|
584
1071
|
await writeFile(configPath, output, "utf8");
|
|
585
1072
|
}
|
|
586
1073
|
return { created, path: configPath };
|
|
@@ -637,7 +1124,7 @@ ${needs.join("\n")}
|
|
|
637
1124
|
return true;
|
|
638
1125
|
}
|
|
639
1126
|
async function ensureMcpJsonGitignored(cwd, messages) {
|
|
640
|
-
const gitignorePath =
|
|
1127
|
+
const gitignorePath = join4(cwd, ".gitignore");
|
|
641
1128
|
const patterns = [".cursor/mcp.json", ".cursor/"];
|
|
642
1129
|
let content = null;
|
|
643
1130
|
try {
|
|
@@ -658,11 +1145,12 @@ async function ensureMcpJsonGitignored(cwd, messages) {
|
|
|
658
1145
|
async function runConnect(opts, baseConfig = {}) {
|
|
659
1146
|
const cwd = resolve(opts.cwd ?? process.cwd());
|
|
660
1147
|
const endpoint = assertEndpoint(opts.endpoint);
|
|
1148
|
+
const projectId = requireUuid(opts.projectId, "projectId");
|
|
661
1149
|
const messages = [];
|
|
662
1150
|
const config = {
|
|
663
1151
|
...baseConfig,
|
|
664
1152
|
apiKey: opts.apiKey,
|
|
665
|
-
projectId
|
|
1153
|
+
projectId,
|
|
666
1154
|
endpoint
|
|
667
1155
|
};
|
|
668
1156
|
saveConfig(config);
|
|
@@ -671,8 +1159,8 @@ async function runConnect(opts, baseConfig = {}) {
|
|
|
671
1159
|
if (opts.writeEnv !== false) {
|
|
672
1160
|
const pkg = readPackageJson(cwd);
|
|
673
1161
|
const framework = detectFramework(cwd, pkg);
|
|
674
|
-
const lines = envVarsToWrite(opts.apiKey,
|
|
675
|
-
envPath =
|
|
1162
|
+
const lines = envVarsToWrite(opts.apiKey, projectId, framework).split("\n");
|
|
1163
|
+
envPath = join4(cwd, ".env.local");
|
|
676
1164
|
const wrote = await mergeEnvFile(envPath, lines);
|
|
677
1165
|
messages.push(
|
|
678
1166
|
wrote ? `\u2713 Env vars merged into ${envPath}` : `\u2713 Env vars already present in ${envPath} (existing values left untouched)`
|
|
@@ -686,12 +1174,12 @@ async function runConnect(opts, baseConfig = {}) {
|
|
|
686
1174
|
}
|
|
687
1175
|
let mcpPath = null;
|
|
688
1176
|
if (opts.wireIde !== false) {
|
|
689
|
-
const mcpDir =
|
|
690
|
-
mcpPath =
|
|
691
|
-
const serverName = buildMcpServerName({ projectId
|
|
1177
|
+
const mcpDir = join4(cwd, ".cursor");
|
|
1178
|
+
mcpPath = join4(mcpDir, "mcp.json");
|
|
1179
|
+
const serverName = buildMcpServerName({ projectId });
|
|
692
1180
|
const serverBlock = buildMcpServerBlock({
|
|
693
1181
|
endpoint,
|
|
694
|
-
projectId
|
|
1182
|
+
projectId,
|
|
695
1183
|
apiKey: opts.apiKey
|
|
696
1184
|
});
|
|
697
1185
|
await writeMcpServerEntry({ configPath: mcpPath, serverName, serverBlock });
|
|
@@ -706,7 +1194,7 @@ async function runConnect(opts, baseConfig = {}) {
|
|
|
706
1194
|
heartbeat = await waitForIngestReady({
|
|
707
1195
|
endpoint,
|
|
708
1196
|
apiKey: opts.apiKey,
|
|
709
|
-
projectId
|
|
1197
|
+
projectId,
|
|
710
1198
|
maxAttempts,
|
|
711
1199
|
onPoll: (payload, attempt) => {
|
|
712
1200
|
if (!opts.json && attempt % 3 === 0) {
|
|
@@ -735,7 +1223,11 @@ async function runConnect(opts, baseConfig = {}) {
|
|
|
735
1223
|
}
|
|
736
1224
|
|
|
737
1225
|
export {
|
|
1226
|
+
printAndExit,
|
|
1227
|
+
installSignalHandlers,
|
|
1228
|
+
MUSHI_CLI_VERSION,
|
|
738
1229
|
loadConfig,
|
|
1230
|
+
ensureClientId,
|
|
739
1231
|
saveConfig,
|
|
740
1232
|
FRAMEWORKS,
|
|
741
1233
|
readPackageJson,
|
|
@@ -743,6 +1235,21 @@ export {
|
|
|
743
1235
|
detectPackageManager,
|
|
744
1236
|
installCommand,
|
|
745
1237
|
envVarsToWrite,
|
|
1238
|
+
consoleUrl,
|
|
1239
|
+
resolveConsoleUrlSync,
|
|
1240
|
+
resolveConsoleUrl,
|
|
1241
|
+
projectIdHint,
|
|
1242
|
+
apiKeyHint,
|
|
1243
|
+
cliSetupDeepLink,
|
|
1244
|
+
reportsUrl,
|
|
1245
|
+
openInBrowser,
|
|
1246
|
+
requireUuid,
|
|
1247
|
+
probeEndpointHealth,
|
|
1248
|
+
apiCall,
|
|
1249
|
+
die,
|
|
1250
|
+
requireConfig,
|
|
1251
|
+
fmtDate,
|
|
1252
|
+
pad,
|
|
746
1253
|
sanitizeApiKey,
|
|
747
1254
|
sanitizeProjectId,
|
|
748
1255
|
sanitizeEndpoint,
|