@mushi-mushi/cli 0.25.0 → 0.26.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
@@ -1,7 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  FRAMEWORKS,
4
- MUSHI_CLI_VERSION,
5
4
  apiCall,
6
5
  apiKeyHeaders,
7
6
  apiKeyHint,
@@ -37,7 +36,10 @@ import {
37
36
  sanitizeProjectId,
38
37
  saveConfig,
39
38
  writeMcpServerEntry
40
- } from "./chunk-3JRQN6S4.js";
39
+ } from "./chunk-VIHWH3IG.js";
40
+ import {
41
+ MUSHI_CLI_VERSION
42
+ } from "./chunk-TYVJU4AH.js";
41
43
  import {
42
44
  TEST_REPORT_FETCH_TIMEOUT_MS,
43
45
  assertEndpoint,
@@ -195,15 +197,15 @@ var DeviceAuthRequestError = class extends Error {
195
197
  this.retryable = status === void 0 || status >= 500 || status === 408;
196
198
  }
197
199
  };
198
- async function withOneRetry(run) {
200
+ async function withOneRetry(run2) {
199
201
  try {
200
- return await run();
202
+ return await run2();
201
203
  } catch (err) {
202
204
  if (err instanceof DeviceAuthRequestError && err.retryable) {
203
205
  if (deviceAuthRetryDelayMs > 0) {
204
206
  await defaultSleep(deviceAuthRetryDelayMs);
205
207
  }
206
- return run();
208
+ return run2();
207
209
  }
208
210
  throw err;
209
211
  }
@@ -519,6 +521,7 @@ async function runInit(options = {}) {
519
521
  persistCliConfig(credentials.apiKey, credentials.projectId, endpoint);
520
522
  emitWizardFunnelEvent(credentials, endpoint, "wizard_env_written", { framework: framework.id });
521
523
  const enableRewards = await maybeEnableRewards(options);
524
+ await maybeInjectSnippet(cwd, framework, options);
522
525
  printNextSteps(framework, consoleBase, enableRewards);
523
526
  await maybeSendTestReport(credentials, { ...options, endpoint, consoleBase });
524
527
  await maybeOfferConnect(credentials, options, consoleBase);
@@ -862,7 +865,12 @@ async function writeEnvFile(cwd, apiKey, projectId, framework, endpoint, overwri
862
865
  const target = ENV_FILES.find((f) => existsSync2(join2(cwd, f))) ?? ENV_FILES[0];
863
866
  const targetPath = join2(cwd, target);
864
867
  const newVars = envVarsToWrite(apiKey, projectId, framework, endpoint);
865
- const existing = existsSync2(targetPath) ? readFileSync2(targetPath, "utf-8") : "";
868
+ let existing = "";
869
+ try {
870
+ existing = readFileSync2(targetPath, "utf-8");
871
+ } catch {
872
+ existing = "";
873
+ }
866
874
  if (existing.includes("MUSHI_PROJECT_ID")) {
867
875
  let shouldOverwrite = overwrite;
868
876
  if (!shouldOverwrite) {
@@ -949,6 +957,31 @@ function emitWizardFunnelEvent(credentials, endpoint, event, metadata = {}) {
949
957
  }).catch(() => {
950
958
  });
951
959
  }
960
+ async function maybeInjectSnippet(cwd, framework, options) {
961
+ if (options.yes || !process.stdin.isTTY) return;
962
+ const { ENTRY_CANDIDATES, injectSnippet, MUSHI_MARKER_START } = await import("./snippet-inject-LPRWTQDE.js");
963
+ const candidates = ENTRY_CANDIDATES[framework.id];
964
+ if (!candidates) return;
965
+ const { readFile: readFile3, writeFile: writeFile2 } = await import("fs/promises");
966
+ const nodePath2 = await import("path");
967
+ for (const rel of candidates) {
968
+ const abs = nodePath2.join(cwd, rel);
969
+ let source;
970
+ try {
971
+ source = await readFile3(abs, "utf8");
972
+ } catch {
973
+ continue;
974
+ }
975
+ const alreadyInjected = source.includes(MUSHI_MARKER_START);
976
+ const confirmed = await p.confirm({
977
+ message: alreadyInjected ? `Refresh the Mushi init block in ${rel}?` : `Add the Mushi init snippet to ${rel}? (wrapped in markers; re-runs update in place)`
978
+ });
979
+ if (p.isCancel(confirmed) || !confirmed) return;
980
+ await writeFile2(abs, injectSnippet(source, framework.snippet()), "utf8");
981
+ p.log.success(`${alreadyInjected ? "Updated" : "Added"} Mushi init block in ${rel}`);
982
+ return;
983
+ }
984
+ }
952
985
  function printNextSteps(framework, consoleBase, enableRewards = false) {
953
986
  p.note(framework.snippet(), "Add this to your app:");
954
987
  if (enableRewards) {
@@ -982,7 +1015,7 @@ async function maybeOfferConnect(credentials, options, _consoleBase) {
982
1015
  if (p.isCancel(answer) || !answer) return;
983
1016
  const endpoint = resolveCloudEndpoint(options.endpoint);
984
1017
  try {
985
- const { runConnect: runConnect2 } = await import("./connect-22IHGT2L.js");
1018
+ const { runConnect: runConnect2 } = await import("./connect-PKWAK4GL.js");
986
1019
  await runConnect2({
987
1020
  apiKey: credentials.apiKey,
988
1021
  projectId: credentials.projectId,
@@ -1109,6 +1142,211 @@ function isSameDirectory(a, b) {
1109
1142
  return a.replace(/\\/g, "/").replace(/\/+$/, "") === b.replace(/\\/g, "/").replace(/\/+$/, "");
1110
1143
  }
1111
1144
 
1145
+ // src/login.ts
1146
+ function getPostLoginBannerMessage(opts) {
1147
+ if (opts.suppressPostLoginBanner) return null;
1148
+ if (opts.upgradeScope) {
1149
+ return " Key upgraded! Re-run 'mushi setup' to update your .cursor/mcp.json, then restart Cursor.";
1150
+ }
1151
+ return " Run 'mushi init' to set up the SDK in this project.";
1152
+ }
1153
+ function printPostLoginBanner(opts) {
1154
+ const message = getPostLoginBannerMessage(opts);
1155
+ if (message) console.log(message);
1156
+ }
1157
+ var MAX_CONSECUTIVE_ERRORS = 5;
1158
+ async function pollUntilApproved(endpoint, deviceData, deps) {
1159
+ const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
1160
+ const now = deps.now ?? Date.now;
1161
+ let pollIntervalMs = (deviceData.interval ?? 5) * 1e3;
1162
+ const deadline = now() + (deviceData.expires_in ?? 600) * 1e3;
1163
+ let consecutiveErrors = 0;
1164
+ let firstPoll = true;
1165
+ while (now() < deadline) {
1166
+ if (!firstPoll) {
1167
+ await sleep(pollIntervalMs);
1168
+ }
1169
+ firstPoll = false;
1170
+ const outcome = await deps.poll(endpoint, deviceData.device_code);
1171
+ switch (outcome.status) {
1172
+ case "approved":
1173
+ return { status: "approved", cliToken: outcome.cliToken };
1174
+ case "pending":
1175
+ consecutiveErrors = 0;
1176
+ continue;
1177
+ case "slow_down":
1178
+ consecutiveErrors = 0;
1179
+ pollIntervalMs = Math.max(pollIntervalMs, outcome.retryAfterMs);
1180
+ continue;
1181
+ case "error":
1182
+ if (outcome.retryable) {
1183
+ consecutiveErrors += 1;
1184
+ if (consecutiveErrors < MAX_CONSECUTIVE_ERRORS) continue;
1185
+ }
1186
+ return { status: "error", message: outcome.message };
1187
+ case "denied":
1188
+ return { status: "denied" };
1189
+ case "expired":
1190
+ return { status: "timeout" };
1191
+ }
1192
+ }
1193
+ return { status: "timeout" };
1194
+ }
1195
+ async function resolveProjectChoice(projectsList, ask) {
1196
+ if (projectsList.length > 0) {
1197
+ console.log("");
1198
+ console.log(" Your projects:");
1199
+ projectsList.forEach((p3, i) => {
1200
+ console.log(` ${i + 1}. ${p3.name} (${p3.id})`);
1201
+ });
1202
+ console.log(` ${projectsList.length + 1}. Create a new project`);
1203
+ console.log("");
1204
+ const choice = await ask(` Pick a project [1-${projectsList.length + 1}]: `);
1205
+ const num = parseInt(choice, 10);
1206
+ if (num >= 1 && num <= projectsList.length) {
1207
+ const picked = projectsList[num - 1];
1208
+ return { kind: "picked", id: picked.id, name: picked.name };
1209
+ }
1210
+ }
1211
+ console.log("");
1212
+ const newName = (await ask(" Project name: ")).trim();
1213
+ if (!newName) return { kind: "empty_name" };
1214
+ return { kind: "create", name: newName };
1215
+ }
1216
+ async function runLogin(opts = {}) {
1217
+ const { CLOUD_API_ENDPOINT, resolveCloudEndpoint: resolveCloudEndpoint2 } = await import("./endpoint-DGS73AVM.js");
1218
+ const endpoint = opts.endpoint ? resolveCloudEndpoint2(opts.endpoint) : CLOUD_API_ENDPOINT;
1219
+ const consoleBase = await resolveConsoleUrl();
1220
+ let apiKey = opts.apiKey;
1221
+ let projectId = opts.projectId;
1222
+ if (apiKey) {
1223
+ const config2 = loadConfig();
1224
+ config2.apiKey = apiKey;
1225
+ config2.endpoint = endpoint;
1226
+ if (projectId) config2.projectId = projectId;
1227
+ config2.consoleUrl = consoleBase;
1228
+ saveConfig(config2);
1229
+ console.log("");
1230
+ console.log(" \u2713 Credentials saved.");
1231
+ const verifyResult = await apiCall("/v1/sync/whoami", { apiKey, endpoint, projectId });
1232
+ if (!verifyResult.ok) {
1233
+ console.warn(` \u26A0 Key saved, but verification failed: ${verifyResult.error?.message ?? "unknown error"}`);
1234
+ console.warn(` Check your key at: ${cliSetupDeepLink(consoleBase)}`);
1235
+ return;
1236
+ }
1237
+ const d = verifyResult.data;
1238
+ if (d.project_name) {
1239
+ if (!projectId && d.project_id) {
1240
+ config2.projectId = d.project_id;
1241
+ saveConfig(config2);
1242
+ }
1243
+ console.log(` \u2713 Project: ${d.project_name} (${d.project_id})`);
1244
+ }
1245
+ console.log(` \u2713 Endpoint: ${endpoint}`);
1246
+ console.log("");
1247
+ if (!opts.suppressPostLoginBanner) {
1248
+ console.log(" Run 'mushi whoami' to verify \xB7 'mushi init' to set up the SDK");
1249
+ }
1250
+ return;
1251
+ }
1252
+ if (!opts.suppressPostLoginBanner) {
1253
+ console.log("");
1254
+ }
1255
+ if (opts.upgradeScope) {
1256
+ console.log(" Mushi login \u2014 scope upgrade");
1257
+ 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");
1258
+ console.log(" Re-authenticating to mint a new key with report:write + mcp:read + mcp:write scopes.");
1259
+ console.log(" This lets the CLI admin commands (including writes) and MCP server both work with a single key.");
1260
+ console.log("");
1261
+ } else if (!opts.suppressPostLoginBanner) {
1262
+ console.log(" Mushi login");
1263
+ console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
1264
+ }
1265
+ let deviceData;
1266
+ try {
1267
+ deviceData = await startDeviceAuth(endpoint, ensureClientId());
1268
+ } catch (err) {
1269
+ process.stderr.write(`
1270
+ error: Could not start login session: ${err instanceof Error ? err.message : String(err)}
1271
+ `);
1272
+ process.stderr.write(` Fallback: mushi login --api-key <key> --project-id <uuid>
1273
+ `);
1274
+ process.exit(1);
1275
+ }
1276
+ const verifyUrl = deviceData.verification_uri;
1277
+ if (opts.browser !== false) {
1278
+ try {
1279
+ await openInBrowser(verifyUrl);
1280
+ } catch {
1281
+ }
1282
+ }
1283
+ printAuthBanner(deviceData.user_code, verifyUrl);
1284
+ const pollResult = await pollUntilApproved(endpoint, deviceData, {
1285
+ poll: pollDeviceToken
1286
+ });
1287
+ if (pollResult.status !== "approved") {
1288
+ if (pollResult.status === "denied") printAuthFailed("denied");
1289
+ else if (pollResult.status === "error") printAuthFailed("error", pollResult.message);
1290
+ else printAuthFailed("timeout");
1291
+ process.exit(1);
1292
+ }
1293
+ const cliToken = pollResult.cliToken;
1294
+ printAuthApproved();
1295
+ const projectsList = await listProjects(endpoint, cliToken);
1296
+ let chosenProjectId = projectId;
1297
+ let chosenProjectName;
1298
+ if (!chosenProjectId) {
1299
+ const { createInterface } = await import("readline");
1300
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
1301
+ const ask = (q) => new Promise((resolve3) => rl.question(q, (a) => resolve3(a.trim())));
1302
+ const choice = await resolveProjectChoice(projectsList, ask);
1303
+ rl.close();
1304
+ if (choice.kind === "empty_name") {
1305
+ process.stderr.write("\nerror: Project name is required.\n");
1306
+ process.exit(2);
1307
+ }
1308
+ if (choice.kind === "picked") {
1309
+ chosenProjectId = choice.id;
1310
+ chosenProjectName = choice.name;
1311
+ } else {
1312
+ try {
1313
+ const created = await createProject(endpoint, cliToken, choice.name);
1314
+ chosenProjectId = created.id;
1315
+ chosenProjectName = created.name;
1316
+ apiKey = created.apiKey ?? void 0;
1317
+ console.log(` \u2713 Created project "${chosenProjectName}"`);
1318
+ } catch (err) {
1319
+ process.stderr.write(`
1320
+ error: Could not create project: ${err instanceof Error ? err.message : String(err)}
1321
+ `);
1322
+ process.exit(1);
1323
+ }
1324
+ }
1325
+ }
1326
+ if (!apiKey && chosenProjectId) {
1327
+ try {
1328
+ apiKey = await mintProjectKey(endpoint, cliToken, chosenProjectId) ?? void 0;
1329
+ } catch {
1330
+ }
1331
+ }
1332
+ const config = loadConfig();
1333
+ config.endpoint = endpoint;
1334
+ config.consoleUrl = consoleBase;
1335
+ if (chosenProjectId) config.projectId = chosenProjectId;
1336
+ if (apiKey) config.apiKey = apiKey;
1337
+ saveConfig(config);
1338
+ console.log("");
1339
+ if (chosenProjectName) console.log(` \u2713 Project: ${chosenProjectName}`);
1340
+ if (apiKey) {
1341
+ console.log(` \u2713 Full-scope CLI key saved (${apiKey.slice(0, 12)}\u2026)`);
1342
+ console.log(` Scopes: report:write \xB7 mcp:read \xB7 mcp:write \u2014 SDK + MCP + admin CLI (incl. writes) all work with this key`);
1343
+ } else {
1344
+ console.log(` \u2139 Get an SDK key at: ${apiKeyHint(consoleBase)}`);
1345
+ }
1346
+ console.log("");
1347
+ printPostLoginBanner(opts);
1348
+ }
1349
+
1112
1350
  // src/migrate.ts
1113
1351
  var DOCS_BASE = "https://kensaur.us/mushi-mushi/docs";
1114
1352
  var COMPETITOR_PACKAGES = {
@@ -1271,7 +1509,9 @@ Examples:
1271
1509
  mushi login # browser-guided (recommended)
1272
1510
  mushi login --api-key mushi_xxx # non-interactive / CI
1273
1511
  mushi login --api-key mushi_xxx --project-id <uuid>
1274
- mushi login --upgrade-scope # re-auth to add mcp:read + mcp:write to your key`).action(runLogin);
1512
+ mushi login --upgrade-scope # re-auth to add mcp:read + mcp:write to your key`).action(async (opts) => {
1513
+ await runLogin(opts);
1514
+ });
1275
1515
  program2.command("whoami").description("Verify API key and display project info").option("--json", "Machine-readable JSON output").addHelpText("after", `
1276
1516
  Verifies that MUSHI_API_KEY is valid and shows which project it belongs to.
1277
1517
  Useful after 'mushi login' to confirm credentials are correct.`).action(async (opts) => {
@@ -1363,189 +1603,6 @@ Examples:
1363
1603
  }
1364
1604
  });
1365
1605
  }
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
- }
1549
1606
 
1550
1607
  // src/commands/deploy.ts
1551
1608
  function registerDeployCommands(program2) {
@@ -2447,7 +2504,7 @@ Supported IDEs:
2447
2504
  continue \u2014 writes .continue/mcp.json
2448
2505
  zed \u2014 writes ~/.config/zed/settings.json mcpServers block
2449
2506
 
2450
- The command reads credentials from ~/.config/mushi/config.json (run \`mushi login\` first).`).action(async (opts) => {
2507
+ The command reads credentials from ~/.config/mushi/config.json. If you are not logged in yet, it runs browser sign-in first, then writes MCP config.`).action(async (opts) => {
2451
2508
  const { writeFile: writeFile2, mkdir, readFile: readFile3 } = await import("fs/promises");
2452
2509
  const { existsSync: existsSync3 } = await import("fs");
2453
2510
  const nodePath2 = await import("path");
@@ -2455,7 +2512,9 @@ The command reads credentials from ~/.config/mushi/config.json (run \`mushi logi
2455
2512
  const existingConfig = loadConfig();
2456
2513
  if (!existingConfig.apiKey) {
2457
2514
  const endpoint = resolveLoginEndpoint(opts.endpoint, existingConfig.endpoint, process.env.MUSHI_API_ENDPOINT);
2458
- await runLogin({ endpoint });
2515
+ await runLogin({ endpoint, suppressPostLoginBanner: true });
2516
+ console.log(" Continuing MCP setup\u2026");
2517
+ console.log("");
2459
2518
  }
2460
2519
  const config = requireConfig({ needsProject: true });
2461
2520
  const redactKeyForDisplay = (text2) => {
@@ -2681,9 +2740,54 @@ The command reads credentials from ~/.config/mushi/config.json (run \`mushi logi
2681
2740
  }
2682
2741
  if (errCode === "INSUFFICIENT_SCOPE") {
2683
2742
  console.log("\n\u26A0 Your key has report:write scope only \u2014 MCP admin tools will not work.");
2684
- console.log(" New keys minted by the wizard include both scopes automatically.");
2685
- console.log(" To upgrade an existing key, run:\n");
2686
- console.log(" mushi login --upgrade-scope\n");
2743
+ const canPrompt = process.stdin.isTTY && process.stdout.isTTY;
2744
+ let upgraded = false;
2745
+ if (canPrompt) {
2746
+ const { createInterface } = await import("readline");
2747
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
2748
+ const answer = await new Promise(
2749
+ (resolve3) => rl.question(" Upgrade this key now via browser sign-in? [Y/n] ", (a) => resolve3(a.trim().toLowerCase()))
2750
+ );
2751
+ rl.close();
2752
+ if (answer === "" || answer === "y" || answer === "yes") {
2753
+ await runLogin({ endpoint: opts.endpoint, upgradeScope: true, suppressPostLoginBanner: true });
2754
+ const fresh = loadConfig();
2755
+ if (fresh.apiKey && ideEntry.format === "mcp-json" && !allProjectsList) {
2756
+ await writeMcpServerEntry({
2757
+ configPath,
2758
+ serverName,
2759
+ serverBlock: buildMcpServerBlock({
2760
+ endpoint: fresh.endpoint ?? config.endpoint,
2761
+ projectId: fresh.projectId ?? config.projectId ?? "",
2762
+ apiKey: fresh.apiKey
2763
+ })
2764
+ });
2765
+ console.log(`\u2713 Rewrote ${configPath} with the upgraded key`);
2766
+ const reprobe = await fetch(
2767
+ `${(fresh.endpoint ?? config.endpoint)?.replace(/\/$/, "")}/v1/admin/mcp/account-overview`,
2768
+ {
2769
+ headers: {
2770
+ "X-Mushi-Api-Key": fresh.apiKey,
2771
+ "X-Mushi-Project": fresh.projectId ?? config.projectId ?? ""
2772
+ },
2773
+ signal: AbortSignal.timeout(6e3)
2774
+ }
2775
+ ).catch(() => null);
2776
+ if (reprobe?.ok) {
2777
+ upgraded = true;
2778
+ console.log("\u2713 MCP key valid \u2014 restart Cursor (or your IDE) to activate");
2779
+ } else {
2780
+ console.log(`\u26A0 Re-probe after upgrade returned HTTP ${reprobe?.status ?? "error"} \u2014 run \`mushi doctor --mcp\`.`);
2781
+ }
2782
+ } else if (fresh.apiKey) {
2783
+ console.log("\u2713 Key upgraded \u2014 re-run `mushi setup` to rewrite the IDE config with it.");
2784
+ }
2785
+ }
2786
+ }
2787
+ if (!upgraded && !canPrompt) {
2788
+ console.log(" To upgrade this key, run:\n");
2789
+ console.log(" mushi login --upgrade-scope\n");
2790
+ }
2687
2791
  } else {
2688
2792
  console.log(`\u26A0 Key probe returned HTTP ${probeRes.status} \u2014 check your credentials.`);
2689
2793
  }
@@ -2701,8 +2805,36 @@ The command reads credentials from ~/.config/mushi/config.json (run \`mushi logi
2701
2805
  console.log(`Tip: run with --with-rules to also write the lesson-library coding hook.`);
2702
2806
  }
2703
2807
  const configRelPath = ideEntry.dir.startsWith("/") ? configPath : nodePath2.relative(cwd, configPath);
2704
- console.log(`
2808
+ if (!ideEntry.dir.startsWith("/")) {
2809
+ const gitignorePath = nodePath2.join(cwd, ".gitignore");
2810
+ const ignoreLine = configRelPath.replaceAll("\\", "/");
2811
+ try {
2812
+ const { readFile: readFile4, writeFile: writeFile3 } = await import("fs/promises");
2813
+ const existing = await readFile4(gitignorePath, "utf8").catch(() => "");
2814
+ const lines = existing.split(/\r?\n/);
2815
+ if (!lines.some((l) => l.trim() === ignoreLine)) {
2816
+ const sep = existing.endsWith("\n") || existing === "" ? "" : "\n";
2817
+ await writeFile3(
2818
+ gitignorePath,
2819
+ `${existing}${sep}# Mushi MCP config holds a plaintext API key
2820
+ ${ignoreLine}
2821
+ `,
2822
+ "utf8"
2823
+ );
2824
+ console.log(`
2825
+ \u2713 Added ${ignoreLine} to .gitignore (it contains your Mushi API key)`);
2826
+ } else {
2827
+ console.log(`
2828
+ Note: ${ignoreLine} is gitignored (it contains your Mushi API key).`);
2829
+ }
2830
+ } catch {
2831
+ console.log(`
2705
2832
  Note: ${configRelPath} contains your Mushi API key \u2014 add it to .gitignore if this is a shared repo.`);
2833
+ }
2834
+ } else {
2835
+ console.log(`
2836
+ Note: ${configRelPath} contains your Mushi API key.`);
2837
+ }
2706
2838
  }
2707
2839
  });
2708
2840
  }
