@mushi-mushi/cli 0.27.1 → 0.28.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 +6 -1
- package/dist/{chunk-M7CDSRZM.js → chunk-AV3RKAEE.js} +77 -47
- package/dist/{chunk-NNB2NONG.js → chunk-FJETUM25.js} +156 -497
- package/dist/chunk-KOBEOGNF.js +475 -0
- package/dist/chunk-SXSI2W6Z.js +42 -0
- package/dist/chunk-U2VGB63C.js +8 -0
- package/dist/chunk-UVO4A2G6.js +10 -0
- package/dist/connect-63GYZLFM.js +13 -0
- package/dist/{connect-5EQA7G3U.js → connect-C673EKS3.js} +74 -42
- package/dist/detect-4PYRZXVK.js +22 -0
- package/dist/endpoint-NHECZL47.js +15 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1202 -814
- package/dist/init.d.ts +179 -1
- package/dist/init.js +468 -225
- package/dist/{version-AYMUW3UQ.js → version-VR37VEE4.js} +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/wizard-args.d.ts +2 -2
- package/dist/wizard-args.js +5 -3
- package/package.json +10 -15
- package/CODE_OF_CONDUCT.md +0 -51
- package/CONTRIBUTING.md +0 -226
- package/SECURITY.md +0 -359
- package/dist/chunk-DBAEB56N.js +0 -10
- package/dist/chunk-XLGHUOKY.js +0 -8
- package/dist/connect-TWO2ZNFB.js +0 -10
package/dist/index.js
CHANGED
|
@@ -1,22 +1,28 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
|
|
3
|
+
CLI_KEY_LABEL,
|
|
4
|
+
CLI_KEY_SCOPES,
|
|
5
|
+
CONFIG_PATH,
|
|
6
|
+
FULL_CLI_KEY_SCOPES,
|
|
7
|
+
MCP_CLIENTS,
|
|
8
|
+
MCP_CLIENT_LABEL,
|
|
9
|
+
SDK_KEY_LABEL,
|
|
10
|
+
SDK_KEY_SCOPES,
|
|
4
11
|
apiCall,
|
|
5
12
|
apiKeyHeaders,
|
|
6
13
|
apiKeyHint,
|
|
14
|
+
buildHostedMcpServerBlock,
|
|
7
15
|
buildMcpServerBlock,
|
|
8
16
|
buildMcpServerName,
|
|
9
17
|
cliSetupDeepLink,
|
|
10
18
|
consoleUrl,
|
|
11
|
-
|
|
12
|
-
detectPackageManager,
|
|
19
|
+
describeUnsafeSdkKey,
|
|
13
20
|
die,
|
|
14
21
|
ensureClientId,
|
|
15
|
-
envVarsToWrite,
|
|
16
22
|
fetchIngestSetup,
|
|
17
23
|
fmtDate,
|
|
18
|
-
installCommand,
|
|
19
24
|
installSignalHandlers,
|
|
25
|
+
isMcpClient,
|
|
20
26
|
listProfiles,
|
|
21
27
|
loadConfig,
|
|
22
28
|
maybeShowTelemetryNotice,
|
|
@@ -27,8 +33,8 @@ import {
|
|
|
27
33
|
printKeyExportHint,
|
|
28
34
|
printResult,
|
|
29
35
|
probeEndpointHealth,
|
|
36
|
+
probeKeyScope,
|
|
30
37
|
projectIdHint,
|
|
31
|
-
readPackageJson,
|
|
32
38
|
reportsUrl,
|
|
33
39
|
requireConfig,
|
|
34
40
|
requireUuid,
|
|
@@ -41,16 +47,26 @@ import {
|
|
|
41
47
|
sanitizeEndpoint,
|
|
42
48
|
sanitizeProjectId,
|
|
43
49
|
saveConfig,
|
|
50
|
+
savedSdkKeyFor,
|
|
44
51
|
setActiveProfile,
|
|
45
52
|
setGlobalOutputFormat,
|
|
46
53
|
trySaveKeyToKeychain,
|
|
47
54
|
writeMcpServerEntry
|
|
48
|
-
} from "./chunk-
|
|
55
|
+
} from "./chunk-FJETUM25.js";
|
|
49
56
|
import {
|
|
50
57
|
MUSHI_CLI_VERSION,
|
|
51
58
|
MUSHI_MCP_PIN_SPEC
|
|
52
|
-
} from "./chunk-
|
|
59
|
+
} from "./chunk-UVO4A2G6.js";
|
|
60
|
+
import {
|
|
61
|
+
FRAMEWORKS,
|
|
62
|
+
detectFramework,
|
|
63
|
+
detectPackageManager,
|
|
64
|
+
envVarsToWrite,
|
|
65
|
+
installCommand,
|
|
66
|
+
readPackageJson
|
|
67
|
+
} from "./chunk-KOBEOGNF.js";
|
|
53
68
|
import {
|
|
69
|
+
CLOUD_API_ENDPOINT,
|
|
54
70
|
TEST_REPORT_FETCH_TIMEOUT_MS,
|
|
55
71
|
assertEndpoint,
|
|
56
72
|
normalizeEndpoint,
|
|
@@ -315,162 +331,6 @@ async function revokeProjectKey(endpoint, cliToken, projectId, keyPrefix, newKey
|
|
|
315
331
|
);
|
|
316
332
|
}
|
|
317
333
|
|
|
318
|
-
// src/freshness.ts
|
|
319
|
-
var REGISTRY = "https://registry.npmjs.org";
|
|
320
|
-
var DEFAULT_TIMEOUT_MS = 2e3;
|
|
321
|
-
async function checkFreshness(packageName, currentVersion, opts = {}) {
|
|
322
|
-
if (!opts.ignoreOptOut && process.env.MUSHI_NO_UPDATE_CHECK === "1") return null;
|
|
323
|
-
const registry = opts.registry ?? REGISTRY;
|
|
324
|
-
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
325
|
-
const controller = new AbortController();
|
|
326
|
-
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
327
|
-
try {
|
|
328
|
-
const res = await fetch(
|
|
329
|
-
`${registry}/${encodeURIComponent(packageName)}/latest`,
|
|
330
|
-
{
|
|
331
|
-
signal: controller.signal,
|
|
332
|
-
headers: { Accept: "application/json" }
|
|
333
|
-
}
|
|
334
|
-
);
|
|
335
|
-
if (!res.ok) return null;
|
|
336
|
-
const body = await res.json();
|
|
337
|
-
const latest = typeof body.version === "string" ? body.version : null;
|
|
338
|
-
if (!latest) return null;
|
|
339
|
-
return {
|
|
340
|
-
current: currentVersion,
|
|
341
|
-
latest,
|
|
342
|
-
isOutdated: isNewerStableVersion(latest, currentVersion)
|
|
343
|
-
};
|
|
344
|
-
} catch {
|
|
345
|
-
return null;
|
|
346
|
-
} finally {
|
|
347
|
-
clearTimeout(timer);
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
function isNewerStableVersion(latest, current) {
|
|
351
|
-
const latestCore = stripPreRelease(latest);
|
|
352
|
-
if (hasPreReleaseTag(latest)) return false;
|
|
353
|
-
const [la, lb, lc] = parse(latestCore);
|
|
354
|
-
const [ca, cb, cc] = parse(stripPreRelease(current));
|
|
355
|
-
if (la !== ca) return la > ca;
|
|
356
|
-
if (lb !== cb) return lb > cb;
|
|
357
|
-
return lc > cc;
|
|
358
|
-
}
|
|
359
|
-
function stripPreRelease(version) {
|
|
360
|
-
const idx = version.search(/[-+]/);
|
|
361
|
-
return idx === -1 ? version : version.slice(0, idx);
|
|
362
|
-
}
|
|
363
|
-
function hasPreReleaseTag(version) {
|
|
364
|
-
const plus = version.indexOf("+");
|
|
365
|
-
const hyphen = version.indexOf("-");
|
|
366
|
-
return hyphen !== -1 && (plus === -1 || hyphen < plus);
|
|
367
|
-
}
|
|
368
|
-
function parse(version) {
|
|
369
|
-
const parts = version.split(".").map((part) => Number(part));
|
|
370
|
-
return [
|
|
371
|
-
Number.isFinite(parts[0]) ? parts[0] : 0,
|
|
372
|
-
Number.isFinite(parts[1]) ? parts[1] : 0,
|
|
373
|
-
Number.isFinite(parts[2]) ? parts[2] : 0
|
|
374
|
-
];
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
// src/monorepo.ts
|
|
378
|
-
import { existsSync, readFileSync, readdirSync, statSync } from "fs";
|
|
379
|
-
import { dirname, join, resolve } from "path";
|
|
380
|
-
var WORKSPACE_GLOB_CANDIDATES = ["apps/*", "packages/*", "examples/*"];
|
|
381
|
-
var FRAMEWORK_DEPS = {
|
|
382
|
-
next: "Next.js",
|
|
383
|
-
nuxt: "Nuxt",
|
|
384
|
-
"@sveltejs/kit": "SvelteKit",
|
|
385
|
-
"@angular/core": "Angular",
|
|
386
|
-
expo: "Expo",
|
|
387
|
-
"react-native": "React Native",
|
|
388
|
-
"@capacitor/core": "Capacitor",
|
|
389
|
-
svelte: "Svelte",
|
|
390
|
-
vue: "Vue",
|
|
391
|
-
react: "React"
|
|
392
|
-
};
|
|
393
|
-
function detectWorkspaceHint(cwd) {
|
|
394
|
-
const root = findWorkspaceRoot(cwd);
|
|
395
|
-
if (!root) return null;
|
|
396
|
-
const rootPkg = readPackageJsonSafely(join(root, "package.json"));
|
|
397
|
-
if (rootPkg && getFrameworkFromPkg(rootPkg)) return null;
|
|
398
|
-
const source = existsSync(join(root, "pnpm-workspace.yaml")) ? "pnpm-workspace" : root === cwd ? "package-json" : "parent";
|
|
399
|
-
const apps = collectAppsFromGlobs(root);
|
|
400
|
-
if (apps.length === 0) return null;
|
|
401
|
-
return { root, apps, source };
|
|
402
|
-
}
|
|
403
|
-
function findWorkspaceRoot(start) {
|
|
404
|
-
let dir = resolve(start);
|
|
405
|
-
for (let i = 0; i < 8; i++) {
|
|
406
|
-
if (isWorkspaceRoot(dir)) return dir;
|
|
407
|
-
const parent = dirname(dir);
|
|
408
|
-
if (parent === dir) break;
|
|
409
|
-
dir = parent;
|
|
410
|
-
}
|
|
411
|
-
return null;
|
|
412
|
-
}
|
|
413
|
-
function isWorkspaceRoot(dir) {
|
|
414
|
-
if (existsSync(join(dir, "pnpm-workspace.yaml"))) return true;
|
|
415
|
-
const pkg = readPackageJsonSafely(join(dir, "package.json"));
|
|
416
|
-
if (!pkg) return false;
|
|
417
|
-
return Boolean(pkg.workspaces);
|
|
418
|
-
}
|
|
419
|
-
function collectAppsFromGlobs(root) {
|
|
420
|
-
const results = [];
|
|
421
|
-
for (const glob of WORKSPACE_GLOB_CANDIDATES) {
|
|
422
|
-
const prefix = glob.replace("/*", "");
|
|
423
|
-
const parentDir = join(root, prefix);
|
|
424
|
-
if (!existsSync(parentDir)) continue;
|
|
425
|
-
let entries;
|
|
426
|
-
try {
|
|
427
|
-
entries = readdirSync(parentDir);
|
|
428
|
-
} catch {
|
|
429
|
-
continue;
|
|
430
|
-
}
|
|
431
|
-
for (const entry of entries) {
|
|
432
|
-
const pkgPath = join(parentDir, entry, "package.json");
|
|
433
|
-
if (!isFileSafe(pkgPath)) continue;
|
|
434
|
-
const pkg = readPackageJsonSafely(pkgPath);
|
|
435
|
-
if (!pkg) continue;
|
|
436
|
-
const framework = getFrameworkFromPkg(pkg);
|
|
437
|
-
if (!framework) continue;
|
|
438
|
-
results.push({
|
|
439
|
-
name: pkg.name ?? `${prefix}/${entry}`,
|
|
440
|
-
relativePath: `${prefix}/${entry}`,
|
|
441
|
-
framework
|
|
442
|
-
});
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
return results;
|
|
446
|
-
}
|
|
447
|
-
function readPackageJsonSafely(path) {
|
|
448
|
-
if (!isFileSafe(path)) return null;
|
|
449
|
-
try {
|
|
450
|
-
return JSON.parse(readFileSync(path, "utf-8"));
|
|
451
|
-
} catch {
|
|
452
|
-
return null;
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
function isFileSafe(path) {
|
|
456
|
-
try {
|
|
457
|
-
return existsSync(path) && statSync(path).isFile();
|
|
458
|
-
} catch {
|
|
459
|
-
return false;
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
function getFrameworkFromPkg(pkg) {
|
|
463
|
-
const deps = {
|
|
464
|
-
...pkg.dependencies ?? {},
|
|
465
|
-
...pkg.devDependencies ?? {},
|
|
466
|
-
...pkg.peerDependencies ?? {}
|
|
467
|
-
};
|
|
468
|
-
for (const dep of Object.keys(FRAMEWORK_DEPS)) {
|
|
469
|
-
if (dep in deps) return FRAMEWORK_DEPS[dep];
|
|
470
|
-
}
|
|
471
|
-
return void 0;
|
|
472
|
-
}
|
|
473
|
-
|
|
474
334
|
// src/auth-ui.ts
|
|
475
335
|
var LINE = "\u2500".repeat(58);
|
|
476
336
|
function printAuthBanner(userCode, verificationUri) {
|
|
@@ -518,54 +378,449 @@ function printAuthFailed(reason, detail) {
|
|
|
518
378
|
console.log("");
|
|
519
379
|
}
|
|
520
380
|
|
|
521
|
-
// src/
|
|
522
|
-
function
|
|
523
|
-
return
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
381
|
+
// src/login.ts
|
|
382
|
+
function getPostLoginBannerMessage(opts) {
|
|
383
|
+
if (opts.suppressPostLoginBanner) return null;
|
|
384
|
+
if (opts.upgradeScope) {
|
|
385
|
+
return " Key upgraded! Re-run 'mushi setup' to update your IDE's MCP config, then restart the IDE.";
|
|
386
|
+
}
|
|
387
|
+
return " Run 'mushi init' to set up the SDK in this project.";
|
|
388
|
+
}
|
|
389
|
+
function printPostLoginBanner(opts) {
|
|
390
|
+
const message = getPostLoginBannerMessage(opts);
|
|
391
|
+
if (message) console.log(message);
|
|
392
|
+
}
|
|
393
|
+
var MAX_CONSECUTIVE_ERRORS = 5;
|
|
394
|
+
async function pollUntilApproved(endpoint, deviceData, deps) {
|
|
395
|
+
const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
396
|
+
const now = deps.now ?? Date.now;
|
|
397
|
+
let pollIntervalMs = (deviceData.interval ?? 5) * 1e3;
|
|
398
|
+
const deadline = now() + (deviceData.expires_in ?? 600) * 1e3;
|
|
399
|
+
let consecutiveErrors = 0;
|
|
400
|
+
let firstPoll = true;
|
|
401
|
+
while (now() < deadline) {
|
|
402
|
+
if (!firstPoll) {
|
|
403
|
+
await sleep(pollIntervalMs);
|
|
538
404
|
}
|
|
539
|
-
|
|
405
|
+
firstPoll = false;
|
|
406
|
+
const outcome = await deps.poll(endpoint, deviceData.device_code);
|
|
407
|
+
switch (outcome.status) {
|
|
408
|
+
case "approved":
|
|
409
|
+
return { status: "approved", cliToken: outcome.cliToken };
|
|
410
|
+
case "pending":
|
|
411
|
+
consecutiveErrors = 0;
|
|
412
|
+
continue;
|
|
413
|
+
case "slow_down":
|
|
414
|
+
consecutiveErrors = 0;
|
|
415
|
+
pollIntervalMs = Math.max(pollIntervalMs, outcome.retryAfterMs);
|
|
416
|
+
continue;
|
|
417
|
+
case "error":
|
|
418
|
+
if (outcome.retryable) {
|
|
419
|
+
consecutiveErrors += 1;
|
|
420
|
+
if (consecutiveErrors < MAX_CONSECUTIVE_ERRORS) continue;
|
|
421
|
+
}
|
|
422
|
+
return { status: "error", message: outcome.message };
|
|
423
|
+
case "denied":
|
|
424
|
+
return { status: "denied" };
|
|
425
|
+
case "expired":
|
|
426
|
+
return { status: "timeout" };
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
return { status: "timeout" };
|
|
540
430
|
}
|
|
541
|
-
async function
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
431
|
+
async function resolveProjectChoice(projectsList, ask) {
|
|
432
|
+
if (projectsList.length > 0) {
|
|
433
|
+
console.log("");
|
|
434
|
+
console.log(" Your projects:");
|
|
435
|
+
projectsList.forEach((p3, i) => {
|
|
436
|
+
console.log(` ${i + 1}. ${p3.name} (${p3.id})`);
|
|
546
437
|
});
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
438
|
+
console.log(` ${projectsList.length + 1}. Create a new project`);
|
|
439
|
+
console.log("");
|
|
440
|
+
const choice = await ask(` Pick a project [1-${projectsList.length + 1}]: `);
|
|
441
|
+
const num = parseInt(choice, 10);
|
|
442
|
+
if (num >= 1 && num <= projectsList.length) {
|
|
443
|
+
const picked = projectsList[num - 1];
|
|
444
|
+
return { kind: "picked", id: picked.id, name: picked.name };
|
|
445
|
+
}
|
|
555
446
|
}
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
447
|
+
console.log("");
|
|
448
|
+
const newName = (await ask(" Project name: ")).trim();
|
|
449
|
+
if (!newName) return { kind: "empty_name" };
|
|
450
|
+
return { kind: "create", name: newName };
|
|
451
|
+
}
|
|
452
|
+
function slugify(value) {
|
|
453
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
454
|
+
}
|
|
455
|
+
function defaultProjectName(packageName, cwd) {
|
|
456
|
+
const fromPkg = packageName?.replace(/^@[^/]+\//, "").trim();
|
|
457
|
+
if (fromPkg) return fromPkg;
|
|
458
|
+
const parts = cwd.split(/[\\/]/).filter(Boolean);
|
|
459
|
+
return parts[parts.length - 1] || "my-app";
|
|
460
|
+
}
|
|
461
|
+
function chooseProjectNonInteractive(projects, preferredName) {
|
|
462
|
+
const wanted = slugify(preferredName);
|
|
463
|
+
const match = projects.find((p3) => slugify(p3.name) === wanted || p3.slug !== void 0 && slugify(p3.slug) === wanted);
|
|
464
|
+
return match ? { kind: "existing", id: match.id, name: match.name } : { kind: "new", name: preferredName };
|
|
465
|
+
}
|
|
466
|
+
async function runLogin(opts = {}) {
|
|
467
|
+
const { CLOUD_API_ENDPOINT: CLOUD_API_ENDPOINT2, resolveCloudEndpoint: resolveCloudEndpoint2 } = await import("./endpoint-TEBF4IH6.js");
|
|
468
|
+
const endpoint = opts.endpoint ? resolveCloudEndpoint2(opts.endpoint) : CLOUD_API_ENDPOINT2;
|
|
469
|
+
const consoleBase = await resolveConsoleUrl();
|
|
470
|
+
let apiKey = opts.apiKey;
|
|
471
|
+
let projectId = opts.projectId;
|
|
472
|
+
if (apiKey) {
|
|
473
|
+
const config2 = loadConfig();
|
|
474
|
+
config2.apiKey = apiKey;
|
|
475
|
+
config2.endpoint = endpoint;
|
|
476
|
+
if (projectId) config2.projectId = projectId;
|
|
477
|
+
config2.consoleUrl = consoleBase;
|
|
478
|
+
saveConfig(config2);
|
|
479
|
+
console.log("");
|
|
480
|
+
console.log(" \u2713 Credentials saved.");
|
|
481
|
+
const verifyResult = await apiCall("/v1/sync/whoami", { apiKey, endpoint, projectId });
|
|
482
|
+
if (!verifyResult.ok) {
|
|
483
|
+
console.warn(` \u26A0 Key saved, but verification failed: ${verifyResult.error?.message ?? "unknown error"}`);
|
|
484
|
+
console.warn(` Check your key at: ${cliSetupDeepLink(consoleBase)}`);
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
const d = verifyResult.data;
|
|
488
|
+
if (d.project_name) {
|
|
489
|
+
if (!projectId && d.project_id) {
|
|
490
|
+
config2.projectId = d.project_id;
|
|
491
|
+
saveConfig(config2);
|
|
492
|
+
}
|
|
493
|
+
console.log(` \u2713 Project: ${d.project_name} (${d.project_id})`);
|
|
494
|
+
}
|
|
495
|
+
console.log(` \u2713 Endpoint: ${endpoint}`);
|
|
496
|
+
console.log("");
|
|
497
|
+
if (!opts.suppressPostLoginBanner) {
|
|
498
|
+
console.log(" Run 'mushi whoami' to verify \xB7 'mushi init' to set up the SDK");
|
|
499
|
+
}
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
if (!opts.suppressPostLoginBanner) {
|
|
503
|
+
console.log("");
|
|
504
|
+
}
|
|
505
|
+
if (opts.upgradeScope) {
|
|
506
|
+
console.log(" Mushi login \u2014 scope upgrade");
|
|
507
|
+
console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
508
|
+
console.log(" Re-authenticating to mint a new key with report:write + mcp:read + mcp:write scopes.");
|
|
509
|
+
console.log(" This lets the CLI admin commands (including writes) and MCP server both work with a single key.");
|
|
510
|
+
console.log("");
|
|
511
|
+
} else if (!opts.suppressPostLoginBanner) {
|
|
512
|
+
console.log(" Mushi login");
|
|
513
|
+
console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
514
|
+
}
|
|
515
|
+
let deviceData;
|
|
516
|
+
try {
|
|
517
|
+
deviceData = await startDeviceAuth(endpoint, ensureClientId());
|
|
518
|
+
} catch (err) {
|
|
519
|
+
process.stderr.write(`
|
|
520
|
+
error: Could not start login session: ${err instanceof Error ? err.message : String(err)}
|
|
521
|
+
`);
|
|
522
|
+
process.stderr.write(` Fallback: mushi login --api-key <key> --project-id <uuid>
|
|
523
|
+
`);
|
|
524
|
+
process.exit(1);
|
|
525
|
+
}
|
|
526
|
+
const verifyUrl = deviceData.verification_uri;
|
|
527
|
+
if (opts.browser !== false) {
|
|
528
|
+
try {
|
|
529
|
+
await openInBrowser(verifyUrl);
|
|
530
|
+
} catch {
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
printAuthBanner(deviceData.user_code, verifyUrl);
|
|
534
|
+
const pollResult = await pollUntilApproved(endpoint, deviceData, {
|
|
535
|
+
poll: pollDeviceToken
|
|
536
|
+
});
|
|
537
|
+
if (pollResult.status !== "approved") {
|
|
538
|
+
if (pollResult.status === "denied") printAuthFailed("denied");
|
|
539
|
+
else if (pollResult.status === "error") printAuthFailed("error", pollResult.message);
|
|
540
|
+
else printAuthFailed("timeout");
|
|
541
|
+
process.exit(1);
|
|
542
|
+
}
|
|
543
|
+
const cliToken = pollResult.cliToken;
|
|
544
|
+
printAuthApproved();
|
|
545
|
+
const projectsList = await listProjects(endpoint, cliToken);
|
|
546
|
+
let chosenProjectId = projectId;
|
|
547
|
+
let chosenProjectName;
|
|
548
|
+
const mintScopes = opts.upgradeScope ? FULL_CLI_KEY_SCOPES : CLI_KEY_SCOPES;
|
|
549
|
+
if (!chosenProjectId) {
|
|
550
|
+
let choice;
|
|
551
|
+
if (process.stdin.isTTY) {
|
|
552
|
+
const { createInterface } = await import("readline");
|
|
553
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
554
|
+
const ask = (q) => new Promise((resolve3) => rl.question(q, (a) => resolve3(a.trim())));
|
|
555
|
+
choice = await resolveProjectChoice(projectsList, ask);
|
|
556
|
+
rl.close();
|
|
557
|
+
} else {
|
|
558
|
+
const { readPackageJson: readPackageJson2 } = await import("./detect-4PYRZXVK.js");
|
|
559
|
+
const cwd = process.cwd();
|
|
560
|
+
const nonInteractive = chooseProjectNonInteractive(
|
|
561
|
+
projectsList,
|
|
562
|
+
defaultProjectName(readPackageJson2(cwd)?.name, cwd)
|
|
563
|
+
);
|
|
564
|
+
choice = nonInteractive.kind === "existing" ? { kind: "picked", id: nonInteractive.id, name: nonInteractive.name } : { kind: "create", name: nonInteractive.name };
|
|
565
|
+
console.log(
|
|
566
|
+
choice.kind === "picked" ? ` Using project "${choice.name}" (matches this folder \u2014 pass --project-id to pick another).` : ` Creating project "${choice.name}" (no project matches this folder \u2014 pass --project-id to use an existing one).`
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
if (choice.kind === "empty_name") {
|
|
570
|
+
process.stderr.write("\nerror: Project name is required.\n");
|
|
571
|
+
process.exit(2);
|
|
572
|
+
}
|
|
573
|
+
if (choice.kind === "picked") {
|
|
574
|
+
chosenProjectId = choice.id;
|
|
575
|
+
chosenProjectName = choice.name;
|
|
576
|
+
} else {
|
|
577
|
+
try {
|
|
578
|
+
const created = await createProject(endpoint, cliToken, choice.name, { scopes: mintScopes });
|
|
579
|
+
chosenProjectId = created.id;
|
|
580
|
+
chosenProjectName = created.name;
|
|
581
|
+
apiKey = created.apiKey ?? void 0;
|
|
582
|
+
console.log(` \u2713 Created project "${chosenProjectName}"`);
|
|
583
|
+
} catch (err) {
|
|
584
|
+
process.stderr.write(`
|
|
585
|
+
error: Could not create project: ${err instanceof Error ? err.message : String(err)}
|
|
586
|
+
`);
|
|
587
|
+
process.exit(1);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
if (!apiKey && chosenProjectId) {
|
|
592
|
+
try {
|
|
593
|
+
apiKey = await mintProjectKey(endpoint, cliToken, chosenProjectId, { scopes: mintScopes }) ?? void 0;
|
|
594
|
+
} catch {
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
const config = loadConfig();
|
|
598
|
+
config.endpoint = endpoint;
|
|
599
|
+
config.consoleUrl = consoleBase;
|
|
600
|
+
if (chosenProjectId) config.projectId = chosenProjectId;
|
|
601
|
+
if (apiKey) config.apiKey = apiKey;
|
|
602
|
+
saveConfig(config);
|
|
603
|
+
if (apiKey) {
|
|
604
|
+
const profile = resolveProfileName(void 0, config.activeProfile);
|
|
605
|
+
trySaveKeyToKeychain(apiKey, profile);
|
|
606
|
+
}
|
|
607
|
+
console.log("");
|
|
608
|
+
if (chosenProjectName) console.log(` \u2713 Project: ${chosenProjectName}`);
|
|
609
|
+
if (apiKey) {
|
|
610
|
+
if (opts.upgradeScope) {
|
|
611
|
+
console.log(` \u2713 Full-scope CLI key saved (${apiKey.slice(0, 12)}\u2026)`);
|
|
612
|
+
console.log(` Scopes: report:write \xB7 mcp:read \xB7 mcp:write \u2014 SDK + MCP + admin CLI (incl. writes) all work with this key`);
|
|
613
|
+
} else {
|
|
614
|
+
console.log(` \u2713 CLI key saved (${apiKey.slice(0, 12)}\u2026)`);
|
|
615
|
+
console.log(` Scopes: report:write \xB7 mcp:read \u2014 SDK ingest + MCP read tools.`);
|
|
616
|
+
console.log(` Admin writes (billing cap, fix merge, pipeline start) need: mushi login --upgrade-scope`);
|
|
617
|
+
}
|
|
618
|
+
} else {
|
|
619
|
+
console.log(` \u2139 Get an SDK key at: ${apiKeyHint(consoleBase)}`);
|
|
620
|
+
}
|
|
621
|
+
console.log("");
|
|
622
|
+
printPostLoginBanner(opts);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// src/freshness.ts
|
|
626
|
+
var REGISTRY = "https://registry.npmjs.org";
|
|
627
|
+
var DEFAULT_TIMEOUT_MS = 2e3;
|
|
628
|
+
async function checkFreshness(packageName, currentVersion, opts = {}) {
|
|
629
|
+
if (!opts.ignoreOptOut && process.env.MUSHI_NO_UPDATE_CHECK === "1") return null;
|
|
630
|
+
const registry = opts.registry ?? REGISTRY;
|
|
631
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
632
|
+
const controller = new AbortController();
|
|
633
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
634
|
+
try {
|
|
635
|
+
const res = await fetch(
|
|
636
|
+
`${registry}/${encodeURIComponent(packageName)}/latest`,
|
|
561
637
|
{
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
detail: "No endpoint configured \u2014 run `npx mushi-mushi` or pass MUSHI_API_ENDPOINT."
|
|
638
|
+
signal: controller.signal,
|
|
639
|
+
headers: { Accept: "application/json" }
|
|
565
640
|
}
|
|
566
|
-
|
|
641
|
+
);
|
|
642
|
+
if (!res.ok) return null;
|
|
643
|
+
const body = await res.json();
|
|
644
|
+
const latest = typeof body.version === "string" ? body.version : null;
|
|
645
|
+
if (!latest) return null;
|
|
646
|
+
return {
|
|
647
|
+
current: currentVersion,
|
|
648
|
+
latest,
|
|
649
|
+
isOutdated: isNewerStableVersion(latest, currentVersion)
|
|
650
|
+
};
|
|
651
|
+
} catch {
|
|
652
|
+
return null;
|
|
653
|
+
} finally {
|
|
654
|
+
clearTimeout(timer);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
function isNewerStableVersion(latest, current) {
|
|
658
|
+
const latestCore = stripPreRelease(latest);
|
|
659
|
+
if (hasPreReleaseTag(latest)) return false;
|
|
660
|
+
const [la, lb, lc] = parse(latestCore);
|
|
661
|
+
const [ca, cb, cc] = parse(stripPreRelease(current));
|
|
662
|
+
if (la !== ca) return la > ca;
|
|
663
|
+
if (lb !== cb) return lb > cb;
|
|
664
|
+
return lc > cc;
|
|
665
|
+
}
|
|
666
|
+
function stripPreRelease(version) {
|
|
667
|
+
const idx = version.search(/[-+]/);
|
|
668
|
+
return idx === -1 ? version : version.slice(0, idx);
|
|
669
|
+
}
|
|
670
|
+
function hasPreReleaseTag(version) {
|
|
671
|
+
const plus = version.indexOf("+");
|
|
672
|
+
const hyphen = version.indexOf("-");
|
|
673
|
+
return hyphen !== -1 && (plus === -1 || hyphen < plus);
|
|
674
|
+
}
|
|
675
|
+
function parse(version) {
|
|
676
|
+
const parts = version.split(".").map((part) => Number(part));
|
|
677
|
+
return [
|
|
678
|
+
Number.isFinite(parts[0]) ? parts[0] : 0,
|
|
679
|
+
Number.isFinite(parts[1]) ? parts[1] : 0,
|
|
680
|
+
Number.isFinite(parts[2]) ? parts[2] : 0
|
|
681
|
+
];
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
// src/monorepo.ts
|
|
685
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "fs";
|
|
686
|
+
import { dirname, join, resolve } from "path";
|
|
687
|
+
var WORKSPACE_GLOB_CANDIDATES = ["apps/*", "packages/*", "examples/*"];
|
|
688
|
+
var FRAMEWORK_DEPS = {
|
|
689
|
+
next: "Next.js",
|
|
690
|
+
nuxt: "Nuxt",
|
|
691
|
+
"@sveltejs/kit": "SvelteKit",
|
|
692
|
+
"@angular/core": "Angular",
|
|
693
|
+
expo: "Expo",
|
|
694
|
+
"react-native": "React Native",
|
|
695
|
+
"@capacitor/core": "Capacitor",
|
|
696
|
+
svelte: "Svelte",
|
|
697
|
+
vue: "Vue",
|
|
698
|
+
react: "React"
|
|
699
|
+
};
|
|
700
|
+
function detectWorkspaceHint(cwd) {
|
|
701
|
+
const root = findWorkspaceRoot(cwd);
|
|
702
|
+
if (!root) return null;
|
|
703
|
+
const rootPkg = readPackageJsonSafely(join(root, "package.json"));
|
|
704
|
+
if (rootPkg && getFrameworkFromPkg(rootPkg)) return null;
|
|
705
|
+
const source = existsSync(join(root, "pnpm-workspace.yaml")) ? "pnpm-workspace" : root === cwd ? "package-json" : "parent";
|
|
706
|
+
const apps = collectAppsFromGlobs(root);
|
|
707
|
+
if (apps.length === 0) return null;
|
|
708
|
+
return { root, apps, source };
|
|
709
|
+
}
|
|
710
|
+
function findWorkspaceRoot(start) {
|
|
711
|
+
let dir = resolve(start);
|
|
712
|
+
for (let i = 0; i < 8; i++) {
|
|
713
|
+
if (isWorkspaceRoot(dir)) return dir;
|
|
714
|
+
const parent = dirname(dir);
|
|
715
|
+
if (parent === dir) break;
|
|
716
|
+
dir = parent;
|
|
567
717
|
}
|
|
568
|
-
|
|
718
|
+
return null;
|
|
719
|
+
}
|
|
720
|
+
function isWorkspaceRoot(dir) {
|
|
721
|
+
if (existsSync(join(dir, "pnpm-workspace.yaml"))) return true;
|
|
722
|
+
const pkg = readPackageJsonSafely(join(dir, "package.json"));
|
|
723
|
+
if (!pkg) return false;
|
|
724
|
+
return Boolean(pkg.workspaces);
|
|
725
|
+
}
|
|
726
|
+
function collectAppsFromGlobs(root) {
|
|
727
|
+
const results = [];
|
|
728
|
+
for (const glob of WORKSPACE_GLOB_CANDIDATES) {
|
|
729
|
+
const prefix = glob.replace("/*", "");
|
|
730
|
+
const parentDir = join(root, prefix);
|
|
731
|
+
if (!existsSync(parentDir)) continue;
|
|
732
|
+
let entries;
|
|
733
|
+
try {
|
|
734
|
+
entries = readdirSync(parentDir);
|
|
735
|
+
} catch {
|
|
736
|
+
continue;
|
|
737
|
+
}
|
|
738
|
+
for (const entry of entries) {
|
|
739
|
+
const pkgPath = join(parentDir, entry, "package.json");
|
|
740
|
+
if (!isFileSafe(pkgPath)) continue;
|
|
741
|
+
const pkg = readPackageJsonSafely(pkgPath);
|
|
742
|
+
if (!pkg) continue;
|
|
743
|
+
const framework = getFrameworkFromPkg(pkg);
|
|
744
|
+
if (!framework) continue;
|
|
745
|
+
results.push({
|
|
746
|
+
name: pkg.name ?? `${prefix}/${entry}`,
|
|
747
|
+
relativePath: `${prefix}/${entry}`,
|
|
748
|
+
framework
|
|
749
|
+
});
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
return results;
|
|
753
|
+
}
|
|
754
|
+
function readPackageJsonSafely(path) {
|
|
755
|
+
if (!isFileSafe(path)) return null;
|
|
756
|
+
try {
|
|
757
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
758
|
+
} catch {
|
|
759
|
+
return null;
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
function isFileSafe(path) {
|
|
763
|
+
try {
|
|
764
|
+
return existsSync(path) && statSync(path).isFile();
|
|
765
|
+
} catch {
|
|
766
|
+
return false;
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
function getFrameworkFromPkg(pkg) {
|
|
770
|
+
const deps = {
|
|
771
|
+
...pkg.dependencies ?? {},
|
|
772
|
+
...pkg.devDependencies ?? {},
|
|
773
|
+
...pkg.peerDependencies ?? {}
|
|
774
|
+
};
|
|
775
|
+
for (const dep of Object.keys(FRAMEWORK_DEPS)) {
|
|
776
|
+
if (dep in deps) return FRAMEWORK_DEPS[dep];
|
|
777
|
+
}
|
|
778
|
+
return void 0;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// src/doctor.ts
|
|
782
|
+
var SIGN_IN_HINT = "Run `npx mushi-mushi` in your app folder \u2014 browser sign-in saves the API key, project and endpoint.";
|
|
783
|
+
function checkCliConfig(config) {
|
|
784
|
+
return [
|
|
785
|
+
{
|
|
786
|
+
// No saved endpoint is not a failure: every command falls back to Mushi
|
|
787
|
+
// Cloud (endpoint.ts), so reporting "No endpoint" first sent people
|
|
788
|
+
// hunting for a URL they never needed.
|
|
789
|
+
name: "CLI config file",
|
|
790
|
+
ok: true,
|
|
791
|
+
detail: config.endpoint ? `endpoint=${config.endpoint}` : `endpoint=${CLOUD_API_ENDPOINT} (Mushi Cloud default \u2014 set MUSHI_API_ENDPOINT for a self-hosted backend)`
|
|
792
|
+
},
|
|
793
|
+
{
|
|
794
|
+
name: "API key configured",
|
|
795
|
+
ok: Boolean(config.apiKey),
|
|
796
|
+
detail: config.apiKey ? `apiKey=${config.apiKey.slice(0, 8)}\u2026${config.apiKey.slice(-4)}` : "No API key saved."
|
|
797
|
+
},
|
|
798
|
+
{
|
|
799
|
+
name: "Project ID configured",
|
|
800
|
+
ok: Boolean(config.projectId),
|
|
801
|
+
detail: config.projectId ? `projectId=${config.projectId}` : "No default project saved."
|
|
802
|
+
}
|
|
803
|
+
];
|
|
804
|
+
}
|
|
805
|
+
async function checkEndpointReachability(endpoint, doFetch = globalThis.fetch) {
|
|
806
|
+
try {
|
|
807
|
+
const safeEndpoint = sanitizeEndpoint(endpoint);
|
|
808
|
+
const res = await doFetch(`${safeEndpoint}/health`, {
|
|
809
|
+
signal: AbortSignal.timeout(5e3)
|
|
810
|
+
});
|
|
811
|
+
return {
|
|
812
|
+
name: "Endpoint reachable",
|
|
813
|
+
ok: res.status === 200,
|
|
814
|
+
detail: `GET ${safeEndpoint}/health \u2192 ${res.status}`
|
|
815
|
+
};
|
|
816
|
+
} catch (err) {
|
|
817
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
818
|
+
return { name: "Endpoint reachable", ok: false, detail: `Fetch failed: ${msg}` };
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
async function checkCliAuthPath(config, doFetch = globalThis.fetch) {
|
|
822
|
+
const checks = [];
|
|
823
|
+
const base = sanitizeEndpoint(config.endpoint ?? CLOUD_API_ENDPOINT);
|
|
569
824
|
let dateHeader = null;
|
|
570
825
|
try {
|
|
571
826
|
const res = await doFetch(`${base}/v1/cli/auth/device/token`, {
|
|
@@ -639,22 +894,27 @@ async function checkCliAuthPath(config, doFetch = globalThis.fetch) {
|
|
|
639
894
|
async function checkSdkInstall(cwd) {
|
|
640
895
|
try {
|
|
641
896
|
const { readFile: readFile3 } = await import("fs/promises");
|
|
642
|
-
const { join:
|
|
897
|
+
const { join: join5, resolve: resolve3 } = await import("path");
|
|
643
898
|
const root = resolve3(cwd);
|
|
644
|
-
const pkgPath =
|
|
899
|
+
const pkgPath = join5(root, "package.json");
|
|
645
900
|
const pkg = JSON.parse(await readFile3(pkgPath, "utf8"));
|
|
646
901
|
const deps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
|
|
647
902
|
const sdks = [
|
|
648
903
|
"@mushi-mushi/react",
|
|
649
904
|
"@mushi-mushi/web",
|
|
650
905
|
"@mushi-mushi/core",
|
|
651
|
-
"@mushi-mushi/react-native"
|
|
906
|
+
"@mushi-mushi/react-native",
|
|
907
|
+
"@mushi-mushi/vue",
|
|
908
|
+
"@mushi-mushi/svelte",
|
|
909
|
+
"@mushi-mushi/angular",
|
|
910
|
+
"@mushi-mushi/capacitor",
|
|
911
|
+
"@mushi-mushi/node"
|
|
652
912
|
];
|
|
653
913
|
const installed = sdks.filter((s) => deps[s]);
|
|
654
914
|
return {
|
|
655
915
|
name: "SDK installed in this repo",
|
|
656
916
|
ok: installed.length > 0,
|
|
657
|
-
detail: installed.length > 0 ? installed.map((s) => `${s}@${deps[s]}`).join(", ") : "No @mushi-mushi/* package in package.json \u2014 run `mushi
|
|
917
|
+
detail: installed.length > 0 ? installed.map((s) => `${s}@${deps[s]}`).join(", ") : "No @mushi-mushi/* package in package.json \u2014 run `npx mushi-mushi` to install"
|
|
658
918
|
};
|
|
659
919
|
} catch {
|
|
660
920
|
return null;
|
|
@@ -846,9 +1106,9 @@ async function checkHostAppWiring(cwd) {
|
|
|
846
1106
|
const checks = [];
|
|
847
1107
|
try {
|
|
848
1108
|
const { readFile: readFile3, access } = await import("fs/promises");
|
|
849
|
-
const { join:
|
|
1109
|
+
const { join: join5, resolve: resolve3 } = await import("path");
|
|
850
1110
|
const root = resolve3(cwd);
|
|
851
|
-
const pkgPath =
|
|
1111
|
+
const pkgPath = join5(root, "package.json");
|
|
852
1112
|
const pkg = JSON.parse(await readFile3(pkgPath, "utf8"));
|
|
853
1113
|
const deps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
|
|
854
1114
|
const isCapHybrid = Boolean(deps["@capacitor/core"] && deps["react"]);
|
|
@@ -856,7 +1116,7 @@ async function checkHostAppWiring(cwd) {
|
|
|
856
1116
|
let envContent = "";
|
|
857
1117
|
for (const f of envCandidates) {
|
|
858
1118
|
try {
|
|
859
|
-
envContent = await readFile3(
|
|
1119
|
+
envContent = await readFile3(join5(root, f), "utf8");
|
|
860
1120
|
break;
|
|
861
1121
|
} catch {
|
|
862
1122
|
}
|
|
@@ -870,16 +1130,19 @@ async function checkHostAppWiring(cwd) {
|
|
|
870
1130
|
ok: hasProjectId && hasApiKey,
|
|
871
1131
|
detail: hasProjectId && hasApiKey ? "VITE_/MUSHI_ project id + API key found" : "Run `mushi connect --write-env` or add VITE_MUSHI_PROJECT_ID + VITE_MUSHI_API_KEY"
|
|
872
1132
|
});
|
|
873
|
-
let
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
1133
|
+
let mcpFound = null;
|
|
1134
|
+
for (const rel of [".mcp.json", join5(".cursor", "mcp.json")]) {
|
|
1135
|
+
try {
|
|
1136
|
+
await access(join5(root, rel));
|
|
1137
|
+
mcpFound = rel;
|
|
1138
|
+
break;
|
|
1139
|
+
} catch {
|
|
1140
|
+
}
|
|
878
1141
|
}
|
|
879
1142
|
checks.push({
|
|
880
|
-
name: "[host]
|
|
881
|
-
ok:
|
|
882
|
-
detail:
|
|
1143
|
+
name: "[host] MCP config",
|
|
1144
|
+
ok: mcpFound !== null,
|
|
1145
|
+
detail: mcpFound ? `${mcpFound} present` : "No .mcp.json (Claude Code) or .cursor/mcp.json (Cursor) \u2014 run `npx mushi-mushi setup --ide claude` or `--ide cursor` for two-way reporter replies"
|
|
883
1146
|
});
|
|
884
1147
|
if (isCapHybrid) {
|
|
885
1148
|
const hasWebSdk = Boolean(deps["@mushi-mushi/web"] || deps["@mushi-mushi/react"]);
|
|
@@ -912,7 +1175,7 @@ var HASH_ROUTER_PATTERNS = [
|
|
|
912
1175
|
var HASH_TEMPLATE_PATTERN = /['"`]#\//;
|
|
913
1176
|
async function scanDirForPattern(dir, patterns, maxDepth = 6, maxFiles = 2e3) {
|
|
914
1177
|
const { readdir: readdir2, readFile: readFile3 } = await import("fs/promises");
|
|
915
|
-
const { join:
|
|
1178
|
+
const { join: join5 } = await import("path");
|
|
916
1179
|
let count = 0;
|
|
917
1180
|
async function walk(d, depth) {
|
|
918
1181
|
if (depth > maxDepth || count > maxFiles) return null;
|
|
@@ -924,7 +1187,7 @@ async function scanDirForPattern(dir, patterns, maxDepth = 6, maxFiles = 2e3) {
|
|
|
924
1187
|
}
|
|
925
1188
|
for (const e of entries) {
|
|
926
1189
|
if (e.name.startsWith(".") || e.name === "node_modules" || e.name === "dist") continue;
|
|
927
|
-
const full =
|
|
1190
|
+
const full = join5(d, e.name);
|
|
928
1191
|
if (e.isDirectory()) {
|
|
929
1192
|
const found = await walk(full, depth + 1);
|
|
930
1193
|
if (found) return found;
|
|
@@ -941,7 +1204,7 @@ async function scanDirForPattern(dir, patterns, maxDepth = 6, maxFiles = 2e3) {
|
|
|
941
1204
|
}
|
|
942
1205
|
const { resolve: resolve3 } = await import("path");
|
|
943
1206
|
for (const sub of ["src", "app", "pages", "lib", "components"]) {
|
|
944
|
-
const found = await walk(
|
|
1207
|
+
const found = await walk(join5(resolve3(dir), sub), 0);
|
|
945
1208
|
if (found) return found;
|
|
946
1209
|
}
|
|
947
1210
|
return walk(resolve3(dir), 0);
|
|
@@ -968,95 +1231,140 @@ async function checkHashRouterCapture(cwd) {
|
|
|
968
1231
|
}
|
|
969
1232
|
];
|
|
970
1233
|
}
|
|
971
|
-
|
|
972
|
-
|
|
1234
|
+
function isJsonObject(value) {
|
|
1235
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1236
|
+
}
|
|
1237
|
+
function normalizeProjectPath(path) {
|
|
1238
|
+
const slashed = path.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
1239
|
+
return process.platform === "win32" ? slashed.toLowerCase() : slashed;
|
|
1240
|
+
}
|
|
1241
|
+
async function collectMcpSources(root, home) {
|
|
973
1242
|
const { readFile: readFile3 } = await import("fs/promises");
|
|
974
|
-
const { join:
|
|
975
|
-
const
|
|
976
|
-
const
|
|
977
|
-
const
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
1243
|
+
const { join: join5 } = await import("path");
|
|
1244
|
+
const sources = [];
|
|
1245
|
+
const invalid = [];
|
|
1246
|
+
const plainFiles = [
|
|
1247
|
+
{ path: join5(root, ".mcp.json"), claudeCode: true },
|
|
1248
|
+
{ path: join5(root, ".cursor", "mcp.json"), claudeCode: false },
|
|
1249
|
+
{ path: join5(home, ".cursor", "mcp.json"), claudeCode: false }
|
|
1250
|
+
];
|
|
1251
|
+
for (const file of plainFiles) {
|
|
1252
|
+
let raw;
|
|
981
1253
|
try {
|
|
982
|
-
|
|
983
|
-
let parsed = {};
|
|
984
|
-
try {
|
|
985
|
-
parsed = JSON.parse(raw);
|
|
986
|
-
} catch {
|
|
987
|
-
}
|
|
988
|
-
const hasEntries = Object.keys(parsed.mcpServers ?? {}).length > 0;
|
|
989
|
-
if (!hasEntries && candidates.indexOf(candidate) < candidates.length - 1) {
|
|
990
|
-
continue;
|
|
991
|
-
}
|
|
992
|
-
mcpRaw = raw;
|
|
993
|
-
mcpPath = candidate;
|
|
994
|
-
break;
|
|
1254
|
+
raw = await readFile3(file.path, "utf8");
|
|
995
1255
|
} catch {
|
|
1256
|
+
continue;
|
|
1257
|
+
}
|
|
1258
|
+
try {
|
|
1259
|
+
const parsed = JSON.parse(raw);
|
|
1260
|
+
const servers = isJsonObject(parsed) && isJsonObject(parsed.mcpServers) ? parsed.mcpServers : {};
|
|
1261
|
+
sources.push({ path: file.path, servers, claudeCode: file.claudeCode });
|
|
1262
|
+
} catch {
|
|
1263
|
+
invalid.push(file.path);
|
|
996
1264
|
}
|
|
997
1265
|
}
|
|
998
|
-
|
|
1266
|
+
const claudeJsonPath = join5(home, ".claude.json");
|
|
1267
|
+
try {
|
|
1268
|
+
const parsed = JSON.parse(await readFile3(claudeJsonPath, "utf8"));
|
|
1269
|
+
if (isJsonObject(parsed)) {
|
|
1270
|
+
const servers = isJsonObject(parsed.mcpServers) ? { ...parsed.mcpServers } : {};
|
|
1271
|
+
const projects = isJsonObject(parsed.projects) ? parsed.projects : {};
|
|
1272
|
+
const wanted = normalizeProjectPath(root);
|
|
1273
|
+
for (const [projectPath, entry] of Object.entries(projects)) {
|
|
1274
|
+
if (normalizeProjectPath(projectPath) === wanted && isJsonObject(entry) && isJsonObject(entry.mcpServers)) {
|
|
1275
|
+
Object.assign(servers, entry.mcpServers);
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
sources.push({ path: claudeJsonPath, servers, claudeCode: true });
|
|
1279
|
+
}
|
|
1280
|
+
} catch {
|
|
1281
|
+
}
|
|
1282
|
+
return { sources, invalid };
|
|
1283
|
+
}
|
|
1284
|
+
function assessMushiEntry(source, name, entry, cliKeySaved) {
|
|
1285
|
+
const label = `${name} (${source.path})`;
|
|
1286
|
+
if (!isJsonObject(entry)) return { usable: false, stdio: false, detail: `${label}: not an object` };
|
|
1287
|
+
if (typeof entry.url === "string") {
|
|
1288
|
+
if (source.claudeCode && entry.type !== "http" && entry.type !== "sse") {
|
|
1289
|
+
return {
|
|
1290
|
+
usable: false,
|
|
1291
|
+
stdio: false,
|
|
1292
|
+
detail: `${label}: has a url but no "type": "http" \u2014 Claude Code skips it`
|
|
1293
|
+
};
|
|
1294
|
+
}
|
|
1295
|
+
return { usable: true, stdio: false, detail: `${label}: hosted OAuth \u2014 sign in from the IDE's MCP panel` };
|
|
1296
|
+
}
|
|
1297
|
+
const env = isJsonObject(entry.env) ? entry.env : {};
|
|
1298
|
+
const key = env.MUSHI_API_KEY;
|
|
1299
|
+
const endpoint = env.MUSHI_API_ENDPOINT;
|
|
1300
|
+
if (typeof endpoint === "string" && !/^https?:\/\//.test(endpoint) && !/^\$\{.+\}$/.test(endpoint)) {
|
|
1301
|
+
return { usable: false, stdio: true, detail: `${label}: MUSHI_API_ENDPOINT is not a URL` };
|
|
1302
|
+
}
|
|
1303
|
+
if (typeof key === "string" && key.startsWith("mushi_")) {
|
|
1304
|
+
return { usable: true, stdio: true, detail: `${label}: key written inline` };
|
|
1305
|
+
}
|
|
1306
|
+
if (key === void 0 || typeof key === "string" && /^\$\{.*\}$/.test(key)) {
|
|
1307
|
+
return cliKeySaved ? { usable: true, stdio: true, detail: `${label}: key from MUSHI_API_KEY or the saved CLI config` } : {
|
|
1308
|
+
usable: false,
|
|
1309
|
+
stdio: true,
|
|
1310
|
+
detail: `${label}: reads the key from MUSHI_API_KEY or the CLI config, and neither is set`
|
|
1311
|
+
};
|
|
1312
|
+
}
|
|
1313
|
+
return { usable: false, stdio: true, detail: `${label}: MUSHI_API_KEY is neither a mushi_ key nor a placeholder` };
|
|
1314
|
+
}
|
|
1315
|
+
async function checkMcpConfig(config, cwd, doFetch = globalThis.fetch, home) {
|
|
1316
|
+
const checks = [];
|
|
1317
|
+
const { resolve: resolve3 } = await import("path");
|
|
1318
|
+
const { homedir } = await import("os");
|
|
1319
|
+
const root = resolve3(cwd);
|
|
1320
|
+
const { sources, invalid } = await collectMcpSources(root, home ?? homedir());
|
|
1321
|
+
if (invalid.length > 0) {
|
|
999
1322
|
checks.push({
|
|
1000
|
-
name: "[mcp]
|
|
1323
|
+
name: "[mcp] MCP config valid JSON",
|
|
1001
1324
|
ok: false,
|
|
1002
|
-
detail:
|
|
1325
|
+
detail: `Not valid JSON: ${invalid.join(", ")}`
|
|
1003
1326
|
});
|
|
1327
|
+
}
|
|
1328
|
+
if (sources.length === 0) {
|
|
1329
|
+
if (invalid.length === 0) {
|
|
1330
|
+
checks.push({
|
|
1331
|
+
name: "[mcp] MCP config present",
|
|
1332
|
+
ok: false,
|
|
1333
|
+
detail: "No .mcp.json, .cursor/mcp.json, ~/.cursor/mcp.json or ~/.claude.json found."
|
|
1334
|
+
});
|
|
1335
|
+
}
|
|
1004
1336
|
return checks;
|
|
1005
1337
|
}
|
|
1006
1338
|
checks.push({
|
|
1007
|
-
name: "[mcp]
|
|
1339
|
+
name: "[mcp] MCP config present",
|
|
1008
1340
|
ok: true,
|
|
1009
|
-
detail: `Found
|
|
1341
|
+
detail: `Found: ${sources.map((s) => s.path).join(", ")}`
|
|
1010
1342
|
});
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
mcpConfig = JSON.parse(mcpRaw);
|
|
1014
|
-
} catch {
|
|
1015
|
-
checks.push({
|
|
1016
|
-
name: "[mcp] mcp.json valid JSON",
|
|
1017
|
-
ok: false,
|
|
1018
|
-
detail: "mcp.json is not valid JSON \u2014 regenerate with `mushi setup`."
|
|
1019
|
-
});
|
|
1020
|
-
return checks;
|
|
1021
|
-
}
|
|
1022
|
-
const servers = mcpConfig.mcpServers ?? {};
|
|
1023
|
-
const mushiEntries = Object.entries(servers).filter(
|
|
1024
|
-
([k]) => k === "mushi" || k.startsWith("mushi-")
|
|
1343
|
+
const mushiEntries = sources.flatMap(
|
|
1344
|
+
(source) => Object.entries(source.servers).filter(([name]) => name === "mushi" || name.startsWith("mushi-")).map(([name, entry]) => ({ source, name, entry }))
|
|
1025
1345
|
);
|
|
1026
1346
|
if (mushiEntries.length === 0) {
|
|
1027
1347
|
checks.push({
|
|
1028
1348
|
name: "[mcp] mushi server entry",
|
|
1029
1349
|
ok: false,
|
|
1030
|
-
detail: "No mushi or mushi-* server
|
|
1350
|
+
detail: "No mushi or mushi-* server in any MCP config."
|
|
1031
1351
|
});
|
|
1032
1352
|
return checks;
|
|
1033
1353
|
}
|
|
1034
1354
|
checks.push({
|
|
1035
1355
|
name: "[mcp] mushi server entry",
|
|
1036
1356
|
ok: true,
|
|
1037
|
-
detail: `Found: ${mushiEntries.map((
|
|
1038
|
-
});
|
|
1039
|
-
let anyKeyValid = false;
|
|
1040
|
-
let anyEndpointSet = false;
|
|
1041
|
-
for (const [, srv] of mushiEntries) {
|
|
1042
|
-
const s = srv;
|
|
1043
|
-
const env = s.env ?? {};
|
|
1044
|
-
const key = env["MUSHI_API_KEY"] ?? "";
|
|
1045
|
-
const endpoint = env["MUSHI_API_ENDPOINT"] ?? "";
|
|
1046
|
-
if (key.startsWith("mushi_")) anyKeyValid = true;
|
|
1047
|
-
if (endpoint.includes("supabase.co") || endpoint.includes("localhost")) anyEndpointSet = true;
|
|
1048
|
-
}
|
|
1049
|
-
checks.push({
|
|
1050
|
-
name: "[mcp] MUSHI_API_KEY set",
|
|
1051
|
-
ok: anyKeyValid,
|
|
1052
|
-
detail: anyKeyValid ? "At least one mushi server has a valid mushi_* API key" : "No mushi_* API key found in any mushi server env. Re-run `mushi setup` to regenerate."
|
|
1357
|
+
detail: `Found: ${mushiEntries.map(({ name }) => name).join(", ")}`
|
|
1053
1358
|
});
|
|
1359
|
+
const assessed = mushiEntries.map(
|
|
1360
|
+
({ source, name, entry }) => assessMushiEntry(source, name, entry, Boolean(config.apiKey))
|
|
1361
|
+
);
|
|
1054
1362
|
checks.push({
|
|
1055
|
-
name: "[mcp]
|
|
1056
|
-
ok:
|
|
1057
|
-
detail:
|
|
1363
|
+
name: "[mcp] mushi entries usable",
|
|
1364
|
+
ok: assessed.every((a) => a.usable),
|
|
1365
|
+
detail: assessed.map((a) => a.detail).join("; ")
|
|
1058
1366
|
});
|
|
1059
|
-
if (
|
|
1367
|
+
if (assessed.some((a) => a.stdio && a.usable) && config.apiKey && config.endpoint) {
|
|
1060
1368
|
try {
|
|
1061
1369
|
const { endpoint, apiKey } = sanitizeCliCredentials(config);
|
|
1062
1370
|
const res = await doFetch(`${endpoint}/v1/admin/mcp/account-overview`, {
|
|
@@ -1100,13 +1408,13 @@ async function checkMcpConfig(config, cwd, doFetch = globalThis.fetch) {
|
|
|
1100
1408
|
return checks;
|
|
1101
1409
|
}
|
|
1102
1410
|
var FIX_HINTS = {
|
|
1103
|
-
"
|
|
1104
|
-
"API key configured":
|
|
1105
|
-
"Project ID configured":
|
|
1411
|
+
"Mushi set up here": "Run `npx mushi-mushi` in your app folder \u2014 it signs you in, installs the SDK and writes the env vars.",
|
|
1412
|
+
"API key configured": SIGN_IN_HINT,
|
|
1413
|
+
"Project ID configured": SIGN_IN_HINT,
|
|
1106
1414
|
"Endpoint reachable": "Check your network and that MUSHI_API_ENDPOINT points at `\u2026/functions/v1/api`.",
|
|
1107
1415
|
"[ingest]": "Open the console Onboarding wizard \u2192 Install SDK \u2192 submit a test report, or run `mushi connect --wait`.",
|
|
1108
1416
|
"[server]": "Open Settings \u2192 Integrations: connect GitHub, index codebase, add Anthropic BYOK key, enable autofix.",
|
|
1109
|
-
"[mcp]": "Run `mushi setup`
|
|
1417
|
+
"[mcp]": "Run `npx mushi-mushi setup --ide cursor` (or `--ide claude`) to rewrite the Mushi MCP entry."
|
|
1110
1418
|
};
|
|
1111
1419
|
function fixHintForCheck(name) {
|
|
1112
1420
|
if (FIX_HINTS[name]) return FIX_HINTS[name];
|
|
@@ -1132,12 +1440,12 @@ async function checkOnboardingStatus(config, _consoleBase, cwd, doFetch = global
|
|
|
1132
1440
|
}
|
|
1133
1441
|
try {
|
|
1134
1442
|
const { readFile: readFile3 } = await import("fs/promises");
|
|
1135
|
-
const { join:
|
|
1443
|
+
const { join: join5, resolve: resolve3 } = await import("path");
|
|
1136
1444
|
const root = resolve3(cwd);
|
|
1137
1445
|
let envContent = "";
|
|
1138
1446
|
for (const f of [".env.local", ".env"]) {
|
|
1139
1447
|
try {
|
|
1140
|
-
envContent = await readFile3(
|
|
1448
|
+
envContent = await readFile3(join5(root, f), "utf8");
|
|
1141
1449
|
break;
|
|
1142
1450
|
} catch {
|
|
1143
1451
|
}
|
|
@@ -1203,7 +1511,7 @@ async function checkOnboardingStatus(config, _consoleBase, cwd, doFetch = global
|
|
|
1203
1511
|
}
|
|
1204
1512
|
async function checkVersionDrift(cwd, doFetch) {
|
|
1205
1513
|
const checks = [];
|
|
1206
|
-
const { MUSHI_CLI_VERSION: MUSHI_CLI_VERSION2 } = await import("./version-
|
|
1514
|
+
const { MUSHI_CLI_VERSION: MUSHI_CLI_VERSION2 } = await import("./version-VR37VEE4.js");
|
|
1207
1515
|
if (MUSHI_CLI_VERSION2 === "0.0.0-dev") return checks;
|
|
1208
1516
|
const parse2 = (v) => {
|
|
1209
1517
|
const m = v.replace(/^[~^]/, "").match(/^(\d+)\.(\d+)\.(\d+)/);
|
|
@@ -1233,8 +1541,8 @@ async function checkVersionDrift(cwd, doFetch) {
|
|
|
1233
1541
|
}
|
|
1234
1542
|
try {
|
|
1235
1543
|
const { readFile: readFile3 } = await import("fs/promises");
|
|
1236
|
-
const { join:
|
|
1237
|
-
const pkg = JSON.parse(await readFile3(
|
|
1544
|
+
const { join: join5 } = await import("path");
|
|
1545
|
+
const pkg = JSON.parse(await readFile3(join5(cwd, "package.json"), "utf8"));
|
|
1238
1546
|
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
1239
1547
|
const candidates = Object.entries(deps).filter(([name]) => name.startsWith("@mushi-mushi/")).filter(([, v]) => /^[~^]?\d/.test(v));
|
|
1240
1548
|
const drifted = (await Promise.all(
|
|
@@ -1301,11 +1609,23 @@ async function runDoctor(config, options = {}) {
|
|
|
1301
1609
|
const isFull = options.full === true;
|
|
1302
1610
|
const runServer = isFull || options.server !== false;
|
|
1303
1611
|
const runIngest = isFull || options.ingest !== false;
|
|
1612
|
+
const sdkCheck = await checkSdkInstall(options.cwd ?? process.cwd());
|
|
1613
|
+
if (!config.apiKey && !config.projectId && !sdkCheck?.ok && !options.auth && !isFull) {
|
|
1614
|
+
return {
|
|
1615
|
+
ready: false,
|
|
1616
|
+
checks: [
|
|
1617
|
+
{
|
|
1618
|
+
name: "Mushi set up here",
|
|
1619
|
+
ok: false,
|
|
1620
|
+
detail: "Not set up yet \u2014 no saved Mushi credentials and no Mushi SDK in this folder."
|
|
1621
|
+
}
|
|
1622
|
+
]
|
|
1623
|
+
};
|
|
1624
|
+
}
|
|
1304
1625
|
checks.push(...checkCliConfig(config));
|
|
1305
1626
|
if (config.endpoint) {
|
|
1306
1627
|
checks.push(await checkEndpointReachability(config.endpoint, doFetch));
|
|
1307
1628
|
}
|
|
1308
|
-
const sdkCheck = await checkSdkInstall(options.cwd ?? process.cwd());
|
|
1309
1629
|
if (sdkCheck) checks.push(sdkCheck);
|
|
1310
1630
|
if (sdkCheck?.ok && !sdkCheck.warn) {
|
|
1311
1631
|
const { findSdkImport } = await import("./snippet-inject-CCXILWYZ.js");
|
|
@@ -1365,13 +1685,17 @@ function formatDoctorResult(result) {
|
|
|
1365
1685
|
const WARN = "WARN";
|
|
1366
1686
|
const FAIL = "FAIL";
|
|
1367
1687
|
const lines = [];
|
|
1688
|
+
const printedHints = /* @__PURE__ */ new Set();
|
|
1368
1689
|
for (const c of result.checks) {
|
|
1369
1690
|
const icon = !c.ok ? FAIL : c.warn ? WARN : PASS;
|
|
1370
1691
|
lines.push(`${icon} ${c.name}`);
|
|
1371
1692
|
if (c.detail) lines.push(` ${c.detail}`);
|
|
1372
1693
|
if (!c.ok) {
|
|
1373
1694
|
const hint = fixHintForCheck(c.name);
|
|
1374
|
-
if (hint
|
|
1695
|
+
if (hint && !printedHints.has(hint)) {
|
|
1696
|
+
printedHints.add(hint);
|
|
1697
|
+
lines.push(` \u2192 Fix: ${hint}`);
|
|
1698
|
+
}
|
|
1375
1699
|
}
|
|
1376
1700
|
}
|
|
1377
1701
|
const failed = result.checks.filter((c) => !c.ok);
|
|
@@ -1395,6 +1719,9 @@ ${failed.length} check${failed.length === 1 ? "" : "s"} failed.`);
|
|
|
1395
1719
|
|
|
1396
1720
|
// src/init.ts
|
|
1397
1721
|
var ENV_FILES = [".env.local", ".env"];
|
|
1722
|
+
function isInteractive() {
|
|
1723
|
+
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
1724
|
+
}
|
|
1398
1725
|
var PROJECT_ID_PATTERN = /^(?:proj_[A-Za-z0-9_-]{10,}|[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/i;
|
|
1399
1726
|
var API_KEY_PATTERN = /^(mushi_|mush_pk_)[A-Za-z0-9_-]{10,}$/;
|
|
1400
1727
|
async function runInit(options = {}) {
|
|
@@ -1404,6 +1731,7 @@ async function runInit(options = {}) {
|
|
|
1404
1731
|
return;
|
|
1405
1732
|
}
|
|
1406
1733
|
ensureInteractiveOrBailOut(options);
|
|
1734
|
+
if (!isInteractive()) options = { ...options, yes: true };
|
|
1407
1735
|
p.intro("Mushi setup");
|
|
1408
1736
|
await printFreshnessHint();
|
|
1409
1737
|
warnIfWorkspaceRoot(cwd);
|
|
@@ -1411,6 +1739,10 @@ async function runInit(options = {}) {
|
|
|
1411
1739
|
maybeSuggestAudit(pkg);
|
|
1412
1740
|
if (!pkg) {
|
|
1413
1741
|
p.log.warn("No package.json found in this directory.");
|
|
1742
|
+
if (!isInteractive()) {
|
|
1743
|
+
p.cancel("Aborted. Run from your project root, or pass --cwd <path>.");
|
|
1744
|
+
process.exit(1);
|
|
1745
|
+
}
|
|
1414
1746
|
const cont = await p.confirm({
|
|
1415
1747
|
message: "Continue anyway? (Mushi will install into the current folder)",
|
|
1416
1748
|
initialValue: false
|
|
@@ -1438,13 +1770,13 @@ async function runInit(options = {}) {
|
|
|
1438
1770
|
}
|
|
1439
1771
|
await writeEnvFile(
|
|
1440
1772
|
cwd,
|
|
1441
|
-
credentials.
|
|
1773
|
+
credentials.sdkKey,
|
|
1442
1774
|
credentials.projectId,
|
|
1443
1775
|
framework,
|
|
1444
1776
|
endpoint,
|
|
1445
1777
|
Boolean(options.yes)
|
|
1446
1778
|
);
|
|
1447
|
-
persistCliConfig(credentials
|
|
1779
|
+
persistCliConfig(credentials, endpoint);
|
|
1448
1780
|
emitWizardFunnelEvent(credentials, endpoint, "wizard_env_written", { framework: framework.id });
|
|
1449
1781
|
const enableRewards = await maybeEnableRewards(options);
|
|
1450
1782
|
await maybeInjectSnippet(cwd, framework, options);
|
|
@@ -1477,14 +1809,11 @@ function maybeSuggestAudit(pkg) {
|
|
|
1477
1809
|
}
|
|
1478
1810
|
}
|
|
1479
1811
|
function ensureInteractiveOrBailOut(options) {
|
|
1480
|
-
|
|
1481
|
-
if (
|
|
1482
|
-
|
|
1483
|
-
(options.framework || options.yes) && options.projectId && options.apiKey
|
|
1484
|
-
);
|
|
1485
|
-
if (hasAllFlags) return;
|
|
1812
|
+
if (isInteractive()) return;
|
|
1813
|
+
if (options.yes) return;
|
|
1814
|
+
if (options.framework && options.projectId && options.apiKey) return;
|
|
1486
1815
|
process.stderr.write(
|
|
1487
|
-
"mushi-mushi: non-interactive terminal detected.\
|
|
1816
|
+
"mushi-mushi: non-interactive terminal detected.\nRe-run with --yes to set up without prompts: it reuses the credentials saved by\n`npx mushi-mushi login`, or prints a sign-in URL and waits while you approve it in a browser.\nExample: npx mushi-mushi --yes\nCI with no browser: npx mushi-mushi --yes --project-id <uuid> --api-key <ingest-only key>\n"
|
|
1488
1817
|
);
|
|
1489
1818
|
process.exit(1);
|
|
1490
1819
|
}
|
|
@@ -1515,14 +1844,16 @@ async function chooseFramework(detected, options) {
|
|
|
1515
1844
|
return FRAMEWORKS[confirmed];
|
|
1516
1845
|
}
|
|
1517
1846
|
async function acquireCredentials(options, consoleBase, endpoint) {
|
|
1847
|
+
const probe = (e, k, pid) => probeKeyScope(e, k, pid);
|
|
1518
1848
|
if (options.projectId && options.apiKey) {
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
};
|
|
1849
|
+
const projectId = sanitizeSecret(options.projectId);
|
|
1850
|
+
const sdkKey = sanitizeSecret(options.apiKey);
|
|
1851
|
+
await assertUserKeyIsIngestOnly(sdkKey, projectId, endpoint, probe);
|
|
1852
|
+
return { projectId, sdkKey };
|
|
1523
1853
|
}
|
|
1524
1854
|
const existing = loadConfig();
|
|
1525
1855
|
maybeShowTelemetryNotice(existing);
|
|
1856
|
+
let browserTried = false;
|
|
1526
1857
|
if (!options.projectId && !options.apiKey && existing.projectId && existing.apiKey) {
|
|
1527
1858
|
const reuse = options.yes ? true : await p.confirm({
|
|
1528
1859
|
message: "Use the Mushi credentials saved from your last sign-in?",
|
|
@@ -1533,13 +1864,32 @@ async function acquireCredentials(options, consoleBase, endpoint) {
|
|
|
1533
1864
|
process.exit(0);
|
|
1534
1865
|
}
|
|
1535
1866
|
if (reuse) {
|
|
1536
|
-
|
|
1537
|
-
projectId:
|
|
1538
|
-
|
|
1539
|
-
|
|
1867
|
+
const saved = await resolveSavedCredentials(
|
|
1868
|
+
{ projectId: existing.projectId, apiKey: existing.apiKey, sdkKey: existing.sdkKey },
|
|
1869
|
+
endpoint,
|
|
1870
|
+
probe
|
|
1871
|
+
);
|
|
1872
|
+
if (saved.kind === "ready") return saved.credentials;
|
|
1873
|
+
if (saved.kind === "needs-sdk-key") {
|
|
1874
|
+
p.log.info(
|
|
1875
|
+
"Your saved sign-in key can read bug reports, so it stays in the CLI config. Approve one browser sign-in to mint a separate ingest-only key for the app's env vars."
|
|
1876
|
+
);
|
|
1877
|
+
browserTried = true;
|
|
1878
|
+
const creds = await runBrowserSignIn(options, endpoint, consoleBase, {
|
|
1879
|
+
projectId: saved.projectId,
|
|
1880
|
+
cliKey: saved.cliKey
|
|
1881
|
+
});
|
|
1882
|
+
if (creds) return creds;
|
|
1883
|
+
} else {
|
|
1884
|
+
p.log.warn(`Saved credentials can't be reused (${saved.reason}) \u2014 signing in again.`);
|
|
1885
|
+
}
|
|
1540
1886
|
}
|
|
1541
1887
|
}
|
|
1542
|
-
if (
|
|
1888
|
+
if (browserTried) {
|
|
1889
|
+
p.log.warn(
|
|
1890
|
+
"Browser sign-in didn't complete \u2014 switching to manual entry. Run `npx mushi-mushi doctor --auth` to diagnose the sign-in path."
|
|
1891
|
+
);
|
|
1892
|
+
} else if (options.yes) {
|
|
1543
1893
|
const creds = await runBrowserSignIn(options, endpoint, consoleBase);
|
|
1544
1894
|
if (creds) return creds;
|
|
1545
1895
|
p.log.warn(
|
|
@@ -1574,9 +1924,70 @@ async function acquireCredentials(options, consoleBase, endpoint) {
|
|
|
1574
1924
|
);
|
|
1575
1925
|
}
|
|
1576
1926
|
}
|
|
1577
|
-
return collectCredentialsManually(options, consoleBase, endpoint);
|
|
1927
|
+
return collectCredentialsManually(options, consoleBase, endpoint, probe);
|
|
1928
|
+
}
|
|
1929
|
+
async function assertUserKeyIsIngestOnly(key, projectId, endpoint, probe) {
|
|
1930
|
+
const result = await probe(endpoint, key, projectId);
|
|
1931
|
+
if (result.result === "mcp" || result.result === "unknown") {
|
|
1932
|
+
throw new Error(
|
|
1933
|
+
`Not writing this API key into your app's env vars: ${describeUnsafeSdkKey(result, endpoint)}. Use an ingest-only (report:write) key from the console, or leave out --api-key to sign in with the browser, which mints one.`
|
|
1934
|
+
);
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1937
|
+
async function resolveSavedCredentials(saved, endpoint, probe) {
|
|
1938
|
+
const projectId = sanitizeSecret(saved.projectId);
|
|
1939
|
+
const apiKey = sanitizeSecret(saved.apiKey);
|
|
1940
|
+
const savedSdkKey = savedSdkKeyFor(saved, projectId);
|
|
1941
|
+
if (savedSdkKey) {
|
|
1942
|
+
const sdkProbe = await probe(endpoint, savedSdkKey, projectId);
|
|
1943
|
+
if (sdkProbe.result === "ingest-only") {
|
|
1944
|
+
return { kind: "ready", credentials: { projectId, sdkKey: savedSdkKey, cliKey: apiKey } };
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
const cliProbe = await probe(endpoint, apiKey, projectId);
|
|
1948
|
+
if (cliProbe.result === "ingest-only") {
|
|
1949
|
+
return { kind: "ready", credentials: { projectId, sdkKey: apiKey } };
|
|
1950
|
+
}
|
|
1951
|
+
if (cliProbe.result === "mcp") {
|
|
1952
|
+
return { kind: "needs-sdk-key", projectId, cliKey: apiKey };
|
|
1953
|
+
}
|
|
1954
|
+
return { kind: "unusable", reason: describeUnsafeSdkKey(cliProbe, endpoint) };
|
|
1578
1955
|
}
|
|
1579
|
-
async function
|
|
1956
|
+
async function mintWizardKeys(deps, args) {
|
|
1957
|
+
let projectId;
|
|
1958
|
+
let createdName;
|
|
1959
|
+
let sdkKey = null;
|
|
1960
|
+
if (args.target.kind === "new") {
|
|
1961
|
+
const created = await deps.createProject(args.endpoint, args.cliToken, args.target.name, {
|
|
1962
|
+
scopes: SDK_KEY_SCOPES
|
|
1963
|
+
});
|
|
1964
|
+
projectId = created.id;
|
|
1965
|
+
createdName = created.name;
|
|
1966
|
+
sdkKey = created.apiKey;
|
|
1967
|
+
} else {
|
|
1968
|
+
projectId = args.target.projectId;
|
|
1969
|
+
}
|
|
1970
|
+
if (!sdkKey) {
|
|
1971
|
+
sdkKey = await deps.mintProjectKey(args.endpoint, args.cliToken, projectId, {
|
|
1972
|
+
scopes: SDK_KEY_SCOPES,
|
|
1973
|
+
label: SDK_KEY_LABEL
|
|
1974
|
+
});
|
|
1975
|
+
}
|
|
1976
|
+
let cliKey;
|
|
1977
|
+
let cliKeyError;
|
|
1978
|
+
if (args.withCliKey) {
|
|
1979
|
+
try {
|
|
1980
|
+
cliKey = await deps.mintProjectKey(args.endpoint, args.cliToken, projectId, {
|
|
1981
|
+
scopes: CLI_KEY_SCOPES,
|
|
1982
|
+
label: CLI_KEY_LABEL
|
|
1983
|
+
});
|
|
1984
|
+
} catch (err) {
|
|
1985
|
+
cliKeyError = err instanceof Error ? err.message : String(err);
|
|
1986
|
+
}
|
|
1987
|
+
}
|
|
1988
|
+
return { projectId, createdName, sdkKey, cliKey, cliKeyError };
|
|
1989
|
+
}
|
|
1990
|
+
async function runBrowserSignIn(options, endpoint, consoleBase, preset) {
|
|
1580
1991
|
const startSpin = p.spinner();
|
|
1581
1992
|
startSpin.start("Starting secure browser sign-in\u2026");
|
|
1582
1993
|
let session;
|
|
@@ -1604,9 +2015,11 @@ async function runBrowserSignIn(options, endpoint, consoleBase) {
|
|
|
1604
2015
|
return null;
|
|
1605
2016
|
}
|
|
1606
2017
|
waitSpin.stop("Approved.");
|
|
1607
|
-
|
|
1608
|
-
let
|
|
1609
|
-
if (
|
|
2018
|
+
const presetProjectId = preset?.projectId ?? (options.projectId ? sanitizeSecret(options.projectId) : void 0);
|
|
2019
|
+
let target;
|
|
2020
|
+
if (presetProjectId) {
|
|
2021
|
+
target = { kind: "existing", projectId: presetProjectId };
|
|
2022
|
+
} else {
|
|
1610
2023
|
let projects = [];
|
|
1611
2024
|
const fetchSpin = p.spinner();
|
|
1612
2025
|
fetchSpin.start("Loading your projects\u2026");
|
|
@@ -1616,91 +2029,117 @@ async function runBrowserSignIn(options, endpoint, consoleBase) {
|
|
|
1616
2029
|
projects.length > 0 ? `Found ${projects.length} project(s).` : "No projects yet."
|
|
1617
2030
|
);
|
|
1618
2031
|
} catch (err) {
|
|
2032
|
+
if (!isInteractive()) {
|
|
2033
|
+
fetchSpin.stop("Could not load projects.");
|
|
2034
|
+
p.log.warn(err instanceof Error ? err.message : String(err));
|
|
2035
|
+
return null;
|
|
2036
|
+
}
|
|
1619
2037
|
fetchSpin.stop("Could not load projects \u2014 you can still create a new one.");
|
|
1620
2038
|
p.log.warn(err instanceof Error ? err.message : String(err));
|
|
1621
2039
|
}
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
const
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
2040
|
+
if (!isInteractive()) {
|
|
2041
|
+
const cwd = options.cwd ?? process.cwd();
|
|
2042
|
+
const choice = chooseProjectNonInteractive(
|
|
2043
|
+
projects,
|
|
2044
|
+
defaultProjectName(readPackageJson(cwd)?.name, cwd)
|
|
2045
|
+
);
|
|
2046
|
+
if (choice.kind === "existing") {
|
|
2047
|
+
p.log.info(`Using project "${choice.name}" (matches this app \u2014 pass --project-id to pick another).`);
|
|
2048
|
+
target = { kind: "existing", projectId: choice.id };
|
|
2049
|
+
} else {
|
|
2050
|
+
p.log.info(`Creating project "${choice.name}" (no project matches this app \u2014 pass --project-id to use an existing one).`);
|
|
2051
|
+
target = { kind: "new", name: choice.name };
|
|
2052
|
+
}
|
|
2053
|
+
} else {
|
|
2054
|
+
const NEW = "__new__";
|
|
2055
|
+
const choice = await p.select({
|
|
2056
|
+
message: "Choose a project",
|
|
2057
|
+
initialValue: projects[0]?.id ?? NEW,
|
|
2058
|
+
options: [
|
|
2059
|
+
...projects.map((pr) => ({ value: pr.id, label: pr.name, hint: pr.id.slice(0, 8) })),
|
|
2060
|
+
{ value: NEW, label: "Create a new project", hint: "mints an SDK key automatically" }
|
|
2061
|
+
]
|
|
1640
2062
|
});
|
|
1641
|
-
if (p.isCancel(
|
|
2063
|
+
if (p.isCancel(choice)) {
|
|
1642
2064
|
p.cancel("Aborted.");
|
|
1643
2065
|
process.exit(0);
|
|
1644
2066
|
}
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
2067
|
+
if (choice === NEW) {
|
|
2068
|
+
const name = await p.text({
|
|
2069
|
+
message: "Project name",
|
|
2070
|
+
placeholder: "My app",
|
|
2071
|
+
validate: (v) => v && v.trim().length > 0 ? void 0 : "Required"
|
|
1650
2072
|
});
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
2073
|
+
if (p.isCancel(name)) {
|
|
2074
|
+
p.cancel("Aborted.");
|
|
2075
|
+
process.exit(0);
|
|
2076
|
+
}
|
|
2077
|
+
target = { kind: "new", name: name.trim() };
|
|
2078
|
+
} else {
|
|
2079
|
+
target = { kind: "existing", projectId: choice };
|
|
1658
2080
|
}
|
|
1659
|
-
} else {
|
|
1660
|
-
projectId = choice;
|
|
1661
2081
|
}
|
|
1662
2082
|
}
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
}
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
2083
|
+
const keySpin = p.spinner();
|
|
2084
|
+
keySpin.start(target.kind === "new" ? `Creating "${target.name}"\u2026` : "Minting keys\u2026");
|
|
2085
|
+
let minted;
|
|
2086
|
+
try {
|
|
2087
|
+
minted = await mintWizardKeys(
|
|
2088
|
+
{ createProject, mintProjectKey },
|
|
2089
|
+
{ endpoint, cliToken, target, withCliKey: !preset }
|
|
2090
|
+
);
|
|
2091
|
+
} catch (err) {
|
|
2092
|
+
keySpin.stop(target.kind === "new" ? "Could not create the project." : "Could not mint an API key.");
|
|
2093
|
+
p.log.warn(err instanceof Error ? err.message : String(err));
|
|
2094
|
+
if (target.kind === "existing") {
|
|
1674
2095
|
p.log.warn(
|
|
1675
2096
|
`You're signed in, but key minting failed. Generate one manually in the console Verify tab: ${consoleUrl(consoleBase, "/onboarding?tab=verify")}`
|
|
1676
2097
|
);
|
|
1677
|
-
return null;
|
|
1678
2098
|
}
|
|
2099
|
+
return null;
|
|
1679
2100
|
}
|
|
1680
|
-
|
|
1681
|
-
|
|
2101
|
+
keySpin.stop(
|
|
2102
|
+
minted.createdName ? `Created project "${minted.createdName}" with an ingest-only SDK key.` : "SDK key ready (ingest-only)."
|
|
2103
|
+
);
|
|
2104
|
+
if (minted.cliKeyError) {
|
|
2105
|
+
p.log.warn(
|
|
2106
|
+
`The CLI/MCP key could not be minted (${minted.cliKeyError}). The SDK install continues; run \`npx mushi-mushi login\` later for MCP and admin commands.`
|
|
2107
|
+
);
|
|
2108
|
+
}
|
|
2109
|
+
return {
|
|
2110
|
+
projectId: minted.projectId,
|
|
2111
|
+
sdkKey: minted.sdkKey,
|
|
2112
|
+
cliKey: preset?.cliKey ?? minted.cliKey
|
|
2113
|
+
};
|
|
1682
2114
|
}
|
|
1683
|
-
async function collectCredentialsManually(options, consoleBase, endpoint) {
|
|
2115
|
+
async function collectCredentialsManually(options, consoleBase, endpoint, probe) {
|
|
1684
2116
|
const existing = loadConfig();
|
|
1685
2117
|
let savedProjectId = existing.projectId;
|
|
1686
|
-
let savedApiKey
|
|
1687
|
-
if (!options.projectId &&
|
|
2118
|
+
let savedApiKey;
|
|
2119
|
+
if (!isInteractive() && !(options.projectId && options.apiKey)) {
|
|
2120
|
+
throw new Error(
|
|
2121
|
+
"Browser sign-in didn't complete and this terminal cannot prompt for a key. Re-run `npx mushi-mushi --yes` and approve the sign-in in the browser, or pass --project-id and an ingest-only --api-key."
|
|
2122
|
+
);
|
|
2123
|
+
}
|
|
2124
|
+
if (!options.projectId && !options.apiKey && savedProjectId && existing.apiKey) {
|
|
2125
|
+
const projectIdForCheck = sanitizeSecret(savedProjectId);
|
|
2126
|
+
const candidate = savedSdkKeyFor(existing, projectIdForCheck) ?? sanitizeSecret(existing.apiKey);
|
|
1688
2127
|
p.log.info(
|
|
1689
|
-
`Found saved credentials from a previous sign-in (project ${
|
|
2128
|
+
`Found saved credentials from a previous sign-in (project ${projectIdForCheck.slice(0, 8)}\u2026).`
|
|
1690
2129
|
);
|
|
1691
2130
|
const checkSpin = p.spinner();
|
|
1692
2131
|
checkSpin.start("Checking saved credentials\u2026");
|
|
1693
|
-
const check = await
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
})
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
} else {
|
|
2132
|
+
const check = await probe(endpoint, candidate, projectIdForCheck);
|
|
2133
|
+
if (check.result === "ingest-only") {
|
|
2134
|
+
checkSpin.stop("Saved ingest key still works.");
|
|
2135
|
+
savedApiKey = candidate;
|
|
2136
|
+
} else if (check.result === "mcp") {
|
|
2137
|
+
checkSpin.stop("Saved key can read reports, so it can't go into your app's env \u2014 paste an ingest-only key below.");
|
|
2138
|
+
} else if (check.result === "invalid") {
|
|
1701
2139
|
checkSpin.stop("Saved credentials no longer authenticate \u2014 enter fresh ones below.");
|
|
1702
2140
|
savedProjectId = void 0;
|
|
1703
|
-
|
|
2141
|
+
} else {
|
|
2142
|
+
checkSpin.stop(`Could not check the saved key (${describeUnsafeSdkKey(check, endpoint)}) \u2014 enter one below.`);
|
|
1704
2143
|
}
|
|
1705
2144
|
}
|
|
1706
2145
|
const rawProjectId = options.projectId ?? savedProjectId ?? await promptText({
|
|
@@ -1727,17 +2166,29 @@ async function collectCredentialsManually(options, consoleBase, endpoint) {
|
|
|
1727
2166
|
`Invalid API key. Expected format: mushi_[A-Za-z0-9_-]{10,}. Got: ${redact(apiKey)}`
|
|
1728
2167
|
);
|
|
1729
2168
|
}
|
|
1730
|
-
|
|
2169
|
+
if (apiKey !== savedApiKey) {
|
|
2170
|
+
await assertUserKeyIsIngestOnly(apiKey, projectId, endpoint, probe);
|
|
2171
|
+
}
|
|
2172
|
+
return { projectId, sdkKey: apiKey };
|
|
1731
2173
|
}
|
|
1732
2174
|
async function verifyCredentials(credentials, options, consoleBase) {
|
|
1733
2175
|
const endpoint = resolveCloudEndpoint(options.endpoint);
|
|
1734
2176
|
const spinner3 = p.spinner();
|
|
1735
2177
|
spinner3.start("Verifying credentials\u2026");
|
|
1736
2178
|
const result = await apiCall("/v1/sync/whoami", {
|
|
1737
|
-
apiKey: credentials.
|
|
2179
|
+
apiKey: credentials.sdkKey,
|
|
1738
2180
|
projectId: credentials.projectId,
|
|
1739
2181
|
endpoint
|
|
1740
2182
|
});
|
|
2183
|
+
if (!result.ok && (result.error.code === "NETWORK_ERROR" || result.error.code === "TIMEOUT")) {
|
|
2184
|
+
spinner3.stop(`Could not reach ${endpoint}.`);
|
|
2185
|
+
p.log.error(result.error.message);
|
|
2186
|
+
p.log.warn("Setup did NOT complete: nothing was installed and no env vars were written.");
|
|
2187
|
+
p.log.info(
|
|
2188
|
+
"Check your network, proxy or VPN, or pass --endpoint <url> if you run a self-hosted backend, then re-run `npx mushi-mushi`."
|
|
2189
|
+
);
|
|
2190
|
+
throw new Error(`Could not reach the Mushi API at ${endpoint} \u2014 nothing was changed.`);
|
|
2191
|
+
}
|
|
1741
2192
|
if (!result.ok) {
|
|
1742
2193
|
spinner3.stop("Credentials could not be verified.");
|
|
1743
2194
|
p.log.error(result.error?.message ?? "Authentication failed.");
|
|
@@ -1893,9 +2344,24 @@ function warnIfMissingFromGitignore(cwd, envFile) {
|
|
|
1893
2344
|
p.log.warn(`${envFile} is not in .gitignore \u2014 add it before committing.`);
|
|
1894
2345
|
}
|
|
1895
2346
|
}
|
|
1896
|
-
function
|
|
2347
|
+
function planCliConfigUpdate(existing, credentials, endpoint) {
|
|
2348
|
+
const keepSavedKey = existing.projectId === credentials.projectId && Boolean(existing.apiKey);
|
|
2349
|
+
const apiKey = credentials.cliKey ?? (keepSavedKey ? existing.apiKey : credentials.sdkKey);
|
|
2350
|
+
return {
|
|
2351
|
+
...existing,
|
|
2352
|
+
apiKey,
|
|
2353
|
+
projectId: credentials.projectId,
|
|
2354
|
+
endpoint,
|
|
2355
|
+
sdkKey: { projectId: credentials.projectId, key: credentials.sdkKey }
|
|
2356
|
+
};
|
|
2357
|
+
}
|
|
2358
|
+
function persistCliConfig(credentials, endpoint) {
|
|
1897
2359
|
const existing = loadConfig();
|
|
1898
|
-
|
|
2360
|
+
const next = planCliConfigUpdate(existing, credentials, endpoint);
|
|
2361
|
+
saveConfig(next);
|
|
2362
|
+
if (next.apiKey && next.apiKey !== existing.apiKey) {
|
|
2363
|
+
trySaveKeyToKeychain(next.apiKey, resolveProfileName());
|
|
2364
|
+
}
|
|
1899
2365
|
}
|
|
1900
2366
|
function emitWizardFunnelEvent(credentials, endpoint, event, metadata = {}) {
|
|
1901
2367
|
if (process.env.MUSHI_NO_TELEMETRY) return;
|
|
@@ -1903,7 +2369,7 @@ function emitWizardFunnelEvent(credentials, endpoint, event, metadata = {}) {
|
|
|
1903
2369
|
method: "POST",
|
|
1904
2370
|
headers: {
|
|
1905
2371
|
"Content-Type": "application/json",
|
|
1906
|
-
"X-Mushi-Api-Key": credentials.
|
|
2372
|
+
"X-Mushi-Api-Key": credentials.sdkKey,
|
|
1907
2373
|
"X-Mushi-Project": credentials.projectId
|
|
1908
2374
|
},
|
|
1909
2375
|
body: JSON.stringify({ event, source: "cli", metadata }),
|
|
@@ -1954,7 +2420,7 @@ import { MushiRewardsBadge } from '@mushi-mushi/react';
|
|
|
1954
2420
|
[
|
|
1955
2421
|
" [ ] 1. Paste the init snippet above into your app entry file",
|
|
1956
2422
|
" [ ] 2. Start your dev server",
|
|
1957
|
-
" [ ] 3.
|
|
2423
|
+
" [ ] 3. Wire your editor: npx mushi-mushi setup --ide cursor (or --ide claude)",
|
|
1958
2424
|
` [ ] 4. Open the Verify tab to send a test report: ${consoleUrl(consoleBase, "/onboarding?tab=verify")}`
|
|
1959
2425
|
].join("\n"),
|
|
1960
2426
|
"Next steps:"
|
|
@@ -1963,353 +2429,143 @@ import { MushiRewardsBadge } from '@mushi-mushi/react';
|
|
|
1963
2429
|
async function maybeOfferConnect(credentials, options, _consoleBase) {
|
|
1964
2430
|
if (options.yes) return;
|
|
1965
2431
|
const answer = await p.confirm({
|
|
1966
|
-
message: "Run `mushi connect --write-env --wire-ide --wait` now? (SDK env + Cursor MCP + heartbeat check)",
|
|
1967
|
-
initialValue: false
|
|
1968
|
-
});
|
|
1969
|
-
if (p.isCancel(answer) || !answer) return;
|
|
1970
|
-
const endpoint = resolveCloudEndpoint(options.endpoint);
|
|
1971
|
-
try {
|
|
1972
|
-
const { runConnect: runConnect2 } = await import("./connect-TWO2ZNFB.js");
|
|
1973
|
-
await runConnect2({
|
|
1974
|
-
apiKey: credentials.apiKey,
|
|
1975
|
-
projectId: credentials.projectId,
|
|
1976
|
-
endpoint,
|
|
1977
|
-
cwd: options.cwd,
|
|
1978
|
-
writeEnv: true,
|
|
1979
|
-
wireIde: true,
|
|
1980
|
-
wait: true
|
|
1981
|
-
});
|
|
1982
|
-
} catch (err) {
|
|
1983
|
-
p.log.warn(err instanceof Error ? err.message : String(err));
|
|
1984
|
-
p.log.info("You can run manually: mushi connect --write-env --wire-ide --wait");
|
|
1985
|
-
}
|
|
1986
|
-
}
|
|
1987
|
-
async function maybeEnableRewards(options) {
|
|
1988
|
-
if (options.yes) return false;
|
|
1989
|
-
const answer = await p.confirm({
|
|
1990
|
-
message: "Enable Mushi Rewards? (users earn points for bug reports + app activity)",
|
|
1991
|
-
initialValue: false
|
|
1992
|
-
});
|
|
1993
|
-
if (p.isCancel(answer)) return false;
|
|
1994
|
-
return Boolean(answer);
|
|
1995
|
-
}
|
|
1996
|
-
async function maybeSendTestReport(credentials, options) {
|
|
1997
|
-
if (options.sendTestReport === false) return;
|
|
1998
|
-
let shouldSend;
|
|
1999
|
-
if (options.sendTestReport === true || options.yes) {
|
|
2000
|
-
shouldSend = true;
|
|
2001
|
-
} else {
|
|
2002
|
-
const answer = await p.confirm({
|
|
2003
|
-
message: "Send a test report now to verify the pipeline?",
|
|
2004
|
-
initialValue: true
|
|
2005
|
-
});
|
|
2006
|
-
if (p.isCancel(answer)) return;
|
|
2007
|
-
shouldSend = answer;
|
|
2008
|
-
}
|
|
2009
|
-
if (!shouldSend) return;
|
|
2010
|
-
const endpoint = normalizeEndpoint(resolveCloudEndpoint(options.endpoint));
|
|
2011
|
-
const spinner3 = p.spinner();
|
|
2012
|
-
spinner3.start("Sending test report\u2026");
|
|
2013
|
-
const controller = new AbortController();
|
|
2014
|
-
const timer = setTimeout(() => controller.abort(), TEST_REPORT_FETCH_TIMEOUT_MS);
|
|
2015
|
-
try {
|
|
2016
|
-
const res = await fetch(`${endpoint}/v1/reports`, {
|
|
2017
|
-
method: "POST",
|
|
2018
|
-
signal: controller.signal,
|
|
2019
|
-
headers: {
|
|
2020
|
-
"Content-Type": "application/json",
|
|
2021
|
-
"X-Mushi-Api-Key": credentials.apiKey,
|
|
2022
|
-
"X-Mushi-Project": credentials.projectId
|
|
2023
|
-
},
|
|
2024
|
-
body: JSON.stringify({
|
|
2025
|
-
projectId: credentials.projectId,
|
|
2026
|
-
description: "Test report from the mushi-mushi setup wizard",
|
|
2027
|
-
category: "other",
|
|
2028
|
-
reporterToken: `wizard-${randomUUID()}`,
|
|
2029
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2030
|
-
environment: {
|
|
2031
|
-
url: "cli://wizard",
|
|
2032
|
-
userAgent: `mushi-wizard/${process.platform}-${process.arch}`,
|
|
2033
|
-
platform: process.platform,
|
|
2034
|
-
language: "en",
|
|
2035
|
-
viewport: { width: 0, height: 0 },
|
|
2036
|
-
referrer: "",
|
|
2037
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2038
|
-
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone
|
|
2039
|
-
}
|
|
2040
|
-
})
|
|
2041
|
-
});
|
|
2042
|
-
if (!res.ok) {
|
|
2043
|
-
spinner3.stop(`Test report rejected (HTTP ${res.status}).`);
|
|
2044
|
-
p.log.warn(
|
|
2045
|
-
res.status === 401 || res.status === 403 ? "Credentials did not authenticate \u2014 double-check the project ID and API key." : "Skipping test report. You can retry with `mushi test`."
|
|
2046
|
-
);
|
|
2047
|
-
return;
|
|
2048
|
-
}
|
|
2049
|
-
spinner3.stop("Test report sent.");
|
|
2050
|
-
let reportId;
|
|
2051
|
-
try {
|
|
2052
|
-
const body = await res.json();
|
|
2053
|
-
reportId = body.data?.reportId;
|
|
2054
|
-
} catch {
|
|
2055
|
-
}
|
|
2056
|
-
const consoleBase = options.consoleBase ?? resolveConsoleUrlSync(options.cwd);
|
|
2057
|
-
p.log.success(`View it at ${reportsUrl(consoleBase, reportId)}`);
|
|
2058
|
-
} catch (err) {
|
|
2059
|
-
const aborted = err instanceof Error && err.name === "AbortError";
|
|
2060
|
-
spinner3.stop(aborted ? "Timed out reaching the Mushi API." : "Could not reach the Mushi API.");
|
|
2061
|
-
p.log.warn(err instanceof Error ? err.message : String(err));
|
|
2062
|
-
} finally {
|
|
2063
|
-
clearTimeout(timer);
|
|
2064
|
-
}
|
|
2065
|
-
}
|
|
2066
|
-
async function printFreshnessHint() {
|
|
2067
|
-
const result = await checkFreshness("mushi-mushi", MUSHI_CLI_VERSION);
|
|
2068
|
-
if (!result || !result.isOutdated) return;
|
|
2069
|
-
p.log.info(
|
|
2070
|
-
`A newer version of mushi-mushi is available: ${result.current} \u2192 ${result.latest}. Run \`npx mushi-mushi@latest\` to get the freshest wizard.`
|
|
2071
|
-
);
|
|
2072
|
-
}
|
|
2073
|
-
function warnIfWorkspaceRoot(cwd) {
|
|
2074
|
-
let hint;
|
|
2075
|
-
try {
|
|
2076
|
-
hint = detectWorkspaceHint(cwd);
|
|
2077
|
-
} catch {
|
|
2078
|
-
return;
|
|
2079
|
-
}
|
|
2080
|
-
if (!hint || hint.apps.length === 0) return;
|
|
2081
|
-
const hasFrameworkAtCwd = hint.apps.some(
|
|
2082
|
-
(app) => isSameDirectory(cwd, resolveWorkspaceAppPath(hint.root, app.relativePath))
|
|
2083
|
-
);
|
|
2084
|
-
if (hasFrameworkAtCwd) return;
|
|
2085
|
-
const apps = hint.apps.slice(0, 5).map((app) => ` \u2022 ${app.relativePath} (${app.framework})`).join("\n");
|
|
2086
|
-
p.log.warn(
|
|
2087
|
-
`You appear to be at a workspace root (source: ${hint.source}). Mushi will install into the current directory, which has no framework dep. You probably meant one of these sub-packages:
|
|
2088
|
-
${apps}
|
|
2089
|
-
Run \`mushi init --cwd <path>\` \u2014 or re-run the wizard from inside that package.`
|
|
2090
|
-
);
|
|
2091
|
-
}
|
|
2092
|
-
function resolveWorkspaceAppPath(root, relativePath) {
|
|
2093
|
-
return `${root}/${relativePath}`.replace(/\\/g, "/");
|
|
2094
|
-
}
|
|
2095
|
-
function isSameDirectory(a, b) {
|
|
2096
|
-
return a.replace(/\\/g, "/").replace(/\/+$/, "") === b.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
2097
|
-
}
|
|
2098
|
-
|
|
2099
|
-
// src/login.ts
|
|
2100
|
-
function getPostLoginBannerMessage(opts) {
|
|
2101
|
-
if (opts.suppressPostLoginBanner) return null;
|
|
2102
|
-
if (opts.upgradeScope) {
|
|
2103
|
-
return " Key upgraded! Re-run 'mushi setup' to update your .cursor/mcp.json, then restart Cursor.";
|
|
2104
|
-
}
|
|
2105
|
-
return " Run 'mushi init' to set up the SDK in this project.";
|
|
2106
|
-
}
|
|
2107
|
-
function printPostLoginBanner(opts) {
|
|
2108
|
-
const message = getPostLoginBannerMessage(opts);
|
|
2109
|
-
if (message) console.log(message);
|
|
2110
|
-
}
|
|
2111
|
-
var MAX_CONSECUTIVE_ERRORS = 5;
|
|
2112
|
-
async function pollUntilApproved(endpoint, deviceData, deps) {
|
|
2113
|
-
const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
2114
|
-
const now = deps.now ?? Date.now;
|
|
2115
|
-
let pollIntervalMs = (deviceData.interval ?? 5) * 1e3;
|
|
2116
|
-
const deadline = now() + (deviceData.expires_in ?? 600) * 1e3;
|
|
2117
|
-
let consecutiveErrors = 0;
|
|
2118
|
-
let firstPoll = true;
|
|
2119
|
-
while (now() < deadline) {
|
|
2120
|
-
if (!firstPoll) {
|
|
2121
|
-
await sleep(pollIntervalMs);
|
|
2122
|
-
}
|
|
2123
|
-
firstPoll = false;
|
|
2124
|
-
const outcome = await deps.poll(endpoint, deviceData.device_code);
|
|
2125
|
-
switch (outcome.status) {
|
|
2126
|
-
case "approved":
|
|
2127
|
-
return { status: "approved", cliToken: outcome.cliToken };
|
|
2128
|
-
case "pending":
|
|
2129
|
-
consecutiveErrors = 0;
|
|
2130
|
-
continue;
|
|
2131
|
-
case "slow_down":
|
|
2132
|
-
consecutiveErrors = 0;
|
|
2133
|
-
pollIntervalMs = Math.max(pollIntervalMs, outcome.retryAfterMs);
|
|
2134
|
-
continue;
|
|
2135
|
-
case "error":
|
|
2136
|
-
if (outcome.retryable) {
|
|
2137
|
-
consecutiveErrors += 1;
|
|
2138
|
-
if (consecutiveErrors < MAX_CONSECUTIVE_ERRORS) continue;
|
|
2139
|
-
}
|
|
2140
|
-
return { status: "error", message: outcome.message };
|
|
2141
|
-
case "denied":
|
|
2142
|
-
return { status: "denied" };
|
|
2143
|
-
case "expired":
|
|
2144
|
-
return { status: "timeout" };
|
|
2145
|
-
}
|
|
2146
|
-
}
|
|
2147
|
-
return { status: "timeout" };
|
|
2148
|
-
}
|
|
2149
|
-
async function resolveProjectChoice(projectsList, ask) {
|
|
2150
|
-
if (projectsList.length > 0) {
|
|
2151
|
-
console.log("");
|
|
2152
|
-
console.log(" Your projects:");
|
|
2153
|
-
projectsList.forEach((p3, i) => {
|
|
2154
|
-
console.log(` ${i + 1}. ${p3.name} (${p3.id})`);
|
|
2155
|
-
});
|
|
2156
|
-
console.log(` ${projectsList.length + 1}. Create a new project`);
|
|
2157
|
-
console.log("");
|
|
2158
|
-
const choice = await ask(` Pick a project [1-${projectsList.length + 1}]: `);
|
|
2159
|
-
const num = parseInt(choice, 10);
|
|
2160
|
-
if (num >= 1 && num <= projectsList.length) {
|
|
2161
|
-
const picked = projectsList[num - 1];
|
|
2162
|
-
return { kind: "picked", id: picked.id, name: picked.name };
|
|
2163
|
-
}
|
|
2164
|
-
}
|
|
2165
|
-
console.log("");
|
|
2166
|
-
const newName = (await ask(" Project name: ")).trim();
|
|
2167
|
-
if (!newName) return { kind: "empty_name" };
|
|
2168
|
-
return { kind: "create", name: newName };
|
|
2169
|
-
}
|
|
2170
|
-
async function runLogin(opts = {}) {
|
|
2171
|
-
const { CLOUD_API_ENDPOINT, resolveCloudEndpoint: resolveCloudEndpoint2 } = await import("./endpoint-TEBF4IH6.js");
|
|
2172
|
-
const endpoint = opts.endpoint ? resolveCloudEndpoint2(opts.endpoint) : CLOUD_API_ENDPOINT;
|
|
2173
|
-
const consoleBase = await resolveConsoleUrl();
|
|
2174
|
-
let apiKey = opts.apiKey;
|
|
2175
|
-
let projectId = opts.projectId;
|
|
2176
|
-
if (apiKey) {
|
|
2177
|
-
const config2 = loadConfig();
|
|
2178
|
-
config2.apiKey = apiKey;
|
|
2179
|
-
config2.endpoint = endpoint;
|
|
2180
|
-
if (projectId) config2.projectId = projectId;
|
|
2181
|
-
config2.consoleUrl = consoleBase;
|
|
2182
|
-
saveConfig(config2);
|
|
2183
|
-
console.log("");
|
|
2184
|
-
console.log(" \u2713 Credentials saved.");
|
|
2185
|
-
const verifyResult = await apiCall("/v1/sync/whoami", { apiKey, endpoint, projectId });
|
|
2186
|
-
if (!verifyResult.ok) {
|
|
2187
|
-
console.warn(` \u26A0 Key saved, but verification failed: ${verifyResult.error?.message ?? "unknown error"}`);
|
|
2188
|
-
console.warn(` Check your key at: ${cliSetupDeepLink(consoleBase)}`);
|
|
2189
|
-
return;
|
|
2190
|
-
}
|
|
2191
|
-
const d = verifyResult.data;
|
|
2192
|
-
if (d.project_name) {
|
|
2193
|
-
if (!projectId && d.project_id) {
|
|
2194
|
-
config2.projectId = d.project_id;
|
|
2195
|
-
saveConfig(config2);
|
|
2196
|
-
}
|
|
2197
|
-
console.log(` \u2713 Project: ${d.project_name} (${d.project_id})`);
|
|
2198
|
-
}
|
|
2199
|
-
console.log(` \u2713 Endpoint: ${endpoint}`);
|
|
2200
|
-
console.log("");
|
|
2201
|
-
if (!opts.suppressPostLoginBanner) {
|
|
2202
|
-
console.log(" Run 'mushi whoami' to verify \xB7 'mushi init' to set up the SDK");
|
|
2203
|
-
}
|
|
2204
|
-
return;
|
|
2205
|
-
}
|
|
2206
|
-
if (!opts.suppressPostLoginBanner) {
|
|
2207
|
-
console.log("");
|
|
2208
|
-
}
|
|
2209
|
-
if (opts.upgradeScope) {
|
|
2210
|
-
console.log(" Mushi login \u2014 scope upgrade");
|
|
2211
|
-
console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
2212
|
-
console.log(" Re-authenticating to mint a new key with report:write + mcp:read + mcp:write scopes.");
|
|
2213
|
-
console.log(" This lets the CLI admin commands (including writes) and MCP server both work with a single key.");
|
|
2214
|
-
console.log("");
|
|
2215
|
-
} else if (!opts.suppressPostLoginBanner) {
|
|
2216
|
-
console.log(" Mushi login");
|
|
2217
|
-
console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
2218
|
-
}
|
|
2219
|
-
let deviceData;
|
|
2432
|
+
message: "Run `mushi connect --write-env --wire-ide --wait` now? (SDK env + Cursor MCP + heartbeat check)",
|
|
2433
|
+
initialValue: false
|
|
2434
|
+
});
|
|
2435
|
+
if (p.isCancel(answer) || !answer) return;
|
|
2436
|
+
const endpoint = resolveCloudEndpoint(options.endpoint);
|
|
2220
2437
|
try {
|
|
2221
|
-
|
|
2438
|
+
const { runConnect: runConnect2 } = await import("./connect-63GYZLFM.js");
|
|
2439
|
+
const saved = loadConfig();
|
|
2440
|
+
const result = await runConnect2(
|
|
2441
|
+
{
|
|
2442
|
+
apiKey: saved.apiKey ?? credentials.sdkKey,
|
|
2443
|
+
sdkKey: credentials.sdkKey,
|
|
2444
|
+
projectId: credentials.projectId,
|
|
2445
|
+
endpoint,
|
|
2446
|
+
cwd: options.cwd,
|
|
2447
|
+
writeEnv: true,
|
|
2448
|
+
wireIde: true,
|
|
2449
|
+
wait: true
|
|
2450
|
+
},
|
|
2451
|
+
saved
|
|
2452
|
+
);
|
|
2453
|
+
for (const line of result.messages) p.log.message(line);
|
|
2222
2454
|
} catch (err) {
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
`);
|
|
2226
|
-
process.stderr.write(` Fallback: mushi login --api-key <key> --project-id <uuid>
|
|
2227
|
-
`);
|
|
2228
|
-
process.exit(1);
|
|
2229
|
-
}
|
|
2230
|
-
const verifyUrl = deviceData.verification_uri;
|
|
2231
|
-
if (opts.browser !== false) {
|
|
2232
|
-
try {
|
|
2233
|
-
await openInBrowser(verifyUrl);
|
|
2234
|
-
} catch {
|
|
2235
|
-
}
|
|
2455
|
+
p.log.warn(err instanceof Error ? err.message : String(err));
|
|
2456
|
+
p.log.info("You can run manually: mushi connect --write-env --wire-ide --wait");
|
|
2236
2457
|
}
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2458
|
+
}
|
|
2459
|
+
async function maybeEnableRewards(options) {
|
|
2460
|
+
if (options.yes) return false;
|
|
2461
|
+
const answer = await p.confirm({
|
|
2462
|
+
message: "Enable Mushi Rewards? (users earn points for bug reports + app activity)",
|
|
2463
|
+
initialValue: false
|
|
2240
2464
|
});
|
|
2241
|
-
if (
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2465
|
+
if (p.isCancel(answer)) return false;
|
|
2466
|
+
return Boolean(answer);
|
|
2467
|
+
}
|
|
2468
|
+
async function maybeSendTestReport(credentials, options) {
|
|
2469
|
+
if (options.sendTestReport === false) return;
|
|
2470
|
+
let shouldSend;
|
|
2471
|
+
if (options.sendTestReport === true || options.yes) {
|
|
2472
|
+
shouldSend = true;
|
|
2473
|
+
} else {
|
|
2474
|
+
const answer = await p.confirm({
|
|
2475
|
+
message: "Send a test report now to verify the pipeline?",
|
|
2476
|
+
initialValue: true
|
|
2477
|
+
});
|
|
2478
|
+
if (p.isCancel(answer)) return;
|
|
2479
|
+
shouldSend = answer;
|
|
2246
2480
|
}
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
const
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
const
|
|
2253
|
-
|
|
2254
|
-
const
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
}
|
|
2481
|
+
if (!shouldSend) return;
|
|
2482
|
+
const endpoint = normalizeEndpoint(resolveCloudEndpoint(options.endpoint));
|
|
2483
|
+
const spinner3 = p.spinner();
|
|
2484
|
+
spinner3.start("Sending test report\u2026");
|
|
2485
|
+
const controller = new AbortController();
|
|
2486
|
+
const timer = setTimeout(() => controller.abort(), TEST_REPORT_FETCH_TIMEOUT_MS);
|
|
2487
|
+
try {
|
|
2488
|
+
const res = await fetch(`${endpoint}/v1/reports`, {
|
|
2489
|
+
method: "POST",
|
|
2490
|
+
signal: controller.signal,
|
|
2491
|
+
headers: {
|
|
2492
|
+
"Content-Type": "application/json",
|
|
2493
|
+
"X-Mushi-Api-Key": credentials.sdkKey,
|
|
2494
|
+
"X-Mushi-Project": credentials.projectId
|
|
2495
|
+
},
|
|
2496
|
+
body: JSON.stringify({
|
|
2497
|
+
projectId: credentials.projectId,
|
|
2498
|
+
description: "Test report from the mushi-mushi setup wizard",
|
|
2499
|
+
category: "other",
|
|
2500
|
+
reporterToken: `wizard-${randomUUID()}`,
|
|
2501
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2502
|
+
environment: {
|
|
2503
|
+
url: "cli://wizard",
|
|
2504
|
+
userAgent: `mushi-wizard/${process.platform}-${process.arch}`,
|
|
2505
|
+
platform: process.platform,
|
|
2506
|
+
language: "en",
|
|
2507
|
+
viewport: { width: 0, height: 0 },
|
|
2508
|
+
referrer: "",
|
|
2509
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2510
|
+
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone
|
|
2511
|
+
}
|
|
2512
|
+
})
|
|
2513
|
+
});
|
|
2514
|
+
if (!res.ok) {
|
|
2515
|
+
spinner3.stop(`Test report rejected (HTTP ${res.status}).`);
|
|
2516
|
+
p.log.warn(
|
|
2517
|
+
res.status === 401 || res.status === 403 ? "Credentials did not authenticate \u2014 double-check the project ID and API key." : "Skipping test report. You can retry with `mushi test`."
|
|
2518
|
+
);
|
|
2519
|
+
return;
|
|
2279
2520
|
}
|
|
2280
|
-
|
|
2281
|
-
|
|
2521
|
+
spinner3.stop("Test report sent.");
|
|
2522
|
+
let reportId;
|
|
2282
2523
|
try {
|
|
2283
|
-
|
|
2524
|
+
const body = await res.json();
|
|
2525
|
+
reportId = body.data?.reportId;
|
|
2284
2526
|
} catch {
|
|
2285
2527
|
}
|
|
2528
|
+
const consoleBase = options.consoleBase ?? resolveConsoleUrlSync(options.cwd);
|
|
2529
|
+
p.log.success(`View it at ${reportsUrl(consoleBase, reportId)}`);
|
|
2530
|
+
} catch (err) {
|
|
2531
|
+
const aborted = err instanceof Error && err.name === "AbortError";
|
|
2532
|
+
spinner3.stop(aborted ? "Timed out reaching the Mushi API." : "Could not reach the Mushi API.");
|
|
2533
|
+
p.log.warn(err instanceof Error ? err.message : String(err));
|
|
2534
|
+
} finally {
|
|
2535
|
+
clearTimeout(timer);
|
|
2286
2536
|
}
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
if (
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
console.log(` \u2713 Full-scope CLI key saved (${apiKey.slice(0, 12)}\u2026)`);
|
|
2302
|
-
console.log(` Scopes: report:write \xB7 mcp:read \xB7 mcp:write \u2014 SDK + MCP + admin CLI (incl. writes) all work with this key`);
|
|
2303
|
-
} else {
|
|
2304
|
-
console.log(` \u2713 CLI key saved (${apiKey.slice(0, 12)}\u2026)`);
|
|
2305
|
-
console.log(` Scopes: report:write \xB7 mcp:read \u2014 SDK ingest + MCP read tools.`);
|
|
2306
|
-
console.log(` Admin writes (billing cap, fix merge, pipeline start) need: mushi login --upgrade-scope`);
|
|
2307
|
-
}
|
|
2308
|
-
} else {
|
|
2309
|
-
console.log(` \u2139 Get an SDK key at: ${apiKeyHint(consoleBase)}`);
|
|
2537
|
+
}
|
|
2538
|
+
async function printFreshnessHint() {
|
|
2539
|
+
const result = await checkFreshness("mushi-mushi", MUSHI_CLI_VERSION);
|
|
2540
|
+
if (!result || !result.isOutdated) return;
|
|
2541
|
+
p.log.info(
|
|
2542
|
+
`A newer version of mushi-mushi is available: ${result.current} \u2192 ${result.latest}. Run \`npx mushi-mushi@latest\` to get the freshest wizard.`
|
|
2543
|
+
);
|
|
2544
|
+
}
|
|
2545
|
+
function warnIfWorkspaceRoot(cwd) {
|
|
2546
|
+
let hint;
|
|
2547
|
+
try {
|
|
2548
|
+
hint = detectWorkspaceHint(cwd);
|
|
2549
|
+
} catch {
|
|
2550
|
+
return;
|
|
2310
2551
|
}
|
|
2311
|
-
|
|
2312
|
-
|
|
2552
|
+
if (!hint || hint.apps.length === 0) return;
|
|
2553
|
+
const hasFrameworkAtCwd = hint.apps.some(
|
|
2554
|
+
(app) => isSameDirectory(cwd, resolveWorkspaceAppPath(hint.root, app.relativePath))
|
|
2555
|
+
);
|
|
2556
|
+
if (hasFrameworkAtCwd) return;
|
|
2557
|
+
const apps = hint.apps.slice(0, 5).map((app) => ` \u2022 ${app.relativePath} (${app.framework})`).join("\n");
|
|
2558
|
+
p.log.warn(
|
|
2559
|
+
`You appear to be at a workspace root (source: ${hint.source}). Mushi will install into the current directory, which has no framework dep. You probably meant one of these sub-packages:
|
|
2560
|
+
${apps}
|
|
2561
|
+
Run \`mushi init --cwd <path>\` \u2014 or re-run the wizard from inside that package.`
|
|
2562
|
+
);
|
|
2563
|
+
}
|
|
2564
|
+
function resolveWorkspaceAppPath(root, relativePath) {
|
|
2565
|
+
return `${root}/${relativePath}`.replace(/\\/g, "/");
|
|
2566
|
+
}
|
|
2567
|
+
function isSameDirectory(a, b) {
|
|
2568
|
+
return a.replace(/\\/g, "/").replace(/\/+$/, "") === b.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
2313
2569
|
}
|
|
2314
2570
|
|
|
2315
2571
|
// src/migrate.ts
|
|
@@ -2562,8 +2818,9 @@ Useful after 'mushi login' to confirm credentials are correct.`).action(async (o
|
|
|
2562
2818
|
console.log("");
|
|
2563
2819
|
console.log(`Fixes: ${d.fixes_count} total \xB7 ${d.fixes_merged} merged`);
|
|
2564
2820
|
console.log(`Lessons: ${d.lessons_count} active rules`);
|
|
2821
|
+
console.log(await formatActivationLine(config));
|
|
2565
2822
|
});
|
|
2566
|
-
program2.command("config").description(
|
|
2823
|
+
program2.command("config").description(`View or update CLI config (stored in ${CONFIG_PATH})`).argument("[key]", "Config key to set: apiKey | endpoint | projectId").argument("[value]", "New value").addHelpText("after", `
|
|
2567
2824
|
Keys:
|
|
2568
2825
|
apiKey \u2014 Mushi API key (mushi_...)
|
|
2569
2826
|
endpoint \u2014 Supabase edge function URL
|
|
@@ -2587,11 +2844,33 @@ Examples:
|
|
|
2587
2844
|
saveConfig(config);
|
|
2588
2845
|
console.log(`\u2713 Set ${key}`);
|
|
2589
2846
|
} else {
|
|
2590
|
-
const safe = {
|
|
2847
|
+
const safe = {
|
|
2848
|
+
...config,
|
|
2849
|
+
apiKey: config.apiKey ? `${config.apiKey.slice(0, 10)}\u2026` : void 0,
|
|
2850
|
+
sdkKey: typeof config.sdkKey?.key === "string" ? { ...config.sdkKey, key: `${config.sdkKey.key.slice(0, 10)}\u2026` } : void 0
|
|
2851
|
+
};
|
|
2591
2852
|
console.log(JSON.stringify(safe, null, 2));
|
|
2592
2853
|
}
|
|
2593
2854
|
});
|
|
2594
2855
|
}
|
|
2856
|
+
async function formatActivationLine(config) {
|
|
2857
|
+
const qs = config.projectId ? `?project_id=${encodeURIComponent(config.projectId)}` : "";
|
|
2858
|
+
const result = await apiCall(`/v1/admin/activation${qs}`, config);
|
|
2859
|
+
if (!result.ok) {
|
|
2860
|
+
const code = result.error.code.toUpperCase();
|
|
2861
|
+
const status = result.httpStatus ?? 0;
|
|
2862
|
+
if (status === 401 || status === 403 || /SCOPE|UNAUTHORIZED|FORBIDDEN|TOKEN/.test(code)) {
|
|
2863
|
+
return "Activation: (needs mcp:read \u2014 run `mushi login --upgrade-scope`)";
|
|
2864
|
+
}
|
|
2865
|
+
return `Activation: unavailable (${result.error.code})`;
|
|
2866
|
+
}
|
|
2867
|
+
const d = result.data;
|
|
2868
|
+
const stats = d.stats ?? {};
|
|
2869
|
+
const firstAt = stats.first_report_at ?? stats.firstReportAt ?? null;
|
|
2870
|
+
const reportCount = typeof stats.reportCount === "number" ? stats.reportCount : 0;
|
|
2871
|
+
const firstReport = firstAt ? String(firstAt).slice(0, 10) : reportCount > 0 ? "received" : "none yet";
|
|
2872
|
+
return `Activation: ${d.phase ?? "ingest"} \xB7 first report ${firstReport}`;
|
|
2873
|
+
}
|
|
2595
2874
|
|
|
2596
2875
|
// src/commands/deploy.ts
|
|
2597
2876
|
function registerDeployCommands(program2) {
|
|
@@ -3389,7 +3668,8 @@ async function writeProjectBootstrapFiles(opts) {
|
|
|
3389
3668
|
const serverBlock = buildMcpServerBlock({
|
|
3390
3669
|
endpoint: opts.endpoint,
|
|
3391
3670
|
projectId: opts.projectId,
|
|
3392
|
-
apiKey: opts.apiKey
|
|
3671
|
+
apiKey: opts.apiKey,
|
|
3672
|
+
client: "cursor"
|
|
3393
3673
|
});
|
|
3394
3674
|
const { created: mcpCreated } = await writeMcpServerEntry({
|
|
3395
3675
|
configPath: mcpPath,
|
|
@@ -3525,38 +3805,121 @@ error: Could not create project: ${err instanceof Error ? err.message : String(e
|
|
|
3525
3805
|
}
|
|
3526
3806
|
|
|
3527
3807
|
// src/commands/setup.ts
|
|
3808
|
+
import { join as join4 } from "path";
|
|
3528
3809
|
function resolveLoginEndpoint(optsEndpoint, existingConfigEndpoint, envEndpoint) {
|
|
3529
3810
|
return optsEndpoint ?? existingConfigEndpoint ?? envEndpoint?.trim();
|
|
3530
3811
|
}
|
|
3812
|
+
function resolveMcpConfigTarget(ide, cwd, home) {
|
|
3813
|
+
switch (ide) {
|
|
3814
|
+
case "cursor":
|
|
3815
|
+
return { dir: join4(cwd, ".cursor"), path: join4(cwd, ".cursor", "mcp.json"), format: "mcp-json", repoLocal: true };
|
|
3816
|
+
case "claude":
|
|
3817
|
+
return { dir: cwd, path: join4(cwd, ".mcp.json"), format: "mcp-json", repoLocal: true };
|
|
3818
|
+
case "continue":
|
|
3819
|
+
return { dir: join4(cwd, ".continue"), path: join4(cwd, ".continue", "mcp.json"), format: "mcp-json", repoLocal: true };
|
|
3820
|
+
case "zed":
|
|
3821
|
+
return {
|
|
3822
|
+
dir: join4(home, ".config", "zed"),
|
|
3823
|
+
path: join4(home, ".config", "zed", "settings.json"),
|
|
3824
|
+
format: "zed",
|
|
3825
|
+
repoLocal: false
|
|
3826
|
+
};
|
|
3827
|
+
}
|
|
3828
|
+
}
|
|
3829
|
+
function deriveHostedMcpUrl(endpoint) {
|
|
3830
|
+
return /\/api\/?$/.test(endpoint) ? endpoint.replace(/\/api\/?$/, "/mcp") : null;
|
|
3831
|
+
}
|
|
3832
|
+
function buildSetupServerBlock(opts) {
|
|
3833
|
+
const stdioBlock = buildMcpServerBlock({
|
|
3834
|
+
endpoint: opts.endpoint,
|
|
3835
|
+
projectId: opts.projectId,
|
|
3836
|
+
apiKey: opts.apiKey,
|
|
3837
|
+
client: opts.ide,
|
|
3838
|
+
inlineKey: opts.inlineKey
|
|
3839
|
+
});
|
|
3840
|
+
const wantsHosted = (opts.ide === "cursor" || opts.ide === "claude") && !opts.stdio && !opts.allProjects;
|
|
3841
|
+
if (!wantsHosted) return { block: stdioBlock, hosted: false };
|
|
3842
|
+
const hostedUrl = deriveHostedMcpUrl(opts.endpoint);
|
|
3843
|
+
if (!hostedUrl) {
|
|
3844
|
+
return {
|
|
3845
|
+
block: stdioBlock,
|
|
3846
|
+
hosted: false,
|
|
3847
|
+
note: "Note: endpoint does not end in /api \u2014 cannot derive the hosted MCP URL; writing a local stdio entry instead."
|
|
3848
|
+
};
|
|
3849
|
+
}
|
|
3850
|
+
return { block: buildHostedMcpServerBlock(hostedUrl), hosted: true };
|
|
3851
|
+
}
|
|
3852
|
+
function buildZedServerBlock(opts) {
|
|
3853
|
+
const { env } = buildMcpServerBlock({ ...opts, client: "zed" });
|
|
3854
|
+
return {
|
|
3855
|
+
command: { path: "npx", args: ["-y", MUSHI_MCP_PIN_SPEC], env },
|
|
3856
|
+
settings: {}
|
|
3857
|
+
};
|
|
3858
|
+
}
|
|
3859
|
+
function slugifyProjectName(name) {
|
|
3860
|
+
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 24);
|
|
3861
|
+
}
|
|
3531
3862
|
function registerSetupCommands(program2) {
|
|
3532
|
-
program2.command("setup").description(
|
|
3863
|
+
program2.command("setup").description(`Wire the Mushi MCP server into Cursor, Claude Code, Continue or Zed (reads ${CONFIG_PATH} \u2014 signs you in first if needed)`).option("--ide <ide>", `Target IDE: ${MCP_CLIENTS.join(" | ")}`, "cursor").option("--project-slug <slug>", "Override the project slug in the server name (default: fetched from API or ID prefix)").option("--all-projects", "Write a separate mushi-<name> server entry for every accessible project").option("--with-rules", "Also write the .cursorrules / .claude/rules/mushi.md lesson-library hook").option("--dry-run", "Print what would be written without making changes (never signs in or saves anything)").option("--verify", "Probe the MCP key after writing to confirm it has mcp:read scope (default: on)").option("--no-verify", "Skip the post-write key probe").option("--endpoint <url>", "Override the Mushi API endpoint (self-hosted) \u2014 used for first-run login if not yet configured").option("--stdio", "Write a local stdio entry instead of the hosted OAuth URL (cursor/claude default is hosted OAuth \u2014 no key on disk)").option("--ci", "Alias for --stdio: headless environments cannot drive the browser OAuth flow").option("--inline-key", "Write the literal API key into the stdio env block (default: no key in the file \u2014 the server reads your saved CLI key)").addHelpText("after", `
|
|
3533
3864
|
Examples:
|
|
3534
3865
|
mushi setup # wire Cursor (default: hosted OAuth \u2014 sign in from the IDE, no key on disk)
|
|
3535
|
-
mushi setup --
|
|
3866
|
+
mushi setup --ide claude # wire Claude Code (.mcp.json at the repo root)
|
|
3867
|
+
mushi setup --stdio # local stdio entry that reads your saved CLI key
|
|
3536
3868
|
mushi setup --ci # same as --stdio, for headless/CI environments
|
|
3537
3869
|
mushi setup --all-projects # one server entry per accessible project (stdio)
|
|
3538
|
-
mushi setup --ide claude # wire Claude Code
|
|
3539
3870
|
mushi setup --ide cursor --with-rules # also write .cursorrules
|
|
3871
|
+
mushi setup --ide claude --dry-run # preview the entry; signs nothing in, writes nothing
|
|
3540
3872
|
|
|
3541
3873
|
Supported IDEs:
|
|
3542
|
-
cursor \u2014
|
|
3543
|
-
claude \u2014
|
|
3544
|
-
continue \u2014
|
|
3545
|
-
zed \u2014
|
|
3874
|
+
cursor \u2014 merges into .cursor/mcp.json
|
|
3875
|
+
claude \u2014 merges into .mcp.json at the repo root (Claude Code project scope)
|
|
3876
|
+
continue \u2014 merges into .continue/mcp.json (stdio only)
|
|
3877
|
+
zed \u2014 merges into ~/.config/zed/settings.json context_servers (stdio only)
|
|
3546
3878
|
|
|
3547
3879
|
For cursor and claude the default entry is the hosted MCP URL: your IDE opens
|
|
3548
3880
|
the browser consent page on first use (OAuth + PKCE) and stores a revocable
|
|
3549
3881
|
key for you \u2014 nothing sensitive is written to the repo. Pass --stdio/--ci to
|
|
3550
|
-
get the local subprocess entry
|
|
3882
|
+
get the local subprocess entry instead; it reads the key from the CLI config
|
|
3883
|
+
unless you pass --inline-key.
|
|
3551
3884
|
|
|
3552
|
-
|
|
3885
|
+
Credentials are read from the CLI config at:
|
|
3886
|
+
${CONFIG_PATH}
|
|
3887
|
+
If you are not signed in yet, setup runs browser sign-in first, then writes the MCP config.`).action(async (opts) => {
|
|
3553
3888
|
const { writeFile: writeFile3, mkdir, readFile: readFile3 } = await import("fs/promises");
|
|
3554
|
-
const { existsSync: existsSync3 } = await import("fs");
|
|
3555
3889
|
const nodePath2 = await import("path");
|
|
3556
3890
|
const os = await import("os");
|
|
3891
|
+
const ide = opts.ide;
|
|
3892
|
+
if (!isMcpClient(ide)) {
|
|
3893
|
+
process.stderr.write(`error: unsupported IDE "${opts.ide}". Supported: ${MCP_CLIENTS.join(", ")}
|
|
3894
|
+
`);
|
|
3895
|
+
process.exit(2);
|
|
3896
|
+
}
|
|
3897
|
+
const ideLabel = MCP_CLIENT_LABEL[ide];
|
|
3898
|
+
const cwd = process.cwd();
|
|
3899
|
+
const target = resolveMcpConfigTarget(ide, cwd, os.homedir());
|
|
3900
|
+
const configPath = target.path;
|
|
3901
|
+
const inlineKey = Boolean(opts.inlineKey);
|
|
3902
|
+
const useStdio = Boolean(opts.stdio || opts.ci);
|
|
3557
3903
|
const existingConfig = loadConfig();
|
|
3558
3904
|
if (!existingConfig.apiKey) {
|
|
3559
3905
|
const endpoint = resolveLoginEndpoint(opts.endpoint, existingConfig.endpoint, process.env.MUSHI_API_ENDPOINT);
|
|
3906
|
+
if (opts.dryRun) {
|
|
3907
|
+
const { block } = buildSetupServerBlock({
|
|
3908
|
+
ide,
|
|
3909
|
+
endpoint: resolveCloudEndpoint(endpoint),
|
|
3910
|
+
projectId: "<project-id>",
|
|
3911
|
+
apiKey: "mushi_\u2026",
|
|
3912
|
+
stdio: useStdio,
|
|
3913
|
+
allProjects: Boolean(opts.allProjects),
|
|
3914
|
+
inlineKey
|
|
3915
|
+
});
|
|
3916
|
+
const name = `mushi-${opts.projectSlug ?? "<project>"}`;
|
|
3917
|
+
const preview = target.format === "zed" ? { context_servers: { [name]: buildZedServerBlock({ endpoint: resolveCloudEndpoint(endpoint), projectId: "<project-id>", apiKey: "mushi_\u2026", inlineKey }) } } : { mcpServers: { [name]: block } };
|
|
3918
|
+
console.log("[dry-run] Not signed in \u2014 a real run starts browser sign-in first. Nothing was sent or saved.");
|
|
3919
|
+
console.log(`[dry-run] Would merge into ${configPath}:`);
|
|
3920
|
+
console.log(JSON.stringify(preview, null, 2));
|
|
3921
|
+
return;
|
|
3922
|
+
}
|
|
3560
3923
|
await runLogin({ endpoint, suppressPostLoginBanner: true });
|
|
3561
3924
|
console.log(" Continuing MCP setup\u2026");
|
|
3562
3925
|
console.log("");
|
|
@@ -3586,11 +3949,7 @@ The command reads credentials from ~/.config/mushi/config.json. If you are not l
|
|
|
3586
3949
|
if (res.ok) {
|
|
3587
3950
|
const body = await res.json();
|
|
3588
3951
|
const project = body?.data?.projects?.find((p3) => p3.id === config.projectId);
|
|
3589
|
-
|
|
3590
|
-
slug = project.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 24);
|
|
3591
|
-
} else {
|
|
3592
|
-
slug = config.projectId?.slice(0, 8) ?? "mushi";
|
|
3593
|
-
}
|
|
3952
|
+
slug = project?.name ? slugifyProjectName(project.name) : config.projectId?.slice(0, 8) ?? "mushi";
|
|
3594
3953
|
} else {
|
|
3595
3954
|
slug = config.projectId?.slice(0, 8) ?? "mushi";
|
|
3596
3955
|
}
|
|
@@ -3598,19 +3957,6 @@ The command reads credentials from ~/.config/mushi/config.json. If you are not l
|
|
|
3598
3957
|
slug = config.projectId?.slice(0, 8) ?? "mushi";
|
|
3599
3958
|
}
|
|
3600
3959
|
}
|
|
3601
|
-
const IDE_CONFIG = {
|
|
3602
|
-
cursor: { dir: ".cursor", file: "mcp.json", format: "mcp-json" },
|
|
3603
|
-
claude: { dir: ".claude", file: "mcp.json", format: "mcp-json" },
|
|
3604
|
-
continue: { dir: ".continue", file: "mcp.json", format: "mcp-json" },
|
|
3605
|
-
zed: { dir: nodePath2.join(os.homedir(), ".config", "zed"), file: "settings.json", format: "zed" }
|
|
3606
|
-
};
|
|
3607
|
-
const ideEntry = IDE_CONFIG[opts.ide];
|
|
3608
|
-
if (!ideEntry) {
|
|
3609
|
-
process.stderr.write(`error: unsupported IDE "${opts.ide}". Supported: ${Object.keys(IDE_CONFIG).join(", ")}
|
|
3610
|
-
`);
|
|
3611
|
-
process.exit(2);
|
|
3612
|
-
}
|
|
3613
|
-
const cwd = process.cwd();
|
|
3614
3960
|
let allProjectsList = null;
|
|
3615
3961
|
if (opts.allProjects && config.endpoint && config.apiKey) {
|
|
3616
3962
|
try {
|
|
@@ -3629,23 +3975,26 @@ The command reads credentials from ~/.config/mushi/config.json. If you are not l
|
|
|
3629
3975
|
}
|
|
3630
3976
|
}
|
|
3631
3977
|
const serverName = `mushi-${slug}`;
|
|
3632
|
-
const
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
|
|
3978
|
+
const writeEntry = async (name, serverBlock) => {
|
|
3979
|
+
try {
|
|
3980
|
+
await writeMcpServerEntry({ configPath, serverName: name, serverBlock });
|
|
3981
|
+
} catch (err) {
|
|
3982
|
+
process.stderr.write(`error: ${err instanceof Error ? err.message : String(err)}
|
|
3983
|
+
`);
|
|
3984
|
+
process.exit(1);
|
|
3985
|
+
}
|
|
3986
|
+
};
|
|
3987
|
+
const { block: chosenBlock, hosted: useHostedOauth, note: note2 } = buildSetupServerBlock({
|
|
3988
|
+
ide,
|
|
3641
3989
|
endpoint: config.endpoint,
|
|
3642
3990
|
projectId: config.projectId ?? "",
|
|
3643
3991
|
apiKey: config.apiKey,
|
|
3992
|
+
stdio: useStdio,
|
|
3993
|
+
allProjects: Boolean(opts.allProjects),
|
|
3644
3994
|
inlineKey
|
|
3645
3995
|
});
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
if (ideEntry.format === "mcp-json") {
|
|
3996
|
+
if (note2) console.log(note2);
|
|
3997
|
+
if (target.format === "mcp-json") {
|
|
3649
3998
|
if (allProjectsList && allProjectsList.length > 0) {
|
|
3650
3999
|
for (const p3 of allProjectsList) {
|
|
3651
4000
|
const pServerName = buildMcpServerName({ projectId: p3.id, projectName: p3.name ?? void 0 });
|
|
@@ -3653,66 +4002,72 @@ The command reads credentials from ~/.config/mushi/config.json. If you are not l
|
|
|
3653
4002
|
endpoint: config.endpoint,
|
|
3654
4003
|
projectId: p3.id,
|
|
3655
4004
|
apiKey: config.apiKey,
|
|
4005
|
+
client: ide,
|
|
3656
4006
|
inlineKey
|
|
3657
4007
|
});
|
|
3658
4008
|
if (!opts.dryRun) {
|
|
3659
|
-
await
|
|
4009
|
+
await writeEntry(pServerName, pBlock);
|
|
3660
4010
|
}
|
|
3661
4011
|
}
|
|
3662
4012
|
if (opts.dryRun) {
|
|
3663
4013
|
console.log(`[dry-run] Would add ${allProjectsList.length} mushi-* entries to ${configPath}`);
|
|
3664
4014
|
} else {
|
|
3665
4015
|
console.log(`\u2713 Added ${allProjectsList.length} mushi-* server entries (${allProjectsList.map((p3) => p3.name ?? p3.id.slice(0, 8)).join(", ")})`);
|
|
4016
|
+
if (!inlineKey) printKeyExportHint(ide);
|
|
3666
4017
|
}
|
|
3667
4018
|
} else {
|
|
3668
|
-
const chosenBlock = useHostedOauth ? hostedServerBlock : mcpServerBlock;
|
|
3669
4019
|
if (opts.dryRun) {
|
|
3670
4020
|
const preview = JSON.stringify({ mcpServers: { [serverName]: chosenBlock } }, null, 2) + "\n";
|
|
3671
|
-
console.log(`[dry-run] Would
|
|
4021
|
+
console.log(`[dry-run] Would merge into ${configPath}:`);
|
|
3672
4022
|
console.log(redactKeyForDisplay(preview));
|
|
3673
4023
|
} else {
|
|
3674
|
-
await
|
|
4024
|
+
await writeEntry(serverName, chosenBlock);
|
|
3675
4025
|
console.log(`\u2713 Written ${configPath}`);
|
|
3676
4026
|
if (useHostedOauth) {
|
|
3677
4027
|
console.log(" Hosted MCP with OAuth login \u2014 no API key was written to this file.");
|
|
3678
|
-
console.log(` Restart ${
|
|
4028
|
+
console.log(` Restart ${ideLabel}, open the MCP panel (/mcp in Claude Code), pick "${serverName}" and sign in via the browser.`);
|
|
4029
|
+
if (ide === "claude") {
|
|
4030
|
+
console.log(" Claude Code asks you to approve servers from .mcp.json the first time \u2014 approve this one.");
|
|
4031
|
+
}
|
|
3679
4032
|
console.log(" Prefer a local key-based entry (headless/CI)? Re-run with --stdio.");
|
|
3680
|
-
} else if (!inlineKey
|
|
3681
|
-
printKeyExportHint(
|
|
4033
|
+
} else if (!inlineKey) {
|
|
4034
|
+
printKeyExportHint(ide);
|
|
3682
4035
|
}
|
|
3683
4036
|
}
|
|
3684
4037
|
}
|
|
3685
|
-
} else
|
|
4038
|
+
} else {
|
|
3686
4039
|
let settings = {};
|
|
3687
|
-
|
|
4040
|
+
let raw = null;
|
|
4041
|
+
try {
|
|
4042
|
+
raw = await readFile3(configPath, "utf8");
|
|
4043
|
+
} catch {
|
|
4044
|
+
}
|
|
4045
|
+
if (raw !== null && raw.trim() !== "") {
|
|
3688
4046
|
try {
|
|
3689
|
-
const raw = await readFile3(configPath, "utf8");
|
|
3690
4047
|
settings = JSON.parse(raw);
|
|
3691
4048
|
} catch {
|
|
4049
|
+
process.stderr.write(`error: ${configPath} is not plain JSON \u2014 add the "${serverName}" entry by hand (run with --dry-run to print it).
|
|
4050
|
+
`);
|
|
4051
|
+
process.exit(1);
|
|
3692
4052
|
}
|
|
3693
4053
|
}
|
|
3694
4054
|
const servers = settings.context_servers ?? {};
|
|
3695
|
-
servers[serverName] = {
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
MUSHI_PROJECT_ID: config.projectId ?? "",
|
|
3702
|
-
MUSHI_API_KEY: inlineKey ? config.apiKey : "${MUSHI_API_KEY}"
|
|
3703
|
-
}
|
|
3704
|
-
},
|
|
3705
|
-
settings: {}
|
|
3706
|
-
};
|
|
4055
|
+
servers[serverName] = buildZedServerBlock({
|
|
4056
|
+
endpoint: config.endpoint,
|
|
4057
|
+
projectId: config.projectId ?? "",
|
|
4058
|
+
apiKey: config.apiKey,
|
|
4059
|
+
inlineKey
|
|
4060
|
+
});
|
|
3707
4061
|
settings.context_servers = servers;
|
|
3708
4062
|
const output = JSON.stringify(settings, null, 2) + "\n";
|
|
3709
4063
|
if (opts.dryRun) {
|
|
3710
4064
|
console.log(`[dry-run] Would write ${configPath}:`);
|
|
3711
4065
|
console.log(redactKeyForDisplay(output));
|
|
3712
4066
|
} else {
|
|
3713
|
-
await mkdir(
|
|
4067
|
+
await mkdir(target.dir, { recursive: true });
|
|
3714
4068
|
await writeFile3(configPath, output, "utf8");
|
|
3715
4069
|
console.log(`\u2713 Written ${configPath}`);
|
|
4070
|
+
if (!inlineKey) printKeyExportHint(ide);
|
|
3716
4071
|
}
|
|
3717
4072
|
}
|
|
3718
4073
|
if (opts.withRules) {
|
|
@@ -3740,7 +4095,7 @@ The command reads credentials from ~/.config/mushi/config.json. If you are not l
|
|
|
3740
4095
|
"<!-- run `mushi sync-lessons` to refresh -->",
|
|
3741
4096
|
""
|
|
3742
4097
|
].join("\n");
|
|
3743
|
-
if (
|
|
4098
|
+
if (ide === "cursor") {
|
|
3744
4099
|
const rulesPath = nodePath2.join(cwd, ".cursorrules");
|
|
3745
4100
|
if (opts.dryRun) {
|
|
3746
4101
|
console.log(`[dry-run] Would write ${rulesPath}`);
|
|
@@ -3748,7 +4103,7 @@ The command reads credentials from ~/.config/mushi/config.json. If you are not l
|
|
|
3748
4103
|
await writeFile3(rulesPath, rulesContent, "utf8");
|
|
3749
4104
|
console.log(`\u2713 Written .cursorrules`);
|
|
3750
4105
|
}
|
|
3751
|
-
} else if (
|
|
4106
|
+
} else if (ide === "claude") {
|
|
3752
4107
|
const rulesDir = nodePath2.join(cwd, ".claude", "rules");
|
|
3753
4108
|
const rulesPath = nodePath2.join(rulesDir, "mushi.md");
|
|
3754
4109
|
if (opts.dryRun) {
|
|
@@ -3775,7 +4130,7 @@ The command reads credentials from ~/.config/mushi/config.json. If you are not l
|
|
|
3775
4130
|
}
|
|
3776
4131
|
);
|
|
3777
4132
|
if (probeRes.ok) {
|
|
3778
|
-
console.log(
|
|
4133
|
+
console.log(`\u2713 MCP key valid \u2014 restart ${ideLabel} to activate`);
|
|
3779
4134
|
if (!process.env.MUSHI_NO_TELEMETRY) void fetch(
|
|
3780
4135
|
`${config.endpoint.replace(/\/$/, "")}/v1/cli/funnel`,
|
|
3781
4136
|
{
|
|
@@ -3811,17 +4166,17 @@ The command reads credentials from ~/.config/mushi/config.json. If you are not l
|
|
|
3811
4166
|
if (answer === "" || answer === "y" || answer === "yes") {
|
|
3812
4167
|
await runLogin({ endpoint: opts.endpoint, upgradeScope: true, suppressPostLoginBanner: true });
|
|
3813
4168
|
const fresh = loadConfig();
|
|
3814
|
-
if (fresh.apiKey &&
|
|
3815
|
-
await
|
|
3816
|
-
configPath,
|
|
4169
|
+
if (fresh.apiKey && target.format === "mcp-json" && !allProjectsList) {
|
|
4170
|
+
await writeEntry(
|
|
3817
4171
|
serverName,
|
|
3818
|
-
|
|
4172
|
+
buildMcpServerBlock({
|
|
3819
4173
|
endpoint: fresh.endpoint ?? config.endpoint,
|
|
3820
4174
|
projectId: fresh.projectId ?? config.projectId ?? "",
|
|
3821
4175
|
apiKey: fresh.apiKey,
|
|
4176
|
+
client: ide,
|
|
3822
4177
|
inlineKey
|
|
3823
4178
|
})
|
|
3824
|
-
|
|
4179
|
+
);
|
|
3825
4180
|
console.log(`\u2713 Rewrote ${configPath} with the upgraded key`);
|
|
3826
4181
|
const reprobe = await fetch(
|
|
3827
4182
|
`${(fresh.endpoint ?? config.endpoint)?.replace(/\/$/, "")}/v1/admin/mcp/account-overview`,
|
|
@@ -3835,7 +4190,7 @@ The command reads credentials from ~/.config/mushi/config.json. If you are not l
|
|
|
3835
4190
|
).catch(() => null);
|
|
3836
4191
|
if (reprobe?.ok) {
|
|
3837
4192
|
upgraded = true;
|
|
3838
|
-
console.log(
|
|
4193
|
+
console.log(`\u2713 MCP key valid \u2014 restart ${ideLabel} to activate`);
|
|
3839
4194
|
} else {
|
|
3840
4195
|
console.log(`\u26A0 Re-probe after upgrade returned HTTP ${reprobe?.status ?? "error"} \u2014 run \`mushi doctor --mcp\`.`);
|
|
3841
4196
|
}
|
|
@@ -3860,24 +4215,23 @@ The command reads credentials from ~/.config/mushi/config.json. If you are not l
|
|
|
3860
4215
|
}
|
|
3861
4216
|
}
|
|
3862
4217
|
console.log("");
|
|
3863
|
-
console.log(`Done! Restart ${
|
|
4218
|
+
console.log(`Done! Restart ${ideLabel} and ask: "list mushi tools"`);
|
|
3864
4219
|
if (!opts.withRules) {
|
|
3865
4220
|
console.log(`Tip: run with --with-rules to also write the lesson-library coding hook.`);
|
|
3866
4221
|
}
|
|
3867
|
-
const configRelPath =
|
|
3868
|
-
if (
|
|
4222
|
+
const configRelPath = target.repoLocal ? nodePath2.relative(cwd, configPath) : configPath;
|
|
4223
|
+
if (!inlineKey) {
|
|
3869
4224
|
console.log(`
|
|
3870
|
-
Note: ${configRelPath} holds no secrets
|
|
3871
|
-
} else if (
|
|
4225
|
+
Note: ${configRelPath} holds no secrets \u2014 safe to commit and share with your team.`);
|
|
4226
|
+
} else if (target.repoLocal) {
|
|
3872
4227
|
const gitignorePath = nodePath2.join(cwd, ".gitignore");
|
|
3873
4228
|
const ignoreLine = configRelPath.replaceAll("\\", "/");
|
|
3874
4229
|
try {
|
|
3875
|
-
const
|
|
3876
|
-
const existing = await readFile4(gitignorePath, "utf8").catch(() => "");
|
|
4230
|
+
const existing = await readFile3(gitignorePath, "utf8").catch(() => "");
|
|
3877
4231
|
const lines = existing.split(/\r?\n/);
|
|
3878
4232
|
if (!lines.some((l) => l.trim() === ignoreLine)) {
|
|
3879
4233
|
const sep = existing.endsWith("\n") || existing === "" ? "" : "\n";
|
|
3880
|
-
await
|
|
4234
|
+
await writeFile3(
|
|
3881
4235
|
gitignorePath,
|
|
3882
4236
|
`${existing}${sep}# Mushi MCP config holds a plaintext API key
|
|
3883
4237
|
${ignoreLine}
|
|
@@ -4333,8 +4687,21 @@ async function planUpgrade(cwd) {
|
|
|
4333
4687
|
installCmd: toBump.length > 0 ? installCommand(pm, toBump) : null
|
|
4334
4688
|
};
|
|
4335
4689
|
}
|
|
4690
|
+
function evaluateUpgradeCheckExit(plan) {
|
|
4691
|
+
if (plan.entries.length === 0) return 1;
|
|
4692
|
+
const semverEntries = plan.entries.filter((e) => /^\d/.test(e.current.replace(/^[\^~>=<]*/, "")));
|
|
4693
|
+
if (semverEntries.length === 0) return 0;
|
|
4694
|
+
if (semverEntries.every((e) => e.latest === null)) return 2;
|
|
4695
|
+
if (plan.entries.some((e) => e.willUpgrade)) return 1;
|
|
4696
|
+
return 0;
|
|
4697
|
+
}
|
|
4336
4698
|
async function runUpgrade(opts = {}) {
|
|
4337
4699
|
const plan = await planUpgrade(opts.cwd ?? process.cwd());
|
|
4700
|
+
if (opts.check) {
|
|
4701
|
+
const exit = evaluateUpgradeCheckExit(plan);
|
|
4702
|
+
const message = exit === 0 ? "All installed Mushi packages are already at the latest stable version." : exit === 2 ? "Could not reach the npm registry to check for updates \u2014 try again in a moment." : plan.entries.length === 0 ? "No @mushi-mushi/* packages in package.json \u2014 run `mushi init` first." : "One or more Mushi packages are behind the latest stable npm release.";
|
|
4703
|
+
return { plan, upgraded: false, message };
|
|
4704
|
+
}
|
|
4338
4705
|
if (plan.entries.length === 0) {
|
|
4339
4706
|
return {
|
|
4340
4707
|
plan,
|
|
@@ -4483,28 +4850,37 @@ ${via}` };
|
|
|
4483
4850
|
|
|
4484
4851
|
// src/commands/upgrade-cli.ts
|
|
4485
4852
|
function registerUpgradeCommand(program2) {
|
|
4486
|
-
program2.command("upgrade").description("Bump installed @mushi-mushi/* packages to the latest stable npm release").option("--cwd <path>", "Target repo (default: cwd)").option("--self", "Upgrade the globally-installed `mushi` CLI itself (not the project SDK deps)").option("--dry-run", "Print the install command without running it").option("--json", "Machine-readable plan + result").addHelpText("after", `
|
|
4853
|
+
program2.command("upgrade").description("Bump installed @mushi-mushi/* packages to the latest stable npm release").option("--cwd <path>", "Target repo (default: cwd)").option("--self", "Upgrade the globally-installed `mushi` CLI itself (not the project SDK deps)").option("--dry-run", "Print the install command without running it").option("--check", "Plan only: exit 0 current, 1 outdated, 2 registry unreachable").option("--json", "Machine-readable plan + result").addHelpText("after", `
|
|
4487
4854
|
Examples:
|
|
4488
4855
|
mushi upgrade
|
|
4489
4856
|
mushi upgrade --dry-run
|
|
4857
|
+
mushi upgrade --check # CI freshness (no install)
|
|
4490
4858
|
mushi upgrade --self # upgrade the CLI binary itself
|
|
4491
4859
|
mushi upgrade --self --dry-run
|
|
4492
4860
|
mushi upgrade --cwd ../glot.it`).action(async (opts) => {
|
|
4493
4861
|
if (opts.self) {
|
|
4494
|
-
const selfResult = await runSelfUpgrade({ dryRun: opts.dryRun });
|
|
4862
|
+
const selfResult = await runSelfUpgrade({ dryRun: opts.dryRun || opts.check });
|
|
4495
4863
|
if (opts.json) {
|
|
4496
4864
|
console.log(JSON.stringify(selfResult, null, 2));
|
|
4497
4865
|
} else {
|
|
4498
4866
|
console.log(selfResult.message);
|
|
4499
4867
|
}
|
|
4500
|
-
if (!selfResult.upgraded && selfResult.command && !opts.dryRun) {
|
|
4868
|
+
if (!selfResult.upgraded && selfResult.command && !opts.dryRun && !opts.check) {
|
|
4501
4869
|
process.exit(1);
|
|
4502
4870
|
}
|
|
4503
4871
|
return;
|
|
4504
4872
|
}
|
|
4505
|
-
const result = await runUpgrade({
|
|
4873
|
+
const result = await runUpgrade({
|
|
4874
|
+
cwd: opts.cwd,
|
|
4875
|
+
dryRun: opts.dryRun,
|
|
4876
|
+
json: opts.json,
|
|
4877
|
+
check: opts.check
|
|
4878
|
+
});
|
|
4506
4879
|
if (opts.json) {
|
|
4507
|
-
console.log(JSON.stringify(
|
|
4880
|
+
console.log(JSON.stringify({
|
|
4881
|
+
...result,
|
|
4882
|
+
...opts.check ? { exit: evaluateUpgradeCheckExit(result.plan) } : {}
|
|
4883
|
+
}, null, 2));
|
|
4508
4884
|
} else {
|
|
4509
4885
|
console.log(result.message);
|
|
4510
4886
|
for (const e of result.plan.entries) {
|
|
@@ -4512,6 +4888,9 @@ Examples:
|
|
|
4512
4888
|
console.log(` ${e.name}@${e.current} ${tag}`);
|
|
4513
4889
|
}
|
|
4514
4890
|
}
|
|
4891
|
+
if (opts.check) {
|
|
4892
|
+
process.exit(evaluateUpgradeCheckExit(result.plan));
|
|
4893
|
+
}
|
|
4515
4894
|
if (!result.upgraded && result.plan.entries.some((e) => e.willUpgrade) && !opts.dryRun) {
|
|
4516
4895
|
process.exit(1);
|
|
4517
4896
|
}
|
|
@@ -4521,10 +4900,18 @@ Examples:
|
|
|
4521
4900
|
|
|
4522
4901
|
// src/commands/connect-cli.ts
|
|
4523
4902
|
function registerConnectCommand(program2) {
|
|
4524
|
-
program2.command("connect").description("Save credentials, merge env vars, wire Cursor MCP, optionally wait for SDK heartbeat").option("--api-key <key>", "Mushi API key (mushi_\u2026) \u2014 or set MUSHI_API_KEY to keep it out of shell history").
|
|
4903
|
+
program2.command("connect").description("Save credentials, merge env vars, wire Cursor MCP, optionally wait for SDK heartbeat").option("--api-key <key>", "Mushi API key (mushi_\u2026) saved to the CLI config \u2014 or set MUSHI_API_KEY to keep it out of shell history").option(
|
|
4904
|
+
"--sdk-key <key>",
|
|
4905
|
+
"Ingest-only (report:write) key for the SDK env vars \u2014 or set MUSHI_SDK_KEY. Default: the key saved for this project, else --api-key if it is ingest-only"
|
|
4906
|
+
).requiredOption("--project-id <id>", "Project UUID").requiredOption("--endpoint <url>", "Supabase edge function URL").option("--cwd <path>", "Target repo").option("--write-env", "Force-write SDK env vars to .env.local (default; overrides --no-env)").option("--wire-ide", "Force-wire Cursor MCP into .cursor/mcp.json (default; overrides --no-ide)").option("--no-env", "Skip writing .env.local").option("--no-ide", "Skip writing .cursor/mcp.json").option("--wait", "Poll ingest-setup until SDK heartbeat lands").option("--wait-timeout <sec>", "Max seconds for --wait", "120").option("--json", "Machine-readable output").addHelpText("after", `
|
|
4525
4907
|
Examples:
|
|
4526
4908
|
MUSHI_API_KEY=mushi_xxx mushi connect --project-id <uuid> --endpoint https://<ref>.supabase.co/functions/v1/api --wait
|
|
4527
|
-
mushi connect --api-key mushi_xxx --project-id <uuid> --endpoint <url> --no-ide
|
|
4909
|
+
mushi connect --api-key mushi_xxx --project-id <uuid> --endpoint <url> --no-ide
|
|
4910
|
+
MUSHI_API_KEY=mushi_cli MUSHI_SDK_KEY=mushi_ingest mushi connect --project-id <uuid> --endpoint <url>
|
|
4911
|
+
|
|
4912
|
+
SDK env vars (VITE_ / NEXT_PUBLIC_ / EXPO_PUBLIC_ \u2026) ship inside your app bundle,
|
|
4913
|
+
so connect only writes a key the backend confirms is ingest-only (report:write).
|
|
4914
|
+
A key that can read reports is refused for .env.local; it still works for the CLI and MCP.`).action(async (opts) => {
|
|
4528
4915
|
const apiKey = process.env.MUSHI_API_KEY ?? opts.apiKey;
|
|
4529
4916
|
if (!apiKey) {
|
|
4530
4917
|
console.error("Provide the API key via the MUSHI_API_KEY env var (recommended) or --api-key <key>.");
|
|
@@ -4532,6 +4919,7 @@ Examples:
|
|
|
4532
4919
|
}
|
|
4533
4920
|
const result = await runConnect({
|
|
4534
4921
|
apiKey,
|
|
4922
|
+
sdkKey: process.env.MUSHI_SDK_KEY?.trim() || opts.sdkKey,
|
|
4535
4923
|
projectId: opts.projectId,
|
|
4536
4924
|
endpoint: opts.endpoint,
|
|
4537
4925
|
cwd: opts.cwd,
|
|
@@ -4540,7 +4928,7 @@ Examples:
|
|
|
4540
4928
|
wait: opts.wait,
|
|
4541
4929
|
waitTimeoutSec: parseInt(opts.waitTimeout, 10) || 120,
|
|
4542
4930
|
json: opts.json
|
|
4543
|
-
});
|
|
4931
|
+
}, loadConfig());
|
|
4544
4932
|
if (opts.json) {
|
|
4545
4933
|
console.log(JSON.stringify(result, null, 2));
|
|
4546
4934
|
} else {
|
|
@@ -6212,7 +6600,7 @@ function registerSelfhostCommands(program2) {
|
|
|
6212
6600
|
"Base URL of your deployed admin console (e.g. https://your-domain.example.com/admin)"
|
|
6213
6601
|
).option(
|
|
6214
6602
|
"--endpoint <url>",
|
|
6215
|
-
|
|
6603
|
+
`Mushi API endpoint to use for the proof step (default: from ${CONFIG_PATH})`
|
|
6216
6604
|
).option(
|
|
6217
6605
|
"--skip-deploy",
|
|
6218
6606
|
"Skip function deploys (run db push + secrets only)"
|
|
@@ -6241,7 +6629,7 @@ After "up" completes:
|
|
|
6241
6629
|
"Check the health of a self-hosted deployment (calls GET /v1/admin/doctor)"
|
|
6242
6630
|
).option(
|
|
6243
6631
|
"--endpoint <url>",
|
|
6244
|
-
|
|
6632
|
+
`Mushi API endpoint (default: from ${CONFIG_PATH})`
|
|
6245
6633
|
).option("--json", "Machine-readable JSON output").addHelpText(
|
|
6246
6634
|
"after",
|
|
6247
6635
|
`
|