@mushi-mushi/cli 0.23.1 → 0.25.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/dist/index.js CHANGED
@@ -37,7 +37,7 @@ import {
37
37
  sanitizeProjectId,
38
38
  saveConfig,
39
39
  writeMcpServerEntry
40
- } from "./chunk-M6L22NJU.js";
40
+ } from "./chunk-3JRQN6S4.js";
41
41
  import {
42
42
  TEST_REPORT_FETCH_TIMEOUT_MS,
43
43
  assertEndpoint,
@@ -81,7 +81,9 @@ async function startDeviceAuth(endpoint, clientId) {
81
81
  });
82
82
  const json = await res.json().catch(() => null);
83
83
  if (!res.ok || !json?.ok || !json.data) {
84
- throw new Error(json?.error?.message ?? `Could not start browser sign-in (HTTP ${res.status}).`);
84
+ throw new Error(
85
+ json?.error?.message ?? `Could not start browser sign-in (HTTP ${res.status}).`
86
+ );
85
87
  }
86
88
  return json.data;
87
89
  }
@@ -122,10 +124,15 @@ async function pollDeviceToken(endpoint, deviceCode) {
122
124
  };
123
125
  }
124
126
  } catch (err) {
125
- return { status: "error", message: err instanceof Error ? err.message : String(err), retryable: true };
127
+ return {
128
+ status: "error",
129
+ message: err instanceof Error ? err.message : String(err),
130
+ retryable: true
131
+ };
126
132
  }
127
133
  }
128
134
  var defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
135
+ var deviceAuthRetryDelayMs = 1e3;
129
136
  async function waitForCliToken(endpoint, session, opts = {}) {
130
137
  const sleep = opts.sleep ?? defaultSleep;
131
138
  const now = opts.now ?? Date.now;
@@ -176,19 +183,51 @@ function cliAuthHeaders(cliToken) {
176
183
  Authorization: `Bearer ${cliToken}`
177
184
  };
178
185
  }
