@mushi-mushi/cli 0.23.0 → 0.24.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/README.md +26 -0
- package/dist/{chunk-GSQO35YD.js → chunk-BWASDQI6.js} +15 -1
- package/dist/{chunk-WLLFV26V.js → chunk-GUVGTFVK.js} +15 -1
- package/dist/chunk-X74T7JKC.js +6 -0
- package/dist/{connect-KX4UGY6Z.js → connect-7LTQRBEW.js} +2 -2
- package/dist/{connect-XFVSH4JZ.js → connect-KKN62YK2.js} +1 -1
- package/dist/index.js +492 -133
- package/dist/init.js +182 -60
- package/dist/version.js +1 -1
- package/package.json +4 -4
- package/dist/chunk-BY5SZYRO.js +0 -6
package/dist/index.js
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
detectFramework,
|
|
13
13
|
detectPackageManager,
|
|
14
14
|
die,
|
|
15
|
+
ensureClientId,
|
|
15
16
|
envVarsToWrite,
|
|
16
17
|
fetchIngestSetup,
|
|
17
18
|
fmtDate,
|
|
@@ -36,7 +37,7 @@ import {
|
|
|
36
37
|
sanitizeProjectId,
|
|
37
38
|
saveConfig,
|
|
38
39
|
writeMcpServerEntry
|
|
39
|
-
} from "./chunk-
|
|
40
|
+
} from "./chunk-BWASDQI6.js";
|
|
40
41
|
import {
|
|
41
42
|
TEST_REPORT_FETCH_TIMEOUT_MS,
|
|
42
43
|
assertEndpoint,
|
|
@@ -56,6 +57,7 @@ import { join as join2 } from "path";
|
|
|
56
57
|
|
|
57
58
|
// src/device-auth.ts
|
|
58
59
|
var DEVICE_FETCH_TIMEOUT_MS = 15e3;
|
|
60
|
+
var DEFAULT_SLOW_DOWN_MS = 5e3;
|
|
59
61
|
function trimTrailingSlash(endpoint) {
|
|
60
62
|
let end = endpoint.length;
|
|
61
63
|
while (end > 0 && endpoint.charCodeAt(end - 1) === 47) end--;
|
|
@@ -70,15 +72,18 @@ async function deviceFetch(url, init = {}) {
|
|
|
70
72
|
clearTimeout(timer);
|
|
71
73
|
}
|
|
72
74
|
}
|
|
73
|
-
async function startDeviceAuth(endpoint) {
|
|
75
|
+
async function startDeviceAuth(endpoint, clientId) {
|
|
74
76
|
const base = trimTrailingSlash(endpoint);
|
|
75
77
|
const res = await deviceFetch(`${base}/v1/cli/auth/device/start`, {
|
|
76
78
|
method: "POST",
|
|
77
|
-
headers: { "Content-Type": "application/json" }
|
|
79
|
+
headers: { "Content-Type": "application/json" },
|
|
80
|
+
body: JSON.stringify(clientId ? { client_id: clientId } : {})
|
|
78
81
|
});
|
|
79
82
|
const json = await res.json().catch(() => null);
|
|
80
83
|
if (!res.ok || !json?.ok || !json.data) {
|
|
81
|
-
throw new Error(
|
|
84
|
+
throw new Error(
|
|
85
|
+
json?.error?.message ?? `Could not start browser sign-in (HTTP ${res.status}).`
|
|
86
|
+
);
|
|
82
87
|
}
|
|
83
88
|
return json.data;
|
|
84
89
|
}
|
|
@@ -97,6 +102,12 @@ async function pollDeviceToken(endpoint, deviceCode) {
|
|
|
97
102
|
switch (json.error) {
|
|
98
103
|
case "authorization_pending":
|
|
99
104
|
return { status: "pending" };
|
|
105
|
+
case "slow_down": {
|
|
106
|
+
const retryAfterHeader = res.headers?.get?.("Retry-After");
|
|
107
|
+
const retryAfterSeconds = retryAfterHeader ? Number(retryAfterHeader) : NaN;
|
|
108
|
+
const retryAfterMs = Number.isFinite(retryAfterSeconds) && retryAfterSeconds > 0 ? retryAfterSeconds * 1e3 : DEFAULT_SLOW_DOWN_MS;
|
|
109
|
+
return { status: "slow_down", retryAfterMs };
|
|
110
|
+
}
|
|
100
111
|
case "access_denied":
|
|
101
112
|
return { status: "denied" };
|
|
102
113
|
case "expired_token":
|
|
@@ -105,20 +116,27 @@ async function pollDeviceToken(endpoint, deviceCode) {
|
|
|
105
116
|
return {
|
|
106
117
|
status: "error",
|
|
107
118
|
message: json.error_description ?? json.error ?? `HTTP ${res.status}`,
|
|
108
|
-
// 5xx is a server hiccup the next poll may clear;
|
|
109
|
-
//
|
|
110
|
-
|
|
119
|
+
// 5xx is a server hiccup the next poll may clear; 408 (gateway
|
|
120
|
+
// timeout) is equally transient. Any other 4xx is a definitive
|
|
121
|
+
// rejection (already claimed / invalid_grant) that won't fix
|
|
122
|
+
// itself. (429/slow_down is handled above as its own outcome.)
|
|
123
|
+
retryable: res.status >= 500 || res.status === 408
|
|
111
124
|
};
|
|
112
125
|
}
|
|
113
126
|
} catch (err) {
|
|
114
|
-
return {
|
|
127
|
+
return {
|
|
128
|
+
status: "error",
|
|
129
|
+
message: err instanceof Error ? err.message : String(err),
|
|
130
|
+
retryable: true
|
|
131
|
+
};
|
|
115
132
|
}
|
|
116
133
|
}
|
|
117
134
|
var defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
135
|
+
var deviceAuthRetryDelayMs = 1e3;
|
|
118
136
|
async function waitForCliToken(endpoint, session, opts = {}) {
|
|
119
137
|
const sleep = opts.sleep ?? defaultSleep;
|
|
120
138
|
const now = opts.now ?? Date.now;
|
|
121
|
-
|
|
139
|
+
let intervalMs = (session.interval || 5) * 1e3;
|
|
122
140
|
const deadline = now() + (session.expires_in || 600) * 1e3;
|
|
123
141
|
const maxConsecutiveErrors = opts.maxConsecutiveErrors ?? 5;
|
|
124
142
|
let consecutiveErrors = 0;
|
|
@@ -136,6 +154,11 @@ async function waitForCliToken(endpoint, session, opts = {}) {
|
|
|
136
154
|
consecutiveErrors = 0;
|
|
137
155
|
opts.onPending?.();
|
|
138
156
|
continue;
|
|
157
|
+
case "slow_down":
|
|
158
|
+
consecutiveErrors = 0;
|
|
159
|
+
intervalMs = Math.max(intervalMs, outcome.retryAfterMs);
|
|
160
|
+
opts.onSlowDown?.(outcome.retryAfterMs);
|
|
161
|
+
continue;
|
|
139
162
|
case "denied":
|
|
140
163
|
throw new Error("Login was denied in the browser.");
|
|
141
164
|
case "expired":
|
|
@@ -160,19 +183,51 @@ function cliAuthHeaders(cliToken) {
|
|
|
160
183
|
Authorization: `Bearer ${cliToken}`
|
|
161
184
|
};
|
|
162
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
|
+
}
|
|
163
211
|
async function listProjects(endpoint, cliToken) {
|
|
164
212
|
const base = trimTrailingSlash(endpoint);
|
|
165
|
-
|
|
166
|
-
|
|
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
|
+
}
|
|
167
223
|
const json = await res.json().catch(() => null);
|
|
168
224
|
if (res.ok && json?.ok) return json.data?.projects ?? [];
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
225
|
+
throw new DeviceAuthRequestError(
|
|
226
|
+
"list_projects",
|
|
227
|
+
json?.error?.message ?? `Could not list projects (HTTP ${res.status}).`,
|
|
228
|
+
res.status
|
|
173
229
|
);
|
|
174
|
-
}
|
|
175
|
-
return [];
|
|
230
|
+
});
|
|
176
231
|
}
|
|
177
232
|
async function createProject(endpoint, cliToken, name) {
|
|
178
233
|
const base = trimTrailingSlash(endpoint);
|
|
@@ -190,17 +245,28 @@ async function createProject(endpoint, cliToken, name) {
|
|
|
190
245
|
}
|
|
191
246
|
async function mintProjectKey(endpoint, cliToken, projectId) {
|
|
192
247
|
const base = trimTrailingSlash(endpoint);
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
248
|
+
return withOneRetry(async () => {
|
|
249
|
+
let res;
|
|
250
|
+
try {
|
|
251
|
+
res = await deviceFetch(`${base}/v1/cli/projects/${projectId}/keys`, {
|
|
252
|
+
method: "POST",
|
|
253
|
+
headers: cliAuthHeaders(cliToken),
|
|
254
|
+
body: JSON.stringify({})
|
|
255
|
+
});
|
|
256
|
+
} catch (err) {
|
|
257
|
+
throw new DeviceAuthRequestError(
|
|
258
|
+
"mint_key",
|
|
259
|
+
`Could not reach ${base}/v1/cli/projects/\u2026/keys: ${err instanceof Error ? err.message : String(err)}`
|
|
260
|
+
);
|
|
261
|
+
}
|
|
199
262
|
const json = await res.json().catch(() => null);
|
|
200
|
-
if (res.ok && json?.ok
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
263
|
+
if (res.ok && json?.ok && json.data?.key) return json.data.key;
|
|
264
|
+
throw new DeviceAuthRequestError(
|
|
265
|
+
"mint_key",
|
|
266
|
+
json?.error?.message ?? `Could not mint an SDK key (HTTP ${res.status}).`,
|
|
267
|
+
res.ok ? void 0 : res.status
|
|
268
|
+
);
|
|
269
|
+
});
|
|
204
270
|
}
|
|
205
271
|
|
|
206
272
|
// src/freshness.ts
|
|
@@ -431,7 +497,10 @@ async function runInit(options = {}) {
|
|
|
431
497
|
const detected = detectFramework(cwd, pkg);
|
|
432
498
|
const framework = await chooseFramework(detected, options);
|
|
433
499
|
const consoleBase = await resolveConsoleUrl({ cwd });
|
|
434
|
-
const endpoint = resolveCloudEndpoint(
|
|
500
|
+
const endpoint = resolveCloudEndpoint(
|
|
501
|
+
options.endpoint ?? process.env.MUSHI_API_ENDPOINT?.trim() ?? loadConfig().endpoint
|
|
502
|
+
);
|
|
503
|
+
options = { ...options, endpoint };
|
|
435
504
|
const credentials = await acquireCredentials(options, consoleBase, endpoint);
|
|
436
505
|
await verifyCredentials(credentials, options, consoleBase);
|
|
437
506
|
const pm = detectPackageManager(cwd);
|
|
@@ -441,8 +510,16 @@ async function runInit(options = {}) {
|
|
|
441
510
|
} else {
|
|
442
511
|
p.log.info(`Skipped install. Run \`${installCommand(pm, packagesToInstall)}\` yourself.`);
|
|
443
512
|
}
|
|
444
|
-
await writeEnvFile(
|
|
513
|
+
await writeEnvFile(
|
|
514
|
+
cwd,
|
|
515
|
+
credentials.apiKey,
|
|
516
|
+
credentials.projectId,
|
|
517
|
+
framework,
|
|
518
|
+
endpoint,
|
|
519
|
+
Boolean(options.yes)
|
|
520
|
+
);
|
|
445
521
|
persistCliConfig(credentials.apiKey, credentials.projectId, endpoint);
|
|
522
|
+
emitWizardFunnelEvent(credentials, endpoint, "wizard_env_written", { framework: framework.id });
|
|
446
523
|
const enableRewards = await maybeEnableRewards(options);
|
|
447
524
|
printNextSteps(framework, consoleBase, enableRewards);
|
|
448
525
|
await maybeSendTestReport(credentials, { ...options, endpoint, consoleBase });
|
|
@@ -514,7 +591,9 @@ async function acquireCredentials(options, consoleBase, endpoint) {
|
|
|
514
591
|
if (options.yes) {
|
|
515
592
|
const creds = await runBrowserSignIn(options, endpoint, consoleBase);
|
|
516
593
|
if (creds) return creds;
|
|
517
|
-
p.log.warn(
|
|
594
|
+
p.log.warn(
|
|
595
|
+
"Browser sign-in didn't complete \u2014 switching to manual entry. Run `npx mushi-mushi doctor --auth` to diagnose the sign-in path."
|
|
596
|
+
);
|
|
518
597
|
} else {
|
|
519
598
|
const method = await p.select({
|
|
520
599
|
message: "Connect this app to Mushi",
|
|
@@ -539,17 +618,19 @@ async function acquireCredentials(options, consoleBase, endpoint) {
|
|
|
539
618
|
if (method === "browser") {
|
|
540
619
|
const creds = await runBrowserSignIn(options, endpoint, consoleBase);
|
|
541
620
|
if (creds) return creds;
|
|
542
|
-
p.log.warn(
|
|
621
|
+
p.log.warn(
|
|
622
|
+
"Browser sign-in didn't complete \u2014 switching to manual entry. Run `npx mushi-mushi doctor --auth` to diagnose the sign-in path."
|
|
623
|
+
);
|
|
543
624
|
}
|
|
544
625
|
}
|
|
545
|
-
return collectCredentialsManually(options, consoleBase);
|
|
626
|
+
return collectCredentialsManually(options, consoleBase, endpoint);
|
|
546
627
|
}
|
|
547
628
|
async function runBrowserSignIn(options, endpoint, consoleBase) {
|
|
548
629
|
const startSpin = p.spinner();
|
|
549
630
|
startSpin.start("Starting secure browser sign-in\u2026");
|
|
550
631
|
let session;
|
|
551
632
|
try {
|
|
552
|
-
session = await startDeviceAuth(endpoint);
|
|
633
|
+
session = await startDeviceAuth(endpoint, ensureClientId());
|
|
553
634
|
} catch (err) {
|
|
554
635
|
startSpin.stop("Could not start browser sign-in.");
|
|
555
636
|
p.log.warn(err instanceof Error ? err.message : String(err));
|
|
@@ -580,7 +661,9 @@ async function runBrowserSignIn(options, endpoint, consoleBase) {
|
|
|
580
661
|
fetchSpin.start("Loading your projects\u2026");
|
|
581
662
|
try {
|
|
582
663
|
projects = await listProjects(endpoint, cliToken);
|
|
583
|
-
fetchSpin.stop(
|
|
664
|
+
fetchSpin.stop(
|
|
665
|
+
projects.length > 0 ? `Found ${projects.length} project(s).` : "No projects yet."
|
|
666
|
+
);
|
|
584
667
|
} catch (err) {
|
|
585
668
|
fetchSpin.stop("Could not load projects \u2014 you can still create a new one.");
|
|
586
669
|
p.log.warn(err instanceof Error ? err.message : String(err));
|
|
@@ -628,34 +711,50 @@ async function runBrowserSignIn(options, endpoint, consoleBase) {
|
|
|
628
711
|
const keySpin = p.spinner();
|
|
629
712
|
keySpin.start("Minting SDK key\u2026");
|
|
630
713
|
try {
|
|
631
|
-
|
|
632
|
-
apiKey = minted ?? void 0;
|
|
633
|
-
if (!apiKey) {
|
|
634
|
-
keySpin.stop("Could not mint an API key.");
|
|
635
|
-
p.log.warn(
|
|
636
|
-
`Open the console Verify tab to generate a key manually: ${consoleUrl(consoleBase, "/onboarding?tab=verify")}`
|
|
637
|
-
);
|
|
638
|
-
return null;
|
|
639
|
-
}
|
|
714
|
+
apiKey = await mintProjectKey(endpoint, cliToken, projectId);
|
|
640
715
|
keySpin.stop("SDK key ready.");
|
|
641
716
|
} catch (err) {
|
|
642
717
|
keySpin.stop("Could not mint an API key.");
|
|
643
718
|
p.log.warn(err instanceof Error ? err.message : String(err));
|
|
719
|
+
p.log.warn(
|
|
720
|
+
`You're signed in, but key minting failed. Generate one manually in the console Verify tab: ${consoleUrl(consoleBase, "/onboarding?tab=verify")}`
|
|
721
|
+
);
|
|
644
722
|
return null;
|
|
645
723
|
}
|
|
646
724
|
}
|
|
647
725
|
if (!projectId || !apiKey) return null;
|
|
648
726
|
return { apiKey, projectId };
|
|
649
727
|
}
|
|
650
|
-
async function collectCredentialsManually(options, consoleBase) {
|
|
728
|
+
async function collectCredentialsManually(options, consoleBase, endpoint) {
|
|
651
729
|
const existing = loadConfig();
|
|
652
|
-
|
|
730
|
+
let savedProjectId = existing.projectId;
|
|
731
|
+
let savedApiKey = existing.apiKey;
|
|
732
|
+
if (!options.projectId && !options.apiKey && savedProjectId && savedApiKey) {
|
|
733
|
+
p.log.info(
|
|
734
|
+
`Found saved credentials from a previous sign-in (project ${sanitizeSecret(savedProjectId).slice(0, 8)}\u2026).`
|
|
735
|
+
);
|
|
736
|
+
const checkSpin = p.spinner();
|
|
737
|
+
checkSpin.start("Checking saved credentials\u2026");
|
|
738
|
+
const check = await apiCall("/v1/sync/whoami", {
|
|
739
|
+
apiKey: sanitizeSecret(savedApiKey),
|
|
740
|
+
projectId: sanitizeSecret(savedProjectId),
|
|
741
|
+
endpoint
|
|
742
|
+
});
|
|
743
|
+
if (check.ok) {
|
|
744
|
+
checkSpin.stop(`Saved credentials still work (${check.data.project_name}).`);
|
|
745
|
+
} else {
|
|
746
|
+
checkSpin.stop("Saved credentials no longer authenticate \u2014 enter fresh ones below.");
|
|
747
|
+
savedProjectId = void 0;
|
|
748
|
+
savedApiKey = void 0;
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
const rawProjectId = options.projectId ?? savedProjectId ?? await promptText({
|
|
653
752
|
message: "Project ID",
|
|
654
753
|
placeholder: "e.g. bdafa28d-b153-482f-bd4f-42981f3fd3a4",
|
|
655
754
|
hint: projectIdHint(consoleBase),
|
|
656
755
|
validate: (v) => PROJECT_ID_PATTERN.test(v.trim()) ? void 0 : "Expected a UUID \u2014 copy it from the Projects page or the panel right after you create a project."
|
|
657
756
|
});
|
|
658
|
-
const rawApiKey = options.apiKey ??
|
|
757
|
+
const rawApiKey = options.apiKey ?? savedApiKey ?? await promptText({
|
|
659
758
|
message: "API key",
|
|
660
759
|
placeholder: "mushi_xxxxxxxxxxxx",
|
|
661
760
|
hint: apiKeyHint(consoleBase),
|
|
@@ -679,17 +778,30 @@ async function verifyCredentials(credentials, options, consoleBase) {
|
|
|
679
778
|
const endpoint = resolveCloudEndpoint(options.endpoint);
|
|
680
779
|
const spinner3 = p.spinner();
|
|
681
780
|
spinner3.start("Verifying credentials\u2026");
|
|
682
|
-
const result = await apiCall(
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
781
|
+
const result = await apiCall("/v1/sync/whoami", {
|
|
782
|
+
apiKey: credentials.apiKey,
|
|
783
|
+
projectId: credentials.projectId,
|
|
784
|
+
endpoint
|
|
785
|
+
});
|
|
686
786
|
if (!result.ok) {
|
|
687
787
|
spinner3.stop("Credentials could not be verified.");
|
|
688
788
|
p.log.error(result.error?.message ?? "Authentication failed.");
|
|
689
|
-
p.log.
|
|
690
|
-
|
|
789
|
+
p.log.warn("Setup did NOT complete: nothing was installed and no env vars were written.");
|
|
790
|
+
p.log.info(
|
|
791
|
+
[
|
|
792
|
+
"To recover:",
|
|
793
|
+
' \u2022 Re-run `npx mushi-mushi` and choose "Sign in with your browser", or',
|
|
794
|
+
` \u2022 Double-check the Project ID / API key in the console: ${cliSetupDeepLink(consoleBase)}`
|
|
795
|
+
].join("\n")
|
|
796
|
+
);
|
|
691
797
|
throw new Error("Credential verification failed \u2014 fix Project ID / API key and re-run.");
|
|
692
798
|
}
|
|
799
|
+
if (result.data.project_id && result.data.project_id !== credentials.projectId) {
|
|
800
|
+
p.log.warn(
|
|
801
|
+
`Project ID ${redact(credentials.projectId)} doesn't match this API key's project \u2014 using the key's actual project (${result.data.project_name}) instead.`
|
|
802
|
+
);
|
|
803
|
+
credentials.projectId = result.data.project_id;
|
|
804
|
+
}
|
|
693
805
|
spinner3.stop(`Connected to ${result.data.project_name}`);
|
|
694
806
|
}
|
|
695
807
|
function sanitizeSecret(raw) {
|
|
@@ -825,6 +937,20 @@ function persistCliConfig(apiKey, projectId, endpoint) {
|
|
|
825
937
|
const existing = loadConfig();
|
|
826
938
|
saveConfig({ ...existing, apiKey, projectId, endpoint });
|
|
827
939
|
}
|
|
940
|
+
function emitWizardFunnelEvent(credentials, endpoint, event, metadata = {}) {
|
|
941
|
+
if (process.env.MUSHI_NO_TELEMETRY) return;
|
|
942
|
+
void fetch(`${endpoint.replace(/\/$/, "")}/v1/cli/funnel`, {
|
|
943
|
+
method: "POST",
|
|
944
|
+
headers: {
|
|
945
|
+
"Content-Type": "application/json",
|
|
946
|
+
"X-Mushi-Api-Key": credentials.apiKey,
|
|
947
|
+
"X-Mushi-Project": credentials.projectId
|
|
948
|
+
},
|
|
949
|
+
body: JSON.stringify({ event, source: "cli", metadata }),
|
|
950
|
+
signal: AbortSignal.timeout(4e3)
|
|
951
|
+
}).catch(() => {
|
|
952
|
+
});
|
|
953
|
+
}
|
|
828
954
|
function printNextSteps(framework, consoleBase, enableRewards = false) {
|
|
829
955
|
p.note(framework.snippet(), "Add this to your app:");
|
|
830
956
|
if (enableRewards) {
|
|
@@ -858,7 +984,7 @@ async function maybeOfferConnect(credentials, options, _consoleBase) {
|
|
|
858
984
|
if (p.isCancel(answer) || !answer) return;
|
|
859
985
|
const endpoint = resolveCloudEndpoint(options.endpoint);
|
|
860
986
|
try {
|
|
861
|
-
const { runConnect: runConnect2 } = await import("./connect-
|
|
987
|
+
const { runConnect: runConnect2 } = await import("./connect-KKN62YK2.js");
|
|
862
988
|
await runConnect2({
|
|
863
989
|
apiKey: credentials.apiKey,
|
|
864
990
|
projectId: credentials.projectId,
|
|
@@ -869,12 +995,8 @@ async function maybeOfferConnect(credentials, options, _consoleBase) {
|
|
|
869
995
|
wait: true
|
|
870
996
|
});
|
|
871
997
|
} catch (err) {
|
|
872
|
-
p.log.warn(
|
|
873
|
-
|
|
874
|
-
);
|
|
875
|
-
p.log.info(
|
|
876
|
-
"You can run manually: mushi connect --write-env --wire-ide --wait"
|
|
877
|
-
);
|
|
998
|
+
p.log.warn(err instanceof Error ? err.message : String(err));
|
|
999
|
+
p.log.info("You can run manually: mushi connect --write-env --wire-ide --wait");
|
|
878
1000
|
}
|
|
879
1001
|
}
|
|
880
1002
|
async function maybeEnableRewards(options) {
|
|
@@ -1201,7 +1323,7 @@ Examples:
|
|
|
1201
1323
|
}
|
|
1202
1324
|
let deviceData;
|
|
1203
1325
|
try {
|
|
1204
|
-
deviceData = await startDeviceAuth(endpoint);
|
|
1326
|
+
deviceData = await startDeviceAuth(endpoint, ensureClientId());
|
|
1205
1327
|
} catch (err) {
|
|
1206
1328
|
process.stderr.write(`
|
|
1207
1329
|
error: Could not start login session: ${err instanceof Error ? err.message : String(err)}
|
|
@@ -2208,7 +2330,7 @@ Typical first-time flow:
|
|
|
2208
2330
|
console.log("");
|
|
2209
2331
|
let session;
|
|
2210
2332
|
try {
|
|
2211
|
-
session = await startDeviceAuth(endpoint);
|
|
2333
|
+
session = await startDeviceAuth(endpoint, ensureClientId());
|
|
2212
2334
|
} catch (err) {
|
|
2213
2335
|
process.stderr.write(`
|
|
2214
2336
|
error: Could not start browser sign-in: ${err instanceof Error ? err.message : String(err)}
|
|
@@ -3141,6 +3263,88 @@ async function checkEndpointReachability(endpoint, doFetch = globalThis.fetch) {
|
|
|
3141
3263
|
return { name: "Endpoint reachable", ok: false, detail: `Fetch failed: ${msg}` };
|
|
3142
3264
|
}
|
|
3143
3265
|
}
|
|
3266
|
+
async function checkCliAuthPath(config, doFetch = globalThis.fetch) {
|
|
3267
|
+
const checks = [];
|
|
3268
|
+
if (!config.endpoint) {
|
|
3269
|
+
return [
|
|
3270
|
+
{
|
|
3271
|
+
name: "Sign-in route reachable",
|
|
3272
|
+
ok: false,
|
|
3273
|
+
detail: "No endpoint configured \u2014 run `npx mushi-mushi` or pass MUSHI_API_ENDPOINT."
|
|
3274
|
+
}
|
|
3275
|
+
];
|
|
3276
|
+
}
|
|
3277
|
+
const base = sanitizeEndpoint(config.endpoint);
|
|
3278
|
+
let dateHeader = null;
|
|
3279
|
+
try {
|
|
3280
|
+
const res = await doFetch(`${base}/v1/cli/auth/device/token`, {
|
|
3281
|
+
method: "POST",
|
|
3282
|
+
headers: { "Content-Type": "application/json" },
|
|
3283
|
+
body: JSON.stringify({ device_code: "doctor-probe" }),
|
|
3284
|
+
signal: AbortSignal.timeout(8e3)
|
|
3285
|
+
});
|
|
3286
|
+
dateHeader = res.headers?.get?.("date") ?? null;
|
|
3287
|
+
const signInRouteReachable = res.status === 400 || res.status === 429;
|
|
3288
|
+
checks.push({
|
|
3289
|
+
name: "Sign-in route reachable",
|
|
3290
|
+
ok: signInRouteReachable,
|
|
3291
|
+
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.`
|
|
3292
|
+
});
|
|
3293
|
+
} catch (err) {
|
|
3294
|
+
checks.push({
|
|
3295
|
+
name: "Sign-in route reachable",
|
|
3296
|
+
ok: false,
|
|
3297
|
+
detail: `Fetch failed: ${err instanceof Error ? err.message : String(err)} \u2014 check network/proxy/firewall to ${base}.`
|
|
3298
|
+
});
|
|
3299
|
+
}
|
|
3300
|
+
if (dateHeader) {
|
|
3301
|
+
const serverMs = Date.parse(dateHeader);
|
|
3302
|
+
if (!Number.isNaN(serverMs)) {
|
|
3303
|
+
const skewSec = Math.round(Math.abs(Date.now() - serverMs) / 1e3);
|
|
3304
|
+
const ok = skewSec <= 120;
|
|
3305
|
+
checks.push({
|
|
3306
|
+
name: "System clock in sync",
|
|
3307
|
+
ok,
|
|
3308
|
+
warn: !ok && skewSec <= 300,
|
|
3309
|
+
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.`
|
|
3310
|
+
});
|
|
3311
|
+
}
|
|
3312
|
+
}
|
|
3313
|
+
if (config.apiKey && config.projectId) {
|
|
3314
|
+
try {
|
|
3315
|
+
const res = await doFetch(`${base}/v1/sync/whoami`, {
|
|
3316
|
+
method: "POST",
|
|
3317
|
+
headers: {
|
|
3318
|
+
"Content-Type": "application/json",
|
|
3319
|
+
...apiKeyHeaders(config.apiKey, config.projectId)
|
|
3320
|
+
},
|
|
3321
|
+
body: JSON.stringify({}),
|
|
3322
|
+
signal: AbortSignal.timeout(8e3)
|
|
3323
|
+
});
|
|
3324
|
+
const json = await res.json().catch(() => null);
|
|
3325
|
+
const ok = res.ok && json?.ok === true;
|
|
3326
|
+
checks.push({
|
|
3327
|
+
name: "Saved credentials valid",
|
|
3328
|
+
ok,
|
|
3329
|
+
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.`
|
|
3330
|
+
});
|
|
3331
|
+
} catch (err) {
|
|
3332
|
+
checks.push({
|
|
3333
|
+
name: "Saved credentials valid",
|
|
3334
|
+
ok: false,
|
|
3335
|
+
detail: `whoami fetch failed: ${err instanceof Error ? err.message : String(err)}`
|
|
3336
|
+
});
|
|
3337
|
+
}
|
|
3338
|
+
} else {
|
|
3339
|
+
checks.push({
|
|
3340
|
+
name: "Saved credentials valid",
|
|
3341
|
+
ok: true,
|
|
3342
|
+
warn: true,
|
|
3343
|
+
detail: "No saved CLI credentials yet \u2014 normal before the first successful sign-in."
|
|
3344
|
+
});
|
|
3345
|
+
}
|
|
3346
|
+
return checks;
|
|
3347
|
+
}
|
|
3144
3348
|
async function checkSdkInstall(cwd) {
|
|
3145
3349
|
try {
|
|
3146
3350
|
const { readFile: readFile3 } = await import("fs/promises");
|
|
@@ -3177,13 +3381,10 @@ async function checkServerPreflight(config, doFetch = globalThis.fetch) {
|
|
|
3177
3381
|
}
|
|
3178
3382
|
try {
|
|
3179
3383
|
const { endpoint, apiKey, projectId } = sanitizeCliCredentials(config);
|
|
3180
|
-
const res = await doFetch(
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
signal: AbortSignal.timeout(8e3)
|
|
3185
|
-
}
|
|
3186
|
-
);
|
|
3384
|
+
const res = await doFetch(`${endpoint}/v1/admin/projects/${projectId}/preflight`, {
|
|
3385
|
+
headers: apiKeyHeaders(apiKey, projectId),
|
|
3386
|
+
signal: AbortSignal.timeout(8e3)
|
|
3387
|
+
});
|
|
3187
3388
|
if (res.ok) {
|
|
3188
3389
|
const body = await res.json();
|
|
3189
3390
|
const serverChecks = body.data?.checks ?? [];
|
|
@@ -3239,7 +3440,13 @@ async function checkIngestSetup(config, doFetch = globalThis.fetch) {
|
|
|
3239
3440
|
doFetch
|
|
3240
3441
|
);
|
|
3241
3442
|
if (!data) {
|
|
3242
|
-
return [
|
|
3443
|
+
return [
|
|
3444
|
+
{
|
|
3445
|
+
name: "Ingest setup",
|
|
3446
|
+
ok: false,
|
|
3447
|
+
detail: "Request to /v1/sync/ingest-setup failed or returned invalid payload"
|
|
3448
|
+
}
|
|
3449
|
+
];
|
|
3243
3450
|
}
|
|
3244
3451
|
const steps = data.steps ?? [];
|
|
3245
3452
|
const checks = steps.filter((s) => s.required).map((s) => ({
|
|
@@ -3275,22 +3482,27 @@ async function checkQaStoriesHealth(config, doFetch = globalThis.fetch) {
|
|
|
3275
3482
|
try {
|
|
3276
3483
|
const { endpoint, apiKey, projectId } = sanitizeCliCredentials(config);
|
|
3277
3484
|
const headers = apiKeyHeaders(apiKey, projectId);
|
|
3278
|
-
const storiesRes = await doFetch(
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
signal: AbortSignal.timeout(8e3)
|
|
3283
|
-
}
|
|
3284
|
-
);
|
|
3485
|
+
const storiesRes = await doFetch(`${endpoint}/v1/admin/projects/${projectId}/qa-coverage`, {
|
|
3486
|
+
headers,
|
|
3487
|
+
signal: AbortSignal.timeout(8e3)
|
|
3488
|
+
});
|
|
3285
3489
|
if (!storiesRes.ok) {
|
|
3286
|
-
checks.push({
|
|
3490
|
+
checks.push({
|
|
3491
|
+
name: "[qa] Fetch QA stories",
|
|
3492
|
+
ok: false,
|
|
3493
|
+
detail: `HTTP ${storiesRes.status}`
|
|
3494
|
+
});
|
|
3287
3495
|
return checks;
|
|
3288
3496
|
}
|
|
3289
3497
|
const storiesBody = await storiesRes.json();
|
|
3290
3498
|
const stories = storiesBody.data?.coverage ?? [];
|
|
3291
3499
|
const enabled = stories.filter((s) => s.enabled);
|
|
3292
3500
|
if (enabled.length === 0) {
|
|
3293
|
-
checks.push({
|
|
3501
|
+
checks.push({
|
|
3502
|
+
name: "[qa] Enabled QA stories",
|
|
3503
|
+
ok: true,
|
|
3504
|
+
detail: "No enabled stories \u2014 create one at /qa-coverage"
|
|
3505
|
+
});
|
|
3294
3506
|
return checks;
|
|
3295
3507
|
}
|
|
3296
3508
|
checks.push({
|
|
@@ -3359,7 +3571,9 @@ async function checkHostAppWiring(cwd) {
|
|
|
3359
3571
|
}
|
|
3360
3572
|
}
|
|
3361
3573
|
const hasProjectId = /VITE_MUSHI_PROJECT_ID=|NEXT_PUBLIC_MUSHI_PROJECT_ID=|MUSHI_PROJECT_ID=/.test(envContent);
|
|
3362
|
-
const hasApiKey = /VITE_MUSHI_API_KEY=|NEXT_PUBLIC_MUSHI_API_KEY=|MUSHI_API_KEY=/.test(
|
|
3574
|
+
const hasApiKey = /VITE_MUSHI_API_KEY=|NEXT_PUBLIC_MUSHI_API_KEY=|MUSHI_API_KEY=/.test(
|
|
3575
|
+
envContent
|
|
3576
|
+
);
|
|
3363
3577
|
checks.push({
|
|
3364
3578
|
name: "[host] Mushi env vars in .env.local",
|
|
3365
3579
|
ok: hasProjectId && hasApiKey,
|
|
@@ -3404,10 +3618,7 @@ async function checkMcpConfig(config, cwd, doFetch = globalThis.fetch) {
|
|
|
3404
3618
|
const { join: join4, resolve: resolve3 } = await import("path");
|
|
3405
3619
|
const { homedir } = await import("os");
|
|
3406
3620
|
const root = resolve3(cwd);
|
|
3407
|
-
const candidates = [
|
|
3408
|
-
join4(root, ".cursor", "mcp.json"),
|
|
3409
|
-
join4(homedir(), ".cursor", "mcp.json")
|
|
3410
|
-
];
|
|
3621
|
+
const candidates = [join4(root, ".cursor", "mcp.json"), join4(homedir(), ".cursor", "mcp.json")];
|
|
3411
3622
|
let mcpPath = null;
|
|
3412
3623
|
let mcpRaw = null;
|
|
3413
3624
|
for (const candidate of candidates) {
|
|
@@ -3445,11 +3656,17 @@ async function checkMcpConfig(config, cwd, doFetch = globalThis.fetch) {
|
|
|
3445
3656
|
try {
|
|
3446
3657
|
mcpConfig = JSON.parse(mcpRaw);
|
|
3447
3658
|
} catch {
|
|
3448
|
-
checks.push({
|
|
3659
|
+
checks.push({
|
|
3660
|
+
name: "[mcp] mcp.json valid JSON",
|
|
3661
|
+
ok: false,
|
|
3662
|
+
detail: "mcp.json is not valid JSON \u2014 regenerate with `mushi setup`."
|
|
3663
|
+
});
|
|
3449
3664
|
return checks;
|
|
3450
3665
|
}
|
|
3451
3666
|
const servers = mcpConfig.mcpServers ?? {};
|
|
3452
|
-
const mushiEntries = Object.entries(servers).filter(
|
|
3667
|
+
const mushiEntries = Object.entries(servers).filter(
|
|
3668
|
+
([k]) => k === "mushi" || k.startsWith("mushi-")
|
|
3669
|
+
);
|
|
3453
3670
|
if (mushiEntries.length === 0) {
|
|
3454
3671
|
checks.push({
|
|
3455
3672
|
name: "[mcp] mushi server entry",
|
|
@@ -3569,7 +3786,9 @@ async function checkOnboardingStatus(config, _consoleBase, cwd, doFetch = global
|
|
|
3569
3786
|
} catch {
|
|
3570
3787
|
}
|
|
3571
3788
|
}
|
|
3572
|
-
const hasEnvVars = /(?:VITE_|NEXT_PUBLIC_|EXPO_PUBLIC_|NUXT_PUBLIC_)?MUSHI_PROJECT_ID=/.test(
|
|
3789
|
+
const hasEnvVars = /(?:VITE_|NEXT_PUBLIC_|EXPO_PUBLIC_|NUXT_PUBLIC_)?MUSHI_PROJECT_ID=/.test(
|
|
3790
|
+
envContent
|
|
3791
|
+
);
|
|
3573
3792
|
if (!hasEnvVars) {
|
|
3574
3793
|
return {
|
|
3575
3794
|
nextAction: "Write env vars to your app: run `npx mushi-mushi` or `mushi connect --write-env`",
|
|
@@ -3658,6 +3877,9 @@ async function runDoctor(config, options = {}) {
|
|
|
3658
3877
|
const mcpChecks = await checkMcpConfig(config, options.cwd ?? process.cwd(), doFetch);
|
|
3659
3878
|
checks.push(...mcpChecks);
|
|
3660
3879
|
}
|
|
3880
|
+
if (isFull || options.auth) {
|
|
3881
|
+
checks.push(...await checkCliAuthPath(config, doFetch));
|
|
3882
|
+
}
|
|
3661
3883
|
const heartbeatPassed = checks.some((c) => c.name === "[ingest] Last SDK heartbeat" && c.ok);
|
|
3662
3884
|
if (heartbeatPassed) {
|
|
3663
3885
|
const idx = checks.findIndex((c) => c.name === "SDK installed in this repo" && !c.ok);
|
|
@@ -3690,8 +3912,10 @@ function formatDoctorResult(result) {
|
|
|
3690
3912
|
const warned = result.checks.filter((c) => c.ok && c.warn);
|
|
3691
3913
|
if (failed.length === 0) {
|
|
3692
3914
|
if (warned.length > 0) {
|
|
3693
|
-
lines.push(
|
|
3694
|
-
|
|
3915
|
+
lines.push(
|
|
3916
|
+
`
|
|
3917
|
+
All checks passed with ${warned.length} advisory warning${warned.length === 1 ? "" : "s"}. The CLI is ready.`
|
|
3918
|
+
);
|
|
3695
3919
|
} else {
|
|
3696
3920
|
lines.push("\nAll checks passed. The CLI is ready.");
|
|
3697
3921
|
}
|
|
@@ -3716,6 +3940,9 @@ function registerDoctorCommand(program2) {
|
|
|
3716
3940
|
).option(
|
|
3717
3941
|
"--mcp",
|
|
3718
3942
|
"Check Cursor MCP config: verify mushi-* server entry, credentials, and probe account-overview connectivity."
|
|
3943
|
+
).option(
|
|
3944
|
+
"--auth",
|
|
3945
|
+
"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."
|
|
3719
3946
|
).option(
|
|
3720
3947
|
"--fix",
|
|
3721
3948
|
"Apply safe local fixes when checks fail: write missing .env.local lines and wire Cursor MCP config."
|
|
@@ -3725,54 +3952,60 @@ function registerDoctorCommand(program2) {
|
|
|
3725
3952
|
).option(
|
|
3726
3953
|
"--full",
|
|
3727
3954
|
"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."
|
|
3728
|
-
).action(
|
|
3729
|
-
|
|
3730
|
-
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
|
|
3735
|
-
|
|
3736
|
-
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3955
|
+
).action(
|
|
3956
|
+
async (opts) => {
|
|
3957
|
+
const config = loadConfig();
|
|
3958
|
+
if (opts.onboarding) {
|
|
3959
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
3960
|
+
const consoleBase = await resolveConsoleUrl({ cwd });
|
|
3961
|
+
const status = await checkOnboardingStatus(config, consoleBase, cwd);
|
|
3962
|
+
const base = consoleBase.replace(/\/$/, "");
|
|
3963
|
+
if (status.done) {
|
|
3964
|
+
console.log(`OK Setup complete. ${status.nextAction}`);
|
|
3965
|
+
console.log(` ${base}${status.ctaPath}`);
|
|
3966
|
+
} else {
|
|
3967
|
+
console.log(`\u2192 Next: ${status.nextAction}`);
|
|
3968
|
+
console.log(` Open: ${base}${status.ctaPath}`);
|
|
3969
|
+
}
|
|
3970
|
+
if (!status.done) process.exit(1);
|
|
3971
|
+
return;
|
|
3741
3972
|
}
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
|
|
3746
|
-
|
|
3747
|
-
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
3973
|
+
const doctorOpts = {
|
|
3974
|
+
cwd: opts.cwd,
|
|
3975
|
+
server: opts.server,
|
|
3976
|
+
ingest: opts.ingest,
|
|
3977
|
+
qaStories: opts.qaStories,
|
|
3978
|
+
hostApp: opts.hostApp,
|
|
3979
|
+
mcp: opts.mcp,
|
|
3980
|
+
auth: opts.auth,
|
|
3981
|
+
full: opts.full
|
|
3982
|
+
};
|
|
3983
|
+
let result = await runDoctor(config, doctorOpts);
|
|
3984
|
+
if (!result.ready && opts.fix && config.apiKey && config.projectId && config.endpoint) {
|
|
3985
|
+
const connectResult = await runConnect(
|
|
3986
|
+
{
|
|
3987
|
+
apiKey: config.apiKey,
|
|
3988
|
+
projectId: config.projectId,
|
|
3989
|
+
endpoint: config.endpoint,
|
|
3990
|
+
cwd: opts.cwd ?? process.cwd(),
|
|
3991
|
+
writeEnv: true,
|
|
3992
|
+
wireIde: true
|
|
3993
|
+
},
|
|
3994
|
+
config
|
|
3995
|
+
);
|
|
3996
|
+
for (const msg of connectResult.messages) console.log(msg);
|
|
3997
|
+
result = await runDoctor(config, { ...doctorOpts, mcp: opts.mcp });
|
|
3998
|
+
}
|
|
3999
|
+
const { checks } = result;
|
|
4000
|
+
if (opts.json) {
|
|
4001
|
+
console.log(JSON.stringify({ checks, ready: result.ready }, null, 2));
|
|
4002
|
+
if (!result.ready) process.exit(1);
|
|
4003
|
+
return;
|
|
4004
|
+
}
|
|
4005
|
+
console.log(formatDoctorResult(result));
|
|
3770
4006
|
if (!result.ready) process.exit(1);
|
|
3771
|
-
return;
|
|
3772
4007
|
}
|
|
3773
|
-
|
|
3774
|
-
if (!result.ready) process.exit(1);
|
|
3775
|
-
});
|
|
4008
|
+
);
|
|
3776
4009
|
}
|
|
3777
4010
|
|
|
3778
4011
|
// src/commands/reset-cli.ts
|
|
@@ -4789,6 +5022,131 @@ Examples:
|
|
|
4789
5022
|
});
|
|
4790
5023
|
}
|
|
4791
5024
|
|
|
5025
|
+
// src/commands/completion-cli.ts
|
|
5026
|
+
import { Argument } from "commander";
|
|
5027
|
+
|
|
5028
|
+
// src/completion.ts
|
|
5029
|
+
var SUPPORTED_SHELLS = ["bash", "zsh", "fish"];
|
|
5030
|
+
function isSupportedShell(value) {
|
|
5031
|
+
return SUPPORTED_SHELLS.includes(value);
|
|
5032
|
+
}
|
|
5033
|
+
function buildCommandTree(program2) {
|
|
5034
|
+
return program2.commands.filter((cmd) => cmd.name() !== "help").map((cmd) => ({
|
|
5035
|
+
name: cmd.name(),
|
|
5036
|
+
subcommands: cmd.commands.filter((sub) => sub.name() !== "help").map((sub) => sub.name()).sort()
|
|
5037
|
+
})).sort((a, b) => a.name.localeCompare(b.name));
|
|
5038
|
+
}
|
|
5039
|
+
var COMPLETION_ARG_VALUES = [...SUPPORTED_SHELLS];
|
|
5040
|
+
function subcommandsFor(tree, name) {
|
|
5041
|
+
if (name === "completion") return COMPLETION_ARG_VALUES;
|
|
5042
|
+
return tree.find((n) => n.name === name)?.subcommands ?? [];
|
|
5043
|
+
}
|
|
5044
|
+
function generateBash(tree) {
|
|
5045
|
+
const topLevel = tree.map((n) => n.name).join(" ");
|
|
5046
|
+
const cases = tree.map((n) => {
|
|
5047
|
+
const subs = subcommandsFor(tree, n.name);
|
|
5048
|
+
if (subs.length === 0) return null;
|
|
5049
|
+
return ` ${n.name})
|
|
5050
|
+
COMPREPLY=( $(compgen -W "${subs.join(" ")}" -- "$cur") )
|
|
5051
|
+
;;`;
|
|
5052
|
+
}).filter((line) => line !== null).join("\n");
|
|
5053
|
+
return `###-begin-mushi-completion-###
|
|
5054
|
+
# mushi(1) completion \u2014 https://github.com/kensaurus/mushi-mushi
|
|
5055
|
+
# Install: mushi completion bash >> ~/.bashrc (then restart your shell)
|
|
5056
|
+
_mushi_completion() {
|
|
5057
|
+
local cur
|
|
5058
|
+
cur="\${COMP_WORDS[COMP_CWORD]}"
|
|
5059
|
+
COMPREPLY=()
|
|
5060
|
+
|
|
5061
|
+
if [ "$COMP_CWORD" -eq 1 ]; then
|
|
5062
|
+
COMPREPLY=( $(compgen -W "${topLevel}" -- "$cur") )
|
|
5063
|
+
return 0
|
|
5064
|
+
fi
|
|
5065
|
+
|
|
5066
|
+
case "\${COMP_WORDS[1]}" in
|
|
5067
|
+
${cases}
|
|
5068
|
+
*)
|
|
5069
|
+
;;
|
|
5070
|
+
esac
|
|
5071
|
+
}
|
|
5072
|
+
complete -F _mushi_completion mushi
|
|
5073
|
+
###-end-mushi-completion-###
|
|
5074
|
+
`;
|
|
5075
|
+
}
|
|
5076
|
+
function generateZsh(tree) {
|
|
5077
|
+
const topLevel = tree.map((n) => n.name).join(" ");
|
|
5078
|
+
const cases = tree.map((n) => {
|
|
5079
|
+
const subs = subcommandsFor(tree, n.name);
|
|
5080
|
+
if (subs.length === 0) return null;
|
|
5081
|
+
return ` ${n.name})
|
|
5082
|
+
local -a subcmds; subcmds=(${subs.join(" ")})
|
|
5083
|
+
_describe '${n.name} subcommand' subcmds
|
|
5084
|
+
;;`;
|
|
5085
|
+
}).filter((line) => line !== null).join("\n");
|
|
5086
|
+
return `#compdef mushi
|
|
5087
|
+
# mushi(1) completion \u2014 https://github.com/kensaurus/mushi-mushi
|
|
5088
|
+
# Install: mushi completion zsh > "\${fpath[1]}/_mushi" (then restart your shell)
|
|
5089
|
+
_mushi() {
|
|
5090
|
+
local -a top_level
|
|
5091
|
+
top_level=(${topLevel})
|
|
5092
|
+
|
|
5093
|
+
if (( CURRENT == 2 )); then
|
|
5094
|
+
_describe 'command' top_level
|
|
5095
|
+
return
|
|
5096
|
+
fi
|
|
5097
|
+
|
|
5098
|
+
case "\${words[2]}" in
|
|
5099
|
+
${cases}
|
|
5100
|
+
*)
|
|
5101
|
+
;;
|
|
5102
|
+
esac
|
|
5103
|
+
}
|
|
5104
|
+
|
|
5105
|
+
_mushi
|
|
5106
|
+
`;
|
|
5107
|
+
}
|
|
5108
|
+
function generateFish(tree) {
|
|
5109
|
+
const topLevel = tree.map((n) => n.name).join(" ");
|
|
5110
|
+
const lines = [
|
|
5111
|
+
"# mushi(1) completion \u2014 https://github.com/kensaurus/mushi-mushi",
|
|
5112
|
+
"# Install: mushi completion fish > ~/.config/fish/completions/mushi.fish",
|
|
5113
|
+
`complete -c mushi -f -n "__fish_use_subcommand" -a "${topLevel}"`
|
|
5114
|
+
];
|
|
5115
|
+
for (const n of tree) {
|
|
5116
|
+
const subs = subcommandsFor(tree, n.name);
|
|
5117
|
+
if (subs.length === 0) continue;
|
|
5118
|
+
lines.push(`complete -c mushi -f -n "__fish_seen_subcommand_from ${n.name}" -a "${subs.join(" ")}"`);
|
|
5119
|
+
}
|
|
5120
|
+
return lines.join("\n") + "\n";
|
|
5121
|
+
}
|
|
5122
|
+
function generateCompletionScript(shell, tree) {
|
|
5123
|
+
switch (shell) {
|
|
5124
|
+
case "bash":
|
|
5125
|
+
return generateBash(tree);
|
|
5126
|
+
case "zsh":
|
|
5127
|
+
return generateZsh(tree);
|
|
5128
|
+
case "fish":
|
|
5129
|
+
return generateFish(tree);
|
|
5130
|
+
}
|
|
5131
|
+
}
|
|
5132
|
+
|
|
5133
|
+
// src/commands/completion-cli.ts
|
|
5134
|
+
function registerCompletionCommand(program2) {
|
|
5135
|
+
program2.command("completion").description("Print a shell completion script for bash, zsh, or fish").addArgument(new Argument("<shell>", "Target shell").choices([...SUPPORTED_SHELLS])).addHelpText("after", `
|
|
5136
|
+
Examples:
|
|
5137
|
+
eval "$(mushi completion bash)" # try it for the current session
|
|
5138
|
+
mushi completion bash >> ~/.bashrc # bash: persist across sessions
|
|
5139
|
+
mushi completion zsh > "\${fpath[1]}/_mushi" # zsh: needs a dir already on $fpath
|
|
5140
|
+
mushi completion fish > ~/.config/fish/completions/mushi.fish`).action((shell) => {
|
|
5141
|
+
if (!isSupportedShell(shell)) {
|
|
5142
|
+
console.error(`Unsupported shell "${shell}". Supported: ${SUPPORTED_SHELLS.join(", ")}`);
|
|
5143
|
+
process.exit(2);
|
|
5144
|
+
}
|
|
5145
|
+
const tree = buildCommandTree(program2);
|
|
5146
|
+
console.log(generateCompletionScript(shell, tree));
|
|
5147
|
+
});
|
|
5148
|
+
}
|
|
5149
|
+
|
|
4792
5150
|
// src/index.ts
|
|
4793
5151
|
installSignalHandlers();
|
|
4794
5152
|
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", `
|
|
@@ -4827,6 +5185,7 @@ registerQaCommands(program);
|
|
|
4827
5185
|
registerAuditCommands(program);
|
|
4828
5186
|
registerSkillsCommands(program);
|
|
4829
5187
|
registerBillingCommands(program);
|
|
5188
|
+
registerCompletionCommand(program);
|
|
4830
5189
|
program.parseAsync().catch((err) => {
|
|
4831
5190
|
printAndExit(err);
|
|
4832
5191
|
});
|