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