@mushi-mushi/cli 0.22.1 → 0.23.1
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 +28 -2
- package/dist/{chunk-JAUKBLPK.js → chunk-AIZRI665.js} +102 -2
- package/dist/chunk-I4UK3WTN.js +6 -0
- package/dist/{chunk-R2HPR2FV.js → chunk-M6L22NJU.js} +534 -27
- package/dist/chunk-NZ64LBLU.js +437 -0
- package/dist/{connect-JIEIWTF3.js → connect-5VPUYE6H.js} +1 -1
- package/dist/{connect-2RM43XHO.js → connect-IWFKKXGZ.js} +10 -7
- package/dist/detect.d.ts +1 -1
- package/dist/detect.js +1 -1
- package/dist/index.js +269 -423
- package/dist/init.js +106 -255
- package/dist/version.js +1 -1
- package/package.json +12 -14
- package/dist/chunk-XEXYGDQV.js +0 -137
- package/dist/chunk-YK4LF7AM.js +0 -6
package/dist/index.js
CHANGED
|
@@ -1,16 +1,35 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
FRAMEWORKS,
|
|
4
|
+
MUSHI_CLI_VERSION,
|
|
5
|
+
apiCall,
|
|
4
6
|
apiKeyHeaders,
|
|
7
|
+
apiKeyHint,
|
|
5
8
|
buildMcpServerBlock,
|
|
6
9
|
buildMcpServerName,
|
|
10
|
+
cliSetupDeepLink,
|
|
11
|
+
consoleUrl,
|
|
7
12
|
detectFramework,
|
|
8
13
|
detectPackageManager,
|
|
14
|
+
die,
|
|
15
|
+
ensureClientId,
|
|
9
16
|
envVarsToWrite,
|
|
10
17
|
fetchIngestSetup,
|
|
18
|
+
fmtDate,
|
|
11
19
|
installCommand,
|
|
20
|
+
installSignalHandlers,
|
|
12
21
|
loadConfig,
|
|
22
|
+
openInBrowser,
|
|
23
|
+
pad,
|
|
24
|
+
printAndExit,
|
|
25
|
+
probeEndpointHealth,
|
|
26
|
+
projectIdHint,
|
|
13
27
|
readPackageJson,
|
|
28
|
+
reportsUrl,
|
|
29
|
+
requireConfig,
|
|
30
|
+
requireUuid,
|
|
31
|
+
resolveConsoleUrl,
|
|
32
|
+
resolveConsoleUrlSync,
|
|
14
33
|
runConnect,
|
|
15
34
|
sanitizeApiKey,
|
|
16
35
|
sanitizeCliCredentials,
|
|
@@ -18,9 +37,8 @@ import {
|
|
|
18
37
|
sanitizeProjectId,
|
|
19
38
|
saveConfig,
|
|
20
39
|
writeMcpServerEntry
|
|
21
|
-
} from "./chunk-
|
|
40
|
+
} from "./chunk-M6L22NJU.js";
|
|
22
41
|
import {
|
|
23
|
-
CLOUD_API_ENDPOINT,
|
|
24
42
|
TEST_REPORT_FETCH_TIMEOUT_MS,
|
|
25
43
|
assertEndpoint,
|
|
26
44
|
normalizeEndpoint,
|
|
@@ -30,372 +48,16 @@ import {
|
|
|
30
48
|
// src/index.ts
|
|
31
49
|
import { Command } from "commander";
|
|
32
50
|
|
|
33
|
-
// src/errors.ts
|
|
34
|
-
var EXIT_CODE_MAP = {
|
|
35
|
-
E_AUTH_MISSING: 2,
|
|
36
|
-
E_AUTH_INVALID: 2,
|
|
37
|
-
E_PROJECT_MISSING: 2,
|
|
38
|
-
E_ENDPOINT_INVALID: 2,
|
|
39
|
-
E_INVALID_INPUT: 2,
|
|
40
|
-
E_NOT_INTERACTIVE: 2,
|
|
41
|
-
E_NETWORK: 3,
|
|
42
|
-
E_TIMEOUT: 3,
|
|
43
|
-
E_API_ERROR: 3,
|
|
44
|
-
E_RATE_LIMITED: 3,
|
|
45
|
-
E_FILE_NOT_FOUND: 1,
|
|
46
|
-
E_FILE_PERMISSION: 1,
|
|
47
|
-
E_FRESHNESS_STALE: 1,
|
|
48
|
-
E_INTERRUPTED: 130,
|
|
49
|
-
// 128 + SIGINT (2). POSIX convention.
|
|
50
|
-
E_INTERNAL: 1
|
|
51
|
-
};
|
|
52
|
-
var MushiCliError = class extends Error {
|
|
53
|
-
code;
|
|
54
|
-
hint;
|
|
55
|
-
cause;
|
|
56
|
-
constructor(code, message, hint, cause) {
|
|
57
|
-
super(message);
|
|
58
|
-
this.name = "MushiCliError";
|
|
59
|
-
this.code = code;
|
|
60
|
-
if (hint) this.hint = hint;
|
|
61
|
-
if (cause !== void 0) this.cause = cause;
|
|
62
|
-
}
|
|
63
|
-
/** Exit code POSIX-aware shell scripts should branch on. */
|
|
64
|
-
get exitCode() {
|
|
65
|
-
return EXIT_CODE_MAP[this.code];
|
|
66
|
-
}
|
|
67
|
-
/**
|
|
68
|
-
* Format for `--json` output. Used by the JSON writer when a CLI
|
|
69
|
-
* subcommand was invoked with `--json` and an error escapes.
|
|
70
|
-
*/
|
|
71
|
-
toJSON() {
|
|
72
|
-
return {
|
|
73
|
-
error: {
|
|
74
|
-
code: this.code,
|
|
75
|
-
message: this.message,
|
|
76
|
-
...this.hint ? { hint: this.hint } : {}
|
|
77
|
-
}
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
};
|
|
81
|
-
function printAndExit(err, asJson = false) {
|
|
82
|
-
const cliErr = err instanceof MushiCliError ? err : new MushiCliError(
|
|
83
|
-
"E_INTERNAL",
|
|
84
|
-
err instanceof Error ? err.message : String(err),
|
|
85
|
-
"this is unexpected \u2014 re-run with MUSHI_DEBUG=1 and report at https://github.com/kensaurus/mushi-mushi/issues",
|
|
86
|
-
err
|
|
87
|
-
);
|
|
88
|
-
if (asJson) {
|
|
89
|
-
process.stderr.write(JSON.stringify(cliErr.toJSON()) + "\n");
|
|
90
|
-
} else {
|
|
91
|
-
process.stderr.write(`error [${cliErr.code}]: ${cliErr.message}
|
|
92
|
-
`);
|
|
93
|
-
if (cliErr.hint) process.stderr.write(` \u2192 fix: ${cliErr.hint}
|
|
94
|
-
`);
|
|
95
|
-
}
|
|
96
|
-
process.exit(cliErr.exitCode);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// src/signals.ts
|
|
100
|
-
var installed = false;
|
|
101
|
-
var activeController = null;
|
|
102
|
-
function installSignalHandlers() {
|
|
103
|
-
if (installed) return;
|
|
104
|
-
installed = true;
|
|
105
|
-
const abortAndExit = (signal) => {
|
|
106
|
-
activeController?.abort(
|
|
107
|
-
new MushiCliError(
|
|
108
|
-
"E_INTERRUPTED",
|
|
109
|
-
`aborted by ${signal}`,
|
|
110
|
-
"partial state on the server is safe to retry \u2014 re-run the same command"
|
|
111
|
-
)
|
|
112
|
-
);
|
|
113
|
-
const code = signal === "SIGINT" ? 130 : signal === "SIGTERM" ? 143 : 1;
|
|
114
|
-
process.nextTick(() => {
|
|
115
|
-
process.exit(code);
|
|
116
|
-
});
|
|
117
|
-
};
|
|
118
|
-
process.on("SIGINT", () => abortAndExit("SIGINT"));
|
|
119
|
-
process.on("SIGTERM", () => abortAndExit("SIGTERM"));
|
|
120
|
-
}
|
|
121
|
-
function getAbortSignal(external) {
|
|
122
|
-
if (external) return external.signal;
|
|
123
|
-
if (!activeController || activeController.signal.aborted) {
|
|
124
|
-
activeController = new AbortController();
|
|
125
|
-
}
|
|
126
|
-
return activeController.signal;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// src/version.ts
|
|
130
|
-
var MUSHI_CLI_VERSION = true ? "0.22.1" : "0.0.0-dev";
|
|
131
|
-
|
|
132
51
|
// src/init.ts
|
|
133
52
|
import * as p from "@clack/prompts";
|
|
134
53
|
import { spawn } from "child_process";
|
|
135
54
|
import { randomUUID } from "crypto";
|
|
136
|
-
import { appendFileSync, existsSync as
|
|
137
|
-
import { join as
|
|
138
|
-
|
|
139
|
-
// src/console-url.ts
|
|
140
|
-
import { existsSync, readFileSync } from "fs";
|
|
141
|
-
import { dirname, join } from "path";
|
|
142
|
-
var HOSTED_CONSOLE_BASE = "https://kensaur.us/mushi-mushi/admin";
|
|
143
|
-
var LOCAL_ADMIN_URL = "http://localhost:6464";
|
|
144
|
-
var LOCAL_PROBE_MS = 800;
|
|
145
|
-
function normalizeConsoleBase(url) {
|
|
146
|
-
const trimmed = url.trim();
|
|
147
|
-
let end = trimmed.length;
|
|
148
|
-
while (end > 0 && trimmed.charCodeAt(end - 1) === 47) end--;
|
|
149
|
-
return trimmed.slice(0, end);
|
|
150
|
-
}
|
|
151
|
-
function consoleUrl(base, route) {
|
|
152
|
-
const normalizedRoute = route.startsWith("/") ? route : `/${route}`;
|
|
153
|
-
return `${normalizeConsoleBase(base)}${normalizedRoute}`;
|
|
154
|
-
}
|
|
155
|
-
function resolveConsoleUrlFromEnv() {
|
|
156
|
-
const raw = process.env["MUSHI_CONSOLE_URL"]?.trim();
|
|
157
|
-
return raw ? normalizeConsoleBase(raw) : void 0;
|
|
158
|
-
}
|
|
159
|
-
function isInsideMushiMonorepo(cwd) {
|
|
160
|
-
let dir = cwd;
|
|
161
|
-
for (let depth = 0; depth < 14; depth += 1) {
|
|
162
|
-
const pkgPath = join(dir, "package.json");
|
|
163
|
-
if (existsSync(pkgPath)) {
|
|
164
|
-
try {
|
|
165
|
-
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
166
|
-
if (pkg.name === "mushi-mushi") return true;
|
|
167
|
-
} catch {
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
const parent = dirname(dir);
|
|
171
|
-
if (parent === dir) break;
|
|
172
|
-
dir = parent;
|
|
173
|
-
}
|
|
174
|
-
return false;
|
|
175
|
-
}
|
|
176
|
-
async function probeLocalAdminConsole() {
|
|
177
|
-
try {
|
|
178
|
-
const controller = new AbortController();
|
|
179
|
-
const timer = setTimeout(() => controller.abort(), LOCAL_PROBE_MS);
|
|
180
|
-
const res = await fetch(LOCAL_ADMIN_URL, {
|
|
181
|
-
signal: controller.signal,
|
|
182
|
-
method: "HEAD"
|
|
183
|
-
});
|
|
184
|
-
clearTimeout(timer);
|
|
185
|
-
if (res.ok || res.status === 404 || res.status === 401) {
|
|
186
|
-
return LOCAL_ADMIN_URL;
|
|
187
|
-
}
|
|
188
|
-
} catch {
|
|
189
|
-
}
|
|
190
|
-
return null;
|
|
191
|
-
}
|
|
192
|
-
function resolveConsoleUrlSync(cwd = process.cwd()) {
|
|
193
|
-
const fromEnv = resolveConsoleUrlFromEnv();
|
|
194
|
-
if (fromEnv) return fromEnv;
|
|
195
|
-
const config = loadConfig();
|
|
196
|
-
if (config.consoleUrl) return normalizeConsoleBase(config.consoleUrl);
|
|
197
|
-
if (isInsideMushiMonorepo(cwd)) return LOCAL_ADMIN_URL;
|
|
198
|
-
return HOSTED_CONSOLE_BASE;
|
|
199
|
-
}
|
|
200
|
-
async function resolveConsoleUrl(options) {
|
|
201
|
-
const fromEnv = resolveConsoleUrlFromEnv();
|
|
202
|
-
if (fromEnv) return fromEnv;
|
|
203
|
-
const config = loadConfig();
|
|
204
|
-
if (config.consoleUrl) return normalizeConsoleBase(config.consoleUrl);
|
|
205
|
-
const local = await probeLocalAdminConsole();
|
|
206
|
-
if (local) return local;
|
|
207
|
-
const cwd = options?.cwd ?? process.cwd();
|
|
208
|
-
if (isInsideMushiMonorepo(cwd)) return LOCAL_ADMIN_URL;
|
|
209
|
-
return HOSTED_CONSOLE_BASE;
|
|
210
|
-
}
|
|
211
|
-
function projectIdHint(base) {
|
|
212
|
-
return `Where to find it: ${consoleUrl(base, "/projects")} \u2192 select your project \u2192 click the UUID chip to copy (or copy it from the panel right after you create the project).`;
|
|
213
|
-
}
|
|
214
|
-
function apiKeyHint(base) {
|
|
215
|
-
return `Where to find it: ${consoleUrl(base, "/onboarding?tab=verify")} \u2192 Generate API key (SDK ingest needs report:write \u2014 not Settings \u2192 BYOK keys). Treat it like a password \u2014 env file only, never commit.`;
|
|
216
|
-
}
|
|
217
|
-
function cliSetupDeepLink(base) {
|
|
218
|
-
return consoleUrl(base, "/onboarding?tab=steps&setup=cli");
|
|
219
|
-
}
|
|
220
|
-
function reportsUrl(base, reportId) {
|
|
221
|
-
return reportId ? consoleUrl(base, `/reports/${reportId}`) : consoleUrl(base, "/reports");
|
|
222
|
-
}
|
|
223
|
-
async function openInBrowser(url) {
|
|
224
|
-
let parsed;
|
|
225
|
-
try {
|
|
226
|
-
parsed = new URL(url);
|
|
227
|
-
} catch {
|
|
228
|
-
return;
|
|
229
|
-
}
|
|
230
|
-
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
231
|
-
return;
|
|
232
|
-
}
|
|
233
|
-
const safeUrl = parsed.toString();
|
|
234
|
-
const { spawn: spawn2 } = await import("child_process");
|
|
235
|
-
const [command, args] = process.platform === "win32" ? (
|
|
236
|
-
// Open via rundll32's FileProtocolHandler rather than `cmd /c start`.
|
|
237
|
-
// `start` is a cmd builtin, so the URL would be re-parsed by cmd.exe and
|
|
238
|
-
// characters like `&` / `|` / `^` could still break out — exactly the
|
|
239
|
-
// injection surface CodeQL flags. rundll32 never invokes a shell: the URL
|
|
240
|
-
// is handed to the default browser as one opaque argument.
|
|
241
|
-
["rundll32", ["url.dll,FileProtocolHandler", safeUrl]]
|
|
242
|
-
) : process.platform === "darwin" ? ["open", [safeUrl]] : ["xdg-open", [safeUrl]];
|
|
243
|
-
await new Promise((resolve3) => {
|
|
244
|
-
try {
|
|
245
|
-
const child = spawn2(command, [...args], { stdio: "ignore", shell: false });
|
|
246
|
-
child.on("error", () => resolve3());
|
|
247
|
-
child.on("exit", () => resolve3());
|
|
248
|
-
child.unref();
|
|
249
|
-
} catch {
|
|
250
|
-
resolve3();
|
|
251
|
-
}
|
|
252
|
-
});
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
// src/cli-shared.ts
|
|
256
|
-
var API_TIMEOUT_MS = 15e3;
|
|
257
|
-
async function probeEndpointHealth(endpoint) {
|
|
258
|
-
const t0 = Date.now();
|
|
259
|
-
const controller = new AbortController();
|
|
260
|
-
const timer = setTimeout(() => controller.abort(), API_TIMEOUT_MS);
|
|
261
|
-
try {
|
|
262
|
-
const res = await fetch(`${endpoint}/health`, { signal: controller.signal });
|
|
263
|
-
clearTimeout(timer);
|
|
264
|
-
const body = res.headers.get("content-type")?.includes("json") ? await res.json().catch(() => ({})) : {};
|
|
265
|
-
return { ok: res.ok, status: res.status, latencyMs: Date.now() - t0, body };
|
|
266
|
-
} catch (err) {
|
|
267
|
-
clearTimeout(timer);
|
|
268
|
-
throw err;
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
async function apiCall(path, config, options = {}) {
|
|
272
|
-
const endpoint = config.endpoint;
|
|
273
|
-
if (!endpoint) {
|
|
274
|
-
return {
|
|
275
|
-
ok: false,
|
|
276
|
-
error: {
|
|
277
|
-
code: "NO_ENDPOINT",
|
|
278
|
-
message: "No API endpoint configured.\n Run: mushi login --api-key <key> --endpoint <url> --project-id <id>\n Or: export MUSHI_API_ENDPOINT=https://<project-ref>.supabase.co/functions/v1/api"
|
|
279
|
-
}
|
|
280
|
-
};
|
|
281
|
-
}
|
|
282
|
-
const controller = new AbortController();
|
|
283
|
-
const timer = setTimeout(() => controller.abort(), API_TIMEOUT_MS);
|
|
284
|
-
const signals = [controller.signal, getAbortSignal()];
|
|
285
|
-
const compositeSignal = AbortSignal.any ? AbortSignal.any(signals) : controller.signal;
|
|
286
|
-
try {
|
|
287
|
-
const res = await fetch(`${endpoint}${path}`, {
|
|
288
|
-
...options,
|
|
289
|
-
signal: compositeSignal,
|
|
290
|
-
headers: {
|
|
291
|
-
"Content-Type": "application/json",
|
|
292
|
-
// `apiKeyAuth` reads X-Mushi-Api-Key; the Authorization header is a
|
|
293
|
-
// fallback accepted by some older middleware. Both are sent.
|
|
294
|
-
"Authorization": `Bearer ${config.apiKey ?? ""}`,
|
|
295
|
-
"X-Mushi-Api-Key": config.apiKey ?? "",
|
|
296
|
-
"X-Mushi-Project": config.projectId ?? "",
|
|
297
|
-
"X-Mushi-Cli-Version": MUSHI_CLI_VERSION,
|
|
298
|
-
...options.headers
|
|
299
|
-
}
|
|
300
|
-
});
|
|
301
|
-
clearTimeout(timer);
|
|
302
|
-
let body;
|
|
303
|
-
const contentType = res.headers.get("content-type") ?? "";
|
|
304
|
-
if (contentType.includes("application/json")) {
|
|
305
|
-
try {
|
|
306
|
-
body = await res.json();
|
|
307
|
-
} catch {
|
|
308
|
-
body = null;
|
|
309
|
-
}
|
|
310
|
-
} else {
|
|
311
|
-
const text2 = await res.text();
|
|
312
|
-
try {
|
|
313
|
-
body = JSON.parse(text2);
|
|
314
|
-
} catch {
|
|
315
|
-
body = {
|
|
316
|
-
ok: false,
|
|
317
|
-
error: {
|
|
318
|
-
code: `HTTP_${res.status}`,
|
|
319
|
-
message: text2.trim().slice(0, 300) || `HTTP ${res.status}`
|
|
320
|
-
}
|
|
321
|
-
};
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
if (!res.ok && typeof body === "object" && body !== null && !("ok" in body)) {
|
|
325
|
-
const b = body;
|
|
326
|
-
const nested = b["error"];
|
|
327
|
-
const errCode = nested?.["code"] ?? b["code"] ?? `HTTP_${res.status}`;
|
|
328
|
-
const errMsg = nested?.["message"] ?? b["message"] ?? `Request failed (${res.status})`;
|
|
329
|
-
return {
|
|
330
|
-
ok: false,
|
|
331
|
-
httpStatus: res.status,
|
|
332
|
-
error: { code: errCode, message: errMsg }
|
|
333
|
-
};
|
|
334
|
-
}
|
|
335
|
-
return body;
|
|
336
|
-
} catch (err) {
|
|
337
|
-
clearTimeout(timer);
|
|
338
|
-
if (err instanceof Error && err.name === "AbortError") {
|
|
339
|
-
return {
|
|
340
|
-
ok: false,
|
|
341
|
-
error: {
|
|
342
|
-
code: "TIMEOUT",
|
|
343
|
-
message: `Request timed out after ${API_TIMEOUT_MS / 1e3}s. Check your network or endpoint.`
|
|
344
|
-
}
|
|
345
|
-
};
|
|
346
|
-
}
|
|
347
|
-
return {
|
|
348
|
-
ok: false,
|
|
349
|
-
error: {
|
|
350
|
-
code: "NETWORK_ERROR",
|
|
351
|
-
message: err instanceof Error ? err.message : String(err)
|
|
352
|
-
}
|
|
353
|
-
};
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
function die(result, exitCode = 1) {
|
|
357
|
-
const { code, message } = result.error;
|
|
358
|
-
const status = result.httpStatus ? ` [${result.httpStatus}]` : "";
|
|
359
|
-
process.stderr.write(`error${status}: ${code} \u2014 ${message}
|
|
360
|
-
`);
|
|
361
|
-
if (code === "INSUFFICIENT_SCOPE") {
|
|
362
|
-
process.stderr.write(
|
|
363
|
-
"\n Your current API key does not have the required scope for this command.\n New keys minted by the wizard already include both scopes. Existing keys\n can be upgraded with:\n\n mushi login --upgrade-scope\n\n Or visit your console \u2192 Projects \u2192 API Keys to mint a new key.\n"
|
|
364
|
-
);
|
|
365
|
-
}
|
|
366
|
-
process.exit(exitCode);
|
|
367
|
-
}
|
|
368
|
-
function requireConfig(opts = {}) {
|
|
369
|
-
const config = loadConfig();
|
|
370
|
-
if (!config.apiKey) {
|
|
371
|
-
printAndExit(new MushiCliError(
|
|
372
|
-
"E_AUTH_MISSING",
|
|
373
|
-
"API key not configured.",
|
|
374
|
-
"Run: mushi login --api-key <key> Or: export MUSHI_API_KEY=<key>"
|
|
375
|
-
));
|
|
376
|
-
}
|
|
377
|
-
if (!config.endpoint) {
|
|
378
|
-
config.endpoint = process.env.MUSHI_API_ENDPOINT?.trim() || CLOUD_API_ENDPOINT;
|
|
379
|
-
}
|
|
380
|
-
if (opts.needsProject && !config.projectId) {
|
|
381
|
-
printAndExit(new MushiCliError(
|
|
382
|
-
"E_PROJECT_MISSING",
|
|
383
|
-
"Project ID not configured.",
|
|
384
|
-
`Run: mushi login --project-id <uuid> Or find your project ID at ${consoleUrl(resolveConsoleUrlSync(), "/projects")}`
|
|
385
|
-
));
|
|
386
|
-
}
|
|
387
|
-
return config;
|
|
388
|
-
}
|
|
389
|
-
function fmtDate(iso) {
|
|
390
|
-
if (!iso) return "\u2014";
|
|
391
|
-
return new Date(iso).toLocaleString(void 0, { dateStyle: "short", timeStyle: "short" });
|
|
392
|
-
}
|
|
393
|
-
function pad(s, width) {
|
|
394
|
-
return s.length >= width ? s : s + " ".repeat(width - s.length);
|
|
395
|
-
}
|
|
55
|
+
import { appendFileSync, existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
56
|
+
import { join as join2 } from "path";
|
|
396
57
|
|
|
397
58
|
// src/device-auth.ts
|
|
398
59
|
var DEVICE_FETCH_TIMEOUT_MS = 15e3;
|
|
60
|
+
var DEFAULT_SLOW_DOWN_MS = 5e3;
|
|
399
61
|
function trimTrailingSlash(endpoint) {
|
|
400
62
|
let end = endpoint.length;
|
|
401
63
|
while (end > 0 && endpoint.charCodeAt(end - 1) === 47) end--;
|
|
@@ -410,11 +72,12 @@ async function deviceFetch(url, init = {}) {
|
|
|
410
72
|
clearTimeout(timer);
|
|
411
73
|
}
|
|
412
74
|
}
|
|
413
|
-
async function startDeviceAuth(endpoint) {
|
|
75
|
+
async function startDeviceAuth(endpoint, clientId) {
|
|
414
76
|
const base = trimTrailingSlash(endpoint);
|
|
415
77
|
const res = await deviceFetch(`${base}/v1/cli/auth/device/start`, {
|
|
416
78
|
method: "POST",
|
|
417
|
-
headers: { "Content-Type": "application/json" }
|
|
79
|
+
headers: { "Content-Type": "application/json" },
|
|
80
|
+
body: JSON.stringify(clientId ? { client_id: clientId } : {})
|
|
418
81
|
});
|
|
419
82
|
const json = await res.json().catch(() => null);
|
|
420
83
|
if (!res.ok || !json?.ok || !json.data) {
|
|
@@ -437,6 +100,12 @@ async function pollDeviceToken(endpoint, deviceCode) {
|
|
|
437
100
|
switch (json.error) {
|
|
438
101
|
case "authorization_pending":
|
|
439
102
|
return { status: "pending" };
|
|
103
|
+
case "slow_down": {
|
|
104
|
+
const retryAfterHeader = res.headers?.get?.("Retry-After");
|
|
105
|
+
const retryAfterSeconds = retryAfterHeader ? Number(retryAfterHeader) : NaN;
|
|
106
|
+
const retryAfterMs = Number.isFinite(retryAfterSeconds) && retryAfterSeconds > 0 ? retryAfterSeconds * 1e3 : DEFAULT_SLOW_DOWN_MS;
|
|
107
|
+
return { status: "slow_down", retryAfterMs };
|
|
108
|
+
}
|
|
440
109
|
case "access_denied":
|
|
441
110
|
return { status: "denied" };
|
|
442
111
|
case "expired_token":
|
|
@@ -445,9 +114,11 @@ async function pollDeviceToken(endpoint, deviceCode) {
|
|
|
445
114
|
return {
|
|
446
115
|
status: "error",
|
|
447
116
|
message: json.error_description ?? json.error ?? `HTTP ${res.status}`,
|
|
448
|
-
// 5xx is a server hiccup the next poll may clear;
|
|
449
|
-
//
|
|
450
|
-
|
|
117
|
+
// 5xx is a server hiccup the next poll may clear; 408 (gateway
|
|
118
|
+
// timeout) is equally transient. Any other 4xx is a definitive
|
|
119
|
+
// rejection (already claimed / invalid_grant) that won't fix
|
|
120
|
+
// itself. (429/slow_down is handled above as its own outcome.)
|
|
121
|
+
retryable: res.status >= 500 || res.status === 408
|
|
451
122
|
};
|
|
452
123
|
}
|
|
453
124
|
} catch (err) {
|
|
@@ -458,7 +129,7 @@ var defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
|
458
129
|
async function waitForCliToken(endpoint, session, opts = {}) {
|
|
459
130
|
const sleep = opts.sleep ?? defaultSleep;
|
|
460
131
|
const now = opts.now ?? Date.now;
|
|
461
|
-
|
|
132
|
+
let intervalMs = (session.interval || 5) * 1e3;
|
|
462
133
|
const deadline = now() + (session.expires_in || 600) * 1e3;
|
|
463
134
|
const maxConsecutiveErrors = opts.maxConsecutiveErrors ?? 5;
|
|
464
135
|
let consecutiveErrors = 0;
|
|
@@ -476,6 +147,11 @@ async function waitForCliToken(endpoint, session, opts = {}) {
|
|
|
476
147
|
consecutiveErrors = 0;
|
|
477
148
|
opts.onPending?.();
|
|
478
149
|
continue;
|
|
150
|
+
case "slow_down":
|
|
151
|
+
consecutiveErrors = 0;
|
|
152
|
+
intervalMs = Math.max(intervalMs, outcome.retryAfterMs);
|
|
153
|
+
opts.onSlowDown?.(outcome.retryAfterMs);
|
|
154
|
+
continue;
|
|
479
155
|
case "denied":
|
|
480
156
|
throw new Error("Login was denied in the browser.");
|
|
481
157
|
case "expired":
|
|
@@ -506,7 +182,11 @@ async function listProjects(endpoint, cliToken) {
|
|
|
506
182
|
const res = await deviceFetch(`${base}/v1/cli/projects`, { headers: cliAuthHeaders(cliToken) });
|
|
507
183
|
const json = await res.json().catch(() => null);
|
|
508
184
|
if (res.ok && json?.ok) return json.data?.projects ?? [];
|
|
509
|
-
} catch {
|
|
185
|
+
} catch (err) {
|
|
186
|
+
console.error(
|
|
187
|
+
"[mushi] Failed to list projects:",
|
|
188
|
+
err instanceof Error ? err.message : err
|
|
189
|
+
);
|
|
510
190
|
}
|
|
511
191
|
return [];
|
|
512
192
|
}
|
|
@@ -599,8 +279,8 @@ function parse(version) {
|
|
|
599
279
|
}
|
|
600
280
|
|
|
601
281
|
// src/monorepo.ts
|
|
602
|
-
import { existsSync
|
|
603
|
-
import { dirname
|
|
282
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "fs";
|
|
283
|
+
import { dirname, join, resolve } from "path";
|
|
604
284
|
var WORKSPACE_GLOB_CANDIDATES = ["apps/*", "packages/*", "examples/*"];
|
|
605
285
|
var FRAMEWORK_DEPS = {
|
|
606
286
|
next: "Next.js",
|
|
@@ -617,9 +297,9 @@ var FRAMEWORK_DEPS = {
|
|
|
617
297
|
function detectWorkspaceHint(cwd) {
|
|
618
298
|
const root = findWorkspaceRoot(cwd);
|
|
619
299
|
if (!root) return null;
|
|
620
|
-
const rootPkg = readPackageJsonSafely(
|
|
300
|
+
const rootPkg = readPackageJsonSafely(join(root, "package.json"));
|
|
621
301
|
if (rootPkg && getFrameworkFromPkg(rootPkg)) return null;
|
|
622
|
-
const source =
|
|
302
|
+
const source = existsSync(join(root, "pnpm-workspace.yaml")) ? "pnpm-workspace" : root === cwd ? "package-json" : "parent";
|
|
623
303
|
const apps = collectAppsFromGlobs(root);
|
|
624
304
|
if (apps.length === 0) return null;
|
|
625
305
|
return { root, apps, source };
|
|
@@ -628,15 +308,15 @@ function findWorkspaceRoot(start) {
|
|
|
628
308
|
let dir = resolve(start);
|
|
629
309
|
for (let i = 0; i < 8; i++) {
|
|
630
310
|
if (isWorkspaceRoot(dir)) return dir;
|
|
631
|
-
const parent =
|
|
311
|
+
const parent = dirname(dir);
|
|
632
312
|
if (parent === dir) break;
|
|
633
313
|
dir = parent;
|
|
634
314
|
}
|
|
635
315
|
return null;
|
|
636
316
|
}
|
|
637
317
|
function isWorkspaceRoot(dir) {
|
|
638
|
-
if (
|
|
639
|
-
const pkg = readPackageJsonSafely(
|
|
318
|
+
if (existsSync(join(dir, "pnpm-workspace.yaml"))) return true;
|
|
319
|
+
const pkg = readPackageJsonSafely(join(dir, "package.json"));
|
|
640
320
|
if (!pkg) return false;
|
|
641
321
|
return Boolean(pkg.workspaces);
|
|
642
322
|
}
|
|
@@ -644,8 +324,8 @@ function collectAppsFromGlobs(root) {
|
|
|
644
324
|
const results = [];
|
|
645
325
|
for (const glob of WORKSPACE_GLOB_CANDIDATES) {
|
|
646
326
|
const prefix = glob.replace("/*", "");
|
|
647
|
-
const parentDir =
|
|
648
|
-
if (!
|
|
327
|
+
const parentDir = join(root, prefix);
|
|
328
|
+
if (!existsSync(parentDir)) continue;
|
|
649
329
|
let entries;
|
|
650
330
|
try {
|
|
651
331
|
entries = readdirSync(parentDir);
|
|
@@ -653,7 +333,7 @@ function collectAppsFromGlobs(root) {
|
|
|
653
333
|
continue;
|
|
654
334
|
}
|
|
655
335
|
for (const entry of entries) {
|
|
656
|
-
const pkgPath =
|
|
336
|
+
const pkgPath = join(parentDir, entry, "package.json");
|
|
657
337
|
if (!isFileSafe(pkgPath)) continue;
|
|
658
338
|
const pkg = readPackageJsonSafely(pkgPath);
|
|
659
339
|
if (!pkg) continue;
|
|
@@ -671,14 +351,14 @@ function collectAppsFromGlobs(root) {
|
|
|
671
351
|
function readPackageJsonSafely(path) {
|
|
672
352
|
if (!isFileSafe(path)) return null;
|
|
673
353
|
try {
|
|
674
|
-
return JSON.parse(
|
|
354
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
675
355
|
} catch {
|
|
676
356
|
return null;
|
|
677
357
|
}
|
|
678
358
|
}
|
|
679
359
|
function isFileSafe(path) {
|
|
680
360
|
try {
|
|
681
|
-
return
|
|
361
|
+
return existsSync(path) && statSync(path).isFile();
|
|
682
362
|
} catch {
|
|
683
363
|
return false;
|
|
684
364
|
}
|
|
@@ -767,7 +447,10 @@ async function runInit(options = {}) {
|
|
|
767
447
|
const detected = detectFramework(cwd, pkg);
|
|
768
448
|
const framework = await chooseFramework(detected, options);
|
|
769
449
|
const consoleBase = await resolveConsoleUrl({ cwd });
|
|
770
|
-
const endpoint = resolveCloudEndpoint(
|
|
450
|
+
const endpoint = resolveCloudEndpoint(
|
|
451
|
+
options.endpoint ?? process.env.MUSHI_API_ENDPOINT?.trim() ?? loadConfig().endpoint
|
|
452
|
+
);
|
|
453
|
+
options = { ...options, endpoint };
|
|
771
454
|
const credentials = await acquireCredentials(options, consoleBase, endpoint);
|
|
772
455
|
await verifyCredentials(credentials, options, consoleBase);
|
|
773
456
|
const pm = detectPackageManager(cwd);
|
|
@@ -878,14 +561,14 @@ async function acquireCredentials(options, consoleBase, endpoint) {
|
|
|
878
561
|
p.log.warn("Browser sign-in didn't complete \u2014 switching to manual entry.");
|
|
879
562
|
}
|
|
880
563
|
}
|
|
881
|
-
return collectCredentialsManually(options, consoleBase);
|
|
564
|
+
return collectCredentialsManually(options, consoleBase, endpoint);
|
|
882
565
|
}
|
|
883
566
|
async function runBrowserSignIn(options, endpoint, consoleBase) {
|
|
884
567
|
const startSpin = p.spinner();
|
|
885
568
|
startSpin.start("Starting secure browser sign-in\u2026");
|
|
886
569
|
let session;
|
|
887
570
|
try {
|
|
888
|
-
session = await startDeviceAuth(endpoint);
|
|
571
|
+
session = await startDeviceAuth(endpoint, ensureClientId());
|
|
889
572
|
} catch (err) {
|
|
890
573
|
startSpin.stop("Could not start browser sign-in.");
|
|
891
574
|
p.log.warn(err instanceof Error ? err.message : String(err));
|
|
@@ -983,15 +666,36 @@ async function runBrowserSignIn(options, endpoint, consoleBase) {
|
|
|
983
666
|
if (!projectId || !apiKey) return null;
|
|
984
667
|
return { apiKey, projectId };
|
|
985
668
|
}
|
|
986
|
-
async function collectCredentialsManually(options, consoleBase) {
|
|
669
|
+
async function collectCredentialsManually(options, consoleBase, endpoint) {
|
|
987
670
|
const existing = loadConfig();
|
|
988
|
-
|
|
671
|
+
let savedProjectId = existing.projectId;
|
|
672
|
+
let savedApiKey = existing.apiKey;
|
|
673
|
+
if (!options.projectId && !options.apiKey && savedProjectId && savedApiKey) {
|
|
674
|
+
p.log.info(
|
|
675
|
+
`Found saved credentials from a previous sign-in (project ${sanitizeSecret(savedProjectId).slice(0, 8)}\u2026).`
|
|
676
|
+
);
|
|
677
|
+
const checkSpin = p.spinner();
|
|
678
|
+
checkSpin.start("Checking saved credentials\u2026");
|
|
679
|
+
const check = await apiCall("/v1/sync/whoami", {
|
|
680
|
+
apiKey: sanitizeSecret(savedApiKey),
|
|
681
|
+
projectId: sanitizeSecret(savedProjectId),
|
|
682
|
+
endpoint
|
|
683
|
+
});
|
|
684
|
+
if (check.ok) {
|
|
685
|
+
checkSpin.stop(`Saved credentials still work (${check.data.project_name}).`);
|
|
686
|
+
} else {
|
|
687
|
+
checkSpin.stop("Saved credentials no longer authenticate \u2014 enter fresh ones below.");
|
|
688
|
+
savedProjectId = void 0;
|
|
689
|
+
savedApiKey = void 0;
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
const rawProjectId = options.projectId ?? savedProjectId ?? await promptText({
|
|
989
693
|
message: "Project ID",
|
|
990
694
|
placeholder: "e.g. bdafa28d-b153-482f-bd4f-42981f3fd3a4",
|
|
991
695
|
hint: projectIdHint(consoleBase),
|
|
992
696
|
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."
|
|
993
697
|
});
|
|
994
|
-
const rawApiKey = options.apiKey ??
|
|
698
|
+
const rawApiKey = options.apiKey ?? savedApiKey ?? await promptText({
|
|
995
699
|
message: "API key",
|
|
996
700
|
placeholder: "mushi_xxxxxxxxxxxx",
|
|
997
701
|
hint: apiKeyHint(consoleBase),
|
|
@@ -1022,10 +726,22 @@ async function verifyCredentials(credentials, options, consoleBase) {
|
|
|
1022
726
|
if (!result.ok) {
|
|
1023
727
|
spinner3.stop("Credentials could not be verified.");
|
|
1024
728
|
p.log.error(result.error?.message ?? "Authentication failed.");
|
|
1025
|
-
p.log.
|
|
1026
|
-
|
|
729
|
+
p.log.warn("Setup did NOT complete: nothing was installed and no env vars were written.");
|
|
730
|
+
p.log.info(
|
|
731
|
+
[
|
|
732
|
+
"To recover:",
|
|
733
|
+
' \u2022 Re-run `npx mushi-mushi` and choose "Sign in with your browser", or',
|
|
734
|
+
` \u2022 Double-check the Project ID / API key in the console: ${cliSetupDeepLink(consoleBase)}`
|
|
735
|
+
].join("\n")
|
|
736
|
+
);
|
|
1027
737
|
throw new Error("Credential verification failed \u2014 fix Project ID / API key and re-run.");
|
|
1028
738
|
}
|
|
739
|
+
if (result.data.project_id && result.data.project_id !== credentials.projectId) {
|
|
740
|
+
p.log.warn(
|
|
741
|
+
`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.`
|
|
742
|
+
);
|
|
743
|
+
credentials.projectId = result.data.project_id;
|
|
744
|
+
}
|
|
1029
745
|
spinner3.stop(`Connected to ${result.data.project_name}`);
|
|
1030
746
|
}
|
|
1031
747
|
function sanitizeSecret(raw) {
|
|
@@ -1085,10 +801,10 @@ function runCommand(pm, packages, cwd) {
|
|
|
1085
801
|
});
|
|
1086
802
|
}
|
|
1087
803
|
async function writeEnvFile(cwd, apiKey, projectId, framework, endpoint, overwrite) {
|
|
1088
|
-
const target = ENV_FILES.find((f) =>
|
|
1089
|
-
const targetPath =
|
|
804
|
+
const target = ENV_FILES.find((f) => existsSync2(join2(cwd, f))) ?? ENV_FILES[0];
|
|
805
|
+
const targetPath = join2(cwd, target);
|
|
1090
806
|
const newVars = envVarsToWrite(apiKey, projectId, framework, endpoint);
|
|
1091
|
-
const existing =
|
|
807
|
+
const existing = existsSync2(targetPath) ? readFileSync2(targetPath, "utf-8") : "";
|
|
1092
808
|
if (existing.includes("MUSHI_PROJECT_ID")) {
|
|
1093
809
|
let shouldOverwrite = overwrite;
|
|
1094
810
|
if (!shouldOverwrite) {
|
|
@@ -1147,12 +863,12 @@ function escapeRegexChar(ch) {
|
|
|
1147
863
|
return /[-/\\^$+?.()|[\]{}]/.test(ch) ? `\\${ch}` : ch;
|
|
1148
864
|
}
|
|
1149
865
|
function warnIfMissingFromGitignore(cwd, envFile) {
|
|
1150
|
-
const gitignorePath =
|
|
1151
|
-
if (!
|
|
866
|
+
const gitignorePath = join2(cwd, ".gitignore");
|
|
867
|
+
if (!existsSync2(gitignorePath)) {
|
|
1152
868
|
p.log.warn(`No .gitignore found \u2014 make sure ${envFile} is not committed.`);
|
|
1153
869
|
return;
|
|
1154
870
|
}
|
|
1155
|
-
const content =
|
|
871
|
+
const content = readFileSync2(gitignorePath, "utf-8");
|
|
1156
872
|
if (!isEnvFileCoveredByGitignore(content, envFile)) {
|
|
1157
873
|
p.log.warn(`${envFile} is not in .gitignore \u2014 add it before committing.`);
|
|
1158
874
|
}
|
|
@@ -1194,7 +910,7 @@ async function maybeOfferConnect(credentials, options, _consoleBase) {
|
|
|
1194
910
|
if (p.isCancel(answer) || !answer) return;
|
|
1195
911
|
const endpoint = resolveCloudEndpoint(options.endpoint);
|
|
1196
912
|
try {
|
|
1197
|
-
const { runConnect: runConnect2 } = await import("./connect-
|
|
913
|
+
const { runConnect: runConnect2 } = await import("./connect-5VPUYE6H.js");
|
|
1198
914
|
await runConnect2({
|
|
1199
915
|
apiKey: credentials.apiKey,
|
|
1200
916
|
projectId: credentials.projectId,
|
|
@@ -1326,7 +1042,7 @@ function isSameDirectory(a, b) {
|
|
|
1326
1042
|
}
|
|
1327
1043
|
|
|
1328
1044
|
// src/migrate.ts
|
|
1329
|
-
var DOCS_BASE = "https://docs
|
|
1045
|
+
var DOCS_BASE = "https://kensaur.us/mushi-mushi/docs";
|
|
1330
1046
|
var COMPETITOR_PACKAGES = {
|
|
1331
1047
|
"instabug-to-mushi": [
|
|
1332
1048
|
"instabug-reactnative",
|
|
@@ -1488,8 +1204,8 @@ Examples:
|
|
|
1488
1204
|
mushi login --api-key mushi_xxx # non-interactive / CI
|
|
1489
1205
|
mushi login --api-key mushi_xxx --project-id <uuid>
|
|
1490
1206
|
mushi login --upgrade-scope # re-auth to add mcp:read + mcp:write to your key`).action(async (opts) => {
|
|
1491
|
-
const { CLOUD_API_ENDPOINT
|
|
1492
|
-
const endpoint = opts.endpoint ? resolveCloudEndpoint2(opts.endpoint) :
|
|
1207
|
+
const { CLOUD_API_ENDPOINT, resolveCloudEndpoint: resolveCloudEndpoint2 } = await import("./endpoint-DGS73AVM.js");
|
|
1208
|
+
const endpoint = opts.endpoint ? resolveCloudEndpoint2(opts.endpoint) : CLOUD_API_ENDPOINT;
|
|
1493
1209
|
const consoleBase = await resolveConsoleUrl();
|
|
1494
1210
|
let apiKey = opts.apiKey;
|
|
1495
1211
|
let projectId = opts.projectId;
|
|
@@ -1537,7 +1253,7 @@ Examples:
|
|
|
1537
1253
|
}
|
|
1538
1254
|
let deviceData;
|
|
1539
1255
|
try {
|
|
1540
|
-
deviceData = await startDeviceAuth(endpoint);
|
|
1256
|
+
deviceData = await startDeviceAuth(endpoint, ensureClientId());
|
|
1541
1257
|
} catch (err) {
|
|
1542
1258
|
process.stderr.write(`
|
|
1543
1259
|
error: Could not start login session: ${err instanceof Error ? err.message : String(err)}
|
|
@@ -2221,14 +1937,14 @@ Typical CI usage:
|
|
|
2221
1937
|
import { createReadStream } from "fs";
|
|
2222
1938
|
import { readFile, readdir } from "fs/promises";
|
|
2223
1939
|
import { createHash } from "crypto";
|
|
2224
|
-
import { join as
|
|
1940
|
+
import { join as join3, relative, basename } from "path";
|
|
2225
1941
|
import * as p2 from "@clack/prompts";
|
|
2226
1942
|
async function findMapFiles(dir) {
|
|
2227
1943
|
const results = [];
|
|
2228
1944
|
async function walk(current) {
|
|
2229
1945
|
const entries = await readdir(current, { withFileTypes: true });
|
|
2230
1946
|
for (const e of entries) {
|
|
2231
|
-
const full =
|
|
1947
|
+
const full = join3(current, e.name);
|
|
2232
1948
|
if (e.isDirectory()) {
|
|
2233
1949
|
await walk(full);
|
|
2234
1950
|
} else if (e.isFile() && (e.name.endsWith(".js.map") || e.name.endsWith(".css.map"))) {
|
|
@@ -2544,7 +2260,7 @@ Typical first-time flow:
|
|
|
2544
2260
|
console.log("");
|
|
2545
2261
|
let session;
|
|
2546
2262
|
try {
|
|
2547
|
-
session = await startDeviceAuth(endpoint);
|
|
2263
|
+
session = await startDeviceAuth(endpoint, ensureClientId());
|
|
2548
2264
|
} catch (err) {
|
|
2549
2265
|
process.stderr.write(`
|
|
2550
2266
|
error: Could not start browser sign-in: ${err instanceof Error ? err.message : String(err)}
|
|
@@ -2661,7 +2377,7 @@ Supported IDEs:
|
|
|
2661
2377
|
|
|
2662
2378
|
The command reads credentials from ~/.config/mushi/config.json (run \`mushi login\` first).`).action(async (opts) => {
|
|
2663
2379
|
const { writeFile: writeFile2, mkdir, readFile: readFile3 } = await import("fs/promises");
|
|
2664
|
-
const { existsSync:
|
|
2380
|
+
const { existsSync: existsSync3 } = await import("fs");
|
|
2665
2381
|
const nodePath2 = await import("path");
|
|
2666
2382
|
const os = await import("os");
|
|
2667
2383
|
const config = requireConfig({ needsProject: true });
|
|
@@ -2773,7 +2489,7 @@ The command reads credentials from ~/.config/mushi/config.json (run \`mushi logi
|
|
|
2773
2489
|
}
|
|
2774
2490
|
} else if (ideEntry.format === "zed") {
|
|
2775
2491
|
let settings = {};
|
|
2776
|
-
if (
|
|
2492
|
+
if (existsSync3(configPath)) {
|
|
2777
2493
|
try {
|
|
2778
2494
|
const raw = await readFile3(configPath, "utf8");
|
|
2779
2495
|
settings = JSON.parse(raw);
|
|
@@ -2895,7 +2611,11 @@ The command reads credentials from ~/.config/mushi/config.json (run \`mushi logi
|
|
|
2895
2611
|
console.log(`\u26A0 Key probe returned HTTP ${probeRes.status} \u2014 check your credentials.`);
|
|
2896
2612
|
}
|
|
2897
2613
|
}
|
|
2898
|
-
} catch {
|
|
2614
|
+
} catch (probeErr) {
|
|
2615
|
+
console.warn(
|
|
2616
|
+
"[mushi setup] API key probe failed \u2014 verify credentials manually.",
|
|
2617
|
+
probeErr instanceof Error ? probeErr.message : probeErr
|
|
2618
|
+
);
|
|
2899
2619
|
}
|
|
2900
2620
|
}
|
|
2901
2621
|
console.log("");
|
|
@@ -2934,6 +2654,7 @@ Examples:
|
|
|
2934
2654
|
|
|
2935
2655
|
# CI: fail the pipeline if the fix errors
|
|
2936
2656
|
mushi fix $REPORT_ID --agent cursor_cloud --wait && echo "Fix PR opened"`).action(async (reportId, opts) => {
|
|
2657
|
+
requireUuid(reportId, "reportId");
|
|
2937
2658
|
const cfg = loadConfig();
|
|
2938
2659
|
if (opts.endpoint) cfg.endpoint = opts.endpoint;
|
|
2939
2660
|
if (opts.apiKey) cfg.apiKey = opts.apiKey;
|
|
@@ -3308,9 +3029,9 @@ async function planUpgrade(cwd) {
|
|
|
3308
3029
|
...pkg?.dependencies ?? {},
|
|
3309
3030
|
...pkg?.devDependencies ?? {}
|
|
3310
3031
|
};
|
|
3311
|
-
const
|
|
3032
|
+
const installed = MUSHI_PACKAGES.filter((name) => deps[name]);
|
|
3312
3033
|
const entries = await Promise.all(
|
|
3313
|
-
|
|
3034
|
+
installed.map(async (name) => {
|
|
3314
3035
|
const current = deps[name] ?? "";
|
|
3315
3036
|
const currentCore = current.replace(/^[\^~>=<]*/, "");
|
|
3316
3037
|
if (!/^\d/.test(currentCore)) {
|
|
@@ -3475,9 +3196,9 @@ async function checkEndpointReachability(endpoint, doFetch = globalThis.fetch) {
|
|
|
3475
3196
|
async function checkSdkInstall(cwd) {
|
|
3476
3197
|
try {
|
|
3477
3198
|
const { readFile: readFile3 } = await import("fs/promises");
|
|
3478
|
-
const { join:
|
|
3199
|
+
const { join: join4, resolve: resolve3 } = await import("path");
|
|
3479
3200
|
const root = resolve3(cwd);
|
|
3480
|
-
const pkgPath =
|
|
3201
|
+
const pkgPath = join4(root, "package.json");
|
|
3481
3202
|
const pkg = JSON.parse(await readFile3(pkgPath, "utf8"));
|
|
3482
3203
|
const deps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
|
|
3483
3204
|
const sdks = [
|
|
@@ -3486,11 +3207,11 @@ async function checkSdkInstall(cwd) {
|
|
|
3486
3207
|
"@mushi-mushi/core",
|
|
3487
3208
|
"@mushi-mushi/react-native"
|
|
3488
3209
|
];
|
|
3489
|
-
const
|
|
3210
|
+
const installed = sdks.filter((s) => deps[s]);
|
|
3490
3211
|
return {
|
|
3491
3212
|
name: "SDK installed in this repo",
|
|
3492
|
-
ok:
|
|
3493
|
-
detail:
|
|
3213
|
+
ok: installed.length > 0,
|
|
3214
|
+
detail: installed.length > 0 ? installed.map((s) => `${s}@${deps[s]}`).join(", ") : "No @mushi-mushi/* package in package.json \u2014 run `mushi init` to install"
|
|
3494
3215
|
};
|
|
3495
3216
|
} catch {
|
|
3496
3217
|
return null;
|
|
@@ -3674,9 +3395,9 @@ async function checkHostAppWiring(cwd) {
|
|
|
3674
3395
|
const checks = [];
|
|
3675
3396
|
try {
|
|
3676
3397
|
const { readFile: readFile3, access } = await import("fs/promises");
|
|
3677
|
-
const { join:
|
|
3398
|
+
const { join: join4, resolve: resolve3 } = await import("path");
|
|
3678
3399
|
const root = resolve3(cwd);
|
|
3679
|
-
const pkgPath =
|
|
3400
|
+
const pkgPath = join4(root, "package.json");
|
|
3680
3401
|
const pkg = JSON.parse(await readFile3(pkgPath, "utf8"));
|
|
3681
3402
|
const deps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
|
|
3682
3403
|
const isCapHybrid = Boolean(deps["@capacitor/core"] && deps["react"]);
|
|
@@ -3684,7 +3405,7 @@ async function checkHostAppWiring(cwd) {
|
|
|
3684
3405
|
let envContent = "";
|
|
3685
3406
|
for (const f of envCandidates) {
|
|
3686
3407
|
try {
|
|
3687
|
-
envContent = await readFile3(
|
|
3408
|
+
envContent = await readFile3(join4(root, f), "utf8");
|
|
3688
3409
|
break;
|
|
3689
3410
|
} catch {
|
|
3690
3411
|
}
|
|
@@ -3698,7 +3419,7 @@ async function checkHostAppWiring(cwd) {
|
|
|
3698
3419
|
});
|
|
3699
3420
|
let mcpPresent = false;
|
|
3700
3421
|
try {
|
|
3701
|
-
await access(
|
|
3422
|
+
await access(join4(root, ".cursor", "mcp.json"));
|
|
3702
3423
|
mcpPresent = true;
|
|
3703
3424
|
} catch {
|
|
3704
3425
|
}
|
|
@@ -3732,12 +3453,12 @@ async function checkHostAppWiring(cwd) {
|
|
|
3732
3453
|
async function checkMcpConfig(config, cwd, doFetch = globalThis.fetch) {
|
|
3733
3454
|
const checks = [];
|
|
3734
3455
|
const { readFile: readFile3 } = await import("fs/promises");
|
|
3735
|
-
const { join:
|
|
3456
|
+
const { join: join4, resolve: resolve3 } = await import("path");
|
|
3736
3457
|
const { homedir } = await import("os");
|
|
3737
3458
|
const root = resolve3(cwd);
|
|
3738
3459
|
const candidates = [
|
|
3739
|
-
|
|
3740
|
-
|
|
3460
|
+
join4(root, ".cursor", "mcp.json"),
|
|
3461
|
+
join4(homedir(), ".cursor", "mcp.json")
|
|
3741
3462
|
];
|
|
3742
3463
|
let mcpPath = null;
|
|
3743
3464
|
let mcpRaw = null;
|
|
@@ -3890,12 +3611,12 @@ async function checkOnboardingStatus(config, _consoleBase, cwd, doFetch = global
|
|
|
3890
3611
|
}
|
|
3891
3612
|
try {
|
|
3892
3613
|
const { readFile: readFile3 } = await import("fs/promises");
|
|
3893
|
-
const { join:
|
|
3614
|
+
const { join: join4, resolve: resolve3 } = await import("path");
|
|
3894
3615
|
const root = resolve3(cwd);
|
|
3895
3616
|
let envContent = "";
|
|
3896
3617
|
for (const f of [".env.local", ".env"]) {
|
|
3897
3618
|
try {
|
|
3898
|
-
envContent = await readFile3(
|
|
3619
|
+
envContent = await readFile3(join4(root, f), "utf8");
|
|
3899
3620
|
break;
|
|
3900
3621
|
} catch {
|
|
3901
3622
|
}
|
|
@@ -5120,6 +4841,131 @@ Examples:
|
|
|
5120
4841
|
});
|
|
5121
4842
|
}
|
|
5122
4843
|
|
|
4844
|
+
// src/commands/completion-cli.ts
|
|
4845
|
+
import { Argument } from "commander";
|
|
4846
|
+
|
|
4847
|
+
// src/completion.ts
|
|
4848
|
+
var SUPPORTED_SHELLS = ["bash", "zsh", "fish"];
|
|
4849
|
+
function isSupportedShell(value) {
|
|
4850
|
+
return SUPPORTED_SHELLS.includes(value);
|
|
4851
|
+
}
|
|
4852
|
+
function buildCommandTree(program2) {
|
|
4853
|
+
return program2.commands.filter((cmd) => cmd.name() !== "help").map((cmd) => ({
|
|
4854
|
+
name: cmd.name(),
|
|
4855
|
+
subcommands: cmd.commands.filter((sub) => sub.name() !== "help").map((sub) => sub.name()).sort()
|
|
4856
|
+
})).sort((a, b) => a.name.localeCompare(b.name));
|
|
4857
|
+
}
|
|
4858
|
+
var COMPLETION_ARG_VALUES = [...SUPPORTED_SHELLS];
|
|
4859
|
+
function subcommandsFor(tree, name) {
|
|
4860
|
+
if (name === "completion") return COMPLETION_ARG_VALUES;
|
|
4861
|
+
return tree.find((n) => n.name === name)?.subcommands ?? [];
|
|
4862
|
+
}
|
|
4863
|
+
function generateBash(tree) {
|
|
4864
|
+
const topLevel = tree.map((n) => n.name).join(" ");
|
|
4865
|
+
const cases = tree.map((n) => {
|
|
4866
|
+
const subs = subcommandsFor(tree, n.name);
|
|
4867
|
+
if (subs.length === 0) return null;
|
|
4868
|
+
return ` ${n.name})
|
|
4869
|
+
COMPREPLY=( $(compgen -W "${subs.join(" ")}" -- "$cur") )
|
|
4870
|
+
;;`;
|
|
4871
|
+
}).filter((line) => line !== null).join("\n");
|
|
4872
|
+
return `###-begin-mushi-completion-###
|
|
4873
|
+
# mushi(1) completion \u2014 https://github.com/kensaurus/mushi-mushi
|
|
4874
|
+
# Install: mushi completion bash >> ~/.bashrc (then restart your shell)
|
|
4875
|
+
_mushi_completion() {
|
|
4876
|
+
local cur
|
|
4877
|
+
cur="\${COMP_WORDS[COMP_CWORD]}"
|
|
4878
|
+
COMPREPLY=()
|
|
4879
|
+
|
|
4880
|
+
if [ "$COMP_CWORD" -eq 1 ]; then
|
|
4881
|
+
COMPREPLY=( $(compgen -W "${topLevel}" -- "$cur") )
|
|
4882
|
+
return 0
|
|
4883
|
+
fi
|
|
4884
|
+
|
|
4885
|
+
case "\${COMP_WORDS[1]}" in
|
|
4886
|
+
${cases}
|
|
4887
|
+
*)
|
|
4888
|
+
;;
|
|
4889
|
+
esac
|
|
4890
|
+
}
|
|
4891
|
+
complete -F _mushi_completion mushi
|
|
4892
|
+
###-end-mushi-completion-###
|
|
4893
|
+
`;
|
|
4894
|
+
}
|
|
4895
|
+
function generateZsh(tree) {
|
|
4896
|
+
const topLevel = tree.map((n) => n.name).join(" ");
|
|
4897
|
+
const cases = tree.map((n) => {
|
|
4898
|
+
const subs = subcommandsFor(tree, n.name);
|
|
4899
|
+
if (subs.length === 0) return null;
|
|
4900
|
+
return ` ${n.name})
|
|
4901
|
+
local -a subcmds; subcmds=(${subs.join(" ")})
|
|
4902
|
+
_describe '${n.name} subcommand' subcmds
|
|
4903
|
+
;;`;
|
|
4904
|
+
}).filter((line) => line !== null).join("\n");
|
|
4905
|
+
return `#compdef mushi
|
|
4906
|
+
# mushi(1) completion \u2014 https://github.com/kensaurus/mushi-mushi
|
|
4907
|
+
# Install: mushi completion zsh > "\${fpath[1]}/_mushi" (then restart your shell)
|
|
4908
|
+
_mushi() {
|
|
4909
|
+
local -a top_level
|
|
4910
|
+
top_level=(${topLevel})
|
|
4911
|
+
|
|
4912
|
+
if (( CURRENT == 2 )); then
|
|
4913
|
+
_describe 'command' top_level
|
|
4914
|
+
return
|
|
4915
|
+
fi
|
|
4916
|
+
|
|
4917
|
+
case "\${words[2]}" in
|
|
4918
|
+
${cases}
|
|
4919
|
+
*)
|
|
4920
|
+
;;
|
|
4921
|
+
esac
|
|
4922
|
+
}
|
|
4923
|
+
|
|
4924
|
+
_mushi
|
|
4925
|
+
`;
|
|
4926
|
+
}
|
|
4927
|
+
function generateFish(tree) {
|
|
4928
|
+
const topLevel = tree.map((n) => n.name).join(" ");
|
|
4929
|
+
const lines = [
|
|
4930
|
+
"# mushi(1) completion \u2014 https://github.com/kensaurus/mushi-mushi",
|
|
4931
|
+
"# Install: mushi completion fish > ~/.config/fish/completions/mushi.fish",
|
|
4932
|
+
`complete -c mushi -f -n "__fish_use_subcommand" -a "${topLevel}"`
|
|
4933
|
+
];
|
|
4934
|
+
for (const n of tree) {
|
|
4935
|
+
const subs = subcommandsFor(tree, n.name);
|
|
4936
|
+
if (subs.length === 0) continue;
|
|
4937
|
+
lines.push(`complete -c mushi -f -n "__fish_seen_subcommand_from ${n.name}" -a "${subs.join(" ")}"`);
|
|
4938
|
+
}
|
|
4939
|
+
return lines.join("\n") + "\n";
|
|
4940
|
+
}
|
|
4941
|
+
function generateCompletionScript(shell, tree) {
|
|
4942
|
+
switch (shell) {
|
|
4943
|
+
case "bash":
|
|
4944
|
+
return generateBash(tree);
|
|
4945
|
+
case "zsh":
|
|
4946
|
+
return generateZsh(tree);
|
|
4947
|
+
case "fish":
|
|
4948
|
+
return generateFish(tree);
|
|
4949
|
+
}
|
|
4950
|
+
}
|
|
4951
|
+
|
|
4952
|
+
// src/commands/completion-cli.ts
|
|
4953
|
+
function registerCompletionCommand(program2) {
|
|
4954
|
+
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", `
|
|
4955
|
+
Examples:
|
|
4956
|
+
eval "$(mushi completion bash)" # try it for the current session
|
|
4957
|
+
mushi completion bash >> ~/.bashrc # bash: persist across sessions
|
|
4958
|
+
mushi completion zsh > "\${fpath[1]}/_mushi" # zsh: needs a dir already on $fpath
|
|
4959
|
+
mushi completion fish > ~/.config/fish/completions/mushi.fish`).action((shell) => {
|
|
4960
|
+
if (!isSupportedShell(shell)) {
|
|
4961
|
+
console.error(`Unsupported shell "${shell}". Supported: ${SUPPORTED_SHELLS.join(", ")}`);
|
|
4962
|
+
process.exit(2);
|
|
4963
|
+
}
|
|
4964
|
+
const tree = buildCommandTree(program2);
|
|
4965
|
+
console.log(generateCompletionScript(shell, tree));
|
|
4966
|
+
});
|
|
4967
|
+
}
|
|
4968
|
+
|
|
5123
4969
|
// src/index.ts
|
|
5124
4970
|
installSignalHandlers();
|
|
5125
4971
|
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", `
|
|
@@ -5158,7 +5004,7 @@ registerQaCommands(program);
|
|
|
5158
5004
|
registerAuditCommands(program);
|
|
5159
5005
|
registerSkillsCommands(program);
|
|
5160
5006
|
registerBillingCommands(program);
|
|
5007
|
+
registerCompletionCommand(program);
|
|
5161
5008
|
program.parseAsync().catch((err) => {
|
|
5162
|
-
|
|
5163
|
-
process.exit(1);
|
|
5009
|
+
printAndExit(err);
|
|
5164
5010
|
});
|