@mushi-mushi/cli 0.24.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-BWASDQI6.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
  }
@@ -245,28 +247,26 @@ async function createProject(endpoint, cliToken, name) {
245
247
  }
246
248
  async function mintProjectKey(endpoint, cliToken, projectId) {
247
249
  const base = trimTrailingSlash(endpoint);
248
- return withOneRetry(async () => {
249
- let res;
250
- try {
251
- res = await deviceFetch(`${base}/v1/cli/projects/${projectId}/keys`, {
252
- method: "POST",
253
- headers: cliAuthHeaders(cliToken),
254
- body: JSON.stringify({})
255
- });
256
- } catch (err) {
257
- throw new DeviceAuthRequestError(
258
- "mint_key",
259
- `Could not reach ${base}/v1/cli/projects/\u2026/keys: ${err instanceof Error ? err.message : String(err)}`
260
- );
261
- }
262
- const json = await res.json().catch(() => null);
263
- if (res.ok && json?.ok && json.data?.key) return json.data.key;
250
+ let res;
251
+ try {
252
+ res = await deviceFetch(`${base}/v1/cli/projects/${projectId}/keys`, {
253
+ method: "POST",
254
+ headers: cliAuthHeaders(cliToken),
255
+ body: JSON.stringify({})
256
+ });
257
+ } catch (err) {
264
258
  throw new DeviceAuthRequestError(
265
259
  "mint_key",
266
- json?.error?.message ?? `Could not mint an SDK key (HTTP ${res.status}).`,
267
- res.ok ? void 0 : res.status
260
+ `Could not reach ${base}/v1/cli/projects/\u2026/keys: ${err instanceof Error ? err.message : String(err)}`
268
261
  );
269
- });
262
+ }
263
+ const json = await res.json().catch(() => null);
264
+ if (res.ok && json?.ok && json.data?.key) return json.data.key;
265
+ throw new DeviceAuthRequestError(
266
+ "mint_key",
267
+ json?.error?.message ?? `Could not mint an SDK key (HTTP ${res.status}).`,
268
+ res.status
269
+ );
270
270
  }
271
271
 
272
272
  // src/freshness.ts