@@ -3852,6 +3984,100 @@ async function checkOnboardingStatus(config, _consoleBase, cwd, doFetch = global
3852
3984
  done: false
3853
3985
  };
3854
3986
  }
3987
+ async function checkVersionDrift(cwd, doFetch) {
3988
+ const checks = [];
3989
+ const { MUSHI_CLI_VERSION: MUSHI_CLI_VERSION2 } = await import("./version-4JARRCFG.js");
3990
+ if (MUSHI_CLI_VERSION2 === "0.0.0-dev") return checks;
3991
+ const parse2 = (v) => {
3992
+ const m = v.replace(/^[~^]/, "").match(/^(\d+)\.(\d+)\.(\d+)/);
3993
+ return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : [0, 0, 0];
3994
+ };
3995
+ const isBehind = (installed, latest) => {
3996
+ const [a1, a2, a3] = parse2(installed);
3997
+ const [b1, b2, b3] = parse2(latest);
3998
+ return a1 !== b1 ? a1 < b1 : a2 !== b2 ? a2 < b2 : a3 < b3;
3999
+ };
4000
+ try {
4001
+ const res = await doFetch("https://registry.npmjs.org/@mushi-mushi/cli/latest", {
4002
+ signal: AbortSignal.timeout(3e3)
4003
+ });
4004
+ if (res.ok) {
4005
+ const { version: latest } = await res.json();
4006
+ if (latest && isBehind(MUSHI_CLI_VERSION2, latest)) {
4007
+ checks.push({
4008
+ name: "CLI version",
4009
+ ok: true,
4010
+ warn: true,
4011
+ detail: `Installed ${MUSHI_CLI_VERSION2}, latest is ${latest}. Run \`mushi upgrade\` (or npm i -g @mushi-mushi/cli@latest).`
4012
+ });
4013
+ }
4014
+ }
4015
+ } catch {
4016
+ }
4017
+ try {
4018
+ const { readFile: readFile3 } = await import("fs/promises");
4019
+ const { join: join4 } = await import("path");
4020
+ const pkg = JSON.parse(await readFile3(join4(cwd, "package.json"), "utf8"));
4021
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
4022
+ const candidates = Object.entries(deps).filter(([name]) => name.startsWith("@mushi-mushi/")).filter(([, v]) => /^[~^]?\d/.test(v));
4023
+ const drifted = (await Promise.all(
4024
+ candidates.map(async ([name, v]) => {
4025
+ try {
4026
+ const res = await doFetch(`https://registry.npmjs.org/${name}/latest`, {
4027
+ signal: AbortSignal.timeout(3e3)
4028
+ });
4029
+ if (!res.ok) return null;
4030
+ const { version: latest } = await res.json();
4031
+ return latest && isBehind(v, latest) ? [name, v, latest] : null;
4032
+ } catch {
4033
+ return null;
4034
+ }
4035
+ })
4036
+ )).filter((d) => d !== null);
4037
+ if (drifted.length > 0) {
4038
+ checks.push({
4039
+ name: "SDK version drift",
4040
+ ok: true,
4041
+ warn: true,
4042
+ detail: `${drifted.map(([n, v, l]) => `${n}@${v} (latest ${l})`).join(", ")} \u2014 behind npm latest. Run \`mushi upgrade\` in this repo.`
4043
+ });
4044
+ }
4045
+ } catch {
4046
+ }
4047
+ return checks;
4048
+ }
4049
+ async function checkPipelineDoctor(config, doFetch) {
4050
+ if (!config.endpoint || !config.apiKey) return [];
4051
+ try {
4052
+ const res = await doFetch(`${config.endpoint.replace(/\/$/, "")}/v1/admin/doctor`, {
4053
+ headers: apiKeyHeaders(config.apiKey),
4054
+ signal: AbortSignal.timeout(1e4)
4055
+ });
4056
+ if (res.status === 401 || res.status === 403) return [];
4057
+ if (!res.ok) {
4058
+ return [{
4059
+ name: "[pipeline] Server doctor",
4060
+ ok: true,
4061
+ warn: true,
4062
+ detail: `GET /v1/admin/doctor returned HTTP ${res.status} \u2014 pipeline health unknown.`
4063
+ }];
4064
+ }
4065
+ const body = await res.json();
4066
+ return (body.data?.checks ?? []).map((ch) => ({
4067
+ name: `[pipeline] ${ch.name}`,
4068
+ ok: ch.status !== "fail",
4069
+ warn: ch.status === "warn",
4070
+ detail: ch.hint ? `${ch.summary} \u2192 Fix: ${ch.hint}` : ch.summary
4071
+ }));
4072
+ } catch {
4073
+ return [{
4074
+ name: "[pipeline] Server doctor",
4075
+ ok: true,
4076
+ warn: true,
4077
+ detail: "Could not reach /v1/admin/doctor (timeout) \u2014 pipeline health unknown."
4078
+ }];
4079
+ }
4080
+ }
3855
4081
  async function runDoctor(config, options = {}) {
3856
4082
  const doFetch = options.fetch ?? globalThis.fetch;
3857
4083
  const checks = [];
@@ -3864,10 +4090,24 @@ async function runDoctor(config, options = {}) {
3864
4090
  }
3865
4091
  const sdkCheck = await checkSdkInstall(options.cwd ?? process.cwd());
3866
4092
  if (sdkCheck) checks.push(sdkCheck);
4093
+ if (sdkCheck?.ok && !sdkCheck.warn) {
4094
+ const { findSdkImport } = await import("./snippet-inject-LPRWTQDE.js");
4095
+ const imported = await findSdkImport(options.cwd ?? process.cwd());
4096
+ checks.push(
4097
+ imported ? { name: "SDK init snippet wired", ok: true, detail: `Found SDK usage in ${imported.file}` } : {
4098
+ name: "SDK init snippet wired",
4099
+ ok: true,
4100
+ warn: true,
4101
+ detail: "Package installed but no @mushi-mushi import found in src/app \u2014 paste the init snippet from `mushi init` into your entry file."
4102
+ }
4103
+ );
4104
+ }
3867
4105
  if (runServer) {
3868
4106
  const serverChecks = await checkServerPreflight(config, doFetch);
3869
4107
  checks.push(...serverChecks);
4108
+ checks.push(...await checkPipelineDoctor(config, doFetch));
3870
4109
  }
4110
+ checks.push(...await checkVersionDrift(options.cwd ?? process.cwd(), doFetch));
3871
4111
  if (runIngest) {
3872
4112
  const ingestChecks = await checkIngestSetup(config, doFetch);
3873
4113
  checks.push(...ingestChecks);
@@ -4004,13 +4244,20 @@ function registerDoctorCommand(program2) {
4004
4244
  result = await runDoctor(config, { ...doctorOpts, mcp: opts.mcp });
4005
4245
  }
4006
4246
  const { checks } = result;
4247
+ const hasWarn = checks.some((c) => c.ok && c.warn);
4007
4248
  if (opts.json) {
4008
- console.log(JSON.stringify({ checks, ready: result.ready }, null, 2));
4249
+ const jsonChecks = checks.map((c) => ({
4250
+ ...c,
4251
+ ...!c.ok ? { hint: fixHintForCheck(c.name) } : {}
4252
+ }));
4253
+ console.log(JSON.stringify({ checks: jsonChecks, ready: result.ready }, null, 2));
4009
4254
  if (!result.ready) process.exit(1);
4255
+ if (hasWarn) process.exit(2);
4010
4256
  return;
4011
4257
  }
4012
4258
  console.log(formatDoctorResult(result));
4013
4259
  if (!result.ready) process.exit(1);
4260
+ if (hasWarn) process.exit(2);
4014
4261
  }
4015
4262
  );
