@mushi-mushi/cli 0.22.1 → 0.23.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/README.md +2 -2
- package/dist/{chunk-JAUKBLPK.js → chunk-AIZRI665.js} +102 -2
- package/dist/chunk-BY5SZYRO.js +6 -0
- package/dist/{chunk-R2HPR2FV.js → chunk-GSQO35YD.js} +520 -27
- package/dist/chunk-WLLFV26V.js +423 -0
- package/dist/{connect-2RM43XHO.js → connect-KX4UGY6Z.js} +10 -7
- package/dist/{connect-JIEIWTF3.js → connect-XFVSH4JZ.js} +1 -1
- package/dist/detect.d.ts +1 -1
- package/dist/detect.js +1 -1
- package/dist/index.js +75 -407
- package/dist/init.js +40 -241
- 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,12 +1,13 @@
|
|
|
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
|
|
12
13
|
import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from "fs";
|
|
@@ -101,9 +102,386 @@ function tightenDirPermissions(path) {
|
|
|
101
102
|
}
|
|
102
103
|
}
|
|
103
104
|
|
|
105
|
+
// src/version.ts
|
|
106
|
+
var MUSHI_CLI_VERSION = true ? "0.23.0" : "0.0.0-dev";
|
|
107
|
+
|
|
108
|
+
// src/errors.ts
|
|
109
|
+
var EXIT_CODE_MAP = {
|
|
110
|
+
E_AUTH_MISSING: 2,
|
|
111
|
+
E_AUTH_INVALID: 2,
|
|
112
|
+
E_PROJECT_MISSING: 2,
|
|
113
|
+
E_ENDPOINT_INVALID: 2,
|
|
114
|
+
E_INVALID_INPUT: 2,
|
|
115
|
+
E_NOT_INTERACTIVE: 2,
|
|
116
|
+
E_NETWORK: 3,
|
|
117
|
+
E_TIMEOUT: 3,
|
|
118
|
+
E_API_ERROR: 3,
|
|
119
|
+
E_RATE_LIMITED: 3,
|
|
120
|
+
E_FILE_NOT_FOUND: 1,
|
|
121
|
+
E_FILE_PERMISSION: 1,
|
|
122
|
+
E_FRESHNESS_STALE: 1,
|
|
123
|
+
E_INTERRUPTED: 130,
|
|
124
|
+
// 128 + SIGINT (2). POSIX convention.
|
|
125
|
+
E_INTERNAL: 1
|
|
126
|
+
};
|
|
127
|
+
var MushiCliError = class extends Error {
|
|
128
|
+
code;
|
|
129
|
+
hint;
|
|
130
|
+
cause;
|
|
131
|
+
constructor(code, message, hint, cause) {
|
|
132
|
+
super(message);
|
|
133
|
+
this.name = "MushiCliError";
|
|
134
|
+
this.code = code;
|
|
135
|
+
if (hint) this.hint = hint;
|
|
136
|
+
if (cause !== void 0) this.cause = cause;
|
|
137
|
+
}
|
|
138
|
+
/** Exit code POSIX-aware shell scripts should branch on. */
|
|
139
|
+
get exitCode() {
|
|
140
|
+
return EXIT_CODE_MAP[this.code];
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Format for `--json` output. Used by the JSON writer when a CLI
|
|
144
|
+
* subcommand was invoked with `--json` and an error escapes.
|
|
145
|
+
*/
|
|
146
|
+
toJSON() {
|
|
147
|
+
return {
|
|
148
|
+
error: {
|
|
149
|
+
code: this.code,
|
|
150
|
+
message: this.message,
|
|
151
|
+
...this.hint ? { hint: this.hint } : {}
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
function printAndExit(err, asJson = false) {
|
|
157
|
+
const cliErr = err instanceof MushiCliError ? err : new MushiCliError(
|
|
158
|
+
"E_INTERNAL",
|
|
159
|
+
err instanceof Error ? err.message : String(err),
|
|
160
|
+
"this is unexpected \u2014 re-run with MUSHI_DEBUG=1 and report at https://github.com/kensaurus/mushi-mushi/issues",
|
|
161
|
+
err
|
|
162
|
+
);
|
|
163
|
+
if (asJson) {
|
|
164
|
+
process.stderr.write(JSON.stringify(cliErr.toJSON()) + "\n");
|
|
165
|
+
} else {
|
|
166
|
+
process.stderr.write(`error [${cliErr.code}]: ${cliErr.message}
|
|
167
|
+
`);
|
|
168
|
+
if (cliErr.hint) process.stderr.write(` \u2192 fix: ${cliErr.hint}
|
|
169
|
+
`);
|
|
170
|
+
}
|
|
171
|
+
if (process.env.MUSHI_CLI_JSON_LOG === "1" || process.env.CI === "true") {
|
|
172
|
+
process.stderr.write(
|
|
173
|
+
JSON.stringify({
|
|
174
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
175
|
+
level: "error",
|
|
176
|
+
scope: "mushi:cli",
|
|
177
|
+
...cliErr.toJSON(),
|
|
178
|
+
exitCode: cliErr.exitCode
|
|
179
|
+
}) + "\n"
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
process.exit(cliErr.exitCode);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// src/signals.ts
|
|
186
|
+
var installed = false;
|
|
187
|
+
var activeController = null;
|
|
188
|
+
function installSignalHandlers() {
|
|
189
|
+
if (installed) return;
|
|
190
|
+
installed = true;
|
|
191
|
+
const abortAndExit = (signal) => {
|
|
192
|
+
activeController?.abort(
|
|
193
|
+
new MushiCliError(
|
|
194
|
+
"E_INTERRUPTED",
|
|
195
|
+
`aborted by ${signal}`,
|
|
196
|
+
"partial state on the server is safe to retry \u2014 re-run the same command"
|
|
197
|
+
)
|
|
198
|
+
);
|
|
199
|
+
const code = signal === "SIGINT" ? 130 : signal === "SIGTERM" ? 143 : 1;
|
|
200
|
+
process.nextTick(() => {
|
|
201
|
+
process.exit(code);
|
|
202
|
+
});
|
|
203
|
+
};
|
|
204
|
+
process.on("SIGINT", () => abortAndExit("SIGINT"));
|
|
205
|
+
process.on("SIGTERM", () => abortAndExit("SIGTERM"));
|
|
206
|
+
}
|
|
207
|
+
function getAbortSignal(external) {
|
|
208
|
+
if (external) return external.signal;
|
|
209
|
+
if (!activeController || activeController.signal.aborted) {
|
|
210
|
+
activeController = new AbortController();
|
|
211
|
+
}
|
|
212
|
+
return activeController.signal;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// src/console-url.ts
|
|
216
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
217
|
+
import { dirname as dirname2, join as join2 } from "path";
|
|
218
|
+
var HOSTED_CONSOLE_BASE = "https://kensaur.us/mushi-mushi/admin";
|
|
219
|
+
var LOCAL_ADMIN_URL = "http://localhost:6464";
|
|
220
|
+
var LOCAL_PROBE_MS = 800;
|
|
221
|
+
function normalizeConsoleBase(url) {
|
|
222
|
+
const trimmed = url.trim();
|
|
223
|
+
let end = trimmed.length;
|
|
224
|
+
while (end > 0 && trimmed.charCodeAt(end - 1) === 47) end--;
|
|
225
|
+
return trimmed.slice(0, end);
|
|
226
|
+
}
|
|
227
|
+
function consoleUrl(base, route) {
|
|
228
|
+
const normalizedRoute = route.startsWith("/") ? route : `/${route}`;
|
|
229
|
+
return `${normalizeConsoleBase(base)}${normalizedRoute}`;
|
|
230
|
+
}
|
|
231
|
+
function resolveConsoleUrlFromEnv() {
|
|
232
|
+
const raw = process.env["MUSHI_CONSOLE_URL"]?.trim();
|
|
233
|
+
return raw ? normalizeConsoleBase(raw) : void 0;
|
|
234
|
+
}
|
|
235
|
+
function isInsideMushiMonorepo(cwd) {
|
|
236
|
+
let dir = cwd;
|
|
237
|
+
for (let depth = 0; depth < 14; depth += 1) {
|
|
238
|
+
const pkgPath = join2(dir, "package.json");
|
|
239
|
+
if (existsSync2(pkgPath)) {
|
|
240
|
+
try {
|
|
241
|
+
const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
|
|
242
|
+
if (pkg.name === "mushi-mushi") return true;
|
|
243
|
+
} catch {
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
const parent = dirname2(dir);
|
|
247
|
+
if (parent === dir) break;
|
|
248
|
+
dir = parent;
|
|
249
|
+
}
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
async function probeLocalAdminConsole() {
|
|
253
|
+
try {
|
|
254
|
+
const controller = new AbortController();
|
|
255
|
+
const timer = setTimeout(() => controller.abort(), LOCAL_PROBE_MS);
|
|
256
|
+
const res = await fetch(LOCAL_ADMIN_URL, {
|
|
257
|
+
signal: controller.signal,
|
|
258
|
+
method: "HEAD"
|
|
259
|
+
});
|
|
260
|
+
clearTimeout(timer);
|
|
261
|
+
if (res.ok || res.status === 404 || res.status === 401) {
|
|
262
|
+
return LOCAL_ADMIN_URL;
|
|
263
|
+
}
|
|
264
|
+
} catch {
|
|
265
|
+
}
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
function resolveConsoleUrlSync(cwd = process.cwd()) {
|
|
269
|
+
const fromEnv = resolveConsoleUrlFromEnv();
|
|
270
|
+
if (fromEnv) return fromEnv;
|
|
271
|
+
const config = loadConfig();
|
|
272
|
+
if (config.consoleUrl) return normalizeConsoleBase(config.consoleUrl);
|
|
273
|
+
if (isInsideMushiMonorepo(cwd)) return LOCAL_ADMIN_URL;
|
|
274
|
+
return HOSTED_CONSOLE_BASE;
|
|
275
|
+
}
|
|
276
|
+
async function resolveConsoleUrl(options) {
|
|
277
|
+
const fromEnv = resolveConsoleUrlFromEnv();
|
|
278
|
+
if (fromEnv) return fromEnv;
|
|
279
|
+
const config = loadConfig();
|
|
280
|
+
if (config.consoleUrl) return normalizeConsoleBase(config.consoleUrl);
|
|
281
|
+
const local = await probeLocalAdminConsole();
|
|
282
|
+
if (local) return local;
|
|
283
|
+
const cwd = options?.cwd ?? process.cwd();
|
|
284
|
+
if (isInsideMushiMonorepo(cwd)) return LOCAL_ADMIN_URL;
|
|
285
|
+
return HOSTED_CONSOLE_BASE;
|
|
286
|
+
}
|
|
287
|
+
function projectIdHint(base) {
|
|
288
|
+
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).`;
|
|
289
|
+
}
|
|
290
|
+
function apiKeyHint(base) {
|
|
291
|
+
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.`;
|
|
292
|
+
}
|
|
293
|
+
function cliSetupDeepLink(base) {
|
|
294
|
+
return consoleUrl(base, "/onboarding?tab=steps&setup=cli");
|
|
295
|
+
}
|
|
296
|
+
function reportsUrl(base, reportId) {
|
|
297
|
+
return reportId ? consoleUrl(base, `/reports/${reportId}`) : consoleUrl(base, "/reports");
|
|
298
|
+
}
|
|
299
|
+
async function openInBrowser(url) {
|
|
300
|
+
let parsed;
|
|
301
|
+
try {
|
|
302
|
+
parsed = new URL(url);
|
|
303
|
+
} catch {
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
const safeUrl = parsed.toString();
|
|
310
|
+
const { spawn } = await import("child_process");
|
|
311
|
+
const [command, args] = process.platform === "win32" ? (
|
|
312
|
+
// Open via rundll32's FileProtocolHandler rather than `cmd /c start`.
|
|
313
|
+
// `start` is a cmd builtin, so the URL would be re-parsed by cmd.exe and
|
|
314
|
+
// characters like `&` / `|` / `^` could still break out — exactly the
|
|
315
|
+
// injection surface CodeQL flags. rundll32 never invokes a shell: the URL
|
|
316
|
+
// is handed to the default browser as one opaque argument.
|
|
317
|
+
["rundll32", ["url.dll,FileProtocolHandler", safeUrl]]
|
|
318
|
+
) : process.platform === "darwin" ? ["open", [safeUrl]] : ["xdg-open", [safeUrl]];
|
|
319
|
+
await new Promise((resolve2) => {
|
|
320
|
+
try {
|
|
321
|
+
const child = spawn(command, [...args], { stdio: "ignore", shell: false });
|
|
322
|
+
child.on("error", () => resolve2());
|
|
323
|
+
child.on("exit", () => resolve2());
|
|
324
|
+
child.unref();
|
|
325
|
+
} catch {
|
|
326
|
+
resolve2();
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// src/cli-shared.ts
|
|
332
|
+
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;
|
|
333
|
+
function requireUuid(value, label = "id") {
|
|
334
|
+
if (UUID_RX.test(value)) return value;
|
|
335
|
+
throw new MushiCliError(
|
|
336
|
+
"E_INVALID_INPUT",
|
|
337
|
+
`${label} must be a valid UUID`,
|
|
338
|
+
"copy the full id from `mushi reports list` or the console URL"
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
var API_TIMEOUT_MS = 15e3;
|
|
342
|
+
async function probeEndpointHealth(endpoint) {
|
|
343
|
+
const t0 = Date.now();
|
|
344
|
+
const controller = new AbortController();
|
|
345
|
+
const timer = setTimeout(() => controller.abort(), API_TIMEOUT_MS);
|
|
346
|
+
try {
|
|
347
|
+
const res = await fetch(`${endpoint}/health`, { signal: controller.signal });
|
|
348
|
+
clearTimeout(timer);
|
|
349
|
+
const body = res.headers.get("content-type")?.includes("json") ? await res.json().catch(() => ({})) : {};
|
|
350
|
+
return { ok: res.ok, status: res.status, latencyMs: Date.now() - t0, body };
|
|
351
|
+
} catch (err) {
|
|
352
|
+
clearTimeout(timer);
|
|
353
|
+
throw err;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
async function apiCall(path, config, options = {}) {
|
|
357
|
+
const endpoint = config.endpoint;
|
|
358
|
+
if (!endpoint) {
|
|
359
|
+
return {
|
|
360
|
+
ok: false,
|
|
361
|
+
error: {
|
|
362
|
+
code: "NO_ENDPOINT",
|
|
363
|
+
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"
|
|
364
|
+
}
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
const controller = new AbortController();
|
|
368
|
+
const timer = setTimeout(() => controller.abort(), API_TIMEOUT_MS);
|
|
369
|
+
const signals = [controller.signal, getAbortSignal()];
|
|
370
|
+
const compositeSignal = AbortSignal.any ? AbortSignal.any(signals) : controller.signal;
|
|
371
|
+
try {
|
|
372
|
+
const res = await fetch(`${endpoint}${path}`, {
|
|
373
|
+
...options,
|
|
374
|
+
signal: compositeSignal,
|
|
375
|
+
headers: {
|
|
376
|
+
"Content-Type": "application/json",
|
|
377
|
+
// `apiKeyAuth` reads X-Mushi-Api-Key; the Authorization header is a
|
|
378
|
+
// fallback accepted by some older middleware. Both are sent.
|
|
379
|
+
"Authorization": `Bearer ${config.apiKey ?? ""}`,
|
|
380
|
+
"X-Mushi-Api-Key": config.apiKey ?? "",
|
|
381
|
+
"X-Mushi-Project": config.projectId ?? "",
|
|
382
|
+
"X-Mushi-Cli-Version": MUSHI_CLI_VERSION,
|
|
383
|
+
...options.headers
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
clearTimeout(timer);
|
|
387
|
+
let body;
|
|
388
|
+
const contentType = res.headers.get("content-type") ?? "";
|
|
389
|
+
if (contentType.includes("application/json")) {
|
|
390
|
+
try {
|
|
391
|
+
body = await res.json();
|
|
392
|
+
} catch {
|
|
393
|
+
body = null;
|
|
394
|
+
}
|
|
395
|
+
} else {
|
|
396
|
+
const text = await res.text();
|
|
397
|
+
try {
|
|
398
|
+
body = JSON.parse(text);
|
|
399
|
+
} catch {
|
|
400
|
+
body = {
|
|
401
|
+
ok: false,
|
|
402
|
+
error: {
|
|
403
|
+
code: `HTTP_${res.status}`,
|
|
404
|
+
message: text.trim().slice(0, 300) || `HTTP ${res.status}`
|
|
405
|
+
}
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
if (!res.ok && typeof body === "object" && body !== null && !("ok" in body)) {
|
|
410
|
+
const b = body;
|
|
411
|
+
const nested = b["error"];
|
|
412
|
+
const errCode = nested?.["code"] ?? b["code"] ?? `HTTP_${res.status}`;
|
|
413
|
+
const errMsg = nested?.["message"] ?? b["message"] ?? `Request failed (${res.status})`;
|
|
414
|
+
return {
|
|
415
|
+
ok: false,
|
|
416
|
+
httpStatus: res.status,
|
|
417
|
+
error: { code: errCode, message: errMsg }
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
return body;
|
|
421
|
+
} catch (err) {
|
|
422
|
+
clearTimeout(timer);
|
|
423
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
424
|
+
return {
|
|
425
|
+
ok: false,
|
|
426
|
+
error: {
|
|
427
|
+
code: "TIMEOUT",
|
|
428
|
+
message: `Request timed out after ${API_TIMEOUT_MS / 1e3}s. Check your network or endpoint.`
|
|
429
|
+
}
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
return {
|
|
433
|
+
ok: false,
|
|
434
|
+
error: {
|
|
435
|
+
code: "NETWORK_ERROR",
|
|
436
|
+
message: err instanceof Error ? err.message : String(err)
|
|
437
|
+
}
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
function die(result, exitCode = 1) {
|
|
442
|
+
const { code, message } = result.error;
|
|
443
|
+
const status = result.httpStatus ? ` [${result.httpStatus}]` : "";
|
|
444
|
+
process.stderr.write(`error${status}: ${code} \u2014 ${message}
|
|
445
|
+
`);
|
|
446
|
+
if (code === "INSUFFICIENT_SCOPE") {
|
|
447
|
+
process.stderr.write(
|
|
448
|
+
"\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"
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
process.exit(exitCode);
|
|
452
|
+
}
|
|
453
|
+
function requireConfig(opts = {}) {
|
|
454
|
+
const config = loadConfig();
|
|
455
|
+
if (!config.apiKey) {
|
|
456
|
+
printAndExit(new MushiCliError(
|
|
457
|
+
"E_AUTH_MISSING",
|
|
458
|
+
"API key not configured.",
|
|
459
|
+
"Run: mushi login --api-key <key> Or: export MUSHI_API_KEY=<key>"
|
|
460
|
+
));
|
|
461
|
+
}
|
|
462
|
+
if (!config.endpoint) {
|
|
463
|
+
config.endpoint = process.env.MUSHI_API_ENDPOINT?.trim() || CLOUD_API_ENDPOINT;
|
|
464
|
+
}
|
|
465
|
+
if (opts.needsProject && !config.projectId) {
|
|
466
|
+
printAndExit(new MushiCliError(
|
|
467
|
+
"E_PROJECT_MISSING",
|
|
468
|
+
"Project ID not configured.",
|
|
469
|
+
`Run: mushi login --project-id <uuid> Or find your project ID at ${consoleUrl(resolveConsoleUrlSync(), "/projects")}`
|
|
470
|
+
));
|
|
471
|
+
}
|
|
472
|
+
return config;
|
|
473
|
+
}
|
|
474
|
+
function fmtDate(iso) {
|
|
475
|
+
if (!iso) return "\u2014";
|
|
476
|
+
return new Date(iso).toLocaleString(void 0, { dateStyle: "short", timeStyle: "short" });
|
|
477
|
+
}
|
|
478
|
+
function pad(s, width) {
|
|
479
|
+
return s.length >= width ? s : s + " ".repeat(width - s.length);
|
|
480
|
+
}
|
|
481
|
+
|
|
104
482
|
// src/detect.ts
|
|
105
|
-
import { readFileSync as
|
|
106
|
-
import { join as
|
|
483
|
+
import { readFileSync as readFileSync3, existsSync as existsSync3 } from "fs";
|
|
484
|
+
import { join as join3 } from "path";
|
|
107
485
|
var FRAMEWORKS = {
|
|
108
486
|
next: {
|
|
109
487
|
id: "next",
|
|
@@ -141,6 +519,90 @@ createRoot(document.getElementById('root')!).render(
|
|
|
141
519
|
<App />
|
|
142
520
|
</MushiProvider>
|
|
143
521
|
)`
|
|
522
|
+
},
|
|
523
|
+
cra: {
|
|
524
|
+
id: "cra",
|
|
525
|
+
label: "Create React App",
|
|
526
|
+
packageName: "@mushi-mushi/react",
|
|
527
|
+
needsWebPackage: false,
|
|
528
|
+
// CRA inlines only REACT_APP_*-prefixed vars into process.env at build time.
|
|
529
|
+
snippet: () => `// src/index.tsx
|
|
530
|
+
import { MushiProvider } from '@mushi-mushi/react'
|
|
531
|
+
|
|
532
|
+
root.render(
|
|
533
|
+
<MushiProvider config={{
|
|
534
|
+
projectId: process.env.REACT_APP_MUSHI_PROJECT_ID,
|
|
535
|
+
apiKey: process.env.REACT_APP_MUSHI_API_KEY,
|
|
536
|
+
}}>
|
|
537
|
+
<App />
|
|
538
|
+
</MushiProvider>
|
|
539
|
+
)`
|
|
540
|
+
},
|
|
541
|
+
remix: {
|
|
542
|
+
id: "remix",
|
|
543
|
+
label: "Remix",
|
|
544
|
+
packageName: "@mushi-mushi/react",
|
|
545
|
+
needsWebPackage: false,
|
|
546
|
+
// Remix doesn't inline client env. Expose public values at runtime via the
|
|
547
|
+
// root loader + window.ENV (works on both the classic compiler and Vite).
|
|
548
|
+
snippet: () => `// app/root.tsx
|
|
549
|
+
import { MushiProvider } from '@mushi-mushi/react'
|
|
550
|
+
import { useLoaderData } from '@remix-run/react'
|
|
551
|
+
|
|
552
|
+
export async function loader() {
|
|
553
|
+
return { ENV: {
|
|
554
|
+
MUSHI_PROJECT_ID: process.env.MUSHI_PROJECT_ID,
|
|
555
|
+
MUSHI_API_KEY: process.env.MUSHI_API_KEY,
|
|
556
|
+
} }
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
export default function App() {
|
|
560
|
+
const { ENV } = useLoaderData<typeof loader>()
|
|
561
|
+
return (
|
|
562
|
+
<html>
|
|
563
|
+
<body>
|
|
564
|
+
<MushiProvider config={{ projectId: ENV.MUSHI_PROJECT_ID, apiKey: ENV.MUSHI_API_KEY }}>
|
|
565
|
+
<Outlet />
|
|
566
|
+
</MushiProvider>
|
|
567
|
+
<Scripts />
|
|
568
|
+
</body>
|
|
569
|
+
</html>
|
|
570
|
+
)
|
|
571
|
+
}`
|
|
572
|
+
},
|
|
573
|
+
astro: {
|
|
574
|
+
id: "astro",
|
|
575
|
+
label: "Astro",
|
|
576
|
+
packageName: "@mushi-mushi/web",
|
|
577
|
+
needsWebPackage: false,
|
|
578
|
+
// Astro exposes only PUBLIC_*-prefixed vars to client code (Vite-based).
|
|
579
|
+
snippet: () => `---
|
|
580
|
+
// src/layouts/Layout.astro (add this <script> once, e.g. in your base layout)
|
|
581
|
+
---
|
|
582
|
+
<script>
|
|
583
|
+
import { Mushi } from '@mushi-mushi/web'
|
|
584
|
+
Mushi.init({
|
|
585
|
+
projectId: import.meta.env.PUBLIC_MUSHI_PROJECT_ID,
|
|
586
|
+
apiKey: import.meta.env.PUBLIC_MUSHI_API_KEY,
|
|
587
|
+
})
|
|
588
|
+
</script>`
|
|
589
|
+
},
|
|
590
|
+
solid: {
|
|
591
|
+
id: "solid",
|
|
592
|
+
label: "Solid",
|
|
593
|
+
packageName: "@mushi-mushi/web",
|
|
594
|
+
needsWebPackage: false,
|
|
595
|
+
// Solid / SolidStart are Vite-based → VITE_*-prefixed client env.
|
|
596
|
+
snippet: () => `// src/index.tsx (or src/app.tsx for SolidStart)
|
|
597
|
+
import { onMount } from 'solid-js'
|
|
598
|
+
import { Mushi } from '@mushi-mushi/web'
|
|
599
|
+
|
|
600
|
+
onMount(() => {
|
|
601
|
+
Mushi.init({
|
|
602
|
+
projectId: import.meta.env.VITE_MUSHI_PROJECT_ID,
|
|
603
|
+
apiKey: import.meta.env.VITE_MUSHI_API_KEY,
|
|
604
|
+
})
|
|
605
|
+
})`
|
|
144
606
|
},
|
|
145
607
|
vue: {
|
|
146
608
|
id: "vue",
|
|
@@ -355,10 +817,10 @@ Mushi.init({
|
|
|
355
817
|
}
|
|
356
818
|
};
|
|
357
819
|
function readPackageJson(cwd) {
|
|
358
|
-
const path =
|
|
359
|
-
if (!
|
|
820
|
+
const path = join3(cwd, "package.json");
|
|
821
|
+
if (!existsSync3(path)) return null;
|
|
360
822
|
try {
|
|
361
|
-
return JSON.parse(
|
|
823
|
+
return JSON.parse(readFileSync3(path, "utf-8"));
|
|
362
824
|
} catch {
|
|
363
825
|
return null;
|
|
364
826
|
}
|
|
@@ -371,29 +833,41 @@ function detectFramework(cwd, pkg) {
|
|
|
371
833
|
if (deps.has("@angular/core")) return FRAMEWORKS.angular;
|
|
372
834
|
if (deps.has("expo")) return FRAMEWORKS.expo;
|
|
373
835
|
if (deps.has("react-native")) return FRAMEWORKS["react-native"];
|
|
836
|
+
if (deps.has("@remix-run/react") || deps.has("@remix-run/node") || deps.has("@remix-run/serve")) {
|
|
837
|
+
return FRAMEWORKS.remix;
|
|
838
|
+
}
|
|
839
|
+
if (deps.has("astro")) return FRAMEWORKS.astro;
|
|
840
|
+
if (deps.has("@solidjs/start") || deps.has("solid-js")) return FRAMEWORKS.solid;
|
|
374
841
|
if (deps.has("@capacitor/core") && deps.has("react")) return FRAMEWORKS.react;
|
|
375
842
|
if (deps.has("@capacitor/core")) return FRAMEWORKS.capacitor;
|
|
376
843
|
if (deps.has("svelte")) return FRAMEWORKS.svelte;
|
|
377
844
|
if (deps.has("vue")) return FRAMEWORKS.vue;
|
|
845
|
+
if (deps.has("react-scripts")) return FRAMEWORKS.cra;
|
|
378
846
|
if (deps.has("react")) return FRAMEWORKS.react;
|
|
379
847
|
if (deps.has("express")) return FRAMEWORKS.express;
|
|
380
848
|
if (deps.has("fastify")) return FRAMEWORKS.fastify;
|
|
381
849
|
if (deps.has("hono") || deps.has("@hono/hono")) return FRAMEWORKS.hono;
|
|
382
|
-
if (
|
|
850
|
+
if (existsSync3(join3(cwd, "next.config.js")) || existsSync3(join3(cwd, "next.config.ts"))) {
|
|
383
851
|
return FRAMEWORKS.next;
|
|
384
852
|
}
|
|
385
|
-
if (
|
|
853
|
+
if (existsSync3(join3(cwd, "nuxt.config.ts")) || existsSync3(join3(cwd, "nuxt.config.js"))) {
|
|
386
854
|
return FRAMEWORKS.nuxt;
|
|
387
855
|
}
|
|
388
|
-
if (
|
|
389
|
-
if (
|
|
856
|
+
if (existsSync3(join3(cwd, "svelte.config.js"))) return FRAMEWORKS.svelte;
|
|
857
|
+
if (existsSync3(join3(cwd, "angular.json"))) return FRAMEWORKS.angular;
|
|
858
|
+
if (existsSync3(join3(cwd, "remix.config.js")) || existsSync3(join3(cwd, "remix.config.ts"))) {
|
|
859
|
+
return FRAMEWORKS.remix;
|
|
860
|
+
}
|
|
861
|
+
if (existsSync3(join3(cwd, "astro.config.mjs")) || existsSync3(join3(cwd, "astro.config.ts")) || existsSync3(join3(cwd, "astro.config.js"))) {
|
|
862
|
+
return FRAMEWORKS.astro;
|
|
863
|
+
}
|
|
390
864
|
return FRAMEWORKS.vanilla;
|
|
391
865
|
}
|
|
392
866
|
function detectPackageManager(cwd) {
|
|
393
|
-
if (
|
|
394
|
-
if (
|
|
395
|
-
if (
|
|
396
|
-
if (
|
|
867
|
+
if (existsSync3(join3(cwd, "bun.lockb")) || existsSync3(join3(cwd, "bun.lock"))) return "bun";
|
|
868
|
+
if (existsSync3(join3(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
869
|
+
if (existsSync3(join3(cwd, "yarn.lock"))) return "yarn";
|
|
870
|
+
if (existsSync3(join3(cwd, "package-lock.json"))) return "npm";
|
|
397
871
|
const userAgent = process.env.npm_config_user_agent ?? "";
|
|
398
872
|
if (userAgent.startsWith("bun")) return "bun";
|
|
399
873
|
if (userAgent.startsWith("pnpm")) return "pnpm";
|
|
@@ -415,7 +889,7 @@ function envVarsToWrite(apiKey, projectId, framework, endpoint) {
|
|
|
415
889
|
endpointLine
|
|
416
890
|
].filter(Boolean).join("\n");
|
|
417
891
|
}
|
|
418
|
-
if (framework.id === "react-native") {
|
|
892
|
+
if (framework.id === "react-native" || framework.id === "remix") {
|
|
419
893
|
return [
|
|
420
894
|
`MUSHI_PROJECT_ID=${projectId}`,
|
|
421
895
|
`MUSHI_API_KEY=${apiKey}`,
|
|
@@ -429,7 +903,7 @@ function envVarsToWrite(apiKey, projectId, framework, endpoint) {
|
|
|
429
903
|
endpointLine ? `EXPO_PUBLIC_${endpointLine}` : null
|
|
430
904
|
].filter(Boolean).join("\n");
|
|
431
905
|
}
|
|
432
|
-
const prefix = framework.id === "next" ? "NEXT_PUBLIC_" : framework.id === "nuxt" ? "NUXT_PUBLIC_" : "VITE_";
|
|
906
|
+
const prefix = framework.id === "next" ? "NEXT_PUBLIC_" : framework.id === "nuxt" ? "NUXT_PUBLIC_" : framework.id === "cra" ? "REACT_APP_" : framework.id === "astro" ? "PUBLIC_" : "VITE_";
|
|
433
907
|
return [
|
|
434
908
|
`${prefix}MUSHI_PROJECT_ID=${projectId}`,
|
|
435
909
|
`${prefix}MUSHI_API_KEY=${apiKey}`,
|
|
@@ -558,7 +1032,7 @@ async function waitForIngestReady(options) {
|
|
|
558
1032
|
|
|
559
1033
|
// src/mcp-config.ts
|
|
560
1034
|
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
561
|
-
import { dirname as
|
|
1035
|
+
import { dirname as dirname3 } from "path";
|
|
562
1036
|
var DEFAULT_MUSHI_FEATURES = "triage,fixes,inventory,setup,docs";
|
|
563
1037
|
async function writeMcpServerEntry(opts) {
|
|
564
1038
|
const { configPath, serverName, serverBlock, dryRun = false } = opts;
|
|
@@ -580,7 +1054,7 @@ async function writeMcpServerEntry(opts) {
|
|
|
580
1054
|
merged.mcpServers = servers;
|
|
581
1055
|
const output = JSON.stringify(merged, null, 2) + "\n";
|
|
582
1056
|
if (!dryRun) {
|
|
583
|
-
await mkdir(
|
|
1057
|
+
await mkdir(dirname3(configPath), { recursive: true });
|
|
584
1058
|
await writeFile(configPath, output, "utf8");
|
|
585
1059
|
}
|
|
586
1060
|
return { created, path: configPath };
|
|
@@ -637,7 +1111,7 @@ ${needs.join("\n")}
|
|
|
637
1111
|
return true;
|
|
638
1112
|
}
|
|
639
1113
|
async function ensureMcpJsonGitignored(cwd, messages) {
|
|
640
|
-
const gitignorePath =
|
|
1114
|
+
const gitignorePath = join4(cwd, ".gitignore");
|
|
641
1115
|
const patterns = [".cursor/mcp.json", ".cursor/"];
|
|
642
1116
|
let content = null;
|
|
643
1117
|
try {
|
|
@@ -658,11 +1132,12 @@ async function ensureMcpJsonGitignored(cwd, messages) {
|
|
|
658
1132
|
async function runConnect(opts, baseConfig = {}) {
|
|
659
1133
|
const cwd = resolve(opts.cwd ?? process.cwd());
|
|
660
1134
|
const endpoint = assertEndpoint(opts.endpoint);
|
|
1135
|
+
const projectId = requireUuid(opts.projectId, "projectId");
|
|
661
1136
|
const messages = [];
|
|
662
1137
|
const config = {
|
|
663
1138
|
...baseConfig,
|
|
664
1139
|
apiKey: opts.apiKey,
|
|
665
|
-
projectId
|
|
1140
|
+
projectId,
|
|
666
1141
|
endpoint
|
|
667
1142
|
};
|
|
668
1143
|
saveConfig(config);
|
|
@@ -671,8 +1146,8 @@ async function runConnect(opts, baseConfig = {}) {
|
|
|
671
1146
|
if (opts.writeEnv !== false) {
|
|
672
1147
|
const pkg = readPackageJson(cwd);
|
|
673
1148
|
const framework = detectFramework(cwd, pkg);
|
|
674
|
-
const lines = envVarsToWrite(opts.apiKey,
|
|
675
|
-
envPath =
|
|
1149
|
+
const lines = envVarsToWrite(opts.apiKey, projectId, framework).split("\n");
|
|
1150
|
+
envPath = join4(cwd, ".env.local");
|
|
676
1151
|
const wrote = await mergeEnvFile(envPath, lines);
|
|
677
1152
|
messages.push(
|
|
678
1153
|
wrote ? `\u2713 Env vars merged into ${envPath}` : `\u2713 Env vars already present in ${envPath} (existing values left untouched)`
|
|
@@ -686,12 +1161,12 @@ async function runConnect(opts, baseConfig = {}) {
|
|
|
686
1161
|
}
|
|
687
1162
|
let mcpPath = null;
|
|
688
1163
|
if (opts.wireIde !== false) {
|
|
689
|
-
const mcpDir =
|
|
690
|
-
mcpPath =
|
|
691
|
-
const serverName = buildMcpServerName({ projectId
|
|
1164
|
+
const mcpDir = join4(cwd, ".cursor");
|
|
1165
|
+
mcpPath = join4(mcpDir, "mcp.json");
|
|
1166
|
+
const serverName = buildMcpServerName({ projectId });
|
|
692
1167
|
const serverBlock = buildMcpServerBlock({
|
|
693
1168
|
endpoint,
|
|
694
|
-
projectId
|
|
1169
|
+
projectId,
|
|
695
1170
|
apiKey: opts.apiKey
|
|
696
1171
|
});
|
|
697
1172
|
await writeMcpServerEntry({ configPath: mcpPath, serverName, serverBlock });
|
|
@@ -706,7 +1181,7 @@ async function runConnect(opts, baseConfig = {}) {
|
|
|
706
1181
|
heartbeat = await waitForIngestReady({
|
|
707
1182
|
endpoint,
|
|
708
1183
|
apiKey: opts.apiKey,
|
|
709
|
-
projectId
|
|
1184
|
+
projectId,
|
|
710
1185
|
maxAttempts,
|
|
711
1186
|
onPoll: (payload, attempt) => {
|
|
712
1187
|
if (!opts.json && attempt % 3 === 0) {
|
|
@@ -735,6 +1210,9 @@ async function runConnect(opts, baseConfig = {}) {
|
|
|
735
1210
|
}
|
|
736
1211
|
|
|
737
1212
|
export {
|
|
1213
|
+
printAndExit,
|
|
1214
|
+
installSignalHandlers,
|
|
1215
|
+
MUSHI_CLI_VERSION,
|
|
738
1216
|
loadConfig,
|
|
739
1217
|
saveConfig,
|
|
740
1218
|
FRAMEWORKS,
|
|
@@ -743,6 +1221,21 @@ export {
|
|
|
743
1221
|
detectPackageManager,
|
|
744
1222
|
installCommand,
|
|
745
1223
|
envVarsToWrite,
|
|
1224
|
+
consoleUrl,
|
|
1225
|
+
resolveConsoleUrlSync,
|
|
1226
|
+
resolveConsoleUrl,
|
|
1227
|
+
projectIdHint,
|
|
1228
|
+
apiKeyHint,
|
|
1229
|
+
cliSetupDeepLink,
|
|
1230
|
+
reportsUrl,
|
|
1231
|
+
openInBrowser,
|
|
1232
|
+
requireUuid,
|
|
1233
|
+
probeEndpointHealth,
|
|
1234
|
+
apiCall,
|
|
1235
|
+
die,
|
|
1236
|
+
requireConfig,
|
|
1237
|
+
fmtDate,
|
|
1238
|
+
pad,
|
|
746
1239
|
sanitizeApiKey,
|
|
747
1240
|
sanitizeProjectId,
|
|
748
1241
|
sanitizeEndpoint,
|