@@ -521,6 +521,7 @@ async function runInit(options = {}) {
521
521
  persistCliConfig(credentials.apiKey, credentials.projectId, endpoint);
522
522
  emitWizardFunnelEvent(credentials, endpoint, "wizard_env_written", { framework: framework.id });
523
523
  const enableRewards = await maybeEnableRewards(options);
524
+ await maybeInjectSnippet(cwd, framework, options);
524
525
  printNextSteps(framework, consoleBase, enableRewards);
525
526
  await maybeSendTestReport(credentials, { ...options, endpoint, consoleBase });
526
527
  await maybeOfferConnect(credentials, options, consoleBase);
@@ -864,7 +865,12 @@ async function writeEnvFile(cwd, apiKey, projectId, framework, endpoint, overwri
864
865
  const target = ENV_FILES.find((f) => existsSync2(join2(cwd, f))) ?? ENV_FILES[0];
865
866
  const targetPath = join2(cwd, target);
866
867
  const newVars = envVarsToWrite(apiKey, projectId, framework, endpoint);
867
- const existing = existsSync2(targetPath) ? readFileSync2(targetPath, "utf-8") : "";
868
+ let existing = "";
869
+ try {
870
+ existing = readFileSync2(targetPath, "utf-8");
871
+ } catch {
872
+ existing = "";
873
+ }
868
874
  if (existing.includes("MUSHI_PROJECT_ID")) {
869
875
  let shouldOverwrite = overwrite;
870
876
  if (!shouldOverwrite) {
@@ -951,6 +957,31 @@ function emitWizardFunnelEvent(credentials, endpoint, event, metadata = {}) {
951
957
  }).catch(() => {
952
958
  });
953
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
+ }
954
985
  function printNextSteps(framework, consoleBase, enableRewards = false) {
955
986
  p.note(framework.snippet(), "Add this to your app:");
956
987
  if (enableRewards) {
@@ -984,7 +1015,7 @@ async function maybeOfferConnect(credentials, options, _consoleBase) {
984
1015
  if (p.isCancel(answer) || !answer) return;
985
1016
  const endpoint = resolveCloudEndpoint(options.endpoint);
986
1017
  try {
987
- const { runConnect: runConnect2 } = await import("./connect-KKN62YK2.js");
1018
+ const { runConnect: runConnect2 } = await import("./connect-PKWAK4GL.js");
988
1019
  await runConnect2({
989
1020
  apiKey: credentials.apiKey,
990
1021
  projectId: credentials.projectId,
@@ -1111,6 +1142,211 @@ function isSameDirectory(a, b) {
1111
1142
  return a.replace(/\\/g, "/").replace(/\/+$/, "") === b.replace(/\\/g, "/").replace(/\/+$/, "");
1112
1143
  }
1113
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
+
1114
1350
  // src/migrate.ts
1115
1351
  var DOCS_BASE = "https://kensaur.us/mushi-mushi/docs";
1116
1352
  var COMPETITOR_PACKAGES = {
@@ -1274,187 +1510,7 @@ Examples:
1274
1510
  mushi login --api-key mushi_xxx # non-interactive / CI
1275
1511
  mushi login --api-key mushi_xxx --project-id <uuid>
1276
1512
  mushi login --upgrade-scope # re-auth to add mcp:read + mcp:write to your key`).action(async (opts) => {
1277
- const { CLOUD_API_ENDPOINT, resolveCloudEndpoint: resolveCloudEndpoint2 } = await import("./endpoint-DGS73AVM.js");
1278
- const endpoint = opts.endpoint ? resolveCloudEndpoint2(opts.endpoint) : CLOUD_API_ENDPOINT;
1279
- const consoleBase = await resolveConsoleUrl();
1280
- let apiKey = opts.apiKey;
1281
- let projectId = opts.projectId;
1282
- if (apiKey) {
1283
- const config2 = loadConfig();
1284
- config2.apiKey = apiKey;
1285
- config2.endpoint = endpoint;
1286
- if (projectId) config2.projectId = projectId;
1287
- config2.consoleUrl = consoleBase;
1288
- saveConfig(config2);
1289
- console.log("");
1290
- console.log(" \u2713 Credentials saved.");
1291
- const verifyResult = await apiCall(
1292
- "/v1/sync/whoami",
1293
- { apiKey, endpoint, projectId }
1294
- );
1295
- if (!verifyResult.ok) {
1296
- console.warn(` \u26A0 Key saved, but verification failed: ${verifyResult.error?.message ?? "unknown error"}`);
1297
- console.warn(` Check your key at: ${cliSetupDeepLink(consoleBase)}`);
1298
- return;
1299
- }
1300
- const d = verifyResult.data;
1301
- if (d.project_name) {
1302
- if (!projectId && d.project_id) {
1303
- config2.projectId = d.project_id;
1304
- saveConfig(config2);
1305
- }
1306
- console.log(` \u2713 Project: ${d.project_name} (${d.project_id})`);
1307
- }
1308
- console.log(` \u2713 Endpoint: ${endpoint}`);
1309
- console.log("");
1310
- console.log(" Run 'mushi whoami' to verify \xB7 'mushi init' to set up the SDK");
1311
- return;
1312
- }
1313
- console.log("");
1314
- if (opts.upgradeScope) {
1315
- console.log(" Mushi login \u2014 scope upgrade");
1316
- 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");
1317
- console.log(" Re-authenticating to mint a new key with report:write + mcp:read + mcp:write scopes.");
1318
- console.log(" This lets the CLI admin commands (including writes) and MCP server both work with a single key.");
1319
- console.log("");
1320
- } else {
1321
- console.log(" Mushi login");
1322
- console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
1323
- }
1324
- let deviceData;
1325
- try {
1326
- deviceData = await startDeviceAuth(endpoint, ensureClientId());
1327
- } catch (err) {
1328
- process.stderr.write(`
1329
- error: Could not start login session: ${err instanceof Error ? err.message : String(err)}
1330
- `);
1331
- process.stderr.write(` Fallback: mushi login --api-key <key> --project-id <uuid>
1332
- `);
1333
- process.exit(1);
1334
- }
1335
- const verifyUrl = deviceData.verification_uri;
1336
- if (opts.browser !== false) {
1337
- try {
1338
- await openInBrowser(verifyUrl);
1339
- } catch {
1340
- }
1341
- }
1342
- printAuthBanner(deviceData.user_code, verifyUrl);
1343
- let cliToken = null;
1344
- const pollIntervalMs = (deviceData.interval ?? 5) * 1e3;
1345
- const deadline = Date.now() + (deviceData.expires_in ?? 600) * 1e3;
1346
- const MAX_CONSECUTIVE_ERRORS = 5;
1347
- let consecutiveErrors = 0;
1348
- let firstPoll = true;
1349
- while (Date.now() < deadline) {
1350
- if (!firstPoll) {
1351
- await new Promise((r) => setTimeout(r, pollIntervalMs));
1352
- }
1353
- firstPoll = false;
1354
- const outcome = await pollDeviceToken(endpoint, deviceData.device_code);
1355
- if (outcome.status === "approved") {
1356
- cliToken = outcome.cliToken;
1357
- break;
1358
- }
1359
- if (outcome.status === "pending") {
1360
- consecutiveErrors = 0;
1361
- continue;
1362
- }
1363
- if (outcome.status === "error") {
1364
- if (outcome.retryable) {
1365
- consecutiveErrors += 1;
1366
- if (consecutiveErrors < MAX_CONSECUTIVE_ERRORS) continue;
1367
- }
1368
- printAuthFailed("error", outcome.message);
1369
- process.exit(1);
1370
- }
1371
- if (outcome.status === "denied") {
1372
- printAuthFailed("denied");
1373
- process.exit(1);
1374
- }
1375
- if (outcome.status === "expired") {
1376
- printAuthFailed("timeout");
1377
- process.exit(1);
1378
- }
1379
- }
1380
- if (!cliToken) {
1381
- printAuthFailed("timeout");
1382
- process.exit(1);
1383
- }
1384
- printAuthApproved();
1385
- const projectsList = await listProjects(endpoint, cliToken);
1386
- let chosenProjectId = projectId;
1387
- let chosenProjectName;
1388
- if (!chosenProjectId) {
1389
- const { createInterface } = await import("readline");
1390
- const rl = createInterface({ input: process.stdin, output: process.stdout });
1391
- const ask = (q) => new Promise((resolve3) => rl.question(q, (a) => resolve3(a.trim())));
1392
- if (projectsList.length > 0) {
1393
- console.log("");
1394
- console.log(" Your projects:");
1395
- projectsList.forEach((p3, i) => {
1396
- console.log(` ${i + 1}. ${p3.name} (${p3.id})`);
1397
- });
1398
- console.log(` ${projectsList.length + 1}. Create a new project`);
1399
- console.log("");
1400
- const choice = await ask(` Pick a project [1-${projectsList.length + 1}]: `);
1401
- const num = parseInt(choice, 10);
1402
- if (num >= 1 && num <= projectsList.length) {
1403
- const picked = projectsList[num - 1];
1404
- chosenProjectId = picked.id;
1405
- chosenProjectName = picked.name;
1406
- }
1407
- }
1408
- if (!chosenProjectId) {
1409
- console.log("");
1410
- const newName = await ask(" Project name: ");
1411
- rl.close();
1412
- if (!newName.trim()) {
1413
- process.stderr.write("\nerror: Project name is required.\n");
1414
- process.exit(2);
1415
- }
1416
- try {
1417
- const created = await createProject(endpoint, cliToken, newName.trim());
1418
- chosenProjectId = created.id;
1419
- chosenProjectName = created.name;
1420
- apiKey = created.apiKey ?? void 0;
1421
- console.log(` \u2713 Created project "${chosenProjectName}"`);
1422
- } catch (err) {
1423
- process.stderr.write(`
1424
- error: Could not create project: ${err instanceof Error ? err.message : String(err)}
1425
- `);
1426
- process.exit(1);
1427
- }
1428
- } else {
1429
- rl.close();
1430
- }
1431
- }
1432
- if (!apiKey && chosenProjectId) {
1433
- try {
1434
- apiKey = await mintProjectKey(endpoint, cliToken, chosenProjectId) ?? void 0;
1435
- } catch {
1436
- }
1437
- }
1438
- const config = loadConfig();
1439
- config.endpoint = endpoint;
1440
- config.consoleUrl = consoleBase;
1441
- if (chosenProjectId) config.projectId = chosenProjectId;
1442
- if (apiKey) config.apiKey = apiKey;
1443
- saveConfig(config);
1444
- console.log("");
1445
- if (chosenProjectName) console.log(` \u2713 Project: ${chosenProjectName}`);
1446
- if (apiKey) {
1447
- console.log(` \u2713 Full-scope CLI key saved (${apiKey.slice(0, 12)}\u2026)`);
1448
- console.log(` Scopes: report:write \xB7 mcp:read \xB7 mcp:write \u2014 SDK + MCP + admin CLI (incl. writes) all work with this key`);
1449
- } else {
1450
- console.log(` \u2139 Get an SDK key at: ${apiKeyHint(consoleBase)}`);
1451
- }
1452
- console.log("");
1453
- if (opts.upgradeScope) {
1454
- console.log(" Key upgraded! Re-run 'mushi setup' to update your .cursor/mcp.json, then restart Cursor.");
1455
- } else {
1456
- console.log(" Run 'mushi init' to set up the SDK in this project.");
1457
- }
1513
+ await runLogin(opts);
1458
1514
  });
1459
1515
  program2.command("whoami").description("Verify API key and display project info").option("--json", "Machine-readable JSON output").addHelpText("after", `
1460
1516
  Verifies that MUSHI_API_KEY is valid and shows which project it belongs to.
@@ -2431,8 +2487,11 @@ error: Could not create project: ${err instanceof Error ? err.message : String(e
2431
2487
  }
2432
2488
 
2433
2489
  // src/commands/setup.ts
2490
+ function resolveLoginEndpoint(optsEndpoint, existingConfigEndpoint, envEndpoint) {
2491
+ return optsEndpoint ?? existingConfigEndpoint ?? envEndpoint?.trim();
2492
+ }
2434
2493
  function registerSetupCommands(program2) {
2435
- 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", `
2494
+ 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", `
2436
2495
  Examples:
2437
2496
  mushi setup # wire Cursor (default)
2438
2497
  mushi setup --all-projects # one server entry per accessible project
@@ -2445,11 +2504,18 @@ Supported IDEs:
2445
2504
  continue \u2014 writes .continue/mcp.json
2446
2505
  zed \u2014 writes ~/.config/zed/settings.json mcpServers block
2447
2506
 
2448
- 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) => {
2449
2508
  const { writeFile: writeFile2, mkdir, readFile: readFile3 } = await import("fs/promises");
2450
2509
  const { existsSync: existsSync3 } = await import("fs");
2451
2510
  const nodePath2 = await import("path");
2452
2511
  const os = await import("os");
2512
+ const existingConfig = loadConfig();
2513
+ if (!existingConfig.apiKey) {
2514
+ const endpoint = resolveLoginEndpoint(opts.endpoint, existingConfig.endpoint, process.env.MUSHI_API_ENDPOINT);
2515
+ await runLogin({ endpoint, suppressPostLoginBanner: true });
2516
+ console.log(" Continuing MCP setup\u2026");
2517
+ console.log("");
2518
+ }
2453
2519
  const config = requireConfig({ needsProject: true });
2454
2520
  const redactKeyForDisplay = (text2) => {
2455
2521
  const key = config.apiKey;
@@ -2674,9 +2740,54 @@ The command reads credentials from ~/.config/mushi/config.json (run \`mushi logi
2674
2740
  }
2675
2741
  if (errCode === "INSUFFICIENT_SCOPE") {
2676
2742
  console.log("\n\u26A0 Your key has report:write scope only \u2014 MCP admin tools will not work.");
2677
- console.log(" New keys minted by the wizard include both scopes automatically.");
2678
- console.log(" To upgrade an existing key, run:\n");
2679
- 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
+ }
2680
2791
  } else {
2681
2792
  console.log(`\u26A0 Key probe returned HTTP ${probeRes.status} \u2014 check your credentials.`);
2682
2793
  }
@@ -2694,8 +2805,36 @@ The command reads credentials from ~/.config/mushi/config.json (run \`mushi logi
2694
2805
  console.log(`Tip: run with --with-rules to also write the lesson-library coding hook.`);
2695
2806
  }
2696
2807
  const configRelPath = ideEntry.dir.startsWith("/") ? configPath : nodePath2.relative(cwd, configPath);
2697
- 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(`
2698
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
+ }
2699
2838
  }
2700
2839
  });
2701
2840
  }
@@ -3845,6 +3984,100 @@ async function checkOnboardingStatus(config, _consoleBase, cwd, doFetch = global
3845
3984
  done: false
3846
3985
  };
3847
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
+ }
3848
4081
  async function runDoctor(config, options = {}) {
3849
4082
  const doFetch = options.fetch ?? globalThis.fetch;
3850
4083
  const checks = [];
@@ -3857,10 +4090,24 @@ async function runDoctor(config, options = {}) {
3857
4090
  }
3858
4091
  const sdkCheck = await checkSdkInstall(options.cwd ?? process.cwd());
3859
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
+ }
3860
4105
  if (runServer) {
3861
4106
  const serverChecks = await checkServerPreflight(config, doFetch);
3862
4107
  checks.push(...serverChecks);
4108
+ checks.push(...await checkPipelineDoctor(config, doFetch));
3863
4109
  }
4110
+ checks.push(...await checkVersionDrift(options.cwd ?? process.cwd(), doFetch));
3864
4111
  if (runIngest) {
3865
4112
  const ingestChecks = await checkIngestSetup(config, doFetch);
3866
4113
  checks.push(...ingestChecks);
@@ -3997,13 +4244,20 @@ function registerDoctorCommand(program2) {
3997
4244
  result = await runDoctor(config, { ...doctorOpts, mcp: opts.mcp });
3998
4245
  }
3999
4246
  const { checks } = result;
4247
+ const hasWarn = checks.some((c) => c.ok && c.warn);
4000
4248
  if (opts.json) {
4001
- 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));
4002
4254
  if (!result.ready) process.exit(1);
4255
+ if (hasWarn) process.exit(2);
4003
4256
  return;
4004
4257
  }
4005
4258
  console.log(formatDoctorResult(result));
4006
4259
  if (!result.ready) process.exit(1);
4260
+ if (hasWarn) process.exit(2);
4007
4261
  }
4008
4262
  );
4009
4263
  }