4016
4263
  }
@@ -4090,18 +4337,18 @@ function registerTddCommands(program2) {
4090
4337
  config
4091
4338
  );
4092
4339
  if (!runsRes.ok) break;
4093
- const run = runsRes.data.runs.find((r) => r.id === res.data.runId);
4094
- if (!run) break;
4095
- if (run.status === "completed") {
4340
+ const run2 = runsRes.data.runs.find((r) => r.id === res.data.runId);
4341
+ if (!run2) break;
4342
+ if (run2.status === "completed") {
4096
4343
  console.log(`
4097
- \u2713 Done! ${run.pages_crawled ?? 0} pages crawled.`);
4098
- if (run.proposal_id) console.log(` Proposal id: ${run.proposal_id}`);
4344
+ \u2713 Done! ${run2.pages_crawled ?? 0} pages crawled.`);
4345
+ if (run2.proposal_id) console.log(` Proposal id: ${run2.proposal_id}`);
4099
4346
  console.log(` Review in the console: Inventory \u2192 Discovery \u2192 Past proposals`);
4100
4347
  break;
4101
4348
  }
4102
- if (run.status === "failed") {
4349
+ if (run2.status === "failed") {
4103
4350
  console.error(`
4104
- \u2717 Crawl failed: ${run.error_message ?? "unknown"}`);
4351
+ \u2717 Crawl failed: ${run2.error_message ?? "unknown"}`);
4105
4352
  process.exit(1);
4106
4353
  }
