@mushi-mushi/cli 0.19.0 → 0.21.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/CONTRIBUTING.md +14 -2
- package/SECURITY.md +1 -1
- package/dist/{chunk-7Q44KWGN.js → chunk-JAUKBLPK.js} +15 -9
- package/dist/chunk-L7KBJ2XY.js +6 -0
- package/dist/{chunk-D5IDNBJO.js → chunk-R2HPR2FV.js} +15 -9
- package/dist/{connect-IC7I76Z2.js → connect-2RM43XHO.js} +1 -1
- package/dist/{connect-W2Q6N7K3.js → connect-JIEIWTF3.js} +1 -1
- package/dist/detect.d.ts +1 -1
- package/dist/detect.js +1 -1
- package/dist/index.js +888 -203
- package/dist/init.js +394 -78
- package/dist/version.js +1 -1
- package/package.json +3 -3
- package/dist/chunk-T4XX5FMH.js +0 -6
package/dist/index.js
CHANGED
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
sanitizeProjectId,
|
|
19
19
|
saveConfig,
|
|
20
20
|
writeMcpServerEntry
|
|
21
|
-
} from "./chunk-
|
|
21
|
+
} from "./chunk-R2HPR2FV.js";
|
|
22
22
|
import {
|
|
23
23
|
CLOUD_API_ENDPOINT,
|
|
24
24
|
TEST_REPORT_FETCH_TIMEOUT_MS,
|
|
@@ -110,13 +110,13 @@ function getAbortSignal(external) {
|
|
|
110
110
|
}
|
|
111
111
|
|
|
112
112
|
// src/version.ts
|
|
113
|
-
var MUSHI_CLI_VERSION = true ? "0.
|
|
113
|
+
var MUSHI_CLI_VERSION = true ? "0.21.0" : "0.0.0-dev";
|
|
114
114
|
|
|
115
115
|
// src/init.ts
|
|
116
116
|
import * as p from "@clack/prompts";
|
|
117
117
|
import { spawn } from "child_process";
|
|
118
118
|
import { randomUUID } from "crypto";
|
|
119
|
-
import { appendFileSync, existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
|
|
119
|
+
import { appendFileSync, existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync } from "fs";
|
|
120
120
|
import { join as join3 } from "path";
|
|
121
121
|
|
|
122
122
|
// src/console-url.ts
|
|
@@ -126,7 +126,10 @@ var HOSTED_CONSOLE_BASE = "https://kensaur.us/mushi-mushi/admin";
|
|
|
126
126
|
var LOCAL_ADMIN_URL = "http://localhost:6464";
|
|
127
127
|
var LOCAL_PROBE_MS = 800;
|
|
128
128
|
function normalizeConsoleBase(url) {
|
|
129
|
-
|
|
129
|
+
const trimmed = url.trim();
|
|
130
|
+
let end = trimmed.length;
|
|
131
|
+
while (end > 0 && trimmed.charCodeAt(end - 1) === 47) end--;
|
|
132
|
+
return trimmed.slice(0, end);
|
|
130
133
|
}
|
|
131
134
|
function consoleUrl(base, route) {
|
|
132
135
|
const normalizedRoute = route.startsWith("/") ? route : `/${route}`;
|
|
@@ -201,10 +204,34 @@ function reportsUrl(base, reportId) {
|
|
|
201
204
|
return reportId ? consoleUrl(base, `/reports/${reportId}`) : consoleUrl(base, "/reports");
|
|
202
205
|
}
|
|
203
206
|
async function openInBrowser(url) {
|
|
204
|
-
|
|
205
|
-
|
|
207
|
+
let parsed;
|
|
208
|
+
try {
|
|
209
|
+
parsed = new URL(url);
|
|
210
|
+
} catch {
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
const safeUrl = parsed.toString();
|
|
217
|
+
const { spawn: spawn2 } = await import("child_process");
|
|
218
|
+
const [command, args] = process.platform === "win32" ? (
|
|
219
|
+
// Open via rundll32's FileProtocolHandler rather than `cmd /c start`.
|
|
220
|
+
// `start` is a cmd builtin, so the URL would be re-parsed by cmd.exe and
|
|
221
|
+
// characters like `&` / `|` / `^` could still break out — exactly the
|
|
222
|
+
// injection surface CodeQL flags. rundll32 never invokes a shell: the URL
|
|
223
|
+
// is handed to the default browser as one opaque argument.
|
|
224
|
+
["rundll32", ["url.dll,FileProtocolHandler", safeUrl]]
|
|
225
|
+
) : process.platform === "darwin" ? ["open", [safeUrl]] : ["xdg-open", [safeUrl]];
|
|
206
226
|
await new Promise((resolve3) => {
|
|
207
|
-
|
|
227
|
+
try {
|
|
228
|
+
const child = spawn2(command, [...args], { stdio: "ignore", shell: false });
|
|
229
|
+
child.on("error", () => resolve3());
|
|
230
|
+
child.on("exit", () => resolve3());
|
|
231
|
+
child.unref();
|
|
232
|
+
} catch {
|
|
233
|
+
resolve3();
|
|
234
|
+
}
|
|
208
235
|
});
|
|
209
236
|
}
|
|
210
237
|
|
|
@@ -265,13 +292,13 @@ async function apiCall(path, config, options = {}) {
|
|
|
265
292
|
}
|
|
266
293
|
if (!res.ok && typeof body === "object" && body !== null && !("ok" in body)) {
|
|
267
294
|
const b = body;
|
|
295
|
+
const nested = b["error"];
|
|
296
|
+
const errCode = nested?.["code"] ?? b["code"] ?? `HTTP_${res.status}`;
|
|
297
|
+
const errMsg = nested?.["message"] ?? b["message"] ?? `Request failed (${res.status})`;
|
|
268
298
|
return {
|
|
269
299
|
ok: false,
|
|
270
300
|
httpStatus: res.status,
|
|
271
|
-
error: {
|
|
272
|
-
code: b["code"] ?? `HTTP_${res.status}`,
|
|
273
|
-
message: b["message"] ?? `Request failed (${res.status})`
|
|
274
|
-
}
|
|
301
|
+
error: { code: errCode, message: errMsg }
|
|
275
302
|
};
|
|
276
303
|
}
|
|
277
304
|
return body;
|
|
@@ -300,6 +327,11 @@ function die(result, exitCode = 1) {
|
|
|
300
327
|
const status = result.httpStatus ? ` [${result.httpStatus}]` : "";
|
|
301
328
|
process.stderr.write(`error${status}: ${code} \u2014 ${message}
|
|
302
329
|
`);
|
|
330
|
+
if (code === "INSUFFICIENT_SCOPE") {
|
|
331
|
+
process.stderr.write(
|
|
332
|
+
"\n Your current API key does not have the required scope for this command.\n New keys minted by the wizard already include both scopes. Existing keys\n can be upgraded with:\n\n mushi login --upgrade-scope\n\n Or visit your console \u2192 Projects \u2192 API Keys to mint a new key.\n"
|
|
333
|
+
);
|
|
334
|
+
}
|
|
303
335
|
process.exit(exitCode);
|
|
304
336
|
}
|
|
305
337
|
function requireConfig(opts = {}) {
|
|
@@ -329,6 +361,151 @@ function pad(s, width) {
|
|
|
329
361
|
return s.length >= width ? s : s + " ".repeat(width - s.length);
|
|
330
362
|
}
|
|
331
363
|
|
|
364
|
+
// src/device-auth.ts
|
|
365
|
+
var DEVICE_FETCH_TIMEOUT_MS = 15e3;
|
|
366
|
+
function trimTrailingSlash(endpoint) {
|
|
367
|
+
let end = endpoint.length;
|
|
368
|
+
while (end > 0 && endpoint.charCodeAt(end - 1) === 47) end--;
|
|
369
|
+
return endpoint.slice(0, end);
|
|
370
|
+
}
|
|
371
|
+
async function deviceFetch(url, init = {}) {
|
|
372
|
+
const controller = new AbortController();
|
|
373
|
+
const timer = setTimeout(() => controller.abort(), DEVICE_FETCH_TIMEOUT_MS);
|
|
374
|
+
try {
|
|
375
|
+
return await fetch(url, { ...init, signal: controller.signal });
|
|
376
|
+
} finally {
|
|
377
|
+
clearTimeout(timer);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
async function startDeviceAuth(endpoint) {
|
|
381
|
+
const base = trimTrailingSlash(endpoint);
|
|
382
|
+
const res = await deviceFetch(`${base}/v1/cli/auth/device/start`, {
|
|
383
|
+
method: "POST",
|
|
384
|
+
headers: { "Content-Type": "application/json" }
|
|
385
|
+
});
|
|
386
|
+
const json = await res.json().catch(() => null);
|
|
387
|
+
if (!res.ok || !json?.ok || !json.data) {
|
|
388
|
+
throw new Error(json?.error?.message ?? `Could not start browser sign-in (HTTP ${res.status}).`);
|
|
389
|
+
}
|
|
390
|
+
return json.data;
|
|
391
|
+
}
|
|
392
|
+
async function pollDeviceToken(endpoint, deviceCode) {
|
|
393
|
+
const base = trimTrailingSlash(endpoint);
|
|
394
|
+
try {
|
|
395
|
+
const res = await deviceFetch(`${base}/v1/cli/auth/device/token`, {
|
|
396
|
+
method: "POST",
|
|
397
|
+
headers: { "Content-Type": "application/json" },
|
|
398
|
+
body: JSON.stringify({ device_code: deviceCode })
|
|
399
|
+
});
|
|
400
|
+
const json = await res.json().catch(() => ({}));
|
|
401
|
+
if (res.ok && json.ok && json.data?.cli_token) {
|
|
402
|
+
return { status: "approved", cliToken: json.data.cli_token, userId: json.data.user_id };
|
|
403
|
+
}
|
|
404
|
+
switch (json.error) {
|
|
405
|
+
case "authorization_pending":
|
|
406
|
+
return { status: "pending" };
|
|
407
|
+
case "access_denied":
|
|
408
|
+
return { status: "denied" };
|
|
409
|
+
case "expired_token":
|
|
410
|
+
return { status: "expired" };
|
|
411
|
+
default:
|
|
412
|
+
return {
|
|
413
|
+
status: "error",
|
|
414
|
+
message: json.error_description ?? json.error ?? `HTTP ${res.status}`,
|
|
415
|
+
// 5xx is a server hiccup the next poll may clear; a 4xx is a definitive
|
|
416
|
+
// rejection (already claimed / invalid_grant) that won't fix itself.
|
|
417
|
+
retryable: res.status >= 500
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
} catch (err) {
|
|
421
|
+
return { status: "error", message: err instanceof Error ? err.message : String(err), retryable: true };
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
var defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
425
|
+
async function waitForCliToken(endpoint, session, opts = {}) {
|
|
426
|
+
const sleep = opts.sleep ?? defaultSleep;
|
|
427
|
+
const now = opts.now ?? Date.now;
|
|
428
|
+
const intervalMs = (session.interval || 5) * 1e3;
|
|
429
|
+
const deadline = now() + (session.expires_in || 600) * 1e3;
|
|
430
|
+
const maxConsecutiveErrors = opts.maxConsecutiveErrors ?? 5;
|
|
431
|
+
let consecutiveErrors = 0;
|
|
432
|
+
let firstPoll = true;
|
|
433
|
+
while (now() < deadline) {
|
|
434
|
+
if (!firstPoll) {
|
|
435
|
+
await sleep(intervalMs);
|
|
436
|
+
}
|
|
437
|
+
firstPoll = false;
|
|
438
|
+
const outcome = await pollDeviceToken(endpoint, session.device_code);
|
|
439
|
+
switch (outcome.status) {
|
|
440
|
+
case "approved":
|
|
441
|
+
return outcome.cliToken;
|
|
442
|
+
case "pending":
|
|
443
|
+
consecutiveErrors = 0;
|
|
444
|
+
opts.onPending?.();
|
|
445
|
+
continue;
|
|
446
|
+
case "denied":
|
|
447
|
+
throw new Error("Login was denied in the browser.");
|
|
448
|
+
case "expired":
|
|
449
|
+
throw new Error("The login code expired. Run sign-in again.");
|
|
450
|
+
case "error":
|
|
451
|
+
if (!outcome.retryable) {
|
|
452
|
+
throw new Error(outcome.message);
|
|
453
|
+
}
|
|
454
|
+
consecutiveErrors += 1;
|
|
455
|
+
if (consecutiveErrors >= maxConsecutiveErrors) {
|
|
456
|
+
throw new Error(outcome.message);
|
|
457
|
+
}
|
|
458
|
+
opts.onTransientError?.(outcome.message, consecutiveErrors);
|
|
459
|
+
continue;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
throw new Error("Login timed out before approval. Run sign-in again.");
|
|
463
|
+
}
|
|
464
|
+
function cliAuthHeaders(cliToken) {
|
|
465
|
+
return {
|
|
466
|
+
"Content-Type": "application/json",
|
|
467
|
+
Authorization: `Bearer ${cliToken}`
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
async function listProjects(endpoint, cliToken) {
|
|
471
|
+
const base = trimTrailingSlash(endpoint);
|
|
472
|
+
try {
|
|
473
|
+
const res = await deviceFetch(`${base}/v1/cli/projects`, { headers: cliAuthHeaders(cliToken) });
|
|
474
|
+
const json = await res.json().catch(() => null);
|
|
475
|
+
if (res.ok && json?.ok) return json.data?.projects ?? [];
|
|
476
|
+
} catch {
|
|
477
|
+
}
|
|
478
|
+
return [];
|
|
479
|
+
}
|
|
480
|
+
async function createProject(endpoint, cliToken, name) {
|
|
481
|
+
const base = trimTrailingSlash(endpoint);
|
|
482
|
+
const res = await deviceFetch(`${base}/v1/cli/projects`, {
|
|
483
|
+
method: "POST",
|
|
484
|
+
headers: cliAuthHeaders(cliToken),
|
|
485
|
+
body: JSON.stringify({ name })
|
|
486
|
+
});
|
|
487
|
+
const json = await res.json().catch(() => null);
|
|
488
|
+
if (!res.ok || !json?.ok || !json.data) {
|
|
489
|
+
throw new Error(json?.error?.message ?? `Could not create project (HTTP ${res.status}).`);
|
|
490
|
+
}
|
|
491
|
+
const { id, name: created, slug, apiKey } = json.data;
|
|
492
|
+
return { id, name: created, slug, apiKey: apiKey ?? null };
|
|
493
|
+
}
|
|
494
|
+
async function mintProjectKey(endpoint, cliToken, projectId) {
|
|
495
|
+
const base = trimTrailingSlash(endpoint);
|
|
496
|
+
try {
|
|
497
|
+
const res = await deviceFetch(`${base}/v1/cli/projects/${projectId}/keys`, {
|
|
498
|
+
method: "POST",
|
|
499
|
+
headers: cliAuthHeaders(cliToken),
|
|
500
|
+
body: JSON.stringify({})
|
|
501
|
+
});
|
|
502
|
+
const json = await res.json().catch(() => null);
|
|
503
|
+
if (res.ok && json?.ok) return json.data?.key ?? null;
|
|
504
|
+
} catch {
|
|
505
|
+
}
|
|
506
|
+
return null;
|
|
507
|
+
}
|
|
508
|
+
|
|
332
509
|
// src/freshness.ts
|
|
333
510
|
var REGISTRY = "https://registry.npmjs.org";
|
|
334
511
|
var DEFAULT_TIMEOUT_MS = 2e3;
|
|
@@ -485,6 +662,53 @@ function getFrameworkFromPkg(pkg) {
|
|
|
485
662
|
return void 0;
|
|
486
663
|
}
|
|
487
664
|
|
|
665
|
+
// src/auth-ui.ts
|
|
666
|
+
var LINE = "\u2500".repeat(58);
|
|
667
|
+
function printAuthBanner(userCode, verificationUri) {
|
|
668
|
+
console.log("");
|
|
669
|
+
console.log(` \u250C${LINE}\u2510`);
|
|
670
|
+
console.log(` \u2502 Browser sign-in \u2014 follow these 3 steps \u2502`);
|
|
671
|
+
console.log(` \u251C${LINE}\u2524`);
|
|
672
|
+
console.log(` \u2502 \u2502`);
|
|
673
|
+
console.log(` \u2502 1. A browser tab will open (or visit the URL below) \u2502`);
|
|
674
|
+
console.log(` \u2502 2. Sign in if prompted, then click "Approve" \u2502`);
|
|
675
|
+
console.log(` \u2502 3. Come back here \u2014 setup continues automatically \u2502`);
|
|
676
|
+
console.log(` \u2502 \u2502`);
|
|
677
|
+
console.log(` \u251C${LINE}\u2524`);
|
|
678
|
+
console.log(` \u2502 Verification code (show in browser \u2014 NOT terminal): \u2502`);
|
|
679
|
+
console.log(` \u2502 \u2502`);
|
|
680
|
+
console.log(` \u2502 ${userCode.padEnd(52)}\u2502`);
|
|
681
|
+
console.log(` \u2502 \u2502`);
|
|
682
|
+
console.log(` \u2502 \u26A0 Do NOT paste or type this code in your terminal \u2502`);
|
|
683
|
+
console.log(` \u2514${LINE}\u2518`);
|
|
684
|
+
console.log("");
|
|
685
|
+
console.log(` If the browser didn't open, visit this URL and approve:`);
|
|
686
|
+
console.log(` ${verificationUri}`);
|
|
687
|
+
console.log("");
|
|
688
|
+
console.log(" \u23F3 Waiting for browser approval\u2026 (Ctrl+C to cancel)");
|
|
689
|
+
console.log("");
|
|
690
|
+
}
|
|
691
|
+
function printAuthApproved() {
|
|
692
|
+
console.log("");
|
|
693
|
+
console.log(" \u2713 Approved! Continuing setup in this terminal\u2026");
|
|
694
|
+
console.log("");
|
|
695
|
+
}
|
|
696
|
+
function printAuthFailed(reason, detail) {
|
|
697
|
+
console.log("");
|
|
698
|
+
if (reason === "denied") {
|
|
699
|
+
console.error(" \u2717 Browser sign-in was denied.");
|
|
700
|
+
console.error(" If that was a mistake, run mushi login again.");
|
|
701
|
+
} else if (reason === "timeout") {
|
|
702
|
+
console.error(" \u2717 Browser sign-in timed out (10 minutes elapsed).");
|
|
703
|
+
console.error(" Run mushi login again \u2014 a new code will be generated.");
|
|
704
|
+
} else {
|
|
705
|
+
console.error(` \u2717 Browser sign-in failed: ${detail ?? "unknown error"}`);
|
|
706
|
+
console.error(" Run mushi login again, or use:");
|
|
707
|
+
console.error(" mushi login --api-key <key> --project-id <uuid>");
|
|
708
|
+
}
|
|
709
|
+
console.log("");
|
|
710
|
+
}
|
|
711
|
+
|
|
488
712
|
// src/init.ts
|
|
489
713
|
var ENV_FILES = [".env.local", ".env"];
|
|
490
714
|
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;
|
|
@@ -510,8 +734,8 @@ async function runInit(options = {}) {
|
|
|
510
734
|
const detected = detectFramework(cwd, pkg);
|
|
511
735
|
const framework = await chooseFramework(detected, options);
|
|
512
736
|
const consoleBase = await resolveConsoleUrl({ cwd });
|
|
513
|
-
|
|
514
|
-
const credentials = await
|
|
737
|
+
const endpoint = resolveCloudEndpoint(options.endpoint);
|
|
738
|
+
const credentials = await acquireCredentials(options, consoleBase, endpoint);
|
|
515
739
|
await verifyCredentials(credentials, options, consoleBase);
|
|
516
740
|
const pm = detectPackageManager(cwd);
|
|
517
741
|
const packagesToInstall = framework.needsWebPackage ? [framework.packageName, "@mushi-mushi/web"] : [framework.packageName];
|
|
@@ -520,8 +744,7 @@ async function runInit(options = {}) {
|
|
|
520
744
|
} else {
|
|
521
745
|
p.log.info(`Skipped install. Run \`${installCommand(pm, packagesToInstall)}\` yourself.`);
|
|
522
746
|
}
|
|
523
|
-
|
|
524
|
-
writeEnvFile(cwd, credentials.apiKey, credentials.projectId, framework);
|
|
747
|
+
await writeEnvFile(cwd, credentials.apiKey, credentials.projectId, framework, endpoint, Boolean(options.yes));
|
|
525
748
|
persistCliConfig(credentials.apiKey, credentials.projectId, endpoint);
|
|
526
749
|
const enableRewards = await maybeEnableRewards(options);
|
|
527
750
|
printNextSteps(framework, consoleBase, enableRewards);
|
|
@@ -567,63 +790,17 @@ async function chooseFramework(detected, options) {
|
|
|
567
790
|
}
|
|
568
791
|
return FRAMEWORKS[confirmed];
|
|
569
792
|
}
|
|
570
|
-
async function
|
|
571
|
-
if (options.
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
message: "Do you already have a Mushi project?",
|
|
577
|
-
options: [
|
|
578
|
-
{
|
|
579
|
-
value: "create",
|
|
580
|
-
label: "No \u2014 open the console to create one",
|
|
581
|
-
hint: "Recommended for first-time setup"
|
|
582
|
-
},
|
|
583
|
-
{
|
|
584
|
-
value: "have",
|
|
585
|
-
label: "Yes \u2014 I have Project ID + API key",
|
|
586
|
-
hint: "Paste values from the admin console"
|
|
587
|
-
},
|
|
588
|
-
{
|
|
589
|
-
value: "login",
|
|
590
|
-
label: "Use mushi login first",
|
|
591
|
-
hint: "Run `mushi login` in another terminal, then re-run this wizard"
|
|
592
|
-
}
|
|
593
|
-
]
|
|
594
|
-
});
|
|
595
|
-
if (p.isCancel(choice)) {
|
|
596
|
-
p.cancel("Aborted.");
|
|
597
|
-
process.exit(0);
|
|
598
|
-
}
|
|
599
|
-
if (choice === "login") {
|
|
600
|
-
p.log.info("Run `mushi login` to save credentials, then re-run `mushi init`.");
|
|
601
|
-
p.cancel("Aborted.");
|
|
602
|
-
process.exit(0);
|
|
603
|
-
}
|
|
604
|
-
if (choice !== "create") return;
|
|
605
|
-
const setupUrl = cliSetupDeepLink(consoleBase);
|
|
606
|
-
p.log.info(`Opening the Mushi console:
|
|
607
|
-
${setupUrl}`);
|
|
608
|
-
await openInBrowser(setupUrl);
|
|
609
|
-
p.log.message("In the console:");
|
|
610
|
-
p.log.message(" 1. Enter a project name and click Create");
|
|
611
|
-
p.log.message(" 2. Copy the Project ID from the success panel");
|
|
612
|
-
p.log.message(" 3. Generate an API key (Verify tab \u2014 report:write scope)");
|
|
613
|
-
p.log.message(" 4. Return here and paste both values");
|
|
614
|
-
const ready = await p.confirm({
|
|
615
|
-
message: "Created your project and ready to paste credentials?",
|
|
616
|
-
initialValue: false
|
|
617
|
-
});
|
|
618
|
-
if (p.isCancel(ready) || !ready) {
|
|
619
|
-
p.log.warn(`Finish in the console first: ${setupUrl}`);
|
|
793
|
+
async function acquireCredentials(options, consoleBase, endpoint) {
|
|
794
|
+
if (options.projectId && options.apiKey) {
|
|
795
|
+
return {
|
|
796
|
+
projectId: sanitizeSecret(options.projectId),
|
|
797
|
+
apiKey: sanitizeSecret(options.apiKey)
|
|
798
|
+
};
|
|
620
799
|
}
|
|
621
|
-
}
|
|
622
|
-
async function collectCredentials(options, consoleBase) {
|
|
623
800
|
const existing = loadConfig();
|
|
624
801
|
if (!options.projectId && !options.apiKey && existing.projectId && existing.apiKey) {
|
|
625
802
|
const reuse = options.yes ? true : await p.confirm({
|
|
626
|
-
message: "Use
|
|
803
|
+
message: "Use the Mushi credentials saved from your last sign-in?",
|
|
627
804
|
initialValue: true
|
|
628
805
|
});
|
|
629
806
|
if (p.isCancel(reuse)) {
|
|
@@ -637,6 +814,144 @@ async function collectCredentials(options, consoleBase) {
|
|
|
637
814
|
};
|
|
638
815
|
}
|
|
639
816
|
}
|
|
817
|
+
if (options.yes) {
|
|
818
|
+
const creds = await runBrowserSignIn(options, endpoint, consoleBase);
|
|
819
|
+
if (creds) return creds;
|
|
820
|
+
p.log.warn("Browser sign-in didn't complete \u2014 switching to manual entry.");
|
|
821
|
+
} else {
|
|
822
|
+
const method = await p.select({
|
|
823
|
+
message: "Connect this app to Mushi",
|
|
824
|
+
initialValue: "browser",
|
|
825
|
+
options: [
|
|
826
|
+
{
|
|
827
|
+
value: "browser",
|
|
828
|
+
label: "Sign in with your browser",
|
|
829
|
+
hint: "Recommended \u2014 no copy-paste, creates the project + key for you"
|
|
830
|
+
},
|
|
831
|
+
{
|
|
832
|
+
value: "manual",
|
|
833
|
+
label: "Paste a Project ID + API key",
|
|
834
|
+
hint: "Self-hosted or expert setup \u2014 run npx mushi-mushi (not mushi setup)"
|
|
835
|
+
}
|
|
836
|
+
]
|
|
837
|
+
});
|
|
838
|
+
if (p.isCancel(method)) {
|
|
839
|
+
p.cancel("Aborted.");
|
|
840
|
+
process.exit(0);
|
|
841
|
+
}
|
|
842
|
+
if (method === "browser") {
|
|
843
|
+
const creds = await runBrowserSignIn(options, endpoint, consoleBase);
|
|
844
|
+
if (creds) return creds;
|
|
845
|
+
p.log.warn("Browser sign-in didn't complete \u2014 switching to manual entry.");
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
return collectCredentialsManually(options, consoleBase);
|
|
849
|
+
}
|
|
850
|
+
async function runBrowserSignIn(options, endpoint, consoleBase) {
|
|
851
|
+
const startSpin = p.spinner();
|
|
852
|
+
startSpin.start("Starting secure browser sign-in\u2026");
|
|
853
|
+
let session;
|
|
854
|
+
try {
|
|
855
|
+
session = await startDeviceAuth(endpoint);
|
|
856
|
+
} catch (err) {
|
|
857
|
+
startSpin.stop("Could not start browser sign-in.");
|
|
858
|
+
p.log.warn(err instanceof Error ? err.message : String(err));
|
|
859
|
+
return null;
|
|
860
|
+
}
|
|
861
|
+
startSpin.stop("Browser sign-in ready.");
|
|
862
|
+
try {
|
|
863
|
+
await openInBrowser(session.verification_uri);
|
|
864
|
+
} catch {
|
|
865
|
+
}
|
|
866
|
+
printAuthBanner(session.user_code, session.verification_uri);
|
|
867
|
+
const waitSpin = p.spinner();
|
|
868
|
+
waitSpin.start("Waiting for you to approve in the browser\u2026");
|
|
869
|
+
let cliToken;
|
|
870
|
+
try {
|
|
871
|
+
cliToken = await waitForCliToken(endpoint, session);
|
|
872
|
+
} catch (err) {
|
|
873
|
+
waitSpin.stop("Browser sign-in didn't complete.");
|
|
874
|
+
p.log.warn(err instanceof Error ? err.message : String(err));
|
|
875
|
+
return null;
|
|
876
|
+
}
|
|
877
|
+
waitSpin.stop("Approved.");
|
|
878
|
+
let projectId = options.projectId ? sanitizeSecret(options.projectId) : void 0;
|
|
879
|
+
let apiKey;
|
|
880
|
+
if (!projectId) {
|
|
881
|
+
let projects = [];
|
|
882
|
+
const fetchSpin = p.spinner();
|
|
883
|
+
fetchSpin.start("Loading your projects\u2026");
|
|
884
|
+
try {
|
|
885
|
+
projects = await listProjects(endpoint, cliToken);
|
|
886
|
+
fetchSpin.stop(projects.length > 0 ? `Found ${projects.length} project(s).` : "No projects yet.");
|
|
887
|
+
} catch (err) {
|
|
888
|
+
fetchSpin.stop("Could not load projects \u2014 you can still create a new one.");
|
|
889
|
+
p.log.warn(err instanceof Error ? err.message : String(err));
|
|
890
|
+
}
|
|
891
|
+
const NEW = "__new__";
|
|
892
|
+
const choice = await p.select({
|
|
893
|
+
message: "Choose a project",
|
|
894
|
+
initialValue: projects[0]?.id ?? NEW,
|
|
895
|
+
options: [
|
|
896
|
+
...projects.map((pr) => ({ value: pr.id, label: pr.name, hint: pr.id.slice(0, 8) })),
|
|
897
|
+
{ value: NEW, label: "Create a new project", hint: "mints an SDK key automatically" }
|
|
898
|
+
]
|
|
899
|
+
});
|
|
900
|
+
if (p.isCancel(choice)) {
|
|
901
|
+
p.cancel("Aborted.");
|
|
902
|
+
process.exit(0);
|
|
903
|
+
}
|
|
904
|
+
if (choice === NEW) {
|
|
905
|
+
const name = await p.text({
|
|
906
|
+
message: "Project name",
|
|
907
|
+
placeholder: "My app",
|
|
908
|
+
validate: (v) => v && v.trim().length > 0 ? void 0 : "Required"
|
|
909
|
+
});
|
|
910
|
+
if (p.isCancel(name)) {
|
|
911
|
+
p.cancel("Aborted.");
|
|
912
|
+
process.exit(0);
|
|
913
|
+
}
|
|
914
|
+
const createSpin = p.spinner();
|
|
915
|
+
createSpin.start(`Creating "${name.trim()}"\u2026`);
|
|
916
|
+
try {
|
|
917
|
+
const created = await createProject(endpoint, cliToken, name.trim());
|
|
918
|
+
projectId = created.id;
|
|
919
|
+
apiKey = created.apiKey ?? void 0;
|
|
920
|
+
createSpin.stop(`Created project "${created.name}".`);
|
|
921
|
+
} catch (err) {
|
|
922
|
+
createSpin.stop("Could not create the project.");
|
|
923
|
+
p.log.warn(err instanceof Error ? err.message : String(err));
|
|
924
|
+
return null;
|
|
925
|
+
}
|
|
926
|
+
} else {
|
|
927
|
+
projectId = choice;
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
if (projectId && !apiKey) {
|
|
931
|
+
const keySpin = p.spinner();
|
|
932
|
+
keySpin.start("Minting SDK key\u2026");
|
|
933
|
+
try {
|
|
934
|
+
const minted = await mintProjectKey(endpoint, cliToken, projectId);
|
|
935
|
+
apiKey = minted ?? void 0;
|
|
936
|
+
if (!apiKey) {
|
|
937
|
+
keySpin.stop("Could not mint an API key.");
|
|
938
|
+
p.log.warn(
|
|
939
|
+
`Open the console Verify tab to generate a key manually: ${consoleUrl(consoleBase, "/onboarding?tab=verify")}`
|
|
940
|
+
);
|
|
941
|
+
return null;
|
|
942
|
+
}
|
|
943
|
+
keySpin.stop("SDK key ready.");
|
|
944
|
+
} catch (err) {
|
|
945
|
+
keySpin.stop("Could not mint an API key.");
|
|
946
|
+
p.log.warn(err instanceof Error ? err.message : String(err));
|
|
947
|
+
return null;
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
if (!projectId || !apiKey) return null;
|
|
951
|
+
return { apiKey, projectId };
|
|
952
|
+
}
|
|
953
|
+
async function collectCredentialsManually(options, consoleBase) {
|
|
954
|
+
const existing = loadConfig();
|
|
640
955
|
const rawProjectId = options.projectId ?? existing.projectId ?? await promptText({
|
|
641
956
|
message: "Project ID",
|
|
642
957
|
placeholder: "e.g. bdafa28d-b153-482f-bd4f-42981f3fd3a4",
|
|
@@ -736,13 +1051,37 @@ function runCommand(pm, packages, cwd) {
|
|
|
736
1051
|
});
|
|
737
1052
|
});
|
|
738
1053
|
}
|
|
739
|
-
function writeEnvFile(cwd, apiKey, projectId, framework) {
|
|
1054
|
+
async function writeEnvFile(cwd, apiKey, projectId, framework, endpoint, overwrite) {
|
|
740
1055
|
const target = ENV_FILES.find((f) => existsSync3(join3(cwd, f))) ?? ENV_FILES[0];
|
|
741
1056
|
const targetPath = join3(cwd, target);
|
|
742
|
-
const newVars = envVarsToWrite(apiKey, projectId, framework);
|
|
1057
|
+
const newVars = envVarsToWrite(apiKey, projectId, framework, endpoint);
|
|
743
1058
|
const existing = existsSync3(targetPath) ? readFileSync3(targetPath, "utf-8") : "";
|
|
744
1059
|
if (existing.includes("MUSHI_PROJECT_ID")) {
|
|
745
|
-
|
|
1060
|
+
let shouldOverwrite = overwrite;
|
|
1061
|
+
if (!shouldOverwrite) {
|
|
1062
|
+
const answer = await p.confirm({
|
|
1063
|
+
message: `Existing MUSHI_* vars found in ${target}. Update with new credentials?`,
|
|
1064
|
+
initialValue: true
|
|
1065
|
+
});
|
|
1066
|
+
if (p.isCancel(answer)) {
|
|
1067
|
+
p.log.info(`Kept existing env vars in ${target}.`);
|
|
1068
|
+
return;
|
|
1069
|
+
}
|
|
1070
|
+
shouldOverwrite = Boolean(answer);
|
|
1071
|
+
}
|
|
1072
|
+
if (!shouldOverwrite) {
|
|
1073
|
+
p.log.info(`Kept existing env vars in ${target}. Re-run and confirm to overwrite.`);
|
|
1074
|
+
return;
|
|
1075
|
+
}
|
|
1076
|
+
const MUSHI_LINE_RE = /^(NEXT_PUBLIC_|NUXT_PUBLIC_|VITE_|EXPO_PUBLIC_)?MUSHI_[A-Z_]+=.*/gm;
|
|
1077
|
+
const stripped = existing.replace(MUSHI_LINE_RE, "").replace(/\n{3,}/g, "\n\n").trimEnd();
|
|
1078
|
+
const prefix2 = stripped.length > 0 ? "\n" : "";
|
|
1079
|
+
writeFileSync(targetPath, `${stripped}${prefix2}
|
|
1080
|
+
# Mushi Mushi
|
|
1081
|
+
${newVars}
|
|
1082
|
+
`);
|
|
1083
|
+
p.log.success(`Updated MUSHI_* env vars in ${target}`);
|
|
1084
|
+
warnIfMissingFromGitignore(cwd, target);
|
|
746
1085
|
return;
|
|
747
1086
|
}
|
|
748
1087
|
const prefix = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
|
|
@@ -803,12 +1142,14 @@ import { MushiRewardsBadge } from '@mushi-mushi/react';
|
|
|
803
1142
|
p.log.info(`Enable rewards in your project settings at ${consoleUrl(consoleBase, "/rewards")}`);
|
|
804
1143
|
p.log.info("Users will earn points for bug reports, screen navigation, and app activity.");
|
|
805
1144
|
}
|
|
806
|
-
p.
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
1145
|
+
p.note(
|
|
1146
|
+
[
|
|
1147
|
+
" [ ] 1. Paste the init snippet above into your app entry file",
|
|
1148
|
+
" [ ] 2. Start your dev server",
|
|
1149
|
+
" [ ] 3. Run: mushi connect --write-env --wire-ide --wait",
|
|
1150
|
+
` [ ] 4. Open the Verify tab to send a test report: ${consoleUrl(consoleBase, "/onboarding?tab=verify")}`
|
|
1151
|
+
].join("\n"),
|
|
1152
|
+
"Next steps:"
|
|
812
1153
|
);
|
|
813
1154
|
}
|
|
814
1155
|
async function maybeOfferConnect(credentials, options, _consoleBase) {
|
|
@@ -820,7 +1161,7 @@ async function maybeOfferConnect(credentials, options, _consoleBase) {
|
|
|
820
1161
|
if (p.isCancel(answer) || !answer) return;
|
|
821
1162
|
const endpoint = resolveCloudEndpoint(options.endpoint);
|
|
822
1163
|
try {
|
|
823
|
-
const { runConnect: runConnect2 } = await import("./connect-
|
|
1164
|
+
const { runConnect: runConnect2 } = await import("./connect-JIEIWTF3.js");
|
|
824
1165
|
await runConnect2({
|
|
825
1166
|
apiKey: credentials.apiKey,
|
|
826
1167
|
projectId: credentials.projectId,
|
|
@@ -1105,11 +1446,15 @@ function registerAccountCommands(program2) {
|
|
|
1105
1446
|
const { matches } = runMigrate({ cwd: opts.cwd, json: opts.json });
|
|
1106
1447
|
if (matches.length === 0) process.exit(1);
|
|
1107
1448
|
});
|
|
1108
|
-
program2.command("login").description("Authenticate the CLI via browser (zero copy-paste), or pass --api-key to skip").option("--api-key <key>", "API key (mushi_...) \u2014 non-interactive / CI path").option("--endpoint <url>", "Override the Mushi API endpoint (self-hosted)").option("--project-id <id>", "Project UUID \u2014 skip the project picker").option("--no-browser", "Print the verification URL instead of opening the browser").
|
|
1449
|
+
program2.command("login").description("Authenticate the CLI via browser (zero copy-paste), or pass --api-key to skip").option("--api-key <key>", "API key (mushi_...) \u2014 non-interactive / CI path").option("--endpoint <url>", "Override the Mushi API endpoint (self-hosted)").option("--project-id <id>", "Project UUID \u2014 skip the project picker").option("--no-browser", "Print the verification URL instead of opening the browser").option(
|
|
1450
|
+
"--upgrade-scope",
|
|
1451
|
+
"Re-authenticate and mint a new key with report:write + mcp:read + mcp:write scopes. Use this to upgrade an existing ingest-only key so MCP and admin commands (including writes like `mushi billing cap`) work."
|
|
1452
|
+
).addHelpText("after", `
|
|
1109
1453
|
Examples:
|
|
1110
1454
|
mushi login # browser-guided (recommended)
|
|
1111
1455
|
mushi login --api-key mushi_xxx # non-interactive / CI
|
|
1112
|
-
mushi login --api-key mushi_xxx --project-id <uuid
|
|
1456
|
+
mushi login --api-key mushi_xxx --project-id <uuid>
|
|
1457
|
+
mushi login --upgrade-scope # re-auth to add mcp:read + mcp:write to your key`).action(async (opts) => {
|
|
1113
1458
|
const { CLOUD_API_ENDPOINT: CLOUD_API_ENDPOINT2, resolveCloudEndpoint: resolveCloudEndpoint2 } = await import("./endpoint-DGS73AVM.js");
|
|
1114
1459
|
const endpoint = opts.endpoint ? resolveCloudEndpoint2(opts.endpoint) : CLOUD_API_ENDPOINT2;
|
|
1115
1460
|
const consoleBase = await resolveConsoleUrl();
|
|
@@ -1147,19 +1492,19 @@ Examples:
|
|
|
1147
1492
|
return;
|
|
1148
1493
|
}
|
|
1149
1494
|
console.log("");
|
|
1150
|
-
|
|
1151
|
-
|
|
1495
|
+
if (opts.upgradeScope) {
|
|
1496
|
+
console.log(" Mushi login \u2014 scope upgrade");
|
|
1497
|
+
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");
|
|
1498
|
+
console.log(" Re-authenticating to mint a new key with report:write + mcp:read + mcp:write scopes.");
|
|
1499
|
+
console.log(" This lets the CLI admin commands (including writes) and MCP server both work with a single key.");
|
|
1500
|
+
console.log("");
|
|
1501
|
+
} else {
|
|
1502
|
+
console.log(" Mushi login");
|
|
1503
|
+
console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
1504
|
+
}
|
|
1152
1505
|
let deviceData;
|
|
1153
1506
|
try {
|
|
1154
|
-
|
|
1155
|
-
method: "POST",
|
|
1156
|
-
headers: { "Content-Type": "application/json" }
|
|
1157
|
-
});
|
|
1158
|
-
const json = await res.json();
|
|
1159
|
-
if (!res.ok || !json.ok || !json.data) {
|
|
1160
|
-
throw new Error(json.error?.message ?? `HTTP ${res.status}`);
|
|
1161
|
-
}
|
|
1162
|
-
deviceData = json.data;
|
|
1507
|
+
deviceData = await startDeviceAuth(endpoint);
|
|
1163
1508
|
} catch (err) {
|
|
1164
1509
|
process.stderr.write(`
|
|
1165
1510
|
error: Could not start login session: ${err instanceof Error ? err.message : String(err)}
|
|
@@ -1169,77 +1514,56 @@ error: Could not start login session: ${err instanceof Error ? err.message : Str
|
|
|
1169
1514
|
process.exit(1);
|
|
1170
1515
|
}
|
|
1171
1516
|
const verifyUrl = deviceData.verification_uri;
|
|
1172
|
-
console.log("");
|
|
1173
|
-
console.log(` Your confirmation code: ${deviceData.user_code}`);
|
|
1174
|
-
console.log("");
|
|
1175
1517
|
if (opts.browser !== false) {
|
|
1176
|
-
console.log(" Opening the Mushi console in your browser\u2026");
|
|
1177
1518
|
try {
|
|
1178
1519
|
await openInBrowser(verifyUrl);
|
|
1179
1520
|
} catch {
|
|
1180
1521
|
}
|
|
1181
1522
|
}
|
|
1182
|
-
|
|
1183
|
-
console.log("");
|
|
1184
|
-
console.log(" Waiting for you to approve in the browser\u2026 (Ctrl+C to cancel)");
|
|
1523
|
+
printAuthBanner(deviceData.user_code, verifyUrl);
|
|
1185
1524
|
let cliToken = null;
|
|
1186
1525
|
const pollIntervalMs = (deviceData.interval ?? 5) * 1e3;
|
|
1187
1526
|
const deadline = Date.now() + (deviceData.expires_in ?? 600) * 1e3;
|
|
1527
|
+
const MAX_CONSECUTIVE_ERRORS = 5;
|
|
1528
|
+
let consecutiveErrors = 0;
|
|
1529
|
+
let firstPoll = true;
|
|
1188
1530
|
while (Date.now() < deadline) {
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
if (errCode === "access_denied") {
|
|
1207
|
-
console.log("");
|
|
1208
|
-
process.stderr.write("\nerror: Login denied in the browser.\n");
|
|
1209
|
-
process.exit(1);
|
|
1210
|
-
}
|
|
1211
|
-
if (errCode === "expired_token") {
|
|
1212
|
-
console.log("");
|
|
1213
|
-
process.stderr.write("\nerror: Login code expired. Run mushi login again.\n");
|
|
1214
|
-
process.exit(1);
|
|
1531
|
+
if (!firstPoll) {
|
|
1532
|
+
await new Promise((r) => setTimeout(r, pollIntervalMs));
|
|
1533
|
+
}
|
|
1534
|
+
firstPoll = false;
|
|
1535
|
+
const outcome = await pollDeviceToken(endpoint, deviceData.device_code);
|
|
1536
|
+
if (outcome.status === "approved") {
|
|
1537
|
+
cliToken = outcome.cliToken;
|
|
1538
|
+
break;
|
|
1539
|
+
}
|
|
1540
|
+
if (outcome.status === "pending") {
|
|
1541
|
+
consecutiveErrors = 0;
|
|
1542
|
+
continue;
|
|
1543
|
+
}
|
|
1544
|
+
if (outcome.status === "error") {
|
|
1545
|
+
if (outcome.retryable) {
|
|
1546
|
+
consecutiveErrors += 1;
|
|
1547
|
+
if (consecutiveErrors < MAX_CONSECUTIVE_ERRORS) continue;
|
|
1215
1548
|
}
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1549
|
+
printAuthFailed("error", outcome.message);
|
|
1550
|
+
process.exit(1);
|
|
1551
|
+
}
|
|
1552
|
+
if (outcome.status === "denied") {
|
|
1553
|
+
printAuthFailed("denied");
|
|
1554
|
+
process.exit(1);
|
|
1555
|
+
}
|
|
1556
|
+
if (outcome.status === "expired") {
|
|
1557
|
+
printAuthFailed("timeout");
|
|
1222
1558
|
process.exit(1);
|
|
1223
1559
|
}
|
|
1224
1560
|
}
|
|
1225
1561
|
if (!cliToken) {
|
|
1226
|
-
|
|
1227
|
-
process.stderr.write("\nerror: Login timed out. Run mushi login again.\n");
|
|
1562
|
+
printAuthFailed("timeout");
|
|
1228
1563
|
process.exit(1);
|
|
1229
1564
|
}
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
const cliHeaders = {
|
|
1233
|
-
"Content-Type": "application/json",
|
|
1234
|
-
"Authorization": `Bearer ${cliToken}`
|
|
1235
|
-
};
|
|
1236
|
-
let projectsList = [];
|
|
1237
|
-
try {
|
|
1238
|
-
const listRes = await fetch(`${endpoint}/v1/cli/projects`, { headers: cliHeaders });
|
|
1239
|
-
const listJson = await listRes.json();
|
|
1240
|
-
if (listRes.ok && listJson.ok) projectsList = listJson.data?.projects ?? [];
|
|
1241
|
-
} catch {
|
|
1242
|
-
}
|
|
1565
|
+
printAuthApproved();
|
|
1566
|
+
const projectsList = await listProjects(endpoint, cliToken);
|
|
1243
1567
|
let chosenProjectId = projectId;
|
|
1244
1568
|
let chosenProjectName;
|
|
1245
1569
|
if (!chosenProjectId) {
|
|
@@ -1271,18 +1595,10 @@ error: Poll failed: ${err instanceof Error ? err.message : String(err)}
|
|
|
1271
1595
|
process.exit(2);
|
|
1272
1596
|
}
|
|
1273
1597
|
try {
|
|
1274
|
-
const
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
});
|
|
1279
|
-
const createJson = await createRes.json();
|
|
1280
|
-
if (!createRes.ok || !createJson.ok || !createJson.data) {
|
|
1281
|
-
throw new Error(createJson.error?.message ?? `HTTP ${createRes.status}`);
|
|
1282
|
-
}
|
|
1283
|
-
chosenProjectId = createJson.data.id;
|
|
1284
|
-
chosenProjectName = createJson.data.name;
|
|
1285
|
-
apiKey = createJson.data.apiKey ?? void 0;
|
|
1598
|
+
const created = await createProject(endpoint, cliToken, newName.trim());
|
|
1599
|
+
chosenProjectId = created.id;
|
|
1600
|
+
chosenProjectName = created.name;
|
|
1601
|
+
apiKey = created.apiKey ?? void 0;
|
|
1286
1602
|
console.log(` \u2713 Created project "${chosenProjectName}"`);
|
|
1287
1603
|
} catch (err) {
|
|
1288
1604
|
process.stderr.write(`
|
|
@@ -1296,15 +1612,7 @@ error: Could not create project: ${err instanceof Error ? err.message : String(e
|
|
|
1296
1612
|
}
|
|
1297
1613
|
if (!apiKey && chosenProjectId) {
|
|
1298
1614
|
try {
|
|
1299
|
-
|
|
1300
|
-
method: "POST",
|
|
1301
|
-
headers: cliHeaders,
|
|
1302
|
-
body: JSON.stringify({})
|
|
1303
|
-
});
|
|
1304
|
-
const keyJson = await keyRes.json();
|
|
1305
|
-
if (keyRes.ok && keyJson.ok) {
|
|
1306
|
-
apiKey = keyJson.data?.key ?? void 0;
|
|
1307
|
-
}
|
|
1615
|
+
apiKey = await mintProjectKey(endpoint, cliToken, chosenProjectId) ?? void 0;
|
|
1308
1616
|
} catch {
|
|
1309
1617
|
}
|
|
1310
1618
|
}
|
|
@@ -1317,12 +1625,17 @@ error: Could not create project: ${err instanceof Error ? err.message : String(e
|
|
|
1317
1625
|
console.log("");
|
|
1318
1626
|
if (chosenProjectName) console.log(` \u2713 Project: ${chosenProjectName}`);
|
|
1319
1627
|
if (apiKey) {
|
|
1320
|
-
console.log(` \u2713
|
|
1628
|
+
console.log(` \u2713 Full-scope CLI key saved (${apiKey.slice(0, 12)}\u2026)`);
|
|
1629
|
+
console.log(` Scopes: report:write \xB7 mcp:read \xB7 mcp:write \u2014 SDK + MCP + admin CLI (incl. writes) all work with this key`);
|
|
1321
1630
|
} else {
|
|
1322
1631
|
console.log(` \u2139 Get an SDK key at: ${apiKeyHint(consoleBase)}`);
|
|
1323
1632
|
}
|
|
1324
1633
|
console.log("");
|
|
1325
|
-
|
|
1634
|
+
if (opts.upgradeScope) {
|
|
1635
|
+
console.log(" Key upgraded! Re-run 'mushi setup' to update your .cursor/mcp.json, then restart Cursor.");
|
|
1636
|
+
} else {
|
|
1637
|
+
console.log(" Run 'mushi init' to set up the SDK in this project.");
|
|
1638
|
+
}
|
|
1326
1639
|
});
|
|
1327
1640
|
program2.command("whoami").description("Verify API key and display project info").option("--json", "Machine-readable JSON output").addHelpText("after", `
|
|
1328
1641
|
Verifies that MUSHI_API_KEY is valid and shows which project it belongs to.
|
|
@@ -2145,56 +2458,115 @@ Examples:
|
|
|
2145
2458
|
// src/commands/project.ts
|
|
2146
2459
|
function registerProjectCommands(program2) {
|
|
2147
2460
|
const project = program2.command("project").description("Project management");
|
|
2148
|
-
project.command("create").description("Create a
|
|
2149
|
-
|
|
2150
|
-
and writes the following to the current directory:
|
|
2461
|
+
project.command("create").description("Create or select a Mushi project via browser sign-in, then write config files").option("--name <name>", "Project name for a new project (skips the prompt)").option("--no-browser", "Print the verification URL instead of opening the browser").option("--endpoint <url>", "Override API endpoint (self-hosted)").addHelpText("after", `
|
|
2462
|
+
Signs you in through the browser (no copy-paste), creates or selects a project,
|
|
2463
|
+
mints a report:write SDK key, and writes the following to the current directory:
|
|
2151
2464
|
.env.local \u2014 MUSHI_API_KEY, MUSHI_PROJECT_ID, MUSHI_API_ENDPOINT
|
|
2152
2465
|
.cursor/mcp.json \u2014 pre-filled mcpServers.mushi block for Cursor
|
|
2153
2466
|
|
|
2154
2467
|
Typical first-time flow:
|
|
2155
2468
|
npx mushi-mushi project create
|
|
2156
|
-
# Browser opens \u2192
|
|
2469
|
+
# Browser opens \u2192 click Approve \u2192 pick or create a project
|
|
2157
2470
|
# CLI writes .env.local and .cursor/mcp.json
|
|
2158
2471
|
# mushi whoami to confirm`).action(async (opts) => {
|
|
2159
2472
|
const { writeFile } = await import("fs/promises");
|
|
2160
2473
|
const { existsSync: existsSync4 } = await import("fs");
|
|
2161
2474
|
const nodePath = await import("path");
|
|
2162
|
-
const
|
|
2163
|
-
const
|
|
2164
|
-
|
|
2475
|
+
const savedConfig = loadConfig();
|
|
2476
|
+
const endpoint = resolveCloudEndpoint(
|
|
2477
|
+
opts.endpoint ?? process.env.MUSHI_API_ENDPOINT?.trim() ?? savedConfig.endpoint
|
|
2478
|
+
);
|
|
2479
|
+
const consoleBase = await resolveConsoleUrl();
|
|
2165
2480
|
console.log("");
|
|
2166
2481
|
console.log(" Mushi project create");
|
|
2167
2482
|
console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
2168
2483
|
console.log("");
|
|
2484
|
+
let session;
|
|
2485
|
+
try {
|
|
2486
|
+
session = await startDeviceAuth(endpoint);
|
|
2487
|
+
} catch (err) {
|
|
2488
|
+
process.stderr.write(`
|
|
2489
|
+
error: Could not start browser sign-in: ${err instanceof Error ? err.message : String(err)}
|
|
2490
|
+
`);
|
|
2491
|
+
process.stderr.write(" Fallback: mushi login --api-key <key> --project-id <uuid>\n");
|
|
2492
|
+
process.exit(1);
|
|
2493
|
+
}
|
|
2169
2494
|
if (opts.browser !== false) {
|
|
2170
|
-
console.log(" 1. Opening the Mushi sign-up page in your browser...");
|
|
2171
2495
|
try {
|
|
2172
|
-
|
|
2173
|
-
const openCmd = process.platform === "win32" ? `start "" "${signUpUrl}"` : process.platform === "darwin" ? `open "${signUpUrl}"` : `xdg-open "${signUpUrl}"`;
|
|
2174
|
-
exec(openCmd);
|
|
2496
|
+
await openInBrowser(session.verification_uri);
|
|
2175
2497
|
} catch {
|
|
2176
2498
|
}
|
|
2177
|
-
} else {
|
|
2178
|
-
console.log(` 1. Sign up or log in at: ${signUpUrl}`);
|
|
2179
2499
|
}
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2500
|
+
printAuthBanner(session.user_code, session.verification_uri);
|
|
2501
|
+
let cliToken;
|
|
2502
|
+
try {
|
|
2503
|
+
cliToken = await waitForCliToken(endpoint, session);
|
|
2504
|
+
} catch (err) {
|
|
2505
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2506
|
+
if (msg.includes("denied")) printAuthFailed("denied");
|
|
2507
|
+
else if (msg.includes("timed out") || msg.includes("expired")) printAuthFailed("timeout");
|
|
2508
|
+
else printAuthFailed("error", msg);
|
|
2509
|
+
process.exit(1);
|
|
2510
|
+
}
|
|
2511
|
+
printAuthApproved();
|
|
2512
|
+
const projectsList = await listProjects(endpoint, cliToken);
|
|
2184
2513
|
const { createInterface } = await import("readline");
|
|
2185
2514
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
2186
2515
|
const ask = (q) => new Promise((resolve3) => rl.question(q, (a) => resolve3(a.trim())));
|
|
2187
|
-
|
|
2188
|
-
|
|
2516
|
+
let chosen;
|
|
2517
|
+
let projectId;
|
|
2518
|
+
let projectName;
|
|
2519
|
+
let apiKey;
|
|
2520
|
+
if (!opts.name && projectsList.length > 0) {
|
|
2521
|
+
console.log("");
|
|
2522
|
+
console.log(" Your projects:");
|
|
2523
|
+
projectsList.forEach((pr, i) => {
|
|
2524
|
+
console.log(` ${i + 1}. ${pr.name} (${pr.id})`);
|
|
2525
|
+
});
|
|
2526
|
+
console.log(` ${projectsList.length + 1}. Create a new project`);
|
|
2527
|
+
console.log("");
|
|
2528
|
+
const choice = await ask(` Pick a project [1-${projectsList.length + 1}]: `);
|
|
2529
|
+
const num = parseInt(choice, 10);
|
|
2530
|
+
if (num >= 1 && num <= projectsList.length) {
|
|
2531
|
+
chosen = projectsList[num - 1];
|
|
2532
|
+
projectId = chosen.id;
|
|
2533
|
+
projectName = chosen.name;
|
|
2534
|
+
}
|
|
2535
|
+
}
|
|
2536
|
+
if (!projectId) {
|
|
2537
|
+
const newName = opts.name?.trim() || await ask(" Project name: ");
|
|
2538
|
+
if (!newName) {
|
|
2539
|
+
rl.close();
|
|
2540
|
+
process.stderr.write("\nerror: Project name is required.\n");
|
|
2541
|
+
process.exit(2);
|
|
2542
|
+
}
|
|
2543
|
+
try {
|
|
2544
|
+
const created = await createProject(endpoint, cliToken, newName);
|
|
2545
|
+
projectId = created.id;
|
|
2546
|
+
projectName = created.name;
|
|
2547
|
+
apiKey = created.apiKey ?? void 0;
|
|
2548
|
+
console.log(` \u2713 Created project "${projectName}"`);
|
|
2549
|
+
} catch (err) {
|
|
2550
|
+
rl.close();
|
|
2551
|
+
process.stderr.write(`
|
|
2552
|
+
error: Could not create project: ${err instanceof Error ? err.message : String(err)}
|
|
2553
|
+
`);
|
|
2554
|
+
process.exit(1);
|
|
2555
|
+
}
|
|
2556
|
+
}
|
|
2189
2557
|
rl.close();
|
|
2558
|
+
if (projectId && !apiKey) {
|
|
2559
|
+
apiKey = await mintProjectKey(endpoint, cliToken, projectId) ?? void 0;
|
|
2560
|
+
}
|
|
2190
2561
|
if (!projectId || !apiKey) {
|
|
2191
|
-
process.stderr.write("\nerror:
|
|
2192
|
-
process.exit(
|
|
2562
|
+
process.stderr.write("\nerror: Could not obtain an SDK key for the project. Run `mushi login` and try again.\n");
|
|
2563
|
+
process.exit(1);
|
|
2193
2564
|
}
|
|
2194
2565
|
const config = loadConfig();
|
|
2195
2566
|
config.apiKey = apiKey;
|
|
2196
2567
|
config.endpoint = endpoint;
|
|
2197
2568
|
config.projectId = projectId;
|
|
2569
|
+
config.consoleUrl = consoleBase;
|
|
2198
2570
|
saveConfig(config);
|
|
2199
2571
|
const cwd = process.cwd();
|
|
2200
2572
|
const envPath = nodePath.join(cwd, ".env.local");
|
|
@@ -2223,7 +2595,7 @@ Typical first-time flow:
|
|
|
2223
2595
|
|
|
2224
2596
|
// src/commands/setup.ts
|
|
2225
2597
|
function registerSetupCommands(program2) {
|
|
2226
|
-
program2.command("setup").description("Wire Cursor MCP into your IDE (reads ~/.config/mushi/config.json \u2014 run `mushi login` first)").option("--ide <ide>", "Target IDE: cursor | claude | continue | zed", "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").addHelpText("after", `
|
|
2598
|
+
program2.command("setup").description("Wire Cursor MCP into your IDE (reads ~/.config/mushi/config.json \u2014 run `mushi login` first)").option("--ide <ide>", "Target IDE: cursor | claude | continue | zed", "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").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").addHelpText("after", `
|
|
2227
2599
|
Examples:
|
|
2228
2600
|
mushi setup # wire Cursor (default)
|
|
2229
2601
|
mushi setup --all-projects # one server entry per accessible project
|
|
@@ -2427,6 +2799,54 @@ The command reads credentials from ~/.config/mushi/config.json (run \`mushi logi
|
|
|
2427
2799
|
}
|
|
2428
2800
|
}
|
|
2429
2801
|
if (!opts.dryRun) {
|
|
2802
|
+
const shouldVerify = opts.verify !== false;
|
|
2803
|
+
if (shouldVerify && config.endpoint && config.apiKey && config.projectId) {
|
|
2804
|
+
try {
|
|
2805
|
+
const probeRes = await fetch(
|
|
2806
|
+
`${config.endpoint.replace(/\/$/, "")}/v1/admin/mcp/account-overview`,
|
|
2807
|
+
{
|
|
2808
|
+
headers: {
|
|
2809
|
+
"X-Mushi-Api-Key": config.apiKey,
|
|
2810
|
+
"X-Mushi-Project": config.projectId
|
|
2811
|
+
},
|
|
2812
|
+
signal: AbortSignal.timeout(6e3)
|
|
2813
|
+
}
|
|
2814
|
+
);
|
|
2815
|
+
if (probeRes.ok) {
|
|
2816
|
+
console.log("\u2713 MCP key valid \u2014 restart Cursor (or your IDE) to activate");
|
|
2817
|
+
void fetch(
|
|
2818
|
+
`${config.endpoint.replace(/\/$/, "")}/v1/cli/funnel`,
|
|
2819
|
+
{
|
|
2820
|
+
method: "POST",
|
|
2821
|
+
headers: {
|
|
2822
|
+
"Content-Type": "application/json",
|
|
2823
|
+
"X-Mushi-Api-Key": config.apiKey,
|
|
2824
|
+
"X-Mushi-Project": config.projectId
|
|
2825
|
+
},
|
|
2826
|
+
body: JSON.stringify({ event: "mcp_setup_done", source: "cli" }),
|
|
2827
|
+
signal: AbortSignal.timeout(4e3)
|
|
2828
|
+
}
|
|
2829
|
+
).catch(() => {
|
|
2830
|
+
});
|
|
2831
|
+
} else if (probeRes.status === 403) {
|
|
2832
|
+
let errCode;
|
|
2833
|
+
try {
|
|
2834
|
+
const b = await probeRes.json();
|
|
2835
|
+
errCode = b?.error?.code;
|
|
2836
|
+
} catch {
|
|
2837
|
+
}
|
|
2838
|
+
if (errCode === "INSUFFICIENT_SCOPE") {
|
|
2839
|
+
console.log("\n\u26A0 Your key has report:write scope only \u2014 MCP admin tools will not work.");
|
|
2840
|
+
console.log(" New keys minted by the wizard include both scopes automatically.");
|
|
2841
|
+
console.log(" To upgrade an existing key, run:\n");
|
|
2842
|
+
console.log(" mushi login --upgrade-scope\n");
|
|
2843
|
+
} else {
|
|
2844
|
+
console.log(`\u26A0 Key probe returned HTTP ${probeRes.status} \u2014 check your credentials.`);
|
|
2845
|
+
}
|
|
2846
|
+
}
|
|
2847
|
+
} catch {
|
|
2848
|
+
}
|
|
2849
|
+
}
|
|
2430
2850
|
console.log("");
|
|
2431
2851
|
console.log(`Done! Restart ${opts.ide === "cursor" ? "Cursor" : opts.ide === "claude" ? "Claude Code" : opts.ide} and ask: "list mushi tools"`);
|
|
2432
2852
|
if (!opts.withRules) {
|
|
@@ -2637,6 +3057,23 @@ async function checkServerPreflight(config, doFetch = globalThis.fetch) {
|
|
|
2637
3057
|
detail: sc.ready ? "" : sc.hint
|
|
2638
3058
|
}));
|
|
2639
3059
|
}
|
|
3060
|
+
if (res.status === 403) {
|
|
3061
|
+
let errCode;
|
|
3062
|
+
try {
|
|
3063
|
+
const body = await res.json();
|
|
3064
|
+
errCode = body?.error?.code;
|
|
3065
|
+
} catch {
|
|
3066
|
+
}
|
|
3067
|
+
if (errCode === "INSUFFICIENT_SCOPE") {
|
|
3068
|
+
return [
|
|
3069
|
+
{
|
|
3070
|
+
name: "Server preflight",
|
|
3071
|
+
ok: true,
|
|
3072
|
+
detail: "Skipped \u2014 your key has report:write scope only (pre-Jun 2026 key). Run `mushi login --upgrade-scope` to get mcp:read and unlock admin checks."
|
|
3073
|
+
}
|
|
3074
|
+
];
|
|
3075
|
+
}
|
|
3076
|
+
}
|
|
2640
3077
|
const text2 = await res.text().catch(() => "");
|
|
2641
3078
|
return [
|
|
2642
3079
|
{
|
|
@@ -2839,7 +3276,17 @@ async function checkMcpConfig(config, cwd, doFetch = globalThis.fetch) {
|
|
|
2839
3276
|
let mcpRaw = null;
|
|
2840
3277
|
for (const candidate of candidates) {
|
|
2841
3278
|
try {
|
|
2842
|
-
|
|
3279
|
+
const raw = await readFile2(candidate, "utf8");
|
|
3280
|
+
let parsed = {};
|
|
3281
|
+
try {
|
|
3282
|
+
parsed = JSON.parse(raw);
|
|
3283
|
+
} catch {
|
|
3284
|
+
}
|
|
3285
|
+
const hasEntries = Object.keys(parsed.mcpServers ?? {}).length > 0;
|
|
3286
|
+
if (!hasEntries && candidates.indexOf(candidate) < candidates.length - 1) {
|
|
3287
|
+
continue;
|
|
3288
|
+
}
|
|
3289
|
+
mcpRaw = raw;
|
|
2843
3290
|
mcpPath = candidate;
|
|
2844
3291
|
break;
|
|
2845
3292
|
} catch {
|
|
@@ -2916,10 +3363,20 @@ async function checkMcpConfig(config, cwd, doFetch = globalThis.fetch) {
|
|
|
2916
3363
|
detail: `Key is valid; ${projectCount} accessible project${projectCount === 1 ? "" : "s"}`
|
|
2917
3364
|
});
|
|
2918
3365
|
} else {
|
|
3366
|
+
let detail = `GET /v1/admin/mcp/account-overview \u2192 HTTP ${res.status}. Verify the API key is active.`;
|
|
3367
|
+
try {
|
|
3368
|
+
const errBody = await res.json();
|
|
3369
|
+
const nested = errBody["error"];
|
|
3370
|
+
const errCode = nested?.["code"] ?? errBody["code"] ?? "";
|
|
3371
|
+
if (errCode === "INSUFFICIENT_SCOPE") {
|
|
3372
|
+
detail = `Key has report:write scope only \u2014 run \`mushi login --upgrade-scope\` then \`mushi setup\` to get mcp:read.`;
|
|
3373
|
+
}
|
|
3374
|
+
} catch {
|
|
3375
|
+
}
|
|
2919
3376
|
checks.push({
|
|
2920
3377
|
name: "[mcp] account-overview reachable",
|
|
2921
3378
|
ok: false,
|
|
2922
|
-
detail
|
|
3379
|
+
detail
|
|
2923
3380
|
});
|
|
2924
3381
|
}
|
|
2925
3382
|
} catch (err) {
|
|
@@ -2949,11 +3406,96 @@ function fixHintForCheck(name) {
|
|
|
2949
3406
|
if (name.startsWith("[mcp]")) return FIX_HINTS["[mcp]"];
|
|
2950
3407
|
return void 0;
|
|
2951
3408
|
}
|
|
3409
|
+
async function checkOnboardingStatus(config, _consoleBase, cwd, doFetch = globalThis.fetch) {
|
|
3410
|
+
if (!config.apiKey || !config.projectId || !config.endpoint) {
|
|
3411
|
+
if (config.apiKey && config.endpoint) {
|
|
3412
|
+
return {
|
|
3413
|
+
nextAction: "Select or create a project: run `mushi project create` or pick one with `mushi project list`",
|
|
3414
|
+
ctaPath: "/onboarding?tab=steps&setup=cli",
|
|
3415
|
+
done: false
|
|
3416
|
+
};
|
|
3417
|
+
}
|
|
3418
|
+
return {
|
|
3419
|
+
nextAction: "Sign in to Mushi: run `npx mushi-mushi` (browser opens automatically \u2014 do NOT type the code in the terminal)",
|
|
3420
|
+
ctaPath: "/onboarding?tab=steps&setup=cli",
|
|
3421
|
+
done: false
|
|
3422
|
+
};
|
|
3423
|
+
}
|
|
3424
|
+
try {
|
|
3425
|
+
const { readFile: readFile2 } = await import("fs/promises");
|
|
3426
|
+
const { join: join5, resolve: resolve3 } = await import("path");
|
|
3427
|
+
const root = resolve3(cwd);
|
|
3428
|
+
let envContent = "";
|
|
3429
|
+
for (const f of [".env.local", ".env"]) {
|
|
3430
|
+
try {
|
|
3431
|
+
envContent = await readFile2(join5(root, f), "utf8");
|
|
3432
|
+
break;
|
|
3433
|
+
} catch {
|
|
3434
|
+
}
|
|
3435
|
+
}
|
|
3436
|
+
const hasEnvVars = /(?:VITE_|NEXT_PUBLIC_|EXPO_PUBLIC_|NUXT_PUBLIC_)?MUSHI_PROJECT_ID=/.test(envContent);
|
|
3437
|
+
if (!hasEnvVars) {
|
|
3438
|
+
return {
|
|
3439
|
+
nextAction: "Write env vars to your app: run `npx mushi-mushi` or `mushi connect --write-env`",
|
|
3440
|
+
ctaPath: "/onboarding?tab=sdk",
|
|
3441
|
+
done: false
|
|
3442
|
+
};
|
|
3443
|
+
}
|
|
3444
|
+
} catch {
|
|
3445
|
+
}
|
|
3446
|
+
try {
|
|
3447
|
+
const sdkCheck = await checkSdkInstall(cwd);
|
|
3448
|
+
if (sdkCheck && !sdkCheck.ok) {
|
|
3449
|
+
return {
|
|
3450
|
+
nextAction: `Install the Mushi SDK: ${sdkCheck.detail}`,
|
|
3451
|
+
ctaPath: "/onboarding?tab=sdk",
|
|
3452
|
+
done: false
|
|
3453
|
+
};
|
|
3454
|
+
}
|
|
3455
|
+
} catch {
|
|
3456
|
+
}
|
|
3457
|
+
try {
|
|
3458
|
+
const data = await fetchIngestSetup(
|
|
3459
|
+
{ endpoint: config.endpoint, apiKey: config.apiKey, projectId: config.projectId },
|
|
3460
|
+
doFetch
|
|
3461
|
+
);
|
|
3462
|
+
if (data) {
|
|
3463
|
+
const steps = data.steps ?? [];
|
|
3464
|
+
const incomplete = steps.find((s) => s.required && !s.complete);
|
|
3465
|
+
if (incomplete) {
|
|
3466
|
+
const tabMap = {
|
|
3467
|
+
project_created: "/onboarding?tab=steps&setup=cli",
|
|
3468
|
+
api_key_generated: "/onboarding?tab=verify",
|
|
3469
|
+
sdk_installed: "/onboarding?tab=sdk",
|
|
3470
|
+
first_report_received: "/onboarding?tab=verify"
|
|
3471
|
+
};
|
|
3472
|
+
const tab = tabMap[incomplete.id] ?? "/onboarding?tab=verify";
|
|
3473
|
+
return {
|
|
3474
|
+
nextAction: incomplete.hint ?? `Complete setup step: ${incomplete.label}`,
|
|
3475
|
+
ctaPath: tab,
|
|
3476
|
+
done: false
|
|
3477
|
+
};
|
|
3478
|
+
}
|
|
3479
|
+
return {
|
|
3480
|
+
nextAction: "All 4 required setup steps are complete! Open the console to explore.",
|
|
3481
|
+
ctaPath: "/reports",
|
|
3482
|
+
done: true
|
|
3483
|
+
};
|
|
3484
|
+
}
|
|
3485
|
+
} catch {
|
|
3486
|
+
}
|
|
3487
|
+
return {
|
|
3488
|
+
nextAction: "Open the onboarding wizard in the console to check your setup status.",
|
|
3489
|
+
ctaPath: "/onboarding",
|
|
3490
|
+
done: false
|
|
3491
|
+
};
|
|
3492
|
+
}
|
|
2952
3493
|
async function runDoctor(config, options = {}) {
|
|
2953
3494
|
const doFetch = options.fetch ?? globalThis.fetch;
|
|
2954
3495
|
const checks = [];
|
|
2955
|
-
const
|
|
2956
|
-
const
|
|
3496
|
+
const isFull = options.full === true;
|
|
3497
|
+
const runServer = isFull || options.server !== false;
|
|
3498
|
+
const runIngest = isFull || options.ingest !== false;
|
|
2957
3499
|
checks.push(...checkCliConfig(config));
|
|
2958
3500
|
if (config.endpoint) {
|
|
2959
3501
|
checks.push(await checkEndpointReachability(config.endpoint, doFetch));
|
|
@@ -2968,26 +3510,40 @@ async function runDoctor(config, options = {}) {
|
|
|
2968
3510
|
const ingestChecks = await checkIngestSetup(config, doFetch);
|
|
2969
3511
|
checks.push(...ingestChecks);
|
|
2970
3512
|
}
|
|
2971
|
-
if (options.qaStories) {
|
|
3513
|
+
if (isFull || options.qaStories) {
|
|
2972
3514
|
const qaChecks = await checkQaStoriesHealth(config, doFetch);
|
|
2973
3515
|
checks.push(...qaChecks);
|
|
2974
3516
|
}
|
|
2975
|
-
if (options.hostApp) {
|
|
3517
|
+
if (isFull || options.hostApp) {
|
|
2976
3518
|
const hostChecks = await checkHostAppWiring(options.cwd ?? process.cwd());
|
|
2977
3519
|
checks.push(...hostChecks);
|
|
2978
3520
|
}
|
|
2979
|
-
if (options.mcp) {
|
|
3521
|
+
if (isFull || options.mcp) {
|
|
2980
3522
|
const mcpChecks = await checkMcpConfig(config, options.cwd ?? process.cwd(), doFetch);
|
|
2981
3523
|
checks.push(...mcpChecks);
|
|
2982
3524
|
}
|
|
3525
|
+
const heartbeatPassed = checks.some((c) => c.name === "[ingest] Last SDK heartbeat" && c.ok);
|
|
3526
|
+
if (heartbeatPassed) {
|
|
3527
|
+
const idx = checks.findIndex((c) => c.name === "SDK installed in this repo" && !c.ok);
|
|
3528
|
+
if (idx >= 0) {
|
|
3529
|
+
checks[idx] = {
|
|
3530
|
+
...checks[idx],
|
|
3531
|
+
ok: true,
|
|
3532
|
+
warn: true,
|
|
3533
|
+
detail: "No @mushi-mushi/* found in cwd package.json \u2014 but live heartbeats confirm the SDK is active. Run from your app repo or install with `mushi init`."
|
|
3534
|
+
};
|
|
3535
|
+
}
|
|
3536
|
+
}
|
|
2983
3537
|
return { checks, ready: checks.every((c) => c.ok) };
|
|
2984
3538
|
}
|
|
2985
3539
|
function formatDoctorResult(result) {
|
|
2986
3540
|
const PASS = "\u2713";
|
|
3541
|
+
const WARN = "\u26A0";
|
|
2987
3542
|
const FAIL = "\u2717";
|
|
2988
3543
|
const lines = [];
|
|
2989
3544
|
for (const c of result.checks) {
|
|
2990
|
-
|
|
3545
|
+
const icon = !c.ok ? FAIL : c.warn ? WARN : PASS;
|
|
3546
|
+
lines.push(`${icon} ${c.name}`);
|
|
2991
3547
|
if (c.detail) lines.push(` ${c.detail}`);
|
|
2992
3548
|
if (!c.ok) {
|
|
2993
3549
|
const hint = fixHintForCheck(c.name);
|
|
@@ -2995,8 +3551,14 @@ function formatDoctorResult(result) {
|
|
|
2995
3551
|
}
|
|
2996
3552
|
}
|
|
2997
3553
|
const failed = result.checks.filter((c) => !c.ok);
|
|
3554
|
+
const warned = result.checks.filter((c) => c.ok && c.warn);
|
|
2998
3555
|
if (failed.length === 0) {
|
|
2999
|
-
|
|
3556
|
+
if (warned.length > 0) {
|
|
3557
|
+
lines.push(`
|
|
3558
|
+
All checks passed with ${warned.length} advisory warning${warned.length === 1 ? "" : "s"}. The CLI is ready.`);
|
|
3559
|
+
} else {
|
|
3560
|
+
lines.push("\nAll checks passed. The CLI is ready.");
|
|
3561
|
+
}
|
|
3000
3562
|
} else {
|
|
3001
3563
|
lines.push(`
|
|
3002
3564
|
${failed.length} check${failed.length === 1 ? "" : "s"} failed.`);
|
|
@@ -3240,7 +3802,7 @@ Examples:
|
|
|
3240
3802
|
}
|
|
3241
3803
|
if (result.plan.entries.length === 0) process.exit(1);
|
|
3242
3804
|
});
|
|
3243
|
-
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").requiredOption("--project-id <id>", "Project UUID").requiredOption("--endpoint <url>", "Supabase edge function URL").option("--cwd <path>", "Target repo").option("--write-env", "
|
|
3805
|
+
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").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", `
|
|
3244
3806
|
Examples:
|
|
3245
3807
|
MUSHI_API_KEY=mushi_xxx mushi connect --project-id <uuid> --endpoint https://<ref>.supabase.co/functions/v1/api --wait
|
|
3246
3808
|
mushi connect --api-key mushi_xxx --project-id <uuid> --endpoint <url> --no-ide`).action(async (opts) => {
|
|
@@ -3254,8 +3816,10 @@ Examples:
|
|
|
3254
3816
|
projectId: opts.projectId,
|
|
3255
3817
|
endpoint: opts.endpoint,
|
|
3256
3818
|
cwd: opts.cwd,
|
|
3257
|
-
|
|
3258
|
-
|
|
3819
|
+
// Explicit --write-env / --wire-ide force the action on even if --no-env /
|
|
3820
|
+
// --no-ide was also passed; otherwise writing is the default.
|
|
3821
|
+
writeEnv: opts.writeEnv === true ? true : opts.env !== false,
|
|
3822
|
+
wireIde: opts.wireIde === true ? true : opts.ide !== false,
|
|
3259
3823
|
wait: opts.wait,
|
|
3260
3824
|
waitTimeoutSec: parseInt(opts.waitTimeout, 10) || 120,
|
|
3261
3825
|
json: opts.json
|
|
@@ -3281,9 +3845,38 @@ Examples:
|
|
|
3281
3845
|
).option(
|
|
3282
3846
|
"--fix",
|
|
3283
3847
|
"Apply safe local fixes when checks fail: write missing .env.local lines and wire Cursor MCP config."
|
|
3848
|
+
).option(
|
|
3849
|
+
"--onboarding",
|
|
3850
|
+
"Focused onboarding check: prints the single next action you need to take to finish SDK setup, with a console deep link."
|
|
3851
|
+
).option(
|
|
3852
|
+
"--full",
|
|
3853
|
+
"Run ALL check categories (server, ingest, host-app, mcp, qa-stories) in one shot. Good for first-run diagnostics. Exit 0 only when all checks pass."
|
|
3284
3854
|
).action(async (opts) => {
|
|
3285
3855
|
const config = loadConfig();
|
|
3286
|
-
|
|
3856
|
+
if (opts.onboarding) {
|
|
3857
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
3858
|
+
const consoleBase = await resolveConsoleUrl({ cwd });
|
|
3859
|
+
const status = await checkOnboardingStatus(config, consoleBase, cwd);
|
|
3860
|
+
const base = consoleBase.replace(/\/$/, "");
|
|
3861
|
+
if (status.done) {
|
|
3862
|
+
console.log(`\u2713 Setup complete. ${status.nextAction}`);
|
|
3863
|
+
console.log(` ${base}${status.ctaPath}`);
|
|
3864
|
+
} else {
|
|
3865
|
+
console.log(`\u2192 Next: ${status.nextAction}`);
|
|
3866
|
+
console.log(` Open: ${base}${status.ctaPath}`);
|
|
3867
|
+
}
|
|
3868
|
+
if (!status.done) process.exit(1);
|
|
3869
|
+
return;
|
|
3870
|
+
}
|
|
3871
|
+
const doctorOpts = {
|
|
3872
|
+
cwd: opts.cwd,
|
|
3873
|
+
server: opts.server,
|
|
3874
|
+
ingest: opts.ingest,
|
|
3875
|
+
qaStories: opts.qaStories,
|
|
3876
|
+
hostApp: opts.hostApp,
|
|
3877
|
+
mcp: opts.mcp,
|
|
3878
|
+
full: opts.full
|
|
3879
|
+
};
|
|
3287
3880
|
let result = await runDoctor(config, doctorOpts);
|
|
3288
3881
|
if (!result.ready && opts.fix && config.apiKey && config.projectId && config.endpoint) {
|
|
3289
3882
|
const connectResult = await runConnect({
|
|
@@ -4367,6 +4960,97 @@ ${"\u2500".repeat(60)}`);
|
|
|
4367
4960
|
});
|
|
4368
4961
|
}
|
|
4369
4962
|
|
|
4963
|
+
// src/commands/billing.ts
|
|
4964
|
+
function registerBillingCommands(program2) {
|
|
4965
|
+
program2.command("usage").description("Show diagnoses used / limit / cap for the current billing period").option("--json", "Machine-readable JSON output").addHelpText("after", `
|
|
4966
|
+
Examples:
|
|
4967
|
+
mushi usage
|
|
4968
|
+
mushi usage --json`).action(async (opts) => {
|
|
4969
|
+
const config = requireConfig();
|
|
4970
|
+
const result = await apiCall("/v1/admin/billing/stats", config);
|
|
4971
|
+
if (!result.ok) die(result);
|
|
4972
|
+
const s = result.data;
|
|
4973
|
+
if (opts.json) {
|
|
4974
|
+
console.log(JSON.stringify(s, null, 2));
|
|
4975
|
+
return;
|
|
4976
|
+
}
|
|
4977
|
+
const limitStr = s.diagnosesLimit != null ? String(s.diagnosesLimit) : "unlimited";
|
|
4978
|
+
const pctStr = s.diagnosesUsagePct != null ? `${Math.round(s.diagnosesUsagePct)}%` : "\u2014";
|
|
4979
|
+
const capStr = s.monthlySpendCapUsd != null ? `$${s.monthlySpendCapUsd}/mo` : "none";
|
|
4980
|
+
const badge = s.overDiagnosisQuota ? " \u26A0 OVER LIMIT" : s.approachingDiagnosisQuota ? " \u26A0 approaching limit" : "";
|
|
4981
|
+
console.log(`
|
|
4982
|
+
Diagnoses this period: ${s.diagnosesUsed} / ${limitStr} (${pctStr})${badge}`);
|
|
4983
|
+
console.log(`Plan: ${s.planId ?? "\u2014"}`);
|
|
4984
|
+
console.log(`Spend cap: ${capStr}`);
|
|
4985
|
+
console.log(`Period ends: ${s.periodEnd ? s.periodEnd.slice(0, 10) : "?"}`);
|
|
4986
|
+
if (s.overDiagnosisQuota) {
|
|
4987
|
+
console.log("\n Diagnoses paused \u2014 upgrade at https://kensaur.us/mushi-mushi/admin/billing");
|
|
4988
|
+
} else if (s.approachingDiagnosisQuota) {
|
|
4989
|
+
console.log("\n You're at 80%+ of your included diagnoses. Consider upgrading.");
|
|
4990
|
+
}
|
|
4991
|
+
});
|
|
4992
|
+
const billing = program2.command("billing").description("Show billing summary and manage spend cap");
|
|
4993
|
+
billing.command("status").description("Show full billing summary (plan, usage, cap)").option("--json", "Machine-readable JSON output").addHelpText("after", `
|
|
4994
|
+
Examples:
|
|
4995
|
+
mushi billing status
|
|
4996
|
+
mushi billing status --json`).action(async (opts) => {
|
|
4997
|
+
const config = requireConfig();
|
|
4998
|
+
const result = await apiCall("/v1/admin/billing", config);
|
|
4999
|
+
if (!result.ok) die(result);
|
|
5000
|
+
const s = result.data;
|
|
5001
|
+
if (opts.json) {
|
|
5002
|
+
console.log(JSON.stringify(s, null, 2));
|
|
5003
|
+
return;
|
|
5004
|
+
}
|
|
5005
|
+
const limitStr = s.diagnosesLimit != null ? String(s.diagnosesLimit) : "unlimited";
|
|
5006
|
+
const pctStr = s.diagnosesUsagePct != null ? `${Math.round(s.diagnosesUsagePct)}%` : "\u2014";
|
|
5007
|
+
const capStr = s.monthlySpendCapUsd != null ? `$${s.monthlySpendCapUsd}/mo` : "none";
|
|
5008
|
+
const overageStr = s.overageRateDiagnoses != null ? `$${s.overageRateDiagnoses}/diagnosis` : "N/A";
|
|
5009
|
+
console.log(`
|
|
5010
|
+
Plan: ${s.planId ?? "\u2014"}`);
|
|
5011
|
+
console.log(`Status: ${s.subscriptionStatus ?? "free"}${s.cancelAtPeriodEnd ? " (cancels at period end)" : ""}`);
|
|
5012
|
+
console.log(`Diagnoses: ${s.diagnosesUsed} / ${limitStr} (${pctStr})`);
|
|
5013
|
+
console.log(`Overage: ${overageStr}`);
|
|
5014
|
+
console.log(`Spend cap: ${capStr}`);
|
|
5015
|
+
console.log(`Renews: ${s.periodEnd ? s.periodEnd.slice(0, 10) : "?"}`);
|
|
5016
|
+
});
|
|
5017
|
+
billing.command("cap [usd]").description("Set or clear the monthly spend cap (pass 0 to clear, omit to show current cap)").addHelpText("after", `
|
|
5018
|
+
Examples:
|
|
5019
|
+
mushi billing cap # show current cap
|
|
5020
|
+
mushi billing cap 100 # set $100/mo hard cap
|
|
5021
|
+
mushi billing cap 0 # clear cap`).action(async (usdArg) => {
|
|
5022
|
+
const config = requireConfig();
|
|
5023
|
+
if (usdArg === void 0) {
|
|
5024
|
+
const result2 = await apiCall("/v1/admin/billing/stats", config);
|
|
5025
|
+
if (!result2.ok) die(result2);
|
|
5026
|
+
const cap2 = result2.data.monthlySpendCapUsd;
|
|
5027
|
+
if (cap2 == null) {
|
|
5028
|
+
console.log("No spend cap set. Pass a dollar amount to set one, e.g.: mushi billing cap 100");
|
|
5029
|
+
} else {
|
|
5030
|
+
console.log(`Current spend cap: $${cap2}/mo`);
|
|
5031
|
+
}
|
|
5032
|
+
return;
|
|
5033
|
+
}
|
|
5034
|
+
const capUsd = parseFloat(usdArg);
|
|
5035
|
+
if (isNaN(capUsd) || capUsd < 0) {
|
|
5036
|
+
console.error("Error: spend cap must be a non-negative number (or 0 to clear).");
|
|
5037
|
+
process.exit(2);
|
|
5038
|
+
}
|
|
5039
|
+
const result = await apiCall("/v1/admin/billing/spend-cap", config, {
|
|
5040
|
+
method: "PUT",
|
|
5041
|
+
headers: { "Content-Type": "application/json" },
|
|
5042
|
+
body: JSON.stringify({ project_id: config.projectId, spend_cap_usd: capUsd === 0 ? null : capUsd })
|
|
5043
|
+
});
|
|
5044
|
+
if (!result.ok) die(result);
|
|
5045
|
+
const cap = result.data.spend_cap_usd;
|
|
5046
|
+
if (cap == null) {
|
|
5047
|
+
console.log("\u2713 Spend cap cleared \u2014 no hard monthly limit.");
|
|
5048
|
+
} else {
|
|
5049
|
+
console.log(`\u2713 Spend cap set to $${cap}/mo. Diagnoses will pause gracefully once reached.`);
|
|
5050
|
+
}
|
|
5051
|
+
});
|
|
5052
|
+
}
|
|
5053
|
+
|
|
4370
5054
|
// src/index.ts
|
|
4371
5055
|
installSignalHandlers();
|
|
4372
5056
|
var program = new Command().name("mushi").description("Mushi Mushi CLI \u2014 set up the SDK, manage bug reports, monitor pipeline").version(MUSHI_CLI_VERSION).addHelpText("after", `
|
|
@@ -4399,6 +5083,7 @@ registerIntegrationsCommands(program);
|
|
|
4399
5083
|
registerQaCommands(program);
|
|
4400
5084
|
registerAuditCommands(program);
|
|
4401
5085
|
registerSkillsCommands(program);
|
|
5086
|
+
registerBillingCommands(program);
|
|
4402
5087
|
program.parseAsync().catch((err) => {
|
|
4403
5088
|
console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
4404
5089
|
process.exit(1);
|