@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/{chunk-GUVGTFVK.js → chunk-L3LUPBFG.js} +1 -1
- package/dist/chunk-MCVRTQAI.js +6 -0
- package/dist/chunk-TYVJU4AH.js +8 -0
- package/dist/{chunk-BWASDQI6.js → chunk-VIHWH3IG.js} +3 -4
- package/dist/{connect-7LTQRBEW.js → connect-HCWJ6NXP.js} +2 -2
- package/dist/{connect-KKN62YK2.js → connect-PKWAK4GL.js} +2 -1
- package/dist/index.js +818 -231
- package/dist/init.js +54 -25
- package/dist/snippet-inject-LPRWTQDE.js +83 -0
- package/dist/snippet-inject-NDAPEL2T.js +81 -0
- package/dist/version-4JARRCFG.js +7 -0
- package/dist/version.js +1 -1
- package/dist/wizard-args.d.ts +44 -0
- package/dist/wizard-args.js +95 -0
- package/package.json +7 -1
- package/dist/chunk-X74T7JKC.js +0 -6
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-
|
|
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(
|
|
200
|
+
async function withOneRetry(run2) {
|
|
199
201
|
try {
|
|
200
|
-
return await
|
|
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
|
|
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
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
4087
|
-
if (!
|
|
4088
|
-
if (
|
|
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! ${
|
|
4091
|
-
if (
|
|
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 (
|
|
4349
|
+
if (run2.status === "failed") {
|
|
4096
4350
|
console.error(`
|
|
4097
|
-
\u2717 Crawl failed: ${
|
|
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
|
|
4626
|
-
const g =
|
|
4627
|
-
console.log(` ${g} ${
|
|
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
|
|
4872
|
-
const statusIcon =
|
|
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 ${
|
|
5128
|
+
${statusIcon} Pipeline ${runId.slice(0, 8)} \xB7 ${run2.root_skill_slug} \xB7 ${run2.mode}
|
|
4875
5129
|
`);
|
|
4876
|
-
const 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 (
|
|
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(
|
|
4889
|
-
if (
|
|
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);
|