@@ -4083,18 +4337,18 @@ function registerTddCommands(program2) {
4083
4337
  config
4084
4338
  );
4085
4339
  if (!runsRes.ok) break;
4086
- const run = runsRes.data.runs.find((r) => r.id === res.data.runId);
4087
- if (!run) break;
4088
- 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") {
4089
4343
  console.log(`
4090
- \u2713 Done! ${run.pages_crawled ?? 0} pages crawled.`);
4091
- 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}`);
4092
4346
  console.log(` Review in the console: Inventory \u2192 Discovery \u2192 Past proposals`);
4093
4347
  break;
4094
4348
  }
4095
- if (run.status === "failed") {
4349
+ if (run2.status === "failed") {
4096
4350
  console.error(`
4097
- \u2717 Crawl failed: ${run.error_message ?? "unknown"}`);
4351
+ \u2717 Crawl failed: ${run2.error_message ?? "unknown"}`);
4098
4352
  process.exit(1);
4099
4353
  }
4100
4354
  process.stdout.write(".");
@@ -4622,9 +4876,9 @@ Examples:
4622
4876
  }
4623
4877
  if (data.gate_runs.length > 0) {
4624
4878
  console.log("\nGate Results:");
4625
- for (const run of data.gate_runs) {
4626
- const g = run.status === "pass" ? "OK" : run.status === "fail" ? "FAIL" : "SKIP";
4627
- 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" : ""})`);
4628
4882
  }
4629
4883
  }