4107
4354
  process.stdout.write(".");
@@ -4629,9 +4876,9 @@ Examples:
4629
4876
  }
4630
4877
  if (data.gate_runs.length > 0) {
4631
4878
  console.log("\nGate Results:");
4632
- for (const run of data.gate_runs) {
4633
- const g = run.status === "pass" ? "OK" : run.status === "fail" ? "FAIL" : "SKIP";
4634
- console.log(` ${g} ${run.gate.padEnd(22)} ${run.status} (${run.findings_count} finding${run.findings_count !== 1 ? "s" : ""})`);
4879
+ for (const run2 of data.gate_runs) {
4880
+ const g = run2.status === "pass" ? "OK" : run2.status === "fail" ? "FAIL" : "SKIP";
4881
+ console.log(` ${g} ${run2.gate.padEnd(22)} ${run2.status} (${run2.findings_count} finding${run2.findings_count !== 1 ? "s" : ""})`);
4635
4882
  }
4636
4883
  }
4637
4884
  if (data.summary.overall === "fail") process.exit(1);
@@ -4875,25 +5122,25 @@ Pipeline started!
4875
5122
  console.log(JSON.stringify(result.data, null, 2));
4876
5123
  return;
4877
5124
  }
4878
- const run = result.data;
4879
- const statusIcon = run.status === "completed" ? "\u2705" : run.status === "failed" ? "\u274C" : run.status === "running" ? "\u23F3" : "\u26AA";
5125
+ const run2 = result.data;
5126
+ const statusIcon = run2.status === "completed" ? "\u2705" : run2.status === "failed" ? "\u274C" : run2.status === "running" ? "\u23F3" : "\u26AA";
4880
5127
  console.log(`
4881
- ${statusIcon} Pipeline ${runId.slice(0, 8)} \xB7 ${run.root_skill_slug} \xB7 ${run.mode}
5128
+ ${statusIcon} Pipeline ${runId.slice(0, 8)} \xB7 ${run2.root_skill_slug} \xB7 ${run2.mode}
4882
5129
  `);
