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