4630
4884
  if (data.summary.overall === "fail") process.exit(1);
@@ -4868,25 +5122,25 @@ Pipeline started!
4868
5122
  console.log(JSON.stringify(result.data, null, 2));
4869
5123
  return;
4870
5124
  }
4871
- const run = result.data;
4872
- 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";
4873
5127
  console.log(`
4874
- ${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}
4875
5129
  `);
4876
- const steps = run.steps ?? [];
5130
+ const steps = run2.steps ?? [];
4877
5131
  for (const step of steps) {
4878
5132
  const icon = step.status === "passed" ? "\u2705" : step.status === "failed" ? "\u274C" : step.status === "running" ? "\u23F3" : "\u26AA";
4879
5133
  const pr = step.pr_url ? ` \u2192 ${step.pr_url}` : "";
4880
5134
  console.log(` ${icon} Step ${step.step_index + 1}: ${step.skill_slug}${pr}`);
4881
5135
  if (step.notes) console.log(` ${step.notes}`);
4882
5136
  }
4883
- if (run.context_packet) {
5137
+ if (run2.context_packet) {
4884
5138
  console.log(`
4885
5139
  ${"\u2500".repeat(60)}`);
4886
5140
  console.log(`Context Packet (paste into your Cursor agent):
4887
5141
  `);
4888
- console.log(run.context_packet.slice(0, 6e3));
4889
- 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]");
4890
5144
  }
4891
5145
  console.log();
4892
5146
  });
@@ -5147,6 +5401,338 @@ Examples:
5147
5401
  });
5148
5402
  }
5149
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
+
5150
5736
  // src/index.ts
5151
5737
  installSignalHandlers();
5152
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", `
@@ -5185,6 +5771,7 @@ registerQaCommands(program);
5185
5771
  registerAuditCommands(program);
5186
5772
  registerSkillsCommands(program);
5187
5773
  registerBillingCommands(program);
5774
+ registerSelfhostCommands(program);
5188
5775
  registerCompletionCommand(program);
5189
5776
  program.parseAsync().catch((err) => {
5190
5777
  printAndExit(err);