4883
- const steps = run.steps ?? [];
5130
+ const steps = run2.steps ?? [];
4884
5131
  for (const step of steps) {
4885
5132
  const icon = step.status === "passed" ? "\u2705" : step.status === "failed" ? "\u274C" : step.status === "running" ? "\u23F3" : "\u26AA";
4886
5133
  const pr = step.pr_url ? ` \u2192 ${step.pr_url}` : "";
4887
5134
  console.log(` ${icon} Step ${step.step_index + 1}: ${step.skill_slug}${pr}`);
4888
5135
  if (step.notes) console.log(` ${step.notes}`);
4889
5136
  }
4890
- if (run.context_packet) {
5137
+ if (run2.context_packet) {
4891
5138
  console.log(`
4892
5139
  ${"\u2500".repeat(60)}`);
4893
5140
  console.log(`Context Packet (paste into your Cursor agent):
4894
5141
  `);
4895
- console.log(run.context_packet.slice(0, 6e3));
4896
- if (run.context_packet.length > 6e3) console.log("\n\u2026 [truncated \u2014 full packet via --json]");
5142
+ console.log(run2.context_packet.slice(0, 6e3));
5143
+ if (run2.context_packet.length > 6e3) console.log("\n\u2026 [truncated \u2014 full packet via --json]");
4897
5144
  }
4898
5145
  console.log();
4899
5146
  });