186
+ var DeviceAuthRequestError = class extends Error {
187
+ step;
188
+ status;
189
+ retryable;
190
+ constructor(step, message, status) {
191
+ super(message);
192
+ this.name = "DeviceAuthRequestError";
193
+ this.step = step;
194
+ this.status = status;
195
+ this.retryable = status === void 0 || status >= 500 || status === 408;
196
+ }
197
+ };
198
+ async function withOneRetry(run) {
199
+ try {
200
+ return await run();
201
+ } catch (err) {
202
+ if (err instanceof DeviceAuthRequestError && err.retryable) {
203
+ if (deviceAuthRetryDelayMs > 0) {
204
+ await defaultSleep(deviceAuthRetryDelayMs);
205
+ }
206
+ return run();
207
+ }
208
+ throw err;
209
+ }
210
+ }
179
211
  async function listProjects(endpoint, cliToken) {
180
212
  const base = trimTrailingSlash(endpoint);
181
- try {
182
- const res = await deviceFetch(`${base}/v1/cli/projects`, { headers: cliAuthHeaders(cliToken) });
213
+ return withOneRetry(async () => {
214
+ let res;
215
+ try {
216
+ res = await deviceFetch(`${base}/v1/cli/projects`, { headers: cliAuthHeaders(cliToken) });
217
+ } catch (err) {
218
+ throw new DeviceAuthRequestError(
219
+ "list_projects",
220
+ `Could not reach ${base}/v1/cli/projects: ${err instanceof Error ? err.message : String(err)}`
221
+ );
222
+ }
183
223
  const json = await res.json().catch(() => null);
184
224
  if (res.ok && json?.ok) return json.data?.projects ?? [];
185
- } catch (err) {
186
- console.error(
187
- "[mushi] Failed to list projects:",
188
- err instanceof Error ? err.message : err
225
+ throw new DeviceAuthRequestError(
226
+ "list_projects",
227
+ json?.error?.message ?? `Could not list projects (HTTP ${res.status}).`,
228
+ res.status
189
229
  );
190
- }
191
- return [];
230
+ });
192
231
  }
193
232
  async function createProject(endpoint, cliToken, name) {
194
233
  const base = trimTrailingSlash(endpoint);
@@ -206,17 +245,26 @@ async function createProject(endpoint, cliToken, name) {
206
245
  }
207
246
  async function mintProjectKey(endpoint, cliToken, projectId) {
208
247
  const base = trimTrailingSlash(endpoint);
248
+ let res;
209
249
  try {
210
- const res = await deviceFetch(`${base}/v1/cli/projects/${projectId}/keys`, {
250
+ res = await deviceFetch(`${base}/v1/cli/projects/${projectId}/keys`, {
211
251
  method: "POST",
212
252
  headers: cliAuthHeaders(cliToken),
213
253
  body: JSON.stringify({})
214
254
  });
215
- const json = await res.json().catch(() => null);
216
- if (res.ok && json?.ok) return json.data?.key ?? null;
217
- } catch {
255
+ } catch (err) {
256
+ throw new DeviceAuthRequestError(
257
+ "mint_key",
258
+ `Could not reach ${base}/v1/cli/projects/\u2026/keys: ${err instanceof Error ? err.message : String(err)}`
259
+ );
218
260
  }
219
- return null;
261
+ const json = await res.json().catch(() => null);
262
+ if (res.ok && json?.ok && json.data?.key) return json.data.key;
263
+ throw new DeviceAuthRequestError(
264
+ "mint_key",
265
+ json?.error?.message ?? `Could not mint an SDK key (HTTP ${res.status}).`,
266
+ res.status
267
+ );
220
268
  }
221
269
 
222
270
  // src/freshness.ts
@@ -460,8 +508,16 @@ async function runInit(options = {}) {
460
508
  } else {
461
509
  p.log.info(`Skipped install. Run \`${installCommand(pm, packagesToInstall)}\` yourself.`);
462
510
  }
463
- await writeEnvFile(cwd, credentials.apiKey, credentials.projectId, framework, endpoint, Boolean(options.yes));
511
+ await writeEnvFile(
512
+ cwd,
513
+ credentials.apiKey,
514
+ credentials.projectId,
515
+ framework,
516
+ endpoint,
517
+ Boolean(options.yes)
518
+ );
464
519
  persistCliConfig(credentials.apiKey, credentials.projectId, endpoint);
520
+ emitWizardFunnelEvent(credentials, endpoint, "wizard_env_written", { framework: framework.id });
465
521
  const enableRewards = await maybeEnableRewards(options);
466
522
  printNextSteps(framework, consoleBase, enableRewards);
467
523
  await maybeSendTestReport(credentials, { ...options, endpoint, consoleBase });
@@ -533,7 +589,9 @@ async function acquireCredentials(options, consoleBase, endpoint) {
533
589
  if (options.yes) {
534
590
  const creds = await runBrowserSignIn(options, endpoint, consoleBase);
535
591
  if (creds) return creds;
536
- p.log.warn("Browser sign-in didn't complete \u2014 switching to manual entry.");
592
+ p.log.warn(
593
+ "Browser sign-in didn't complete \u2014 switching to manual entry. Run `npx mushi-mushi doctor --auth` to diagnose the sign-in path."
594
+ );
537
595
  } else {
538
596
  const method = await p.select({
539
597
  message: "Connect this app to Mushi",
@@ -558,7 +616,9 @@ async function acquireCredentials(options, consoleBase, endpoint) {
558
616
  if (method === "browser") {
559
617
  const creds = await runBrowserSignIn(options, endpoint, consoleBase);
560
618
  if (creds) return creds;
561
- p.log.warn("Browser sign-in didn't complete \u2014 switching to manual entry.");
619
+ p.log.warn(
620
+ "Browser sign-in didn't complete \u2014 switching to manual entry. Run `npx mushi-mushi doctor --auth` to diagnose the sign-in path."
621
+ );
562
622
  }
563
623
  }
564
624
  return collectCredentialsManually(options, consoleBase, endpoint);
@@ -599,7 +659,9 @@ async function runBrowserSignIn(options, endpoint, consoleBase) {
599
659
  fetchSpin.start("Loading your projects\u2026");
600
660
  try {
601
661
  projects = await listProjects(endpoint, cliToken);
602
- fetchSpin.stop(projects.length > 0 ? `Found ${projects.length} project(s).` : "No projects yet.");
662
+ fetchSpin.stop(
663
+ projects.length > 0 ? `Found ${projects.length} project(s).` : "No projects yet."
664
+ );
603
665
  } catch (err) {
604
666
  fetchSpin.stop("Could not load projects \u2014 you can still create a new one.");
605
667
  p.log.warn(err instanceof Error ? err.message : String(err));
@@ -647,19 +709,14 @@ async function runBrowserSignIn(options, endpoint, consoleBase) {
647
709
  const keySpin = p.spinner();
648
710
  keySpin.start("Minting SDK key\u2026");
649
711
  try {
650
- const minted = await mintProjectKey(endpoint, cliToken, projectId);
651
- apiKey = minted ?? void 0;
652
- if (!apiKey) {
653
- keySpin.stop("Could not mint an API key.");
654
- p.log.warn(
655
- `Open the console Verify tab to generate a key manually: ${consoleUrl(consoleBase, "/onboarding?tab=verify")}`
656
- );
657
- return null;
658
- }
712
+ apiKey = await mintProjectKey(endpoint, cliToken, projectId);
659
713
  keySpin.stop("SDK key ready.");
660
714
  } catch (err) {
661
715
  keySpin.stop("Could not mint an API key.");
662
716
  p.log.warn(err instanceof Error ? err.message : String(err));
717
+ p.log.warn(
718
+ `You're signed in, but key minting failed. Generate one manually in the console Verify tab: ${consoleUrl(consoleBase, "/onboarding?tab=verify")}`
719
+ );
663
720
  return null;
664
721
  }
665
722
  }
@@ -719,10 +776,11 @@ async function verifyCredentials(credentials, options, consoleBase) {
719
776
  const endpoint = resolveCloudEndpoint(options.endpoint);
720
777
  const spinner3 = p.spinner();
721
778
  spinner3.start("Verifying credentials\u2026");
722
- const result = await apiCall(
723
- "/v1/sync/whoami",
724
- { apiKey: credentials.apiKey, projectId: credentials.projectId, endpoint }
725
- );
779
+ const result = await apiCall("/v1/sync/whoami", {
780
+ apiKey: credentials.apiKey,
781
+ projectId: credentials.projectId,
782
+ endpoint
783
+ });
726
784
  if (!result.ok) {
727
785
  spinner3.stop("Credentials could not be verified.");
728
786
  p.log.error(result.error?.message ?? "Authentication failed.");
@@ -877,6 +935,20 @@ function persistCliConfig(apiKey, projectId, endpoint) {
877
935
  const existing = loadConfig();
878
936
  saveConfig({ ...existing, apiKey, projectId, endpoint });
879
937
  }
938
+ function emitWizardFunnelEvent(credentials, endpoint, event, metadata = {}) {
939
+ if (process.env.MUSHI_NO_TELEMETRY) return;
940
+ void fetch(`${endpoint.replace(/\/$/, "")}/v1/cli/funnel`, {
941
+ method: "POST",
942
+ headers: {
943
+ "Content-Type": "application/json",
944
+ "X-Mushi-Api-Key": credentials.apiKey,
945
+ "X-Mushi-Project": credentials.projectId
946
+ },
947
+ body: JSON.stringify({ event, source: "cli", metadata }),
948
+ signal: AbortSignal.timeout(4e3)
949
+ }).catch(() => {
950
+ });
951
+ }
880
952
  function printNextSteps(framework, consoleBase, enableRewards = false) {
881
953
  p.note(framework.snippet(), "Add this to your app:");
882
954
  if (enableRewards) {
@@ -910,7 +982,7 @@ async function maybeOfferConnect(credentials, options, _consoleBase) {
910
982
  if (p.isCancel(answer) || !answer) return;
911
983
  const endpoint = resolveCloudEndpoint(options.endpoint);
912
984
  try {
913
- const { runConnect: runConnect2 } = await import("./connect-5VPUYE6H.js");
985
+ const { runConnect: runConnect2 } = await import("./connect-22IHGT2L.js");
914
986
  await runConnect2({
915
987
  apiKey: credentials.apiKey,
916
988
  projectId: credentials.projectId,
@@ -921,12 +993,8 @@ async function maybeOfferConnect(credentials, options, _consoleBase) {
921
993
  wait: true
922
994
  });
923
995
  } catch (err) {
924
- p.log.warn(
925
- err instanceof Error ? err.message : String(err)
926
- );
927
- p.log.info(
928
- "You can run manually: mushi connect --write-env --wire-ide --wait"
929
- );
996
+ p.log.warn(err instanceof Error ? err.message : String(err));
997
+ p.log.info("You can run manually: mushi connect --write-env --wire-ide --wait");
930
998
  }
931
999
  }
932
1000
  async function maybeEnableRewards(options) {
@@ -1203,189 +1271,7 @@ Examples:
1203
1271
  mushi login # browser-guided (recommended)
1204
1272
  mushi login --api-key mushi_xxx # non-interactive / CI
1205
1273
  mushi login --api-key mushi_xxx --project-id <uuid>
1206
- mushi login --upgrade-scope # re-auth to add mcp:read + mcp:write to your key`).action(async (opts) => {
1207
- const { CLOUD_API_ENDPOINT, resolveCloudEndpoint: resolveCloudEndpoint2 } = await import("./endpoint-DGS73AVM.js");
1208
- const endpoint = opts.endpoint ? resolveCloudEndpoint2(opts.endpoint) : CLOUD_API_ENDPOINT;
1209
- const consoleBase = await resolveConsoleUrl();
1210
- let apiKey = opts.apiKey;
1211
- let projectId = opts.projectId;
1212
- if (apiKey) {
1213
- const config2 = loadConfig();
1214
- config2.apiKey = apiKey;
1215
- config2.endpoint = endpoint;
1216
- if (projectId) config2.projectId = projectId;
1217
- config2.consoleUrl = consoleBase;
1218
- saveConfig(config2);
1219
- console.log("");
1220
- console.log(" \u2713 Credentials saved.");
1221
- const verifyResult = await apiCall(
1222
- "/v1/sync/whoami",
1223
- { apiKey, endpoint, projectId }
1224
- );
1225
- if (!verifyResult.ok) {
1226
- console.warn(` \u26A0 Key saved, but verification failed: ${verifyResult.error?.message ?? "unknown error"}`);
1227
- console.warn(` Check your key at: ${cliSetupDeepLink(consoleBase)}`);
1228
- return;
1229
- }
1230
- const d = verifyResult.data;
1231
- if (d.project_name) {
1232
- if (!projectId && d.project_id) {
1233
- config2.projectId = d.project_id;
1234
- saveConfig(config2);
1235
- }
1236
- console.log(` \u2713 Project: ${d.project_name} (${d.project_id})`);
1237
- }
1238
- console.log(` \u2713 Endpoint: ${endpoint}`);
1239
- console.log("");
1240
- console.log(" Run 'mushi whoami' to verify \xB7 'mushi init' to set up the SDK");
1241
- return;
1242
- }
1243
- console.log("");
1244
- if (opts.upgradeScope) {
1245
- console.log(" Mushi login \u2014 scope upgrade");
1246
- 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");
1247
- console.log(" Re-authenticating to mint a new key with report:write + mcp:read + mcp:write scopes.");
1248
- console.log(" This lets the CLI admin commands (including writes) and MCP server both work with a single key.");
1249
- console.log("");
1250
- } else {
1251
- console.log(" Mushi login");
1252
- console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
1253
- }
1254
- let deviceData;
1255
- try {
1256
- deviceData = await startDeviceAuth(endpoint, ensureClientId());
1257
- } catch (err) {
1258
- process.stderr.write(`
1259
- error: Could not start login session: ${err instanceof Error ? err.message : String(err)}
1260
- `);
1261
- process.stderr.write(` Fallback: mushi login --api-key <key> --project-id <uuid>
1262
- `);
1263
- process.exit(1);
1264
- }
1265
- const verifyUrl = deviceData.verification_uri;
1266
- if (opts.browser !== false) {
1267
- try {
1268
- await openInBrowser(verifyUrl);
1269
- } catch {
1270
- }
1271
- }
1272
- printAuthBanner(deviceData.user_code, verifyUrl);
1273
- let cliToken = null;
1274
- const pollIntervalMs = (deviceData.interval ?? 5) * 1e3;
1275
- const deadline = Date.now() + (deviceData.expires_in ?? 600) * 1e3;
1276
- const MAX_CONSECUTIVE_ERRORS = 5;
1277
- let consecutiveErrors = 0;
1278
- let firstPoll = true;
1279
- while (Date.now() < deadline) {
1280
- if (!firstPoll) {
1281
- await new Promise((r) => setTimeout(r, pollIntervalMs));
1282
- }
1283
- firstPoll = false;
1284
- const outcome = await pollDeviceToken(endpoint, deviceData.device_code);
1285
- if (outcome.status === "approved") {
1286
- cliToken = outcome.cliToken;
1287
- break;
1288
- }
1289
- if (outcome.status === "pending") {
1290
- consecutiveErrors = 0;
1291
- continue;
1292
- }
1293
- if (outcome.status === "error") {
1294
- if (outcome.retryable) {
1295
- consecutiveErrors += 1;
1296
- if (consecutiveErrors < MAX_CONSECUTIVE_ERRORS) continue;
1297
- }
1298
- printAuthFailed("error", outcome.message);
1299
- process.exit(1);
1300
- }
1301
- if (outcome.status === "denied") {
1302
- printAuthFailed("denied");
1303
- process.exit(1);
1304
- }
1305
- if (outcome.status === "expired") {
1306
- printAuthFailed("timeout");
1307
- process.exit(1);
1308
- }
1309
- }
1310
- if (!cliToken) {
1311
- printAuthFailed("timeout");
1312
- process.exit(1);
1313
- }
1314
- printAuthApproved();
1315
- const projectsList = await listProjects(endpoint, cliToken);
1316
- let chosenProjectId = projectId;
1317
- let chosenProjectName;
1318
- if (!chosenProjectId) {
1319
- const { createInterface } = await import("readline");
1320
- const rl = createInterface({ input: process.stdin, output: process.stdout });
1321
- const ask = (q) => new Promise((resolve3) => rl.question(q, (a) => resolve3(a.trim())));
1322
- if (projectsList.length > 0) {
1323
- console.log("");
1324
- console.log(" Your projects:");
1325
- projectsList.forEach((p3, i) => {
1326
- console.log(` ${i + 1}. ${p3.name} (${p3.id})`);
1327
- });
1328
- console.log(` ${projectsList.length + 1}. Create a new project`);
1329
- console.log("");
1330
- const choice = await ask(` Pick a project [1-${projectsList.length + 1}]: `);
1331
- const num = parseInt(choice, 10);
1332
- if (num >= 1 && num <= projectsList.length) {
1333
- const picked = projectsList[num - 1];
1334
- chosenProjectId = picked.id;
1335
- chosenProjectName = picked.name;
1336
- }
1337
- }
1338
- if (!chosenProjectId) {
1339
- console.log("");
1340
- const newName = await ask(" Project name: ");
1341
- rl.close();
1342
- if (!newName.trim()) {
1343
- process.stderr.write("\nerror: Project name is required.\n");
1344
- process.exit(2);
1345
- }
1346
- try {
1347
- const created = await createProject(endpoint, cliToken, newName.trim());
1348
- chosenProjectId = created.id;
1349
- chosenProjectName = created.name;
1350
- apiKey = created.apiKey ?? void 0;
1351
- console.log(` \u2713 Created project "${chosenProjectName}"`);
1352
- } catch (err) {
1353
- process.stderr.write(`
1354
- error: Could not create project: ${err instanceof Error ? err.message : String(err)}
1355
- `);
1356
- process.exit(1);
1357
- }
1358
- } else {
1359
- rl.close();
1360
- }
1361
- }
1362
- if (!apiKey && chosenProjectId) {
1363
- try {
1364
- apiKey = await mintProjectKey(endpoint, cliToken, chosenProjectId) ?? void 0;
1365
- } catch {
1366
- }
1367
- }
1368
- const config = loadConfig();
1369
- config.endpoint = endpoint;
1370
- config.consoleUrl = consoleBase;
1371
- if (chosenProjectId) config.projectId = chosenProjectId;
1372
- if (apiKey) config.apiKey = apiKey;
1373
- saveConfig(config);
1374
- console.log("");
1375
- if (chosenProjectName) console.log(` \u2713 Project: ${chosenProjectName}`);
1376
- if (apiKey) {
1377
- console.log(` \u2713 Full-scope CLI key saved (${apiKey.slice(0, 12)}\u2026)`);
1378
- console.log(` Scopes: report:write \xB7 mcp:read \xB7 mcp:write \u2014 SDK + MCP + admin CLI (incl. writes) all work with this key`);
1379
- } else {
1380
- console.log(` \u2139 Get an SDK key at: ${apiKeyHint(consoleBase)}`);
1381
- }
1382
- console.log("");
1383
- if (opts.upgradeScope) {
1384
- console.log(" Key upgraded! Re-run 'mushi setup' to update your .cursor/mcp.json, then restart Cursor.");
1385
- } else {
1386
- console.log(" Run 'mushi init' to set up the SDK in this project.");
1387
- }
1388
- });
1274
+ mushi login --upgrade-scope # re-auth to add mcp:read + mcp:write to your key`).action(runLogin);
1389
1275
  program2.command("whoami").description("Verify API key and display project info").option("--json", "Machine-readable JSON output").addHelpText("after", `
1390
1276
  Verifies that MUSHI_API_KEY is valid and shows which project it belongs to.
1391
1277
  Useful after 'mushi login' to confirm credentials are correct.`).action(async (opts) => {
@@ -1477,6 +1363,189 @@ Examples:
1477
1363
  }
1478
1364
  });
1479
1365
  }
1366
+ async function runLogin(opts) {
1367
+ const { CLOUD_API_ENDPOINT, resolveCloudEndpoint: resolveCloudEndpoint2 } = await import("./endpoint-DGS73AVM.js");
1368
+ const endpoint = opts.endpoint ? resolveCloudEndpoint2(opts.endpoint) : CLOUD_API_ENDPOINT;
1369
+ const consoleBase = await resolveConsoleUrl();
1370
+ let apiKey = opts.apiKey;
1371
+ let projectId = opts.projectId;
1372
+ if (apiKey) {
1373
+ const config2 = loadConfig();
1374
+ config2.apiKey = apiKey;
1375
+ config2.endpoint = endpoint;
1376
+ if (projectId) config2.projectId = projectId;
1377
+ config2.consoleUrl = consoleBase;
1378
+ saveConfig(config2);
1379
+ console.log("");
1380
+ console.log(" \u2713 Credentials saved.");
1381
+ const verifyResult = await apiCall(
1382
+ "/v1/sync/whoami",
1383
+ { apiKey, endpoint, projectId }
1384
+ );
1385
+ if (!verifyResult.ok) {
1386
+ console.warn(` \u26A0 Key saved, but verification failed: ${verifyResult.error?.message ?? "unknown error"}`);
1387
+ console.warn(` Check your key at: ${cliSetupDeepLink(consoleBase)}`);
1388
+ return;
1389
+ }
1390
+ const d = verifyResult.data;
1391
+ if (d.project_name) {
1392
+ if (!projectId && d.project_id) {
1393
+ config2.projectId = d.project_id;
1394
+ saveConfig(config2);
1395
+ }
1396
+ console.log(` \u2713 Project: ${d.project_name} (${d.project_id})`);
1397
+ }
1398
+ console.log(` \u2713 Endpoint: ${endpoint}`);
1399
+ console.log("");
1400
+ console.log(" Run 'mushi whoami' to verify \xB7 'mushi init' to set up the SDK");
1401
+ return;
1402
+ }
1403
+ console.log("");
1404
+ if (opts.upgradeScope) {
1405
+ console.log(" Mushi login \u2014 scope upgrade");
1406
+ 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");
1407
+ console.log(" Re-authenticating to mint a new key with report:write + mcp:read + mcp:write scopes.");
1408
+ console.log(" This lets the CLI admin commands (including writes) and MCP server both work with a single key.");
1409
+ console.log("");
1410
+ } else {
1411
+ console.log(" Mushi login");
1412
+ console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
1413
+ }
1414
+ let deviceData;
1415
+ try {
1416
+ deviceData = await startDeviceAuth(endpoint, ensureClientId());
1417
+ } catch (err) {
1418
+ process.stderr.write(`
1419
+ error: Could not start login session: ${err instanceof Error ? err.message : String(err)}
1420
+ `);
1421
+ process.stderr.write(` Fallback: mushi login --api-key <key> --project-id <uuid>
1422
+ `);
1423
+ process.exit(1);
1424
+ }
1425
+ const verifyUrl = deviceData.verification_uri;
1426
+ if (opts.browser !== false) {
1427
+ try {
1428
+ await openInBrowser(verifyUrl);
1429
+ } catch {
1430
+ }
1431
+ }
1432
+ printAuthBanner(deviceData.user_code, verifyUrl);
1433
+ let cliToken = null;
1434
+ const pollIntervalMs = (deviceData.interval ?? 5) * 1e3;
1435
+ const deadline = Date.now() + (deviceData.expires_in ?? 600) * 1e3;
1436
+ const MAX_CONSECUTIVE_ERRORS = 5;
1437
+ let consecutiveErrors = 0;
1438
+ let firstPoll = true;
1439
+ while (Date.now() < deadline) {
1440
+ if (!firstPoll) {
1441
+ await new Promise((r) => setTimeout(r, pollIntervalMs));
1442
+ }
1443
+ firstPoll = false;
1444
+ const outcome = await pollDeviceToken(endpoint, deviceData.device_code);
1445
+ if (outcome.status === "approved") {
1446
+ cliToken = outcome.cliToken;
1447
+ break;
1448
+ }
1449
+ if (outcome.status === "pending") {
1450
+ consecutiveErrors = 0;
1451
+ continue;
1452
+ }
1453
+ if (outcome.status === "error") {
1454
+ if (outcome.retryable) {
1455
+ consecutiveErrors += 1;
1456
+ if (consecutiveErrors < MAX_CONSECUTIVE_ERRORS) continue;
1457
+ }
1458
+ printAuthFailed("error", outcome.message);
1459
+ process.exit(1);
1460
+ }
1461
+ if (outcome.status === "denied") {
1462
+ printAuthFailed("denied");
1463
+ process.exit(1);
1464
+ }
1465
+ if (outcome.status === "expired") {
1466
+ printAuthFailed("timeout");
1467
+ process.exit(1);
1468
+ }
1469
+ }
1470
+ if (!cliToken) {
1471
+ printAuthFailed("timeout");
1472
+ process.exit(1);
1473
+ }
1474
+ printAuthApproved();
1475
+ const projectsList = await listProjects(endpoint, cliToken);
1476
+ let chosenProjectId = projectId;
1477
+ let chosenProjectName;
1478
+ if (!chosenProjectId) {
1479
+ const { createInterface } = await import("readline");
1480
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
1481
+ const ask = (q) => new Promise((resolve3) => rl.question(q, (a) => resolve3(a.trim())));
1482
+ if (projectsList.length > 0) {
1483
+ console.log("");
1484
+ console.log(" Your projects:");
1485
+ projectsList.forEach((p3, i) => {
1486
+ console.log(` ${i + 1}. ${p3.name} (${p3.id})`);
1487
+ });
1488
+ console.log(` ${projectsList.length + 1}. Create a new project`);
1489
+ console.log("");
1490
+ const choice = await ask(` Pick a project [1-${projectsList.length + 1}]: `);
1491
+ const num = parseInt(choice, 10);
1492
+ if (num >= 1 && num <= projectsList.length) {
1493
+ const picked = projectsList[num - 1];
1494
+ chosenProjectId = picked.id;
1495
+ chosenProjectName = picked.name;
1496
+ }
1497
+ }
1498
+ if (!chosenProjectId) {
1499
+ console.log("");
1500
+ const newName = await ask(" Project name: ");
1501
+ rl.close();
1502
+ if (!newName.trim()) {
1503
+ process.stderr.write("\nerror: Project name is required.\n");
1504
+ process.exit(2);
1505
+ }
1506
+ try {
1507
+ const created = await createProject(endpoint, cliToken, newName.trim());
1508
+ chosenProjectId = created.id;
1509
+ chosenProjectName = created.name;
1510
+ apiKey = created.apiKey ?? void 0;
1511
+ console.log(` \u2713 Created project "${chosenProjectName}"`);
1512
+ } catch (err) {
1513
+ process.stderr.write(`
1514
+ error: Could not create project: ${err instanceof Error ? err.message : String(err)}
1515
+ `);
1516
+ process.exit(1);
1517
+ }
1518
+ } else {
1519
+ rl.close();
1520
+ }
1521
+ }
1522
+ if (!apiKey && chosenProjectId) {
1523
+ try {
1524
+ apiKey = await mintProjectKey(endpoint, cliToken, chosenProjectId) ?? void 0;
1525
+ } catch {
1526
+ }
1527
+ }
1528
+ const config = loadConfig();
1529
+ config.endpoint = endpoint;
1530
+ config.consoleUrl = consoleBase;
1531
+ if (chosenProjectId) config.projectId = chosenProjectId;
1532
+ if (apiKey) config.apiKey = apiKey;
1533
+ saveConfig(config);
1534
+ console.log("");
1535
+ if (chosenProjectName) console.log(` \u2713 Project: ${chosenProjectName}`);
1536
+ if (apiKey) {
1537
+ console.log(` \u2713 Full-scope CLI key saved (${apiKey.slice(0, 12)}\u2026)`);
1538
+ console.log(` Scopes: report:write \xB7 mcp:read \xB7 mcp:write \u2014 SDK + MCP + admin CLI (incl. writes) all work with this key`);
1539
+ } else {
1540
+ console.log(` \u2139 Get an SDK key at: ${apiKeyHint(consoleBase)}`);
1541
+ }
1542
+ console.log("");
1543
+ if (opts.upgradeScope) {
1544
+ console.log(" Key upgraded! Re-run 'mushi setup' to update your .cursor/mcp.json, then restart Cursor.");
1545
+ } else {
1546
+ console.log(" Run 'mushi init' to set up the SDK in this project.");
1547
+ }
1548
+ }
1480
1549
 
1481
1550
  // src/commands/deploy.ts
1482
1551
  function registerDeployCommands(program2) {
@@ -2361,8 +2430,11 @@ error: Could not create project: ${err instanceof Error ? err.message : String(e
2361
2430
  }
2362
2431
 
2363
2432
  // src/commands/setup.ts
2433
+ function resolveLoginEndpoint(optsEndpoint, existingConfigEndpoint, envEndpoint) {
2434
+ return optsEndpoint ?? existingConfigEndpoint ?? envEndpoint?.trim();
2435
+ }
2364
2436
  function registerSetupCommands(program2) {
2365
- 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", `
2437
+ 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").option("--endpoint <url>", "Override the Mushi API endpoint (self-hosted) \u2014 used for first-run login if not yet configured").addHelpText("after", `
2366
2438
  Examples:
2367
2439
  mushi setup # wire Cursor (default)
2368
2440
  mushi setup --all-projects # one server entry per accessible project
@@ -2380,6 +2452,11 @@ The command reads credentials from ~/.config/mushi/config.json (run \`mushi logi
2380
2452
  const { existsSync: existsSync3 } = await import("fs");
2381
2453
  const nodePath2 = await import("path");
2382
2454
  const os = await import("os");
2455
+ const existingConfig = loadConfig();
2456
+ if (!existingConfig.apiKey) {
2457
+ const endpoint = resolveLoginEndpoint(opts.endpoint, existingConfig.endpoint, process.env.MUSHI_API_ENDPOINT);
2458
+ await runLogin({ endpoint });
2459
+ }
2383
2460
  const config = requireConfig({ needsProject: true });
2384
2461
  const redactKeyForDisplay = (text2) => {
2385
2462
  const key = config.apiKey;
@@ -3193,6 +3270,88 @@ async function checkEndpointReachability(endpoint, doFetch = globalThis.fetch) {
3193
3270
  return { name: "Endpoint reachable", ok: false, detail: `Fetch failed: ${msg}` };
3194
3271
  }
3195
3272
  }
3273
+ async function checkCliAuthPath(config, doFetch = globalThis.fetch) {
3274
+ const checks = [];
3275
+ if (!config.endpoint) {
3276
+ return [
3277
+ {
3278
+ name: "Sign-in route reachable",
3279
+ ok: false,
3280
+ detail: "No endpoint configured \u2014 run `npx mushi-mushi` or pass MUSHI_API_ENDPOINT."
3281
+ }
3282
+ ];
3283
+ }
3284
+ const base = sanitizeEndpoint(config.endpoint);
3285
+ let dateHeader = null;
3286
+ try {
3287
+ const res = await doFetch(`${base}/v1/cli/auth/device/token`, {
3288
+ method: "POST",
3289
+ headers: { "Content-Type": "application/json" },
3290
+ body: JSON.stringify({ device_code: "doctor-probe" }),
3291
+ signal: AbortSignal.timeout(8e3)
3292
+ });
3293
+ dateHeader = res.headers?.get?.("date") ?? null;
3294
+ const signInRouteReachable = res.status === 400 || res.status === 429;
3295
+ checks.push({
3296
+ name: "Sign-in route reachable",
3297
+ ok: signInRouteReachable,
3298
+ detail: signInRouteReachable ? `POST ${base}/v1/cli/auth/device/token \u2192 ${res.status} (route deployed and answering)` : `POST ${base}/v1/cli/auth/device/token \u2192 ${res.status} \u2014 expected HTTP 400 (invalid device_code) or 429 (slow_down); the API may be down or mis-deployed.`
3299
+ });
3300
+ } catch (err) {
3301
+ checks.push({
3302
+ name: "Sign-in route reachable",
3303
+ ok: false,
3304
+ detail: `Fetch failed: ${err instanceof Error ? err.message : String(err)} \u2014 check network/proxy/firewall to ${base}.`
3305
+ });
3306
+ }
3307
+ if (dateHeader) {
3308
+ const serverMs = Date.parse(dateHeader);
3309
+ if (!Number.isNaN(serverMs)) {
3310
+ const skewSec = Math.round(Math.abs(Date.now() - serverMs) / 1e3);
3311
+ const ok = skewSec <= 120;
3312
+ checks.push({
3313
+ name: "System clock in sync",
3314
+ ok,
3315
+ warn: !ok && skewSec <= 300,
3316
+ detail: ok ? `Local clock within ${skewSec}s of the server.` : `Local clock is ${skewSec}s off the server \u2014 sign-in codes expire fast; fix your system time.`
3317
+ });
3318
+ }
3319
+ }
3320
+ if (config.apiKey && config.projectId) {
3321
+ try {
3322
+ const res = await doFetch(`${base}/v1/sync/whoami`, {
3323
+ method: "POST",
3324
+ headers: {
3325
+ "Content-Type": "application/json",
3326
+ ...apiKeyHeaders(config.apiKey, config.projectId)
3327
+ },
3328
+ body: JSON.stringify({}),
3329
+ signal: AbortSignal.timeout(8e3)
3330
+ });
3331
+ const json = await res.json().catch(() => null);
3332
+ const ok = res.ok && json?.ok === true;
3333
+ checks.push({
3334
+ name: "Saved credentials valid",
3335
+ ok,
3336
+ detail: ok ? `API key resolves to project "${json?.data?.project_name ?? config.projectId}".` : `${json?.error?.message ?? `whoami \u2192 HTTP ${res.status}`} \u2014 re-run \`npx mushi-mushi\` to sign in again.`
3337
+ });
3338
+ } catch (err) {
3339
+ checks.push({
3340
+ name: "Saved credentials valid",
3341
+ ok: false,
3342
+ detail: `whoami fetch failed: ${err instanceof Error ? err.message : String(err)}`
3343
+ });
3344
+ }
3345
+ } else {
3346
+ checks.push({
3347
+ name: "Saved credentials valid",
3348
+ ok: true,
3349
+ warn: true,
3350
+ detail: "No saved CLI credentials yet \u2014 normal before the first successful sign-in."
3351
+ });
3352
+ }
3353
+ return checks;
3354
+ }
3196
3355
  async function checkSdkInstall(cwd) {
3197
3356
  try {
3198
3357
  const { readFile: readFile3 } = await import("fs/promises");
@@ -3229,13 +3388,10 @@ async function checkServerPreflight(config, doFetch = globalThis.fetch) {
3229
3388
  }
3230
3389
  try {
3231
3390
  const { endpoint, apiKey, projectId } = sanitizeCliCredentials(config);
3232
- const res = await doFetch(
3233
- `${endpoint}/v1/admin/projects/${projectId}/preflight`,
3234
- {
3235
- headers: apiKeyHeaders(apiKey, projectId),
3236
- signal: AbortSignal.timeout(8e3)
3237
- }
3238
- );
3391
+ const res = await doFetch(`${endpoint}/v1/admin/projects/${projectId}/preflight`, {
3392
+ headers: apiKeyHeaders(apiKey, projectId),
3393
+ signal: AbortSignal.timeout(8e3)
3394
+ });
3239
3395
  if (res.ok) {
3240
3396
  const body = await res.json();
3241
3397
  const serverChecks = body.data?.checks ?? [];
@@ -3291,7 +3447,13 @@ async function checkIngestSetup(config, doFetch = globalThis.fetch) {
3291
3447
  doFetch
3292
3448
  );
3293
3449
  if (!data) {
3294
- return [{ name: "Ingest setup", ok: false, detail: "Request to /v1/sync/ingest-setup failed or returned invalid payload" }];
3450
+ return [
3451
+ {
3452
+ name: "Ingest setup",
3453
+ ok: false,
3454
+ detail: "Request to /v1/sync/ingest-setup failed or returned invalid payload"
3455
+ }
3456
+ ];
3295
3457
  }
3296
3458
  const steps = data.steps ?? [];
3297
3459
  const checks = steps.filter((s) => s.required).map((s) => ({
@@ -3327,22 +3489,27 @@ async function checkQaStoriesHealth(config, doFetch = globalThis.fetch) {
3327
3489
  try {
3328
3490
  const { endpoint, apiKey, projectId } = sanitizeCliCredentials(config);
3329
3491
  const headers = apiKeyHeaders(apiKey, projectId);
3330
- const storiesRes = await doFetch(
3331
- `${endpoint}/v1/admin/projects/${projectId}/qa-coverage`,
3332
- {
3333
- headers,
3334
- signal: AbortSignal.timeout(8e3)
3335
- }
3336
- );
3492
+ const storiesRes = await doFetch(`${endpoint}/v1/admin/projects/${projectId}/qa-coverage`, {
3493
+ headers,
3494
+ signal: AbortSignal.timeout(8e3)
3495
+ });
3337
3496
  if (!storiesRes.ok) {
3338
- checks.push({ name: "[qa] Fetch QA stories", ok: false, detail: `HTTP ${storiesRes.status}` });
3497
+ checks.push({
3498
+ name: "[qa] Fetch QA stories",
3499
+ ok: false,
3500
+ detail: `HTTP ${storiesRes.status}`
3501
+ });
3339
3502
  return checks;
3340
3503
  }
3341
3504
  const storiesBody = await storiesRes.json();
3342
3505
  const stories = storiesBody.data?.coverage ?? [];
3343
3506
  const enabled = stories.filter((s) => s.enabled);
3344
3507
  if (enabled.length === 0) {
3345
- checks.push({ name: "[qa] Enabled QA stories", ok: true, detail: "No enabled stories \u2014 create one at /qa-coverage" });
3508
+ checks.push({
3509
+ name: "[qa] Enabled QA stories",
3510
+ ok: true,
3511
+ detail: "No enabled stories \u2014 create one at /qa-coverage"
3512
+ });
3346
3513
  return checks;
3347
3514
  }
3348
3515
  checks.push({
@@ -3411,7 +3578,9 @@ async function checkHostAppWiring(cwd) {
3411
3578
  }
3412
3579
  }
3413
3580
  const hasProjectId = /VITE_MUSHI_PROJECT_ID=|NEXT_PUBLIC_MUSHI_PROJECT_ID=|MUSHI_PROJECT_ID=/.test(envContent);
3414
- const hasApiKey = /VITE_MUSHI_API_KEY=|NEXT_PUBLIC_MUSHI_API_KEY=|MUSHI_API_KEY=/.test(envContent);
3581
+ const hasApiKey = /VITE_MUSHI_API_KEY=|NEXT_PUBLIC_MUSHI_API_KEY=|MUSHI_API_KEY=/.test(
3582
+ envContent
3583
+ );
3415
3584
  checks.push({
3416
3585
  name: "[host] Mushi env vars in .env.local",
3417
3586
  ok: hasProjectId && hasApiKey,
@@ -3456,10 +3625,7 @@ async function checkMcpConfig(config, cwd, doFetch = globalThis.fetch) {
3456
3625
  const { join: join4, resolve: resolve3 } = await import("path");
3457
3626
  const { homedir } = await import("os");
3458
3627
  const root = resolve3(cwd);
3459
- const candidates = [
3460
- join4(root, ".cursor", "mcp.json"),
3461
- join4(homedir(), ".cursor", "mcp.json")
3462
- ];
3628
+ const candidates = [join4(root, ".cursor", "mcp.json"), join4(homedir(), ".cursor", "mcp.json")];
3463
3629
  let mcpPath = null;
3464
3630
  let mcpRaw = null;
3465
3631
  for (const candidate of candidates) {
@@ -3497,11 +3663,17 @@ async function checkMcpConfig(config, cwd, doFetch = globalThis.fetch) {
3497
3663
  try {
3498
3664
  mcpConfig = JSON.parse(mcpRaw);
3499
3665
  } catch {
3500
- checks.push({ name: "[mcp] mcp.json valid JSON", ok: false, detail: "mcp.json is not valid JSON \u2014 regenerate with `mushi setup`." });
3666
+ checks.push({
3667
+ name: "[mcp] mcp.json valid JSON",
3668
+ ok: false,
3669
+ detail: "mcp.json is not valid JSON \u2014 regenerate with `mushi setup`."
3670
+ });
3501
3671
  return checks;
3502
3672
  }
3503
3673
  const servers = mcpConfig.mcpServers ?? {};
3504
- const mushiEntries = Object.entries(servers).filter(([k]) => k === "mushi" || k.startsWith("mushi-"));
3674
+ const mushiEntries = Object.entries(servers).filter(
3675
+ ([k]) => k === "mushi" || k.startsWith("mushi-")
3676
+ );
3505
3677
  if (mushiEntries.length === 0) {
3506
3678
  checks.push({
3507
3679
  name: "[mcp] mushi server entry",
@@ -3621,7 +3793,9 @@ async function checkOnboardingStatus(config, _consoleBase, cwd, doFetch = global
3621
3793
  } catch {
3622
3794
  }
3623
3795
  }
3624
- const hasEnvVars = /(?:VITE_|NEXT_PUBLIC_|EXPO_PUBLIC_|NUXT_PUBLIC_)?MUSHI_PROJECT_ID=/.test(envContent);
3796
+ const hasEnvVars = /(?:VITE_|NEXT_PUBLIC_|EXPO_PUBLIC_|NUXT_PUBLIC_)?MUSHI_PROJECT_ID=/.test(
3797
+ envContent
3798
+ );
3625
3799
  if (!hasEnvVars) {
3626
3800
  return {
3627
3801
  nextAction: "Write env vars to your app: run `npx mushi-mushi` or `mushi connect --write-env`",
@@ -3710,6 +3884,9 @@ async function runDoctor(config, options = {}) {
3710
3884
  const mcpChecks = await checkMcpConfig(config, options.cwd ?? process.cwd(), doFetch);
3711
3885
  checks.push(...mcpChecks);
3712
3886
  }
3887
+ if (isFull || options.auth) {
3888
+ checks.push(...await checkCliAuthPath(config, doFetch));
3889
+ }
3713
3890
  const heartbeatPassed = checks.some((c) => c.name === "[ingest] Last SDK heartbeat" && c.ok);
3714
3891
  if (heartbeatPassed) {
3715
3892
  const idx = checks.findIndex((c) => c.name === "SDK installed in this repo" && !c.ok);
@@ -3742,8 +3919,10 @@ function formatDoctorResult(result) {
3742
3919
  const warned = result.checks.filter((c) => c.ok && c.warn);
3743
3920
  if (failed.length === 0) {
3744
3921
  if (warned.length > 0) {
3745
- lines.push(`
3746
- All checks passed with ${warned.length} advisory warning${warned.length === 1 ? "" : "s"}. The CLI is ready.`);
3922
+ lines.push(
3923
+ `
3924
+ All checks passed with ${warned.length} advisory warning${warned.length === 1 ? "" : "s"}. The CLI is ready.`
3925
+ );
3747
3926
  } else {
3748
3927
  lines.push("\nAll checks passed. The CLI is ready.");
3749
3928
  }
@@ -3768,6 +3947,9 @@ function registerDoctorCommand(program2) {
3768
3947
  ).option(
3769
3948
  "--mcp",
3770
3949
  "Check Cursor MCP config: verify mushi-* server entry, credentials, and probe account-overview connectivity."
3950
+ ).option(
3951
+ "--auth",
3952
+ "Diagnose the browser sign-in handshake: device-auth route reachability, system clock skew, and saved-credential validity. Run this when `npx mushi-mushi` browser sign-in does not complete."
3771
3953
  ).option(
3772
3954
  "--fix",
3773
3955
  "Apply safe local fixes when checks fail: write missing .env.local lines and wire Cursor MCP config."
@@ -3777,54 +3959,60 @@ function registerDoctorCommand(program2) {
3777
3959
  ).option(
3778
3960
  "--full",
3779
3961
  "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."
3780
- ).action(async (opts) => {
3781
- const config = loadConfig();
3782
- if (opts.onboarding) {
3783
- const cwd = opts.cwd ?? process.cwd();
3784
- const consoleBase = await resolveConsoleUrl({ cwd });
3785
- const status = await checkOnboardingStatus(config, consoleBase, cwd);
3786
- const base = consoleBase.replace(/\/$/, "");
3787
- if (status.done) {
3788
- console.log(`OK Setup complete. ${status.nextAction}`);
3789
- console.log(` ${base}${status.ctaPath}`);
3790
- } else {
3791
- console.log(`\u2192 Next: ${status.nextAction}`);
3792
- console.log(` Open: ${base}${status.ctaPath}`);
3962
+ ).action(
3963
+ async (opts) => {
3964
+ const config = loadConfig();
3965
+ if (opts.onboarding) {
3966
+ const cwd = opts.cwd ?? process.cwd();
3967
+ const consoleBase = await resolveConsoleUrl({ cwd });
3968
+ const status = await checkOnboardingStatus(config, consoleBase, cwd);
3969
+ const base = consoleBase.replace(/\/$/, "");
3970
+ if (status.done) {
3971
+ console.log(`OK Setup complete. ${status.nextAction}`);
3972
+ console.log(` ${base}${status.ctaPath}`);
3973
+ } else {
3974
+ console.log(`\u2192 Next: ${status.nextAction}`);
3975
+ console.log(` Open: ${base}${status.ctaPath}`);
3976
+ }
3977
+ if (!status.done) process.exit(1);
3978
+ return;
3793
3979
  }
3794
- if (!status.done) process.exit(1);
3795
- return;
3796
- }
3797
- const doctorOpts = {
3798
- cwd: opts.cwd,
3799
- server: opts.server,
3800
- ingest: opts.ingest,
3801
- qaStories: opts.qaStories,
3802
- hostApp: opts.hostApp,
3803
- mcp: opts.mcp,
3804
- full: opts.full
3805
- };
3806
- let result = await runDoctor(config, doctorOpts);
3807
- if (!result.ready && opts.fix && config.apiKey && config.projectId && config.endpoint) {
3808
- const connectResult = await runConnect({
3809
- apiKey: config.apiKey,
3810
- projectId: config.projectId,
3811
- endpoint: config.endpoint,
3812
- cwd: opts.cwd ?? process.cwd(),
3813
- writeEnv: true,
3814
- wireIde: true
3815
- }, config);
3816
- for (const msg of connectResult.messages) console.log(msg);
3817
- result = await runDoctor(config, { ...doctorOpts, mcp: opts.mcp });
3818
- }
3819
- const { checks } = result;
3820
- if (opts.json) {
3821
- console.log(JSON.stringify({ checks, ready: result.ready }, null, 2));
3980
+ const doctorOpts = {
3981
+ cwd: opts.cwd,
3982
+ server: opts.server,
3983
+ ingest: opts.ingest,
3984
+ qaStories: opts.qaStories,
3985
+ hostApp: opts.hostApp,
3986
+ mcp: opts.mcp,
3987
+ auth: opts.auth,
3988
+ full: opts.full
3989
+ };
3990
+ let result = await runDoctor(config, doctorOpts);
3991
+ if (!result.ready && opts.fix && config.apiKey && config.projectId && config.endpoint) {
3992
+ const connectResult = await runConnect(
3993
+ {
3994
+ apiKey: config.apiKey,
3995
+ projectId: config.projectId,
3996
+ endpoint: config.endpoint,
3997
+ cwd: opts.cwd ?? process.cwd(),
3998
+ writeEnv: true,
3999
+ wireIde: true
4000
+ },
4001
+ config
4002
+ );
4003
+ for (const msg of connectResult.messages) console.log(msg);
4004
+ result = await runDoctor(config, { ...doctorOpts, mcp: opts.mcp });
4005
+ }
4006
+ const { checks } = result;
4007
+ if (opts.json) {
4008
+ console.log(JSON.stringify({ checks, ready: result.ready }, null, 2));
4009
+ if (!result.ready) process.exit(1);
4010
+ return;
4011
+ }
4012
+ console.log(formatDoctorResult(result));
3822
4013
  if (!result.ready) process.exit(1);
3823
- return;
3824
4014
  }
3825
- console.log(formatDoctorResult(result));
3826
- if (!result.ready) process.exit(1);
3827
- });
4015
+ );
3828
4016
  }
3829
4017
 
3830
4018
  // src/commands/reset-cli.ts