@@ -5154,6 +5401,338 @@ Examples:
5154
5401
  });
5155
5402
  }
5156
5403
 
5404
+ // src/commands/selfhost.ts
5405
+ import { execSync as execSync2, spawnSync } from "child_process";
5406
+ import { randomBytes } from "crypto";
5407
+ function hasSupabaseCli() {
5408
+ try {
5409
+ const r = spawnSync("supabase", ["--version"], { encoding: "utf8", timeout: 5e3 });
5410
+ return r.status === 0;
5411
+ } catch {
5412
+ return false;
5413
+ }
5414
+ }
5415
+ function run(cmd, opts) {
5416
+ if (opts.printOnly) {
5417
+ console.log(` $ ${cmd}`);
5418
+ return { ok: true, stdout: "", stderr: "" };
5419
+ }
5420
+ const label = opts.label ?? cmd.slice(0, 60);
5421
+ process.stdout.write(` \u203A ${label} \u2026 `);
5422
+ try {
5423
+ const out = execSync2(cmd, {
5424
+ cwd: opts.cwd,
5425
+ encoding: "utf8",
5426
+ timeout: 3e5,
5427
+ stdio: ["ignore", "pipe", "pipe"]
5428
+ });
5429
+ process.stdout.write("\u2713\n");
5430
+ return { ok: true, stdout: out, stderr: "" };
5431
+ } catch (err) {
5432
+ process.stdout.write("\u2717\n");
5433
+ const msg = (err instanceof Error ? err.message : String(err)).trim();
5434
+ const stderr = err.stderr ?? "";
5435
+ return { ok: false, stdout: "", stderr: stderr || msg };
5436
+ }
5437
+ }
5438
+ var REQUIRED_FUNCTIONS = [
5439
+ "api",
5440
+ "fast-filter",
5441
+ "classify-report",
5442
+ "mcp",
5443
+ "fix-worker",
5444
+ "webhooks-github-indexer"
5445
+ ];
5446
+ async function runSelfhostUp(opts) {
5447
+ const supabaseDir = opts.cwd ? `${opts.cwd}/packages/server/supabase` : "packages/server/supabase";
5448
+ const printOnly = !!(opts.printCommands || !hasSupabaseCli());
5449
+ if (printOnly) {
5450
+ console.log("");
5451
+ console.log("supabase CLI not found (or --print-commands was passed).");
5452
+ console.log("Run the following commands manually:\n");
5453
+ } else {
5454
+ console.log("");
5455
+ console.log("mushi selfhost up \u2014 deploying self-hosted Mushi Mushi\n");
5456
+ }
5457
+ const steps = [];
5458
+ if (opts.projectRef && !/^[a-z0-9-]+$/i.test(opts.projectRef)) {
5459
+ process.stderr.write(`\u2717 --project-ref must be alphanumeric (got: ${opts.projectRef}).
5460
+ `);
5461
+ process.exit(1);
5462
+ }
5463
+ if (opts.projectRef) {
5464
+ steps.push({
5465
+ label: `link to Supabase project ${opts.projectRef}`,
5466
+ cmd: `supabase link --project-ref ${opts.projectRef}`,
5467
+ critical: true
5468
+ });
5469
+ }
5470
+ steps.push({
5471
+ label: "apply database migrations",
5472
+ cmd: "supabase db push",
5473
+ critical: true
5474
+ });
5475
+ if (opts.anthropicKey && printOnly) {
5476
+ steps.push({
5477
+ label: "set ANTHROPIC_API_KEY secret",
5478
+ cmd: "supabase secrets set ANTHROPIC_API_KEY=sk-ant-..."
5479
+ });
5480
+ } else if (opts.anthropicKey) {
5481
+ if (!/^[A-Za-z0-9_-]+$/.test(opts.anthropicKey)) {
5482
+ process.stderr.write(
5483
+ "\u2717 --anthropic-key contains characters that cannot be passed through the shell safely.\n Set it manually instead: supabase secrets set ANTHROPIC_API_KEY=<your key>\n"
5484
+ );
5485
+ process.exit(1);
5486
+ }
5487
+ steps.push({
5488
+ label: "set ANTHROPIC_API_KEY secret",
5489
+ cmd: `supabase secrets set ANTHROPIC_API_KEY=${opts.anthropicKey}`
5490
+ });
5491
+ } else if (printOnly) {
5492
+ steps.push({
5493
+ label: "set ANTHROPIC_API_KEY secret",
5494
+ cmd: "supabase secrets set ANTHROPIC_API_KEY=sk-ant-..."
5495
+ });
5496
+ }
5497
+ if (opts.adminBaseUrl) {
5498
+ let adminUrlOk = false;
5499
+ try {
5500
+ const u = new URL(opts.adminBaseUrl);
5501
+ adminUrlOk = (u.protocol === "https:" || u.protocol === "http:") && /^[A-Za-z0-9:/._~-]+$/.test(opts.adminBaseUrl);
5502
+ } catch {
5503
+ adminUrlOk = false;
5504
+ }
5505
+ if (!adminUrlOk) {
5506
+ process.stderr.write(
5507
+ `\u2717 --admin-base-url must be a plain http(s) URL (got: ${opts.adminBaseUrl}).
5508
+ `
5509
+ );
5510
+ process.exit(1);
5511
+ }
5512
+ steps.push({
5513
+ label: "set ADMIN_BASE_URL secret",
5514
+ cmd: `supabase secrets set ADMIN_BASE_URL=${opts.adminBaseUrl}`
5515
+ });
5516
+ }
5517
+ const internalToken = randomBytes(32).toString("hex");
5518
+ steps.push({
5519
+ label: "set MUSHI_INTERNAL_CALLER_SECRET (recovery cron auth)",
5520
+ cmd: `supabase secrets set MUSHI_INTERNAL_CALLER_SECRET=${printOnly ? "<openssl rand -hex 32>" : internalToken}`
5521
+ });
5522
+ if (!opts.skipDeploy) {
5523
+ for (const fn of REQUIRED_FUNCTIONS) {
5524
+ steps.push({
5525
+ label: `deploy edge function: ${fn}`,
5526
+ cmd: `supabase functions deploy ${fn} --no-verify-jwt`
5527
+ });
5528
+ }
5529
+ }
5530
+ steps.push({
5531
+ label: "create screenshots storage bucket",
5532
+ cmd: "supabase storage create screenshots --public || true"
5533
+ });
5534
+ let aborted = false;
5535
+ for (const step of steps) {
5536
+ if (printOnly) {
5537
+ console.log(`# ${step.label}`);
5538
+ run(step.cmd, { printOnly: true });
5539
+ console.log("");
5540
+ } else {
5541
+ const result = run(step.cmd, { label: step.label, cwd: supabaseDir });
5542
+ if (!result.ok) {
5543
+ if (result.stderr) {
5544
+ process.stderr.write(` ${result.stderr.split("\n")[0]}
5545
+ `);
5546
+ }
5547
+ if (step.critical) {
5548
+ process.stderr.write(
5549
+ `
5550
+ \u2717 Critical step failed \u2014 resolve the error above and re-run.
5551
+ `
5552
+ );
5553
+ process.exit(1);
5554
+ }
5555
+ aborted = true;
5556
+ }
5557
+ }
5558
+ }
5559
+ const endpoint = opts.endpoint || loadConfig().endpoint;
5560
+ if (endpoint) {
5561
+ if (printOnly) {
5562
+ console.log("# seed mushi_runtime_config via API");
5563
+ console.log(
5564
+ `# mushi login --endpoint ${endpoint} --api-key <key> --project-id <pid>`
5565
+ );
5566
+ console.log(
5567
+ `# then: supabase sql --file <(echo "INSERT INTO public.mushi_runtime_config(key,value)`
5568
+ );
5569
+ console.log(
5570
+ `# VALUES ('internal_caller_token','<token>') ON CONFLICT (key) DO UPDATE SET value=EXCLUDED.value,updated_at=now()")`
5571
+ );
5572
+ console.log("");
5573
+ } else if (!aborted) {
5574
+ process.stdout.write(` \u203A POST /v1/admin/bootstrap \u2026 `);
5575
+ try {
5576
+ const config = loadConfig();
5577
+ if (config.apiKey) {
5578
+ await apiCall("/admin/bootstrap", config, { method: "POST" });
5579
+ process.stdout.write("\u2713\n");
5580
+ } else {
5581
+ process.stdout.write("skipped (not logged in)\n");
5582
+ }
5583
+ } catch {
5584
+ process.stdout.write("warn (non-critical)\n");
5585
+ }
5586
+ }
5587
+ }
5588
+ if (printOnly) {
5589
+ 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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
5590
+ console.log("Once commands above are done, run:");
5591
+ console.log(" mushi selfhost doctor --endpoint <your-endpoint>");
5592
+ return;
5593
+ }
5594
+ if (!aborted && endpoint) {
5595
+ console.log("");
5596
+ console.log("Running proof step\u2026");
5597
+ const probe = await probeEndpointHealth(endpoint).catch(() => null);
5598
+ if (!probe?.ok) {
5599
+ console.log(" WARN /health did not return 200 \u2014 check function logs.");
5600
+ } else {
5601
+ console.log(` OK /health ${probe.status} ${probe.latencyMs}ms`);
5602
+ }
5603
+ }
5604
+ console.log("");
5605
+ if (aborted) {
5606
+ console.log(
5607
+ "Some non-critical steps failed \u2014 check stderr above, then re-run or fix manually."
5608
+ );
5609
+ console.log("Run `mushi selfhost doctor` once the endpoint is reachable.");
5610
+ process.exit(2);
5611
+ } else {
5612
+ console.log("\u2713 selfhost up complete.");
5613
+ console.log("");
5614
+ console.log("Next steps:");
5615
+ console.log(
5616
+ " 1. mushi login --endpoint https://<ref>.supabase.co/functions/v1/api"
5617
+ );
5618
+ console.log(" 2. mushi selfhost doctor");
5619
+ console.log(" 3. mushi init (in your app repo)");
5620
+ }
5621
+ }
5622
+ async function runSelfhostDoctor(opts) {
5623
+ const config = loadConfig();
5624
+ const endpoint = opts.endpoint || config.endpoint;
5625
+ if (!endpoint) {
5626
+ process.stderr.write(
5627
+ "error: endpoint required \u2014 pass --endpoint or run `mushi login` first.\n"
5628
+ );
5629
+ process.exit(1);
5630
+ }
5631
+ let serverChecks = [];
5632
+ try {
5633
+ const res = await fetch(`${endpoint}/v1/admin/doctor`, {
5634
+ headers: config.apiKey ? { Authorization: `Bearer ${config.apiKey}` } : {},
5635
+ signal: AbortSignal.timeout(15e3)
5636
+ });
5637
+ if (res.ok) {
5638
+ const body = await res.json();
5639
+ serverChecks = body.data?.checks ?? [];
5640
+ } else {
5641
+ serverChecks = [
5642
+ {
5643
+ name: "server_doctor",
5644
+ status: "fail",
5645
+ summary: `/v1/admin/doctor returned HTTP ${res.status}`,
5646
+ hint: "401 \u2192 check API key scope (needs mcp:read or admin); 403 \u2192 key not owner of this project"
5647
+ }
5648
+ ];
5649
+ }
5650
+ } catch (err) {
5651
+ serverChecks = [
5652
+ {
5653
+ name: "server_doctor",
5654
+ status: "fail",
5655
+ summary: `unreachable: ${err instanceof Error ? err.message : String(err)}`,
5656
+ hint: `Check that ${endpoint} is deployed and reachable.`
5657
+ }
5658
+ ];
5659
+ }
5660
+ if (opts.json) {
5661
+ const overallStatus = serverChecks.some((c) => c.status === "fail") ? "fail" : serverChecks.some((c) => c.status === "warn") ? "warn" : "pass";
5662
+ console.log(JSON.stringify({ status: overallStatus, checks: serverChecks }, null, 2));
5663
+ } else {
5664
+ for (const check of serverChecks) {
5665
+ const tag = check.status === "pass" ? "OK " : check.status === "warn" ? "WARN" : "FAIL";
5666
+ console.log(`${tag} ${check.name} ${check.summary}`);
5667
+ if (check.hint && check.status !== "pass") {
5668
+ console.log(` \u2192 Fix: ${check.hint}`);
5669
+ }
5670
+ }
5671
+ }
5672
+ const hasFailure = serverChecks.some((c) => c.status === "fail");
5673
+ const hasWarn = serverChecks.some((c) => c.status === "warn");
5674
+ if (hasFailure) process.exit(1);
5675
+ if (hasWarn) process.exit(2);
5676
+ }
5677
+ function registerSelfhostCommands(program2) {
5678
+ const selfhost = program2.command("selfhost").description("Self-hosted deployment tools");
5679
+ selfhost.command("up").description(
5680
+ "Bootstrap a self-hosted Mushi Mushi deployment on a Supabase project"
5681
+ ).option(
5682
+ "--project-ref <ref>",
5683
+ "Supabase project ref (runs `supabase link` first)"
5684
+ ).option(
5685
+ "--anthropic-key <key>",
5686
+ "Anthropic API key to set as a Supabase function secret"
5687
+ ).option(
5688
+ "--admin-base-url <url>",
5689
+ "Base URL of your deployed admin console (e.g. https://your-domain.example.com/admin)"
5690
+ ).option(
5691
+ "--endpoint <url>",
5692
+ "Mushi API endpoint to use for the proof step (default: from ~/.config/mushi/config.json)"
5693
+ ).option(
5694
+ "--skip-deploy",
5695
+ "Skip function deploys (run db push + secrets only)"
5696
+ ).option(
5697
+ "--print-commands",
5698
+ "Print the supabase CLI commands instead of running them (non-interactive / CI)"
5699
+ ).option("--cwd <path>", "Root of the mushi-mushi repo checkout").addHelpText(
5700
+ "after",
5701
+ `
5702
+ Examples:
5703
+ mushi selfhost up --project-ref abcxyzabcxyz
5704
+ mushi selfhost up --print-commands # copy-paste mode for CI
5705
+ mushi selfhost up --skip-deploy # re-run secrets/migrations only
5706
+
5707
+ Requires the Supabase CLI (https://supabase.com/docs/guides/cli).
5708
+ If not installed, pass --print-commands to get the exact commands.
5709
+
5710
+ After "up" completes:
5711
+ mushi selfhost doctor --endpoint https://<ref>.supabase.co/functions/v1/api`
5712
+ ).action(
5713
+ async (opts) => {
5714
+ await runSelfhostUp(opts);
5715
+ }
5716
+ );
5717
+ selfhost.command("doctor").description(
5718
+ "Check the health of a self-hosted deployment (calls GET /v1/admin/doctor)"
5719
+ ).option(
5720
+ "--endpoint <url>",
5721
+ "Mushi API endpoint (default: from ~/.config/mushi/config.json)"
5722
+ ).option("--json", "Machine-readable JSON output").addHelpText(
5723
+ "after",
5724
+ `
5725
+ Exit codes: 0 all pass \xB7 2 warnings only \xB7 1 any hard failure
5726
+
5727
+ Examples:
5728
+ mushi selfhost doctor
5729
+ mushi selfhost doctor --endpoint https://<ref>.supabase.co/functions/v1/api
5730
+ mushi selfhost doctor --json | jq '.checks[] | select(.status != "pass")'`
5731
+ ).action(async (opts) => {
5732
+ await runSelfhostDoctor(opts);
5733
+ });
5734
+ }
5735
+
5157
5736
  // src/index.ts
5158
5737
  installSignalHandlers();
5159
5738
  var program = new Command().name("mushi").description("Mushi CLI \u2014 set up the SDK, triage a report, fix from your editor").version(MUSHI_CLI_VERSION).addHelpText("after", `
@@ -5192,6 +5771,7 @@ registerQaCommands(program);
5192
5771
  registerAuditCommands(program);
5193
5772
  registerSkillsCommands(program);
5194
5773
  registerBillingCommands(program);
5774
+ registerSelfhostCommands(program);
5195
5775
  registerCompletionCommand(program);
5196
5776
  program.parseAsync().catch((err) => {
5197
5777
  printAndExit(err);