@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/init.js
CHANGED
|
@@ -1,13 +1,20 @@
|
|
|
1
|
-
import {
|
|
2
|
-
MUSHI_CLI_VERSION
|
|
3
|
-
} from "./chunk-YK4LF7AM.js";
|
|
4
1
|
import {
|
|
5
2
|
TEST_REPORT_FETCH_TIMEOUT_MS,
|
|
3
|
+
apiCall,
|
|
4
|
+
apiKeyHint,
|
|
5
|
+
cliSetupDeepLink,
|
|
6
|
+
consoleUrl,
|
|
7
|
+
ensureClientId,
|
|
6
8
|
loadConfig,
|
|
7
9
|
normalizeEndpoint,
|
|
10
|
+
openInBrowser,
|
|
11
|
+
projectIdHint,
|
|
12
|
+
reportsUrl,
|
|
8
13
|
resolveCloudEndpoint,
|
|
14
|
+
resolveConsoleUrl,
|
|
15
|
+
resolveConsoleUrlSync,
|
|
9
16
|
saveConfig
|
|
10
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-NZ64LBLU.js";
|
|
11
18
|
import {
|
|
12
19
|
FRAMEWORKS,
|
|
13
20
|
detectFramework,
|
|
@@ -15,231 +22,21 @@ import {
|
|
|
15
22
|
envVarsToWrite,
|
|
16
23
|
installCommand,
|
|
17
24
|
readPackageJson
|
|
18
|
-
} from "./chunk-
|
|
25
|
+
} from "./chunk-AIZRI665.js";
|
|
26
|
+
import {
|
|
27
|
+
MUSHI_CLI_VERSION
|
|
28
|
+
} from "./chunk-I4UK3WTN.js";
|
|
19
29
|
|
|
20
30
|
// src/init.ts
|
|
21
31
|
import * as p from "@clack/prompts";
|
|
22
32
|
import { spawn } from "child_process";
|
|
23
33
|
import { randomUUID } from "crypto";
|
|
24
|
-
import { appendFileSync, existsSync as
|
|
25
|
-
import { join as
|
|
26
|
-
|
|
27
|
-
// src/console-url.ts
|
|
28
|
-
import { existsSync, readFileSync } from "fs";
|
|
29
|
-
import { dirname, join } from "path";
|
|
30
|
-
var HOSTED_CONSOLE_BASE = "https://kensaur.us/mushi-mushi/admin";
|
|
31
|
-
var LOCAL_ADMIN_URL = "http://localhost:6464";
|
|
32
|
-
var LOCAL_PROBE_MS = 800;
|
|
33
|
-
function normalizeConsoleBase(url) {
|
|
34
|
-
const trimmed = url.trim();
|
|
35
|
-
let end = trimmed.length;
|
|
36
|
-
while (end > 0 && trimmed.charCodeAt(end - 1) === 47) end--;
|
|
37
|
-
return trimmed.slice(0, end);
|
|
38
|
-
}
|
|
39
|
-
function consoleUrl(base, route) {
|
|
40
|
-
const normalizedRoute = route.startsWith("/") ? route : `/${route}`;
|
|
41
|
-
return `${normalizeConsoleBase(base)}${normalizedRoute}`;
|
|
42
|
-
}
|
|
43
|
-
function resolveConsoleUrlFromEnv() {
|
|
44
|
-
const raw = process.env["MUSHI_CONSOLE_URL"]?.trim();
|
|
45
|
-
return raw ? normalizeConsoleBase(raw) : void 0;
|
|
46
|
-
}
|
|
47
|
-
function isInsideMushiMonorepo(cwd) {
|
|
48
|
-
let dir = cwd;
|
|
49
|
-
for (let depth = 0; depth < 14; depth += 1) {
|
|
50
|
-
const pkgPath = join(dir, "package.json");
|
|
51
|
-
if (existsSync(pkgPath)) {
|
|
52
|
-
try {
|
|
53
|
-
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
54
|
-
if (pkg.name === "mushi-mushi") return true;
|
|
55
|
-
} catch {
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
const parent = dirname(dir);
|
|
59
|
-
if (parent === dir) break;
|
|
60
|
-
dir = parent;
|
|
61
|
-
}
|
|
62
|
-
return false;
|
|
63
|
-
}
|
|
64
|
-
async function probeLocalAdminConsole() {
|
|
65
|
-
try {
|
|
66
|
-
const controller = new AbortController();
|
|
67
|
-
const timer = setTimeout(() => controller.abort(), LOCAL_PROBE_MS);
|
|
68
|
-
const res = await fetch(LOCAL_ADMIN_URL, {
|
|
69
|
-
signal: controller.signal,
|
|
70
|
-
method: "HEAD"
|
|
71
|
-
});
|
|
72
|
-
clearTimeout(timer);
|
|
73
|
-
if (res.ok || res.status === 404 || res.status === 401) {
|
|
74
|
-
return LOCAL_ADMIN_URL;
|
|
75
|
-
}
|
|
76
|
-
} catch {
|
|
77
|
-
}
|
|
78
|
-
return null;
|
|
79
|
-
}
|
|
80
|
-
function resolveConsoleUrlSync(cwd = process.cwd()) {
|
|
81
|
-
const fromEnv = resolveConsoleUrlFromEnv();
|
|
82
|
-
if (fromEnv) return fromEnv;
|
|
83
|
-
const config = loadConfig();
|
|
84
|
-
if (config.consoleUrl) return normalizeConsoleBase(config.consoleUrl);
|
|
85
|
-
if (isInsideMushiMonorepo(cwd)) return LOCAL_ADMIN_URL;
|
|
86
|
-
return HOSTED_CONSOLE_BASE;
|
|
87
|
-
}
|
|
88
|
-
async function resolveConsoleUrl(options) {
|
|
89
|
-
const fromEnv = resolveConsoleUrlFromEnv();
|
|
90
|
-
if (fromEnv) return fromEnv;
|
|
91
|
-
const config = loadConfig();
|
|
92
|
-
if (config.consoleUrl) return normalizeConsoleBase(config.consoleUrl);
|
|
93
|
-
const local = await probeLocalAdminConsole();
|
|
94
|
-
if (local) return local;
|
|
95
|
-
const cwd = options?.cwd ?? process.cwd();
|
|
96
|
-
if (isInsideMushiMonorepo(cwd)) return LOCAL_ADMIN_URL;
|
|
97
|
-
return HOSTED_CONSOLE_BASE;
|
|
98
|
-
}
|
|
99
|
-
function projectIdHint(base) {
|
|
100
|
-
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).`;
|
|
101
|
-
}
|
|
102
|
-
function apiKeyHint(base) {
|
|
103
|
-
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.`;
|
|
104
|
-
}
|
|
105
|
-
function cliSetupDeepLink(base) {
|
|
106
|
-
return consoleUrl(base, "/onboarding?tab=steps&setup=cli");
|
|
107
|
-
}
|
|
108
|
-
function reportsUrl(base, reportId) {
|
|
109
|
-
return reportId ? consoleUrl(base, `/reports/${reportId}`) : consoleUrl(base, "/reports");
|
|
110
|
-
}
|
|
111
|
-
async function openInBrowser(url) {
|
|
112
|
-
let parsed;
|
|
113
|
-
try {
|
|
114
|
-
parsed = new URL(url);
|
|
115
|
-
} catch {
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
119
|
-
return;
|
|
120
|
-
}
|
|
121
|
-
const safeUrl = parsed.toString();
|
|
122
|
-
const { spawn: spawn2 } = await import("child_process");
|
|
123
|
-
const [command, args] = process.platform === "win32" ? (
|
|
124
|
-
// Open via rundll32's FileProtocolHandler rather than `cmd /c start`.
|
|
125
|
-
// `start` is a cmd builtin, so the URL would be re-parsed by cmd.exe and
|
|
126
|
-
// characters like `&` / `|` / `^` could still break out — exactly the
|
|
127
|
-
// injection surface CodeQL flags. rundll32 never invokes a shell: the URL
|
|
128
|
-
// is handed to the default browser as one opaque argument.
|
|
129
|
-
["rundll32", ["url.dll,FileProtocolHandler", safeUrl]]
|
|
130
|
-
) : process.platform === "darwin" ? ["open", [safeUrl]] : ["xdg-open", [safeUrl]];
|
|
131
|
-
await new Promise((resolve2) => {
|
|
132
|
-
try {
|
|
133
|
-
const child = spawn2(command, [...args], { stdio: "ignore", shell: false });
|
|
134
|
-
child.on("error", () => resolve2());
|
|
135
|
-
child.on("exit", () => resolve2());
|
|
136
|
-
child.unref();
|
|
137
|
-
} catch {
|
|
138
|
-
resolve2();
|
|
139
|
-
}
|
|
140
|
-
});
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// src/signals.ts
|
|
144
|
-
var activeController = null;
|
|
145
|
-
function getAbortSignal(external) {
|
|
146
|
-
if (external) return external.signal;
|
|
147
|
-
if (!activeController || activeController.signal.aborted) {
|
|
148
|
-
activeController = new AbortController();
|
|
149
|
-
}
|
|
150
|
-
return activeController.signal;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// src/cli-shared.ts
|
|
154
|
-
var API_TIMEOUT_MS = 15e3;
|
|
155
|
-
async function apiCall(path, config, options = {}) {
|
|
156
|
-
const endpoint = config.endpoint;
|
|
157
|
-
if (!endpoint) {
|
|
158
|
-
return {
|
|
159
|
-
ok: false,
|
|
160
|
-
error: {
|
|
161
|
-
code: "NO_ENDPOINT",
|
|
162
|
-
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"
|
|
163
|
-
}
|
|
164
|
-
};
|
|
165
|
-
}
|
|
166
|
-
const controller = new AbortController();
|
|
167
|
-
const timer = setTimeout(() => controller.abort(), API_TIMEOUT_MS);
|
|
168
|
-
const signals = [controller.signal, getAbortSignal()];
|
|
169
|
-
const compositeSignal = AbortSignal.any ? AbortSignal.any(signals) : controller.signal;
|
|
170
|
-
try {
|
|
171
|
-
const res = await fetch(`${endpoint}${path}`, {
|
|
172
|
-
...options,
|
|
173
|
-
signal: compositeSignal,
|
|
174
|
-
headers: {
|
|
175
|
-
"Content-Type": "application/json",
|
|
176
|
-
// `apiKeyAuth` reads X-Mushi-Api-Key; the Authorization header is a
|
|
177
|
-
// fallback accepted by some older middleware. Both are sent.
|
|
178
|
-
"Authorization": `Bearer ${config.apiKey ?? ""}`,
|
|
179
|
-
"X-Mushi-Api-Key": config.apiKey ?? "",
|
|
180
|
-
"X-Mushi-Project": config.projectId ?? "",
|
|
181
|
-
"X-Mushi-Cli-Version": MUSHI_CLI_VERSION,
|
|
182
|
-
...options.headers
|
|
183
|
-
}
|
|
184
|
-
});
|
|
185
|
-
clearTimeout(timer);
|
|
186
|
-
let body;
|
|
187
|
-
const contentType = res.headers.get("content-type") ?? "";
|
|
188
|
-
if (contentType.includes("application/json")) {
|
|
189
|
-
try {
|
|
190
|
-
body = await res.json();
|
|
191
|
-
} catch {
|
|
192
|
-
body = null;
|
|
193
|
-
}
|
|
194
|
-
} else {
|
|
195
|
-
const text2 = await res.text();
|
|
196
|
-
try {
|
|
197
|
-
body = JSON.parse(text2);
|
|
198
|
-
} catch {
|
|
199
|
-
body = {
|
|
200
|
-
ok: false,
|
|
201
|
-
error: {
|
|
202
|
-
code: `HTTP_${res.status}`,
|
|
203
|
-
message: text2.trim().slice(0, 300) || `HTTP ${res.status}`
|
|
204
|
-
}
|
|
205
|
-
};
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
if (!res.ok && typeof body === "object" && body !== null && !("ok" in body)) {
|
|
209
|
-
const b = body;
|
|
210
|
-
const nested = b["error"];
|
|
211
|
-
const errCode = nested?.["code"] ?? b["code"] ?? `HTTP_${res.status}`;
|
|
212
|
-
const errMsg = nested?.["message"] ?? b["message"] ?? `Request failed (${res.status})`;
|
|
213
|
-
return {
|
|
214
|
-
ok: false,
|
|
215
|
-
httpStatus: res.status,
|
|
216
|
-
error: { code: errCode, message: errMsg }
|
|
217
|
-
};
|
|
218
|
-
}
|
|
219
|
-
return body;
|
|
220
|
-
} catch (err) {
|
|
221
|
-
clearTimeout(timer);
|
|
222
|
-
if (err instanceof Error && err.name === "AbortError") {
|
|
223
|
-
return {
|
|
224
|
-
ok: false,
|
|
225
|
-
error: {
|
|
226
|
-
code: "TIMEOUT",
|
|
227
|
-
message: `Request timed out after ${API_TIMEOUT_MS / 1e3}s. Check your network or endpoint.`
|
|
228
|
-
}
|
|
229
|
-
};
|
|
230
|
-
}
|
|
231
|
-
return {
|
|
232
|
-
ok: false,
|
|
233
|
-
error: {
|
|
234
|
-
code: "NETWORK_ERROR",
|
|
235
|
-
message: err instanceof Error ? err.message : String(err)
|
|
236
|
-
}
|
|
237
|
-
};
|
|
238
|
-
}
|
|
239
|
-
}
|
|
34
|
+
import { appendFileSync, existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
35
|
+
import { join as join2 } from "path";
|
|
240
36
|
|
|
241
37
|
// src/device-auth.ts
|
|
242
38
|
var DEVICE_FETCH_TIMEOUT_MS = 15e3;
|
|
39
|
+
var DEFAULT_SLOW_DOWN_MS = 5e3;
|
|
243
40
|
function trimTrailingSlash(endpoint) {
|
|
244
41
|
let end = endpoint.length;
|
|
245
42
|
while (end > 0 && endpoint.charCodeAt(end - 1) === 47) end--;
|
|
@@ -254,11 +51,12 @@ async function deviceFetch(url, init = {}) {
|
|
|
254
51
|
clearTimeout(timer);
|
|
255
52
|
}
|
|
256
53
|
}
|
|
257
|
-
async function startDeviceAuth(endpoint) {
|
|
54
|
+
async function startDeviceAuth(endpoint, clientId) {
|
|
258
55
|
const base = trimTrailingSlash(endpoint);
|
|
259
56
|
const res = await deviceFetch(`${base}/v1/cli/auth/device/start`, {
|
|
260
57
|
method: "POST",
|
|
261
|
-
headers: { "Content-Type": "application/json" }
|
|
58
|
+
headers: { "Content-Type": "application/json" },
|
|
59
|
+
body: JSON.stringify(clientId ? { client_id: clientId } : {})
|
|
262
60
|
});
|
|
263
61
|
const json = await res.json().catch(() => null);
|
|
264
62
|
if (!res.ok || !json?.ok || !json.data) {
|
|
@@ -281,6 +79,12 @@ async function pollDeviceToken(endpoint, deviceCode) {
|
|
|
281
79
|
switch (json.error) {
|
|
282
80
|
case "authorization_pending":
|
|
283
81
|
return { status: "pending" };
|
|
82
|
+
case "slow_down": {
|
|
83
|
+
const retryAfterHeader = res.headers?.get?.("Retry-After");
|
|
84
|
+
const retryAfterSeconds = retryAfterHeader ? Number(retryAfterHeader) : NaN;
|
|
85
|
+
const retryAfterMs = Number.isFinite(retryAfterSeconds) && retryAfterSeconds > 0 ? retryAfterSeconds * 1e3 : DEFAULT_SLOW_DOWN_MS;
|
|
86
|
+
return { status: "slow_down", retryAfterMs };
|
|
87
|
+
}
|
|
284
88
|
case "access_denied":
|
|
285
89
|
return { status: "denied" };
|
|
286
90
|
case "expired_token":
|
|
@@ -289,9 +93,11 @@ async function pollDeviceToken(endpoint, deviceCode) {
|
|
|
289
93
|
return {
|
|
290
94
|
status: "error",
|
|
291
95
|
message: json.error_description ?? json.error ?? `HTTP ${res.status}`,
|
|
292
|
-
// 5xx is a server hiccup the next poll may clear;
|
|
293
|
-
//
|
|
294
|
-
|
|
96
|
+
// 5xx is a server hiccup the next poll may clear; 408 (gateway
|
|
97
|
+
// timeout) is equally transient. Any other 4xx is a definitive
|
|
98
|
+
// rejection (already claimed / invalid_grant) that won't fix
|
|
99
|
+
// itself. (429/slow_down is handled above as its own outcome.)
|
|
100
|
+
retryable: res.status >= 500 || res.status === 408
|
|
295
101
|
};
|
|
296
102
|
}
|
|
297
103
|
} catch (err) {
|
|
@@ -302,7 +108,7 @@ var defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
|
302
108
|
async function waitForCliToken(endpoint, session, opts = {}) {
|
|
303
109
|
const sleep = opts.sleep ?? defaultSleep;
|
|
304
110
|
const now = opts.now ?? Date.now;
|
|
305
|
-
|
|
111
|
+
let intervalMs = (session.interval || 5) * 1e3;
|
|
306
112
|
const deadline = now() + (session.expires_in || 600) * 1e3;
|
|
307
113
|
const maxConsecutiveErrors = opts.maxConsecutiveErrors ?? 5;
|
|
308
114
|
let consecutiveErrors = 0;
|
|
@@ -320,6 +126,11 @@ async function waitForCliToken(endpoint, session, opts = {}) {
|
|
|
320
126
|
consecutiveErrors = 0;
|
|
321
127
|
opts.onPending?.();
|
|
322
128
|
continue;
|
|
129
|
+
case "slow_down":
|
|
130
|
+
consecutiveErrors = 0;
|
|
131
|
+
intervalMs = Math.max(intervalMs, outcome.retryAfterMs);
|
|
132
|
+
opts.onSlowDown?.(outcome.retryAfterMs);
|
|
133
|
+
continue;
|
|
323
134
|
case "denied":
|
|
324
135
|
throw new Error("Login was denied in the browser.");
|
|
325
136
|
case "expired":
|
|
@@ -350,7 +161,11 @@ async function listProjects(endpoint, cliToken) {
|
|
|
350
161
|
const res = await deviceFetch(`${base}/v1/cli/projects`, { headers: cliAuthHeaders(cliToken) });
|
|
351
162
|
const json = await res.json().catch(() => null);
|
|
352
163
|
if (res.ok && json?.ok) return json.data?.projects ?? [];
|
|
353
|
-
} catch {
|
|
164
|
+
} catch (err) {
|
|
165
|
+
console.error(
|
|
166
|
+
"[mushi] Failed to list projects:",
|
|
167
|
+
err instanceof Error ? err.message : err
|
|
168
|
+
);
|
|
354
169
|
}
|
|
355
170
|
return [];
|
|
356
171
|
}
|
|
@@ -443,8 +258,8 @@ function parse(version) {
|
|
|
443
258
|
}
|
|
444
259
|
|
|
445
260
|
// src/monorepo.ts
|
|
446
|
-
import { existsSync
|
|
447
|
-
import { dirname
|
|
261
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "fs";
|
|
262
|
+
import { dirname, join, resolve } from "path";
|
|
448
263
|
var WORKSPACE_GLOB_CANDIDATES = ["apps/*", "packages/*", "examples/*"];
|
|
449
264
|
var FRAMEWORK_DEPS = {
|
|
450
265
|
next: "Next.js",
|
|
@@ -461,9 +276,9 @@ var FRAMEWORK_DEPS = {
|
|
|
461
276
|
function detectWorkspaceHint(cwd) {
|
|
462
277
|
const root = findWorkspaceRoot(cwd);
|
|
463
278
|
if (!root) return null;
|
|
464
|
-
const rootPkg = readPackageJsonSafely(
|
|
279
|
+
const rootPkg = readPackageJsonSafely(join(root, "package.json"));
|
|
465
280
|
if (rootPkg && getFrameworkFromPkg(rootPkg)) return null;
|
|
466
|
-
const source =
|
|
281
|
+
const source = existsSync(join(root, "pnpm-workspace.yaml")) ? "pnpm-workspace" : root === cwd ? "package-json" : "parent";
|
|
467
282
|
const apps = collectAppsFromGlobs(root);
|
|
468
283
|
if (apps.length === 0) return null;
|
|
469
284
|
return { root, apps, source };
|
|
@@ -472,15 +287,15 @@ function findWorkspaceRoot(start) {
|
|
|
472
287
|
let dir = resolve(start);
|
|
473
288
|
for (let i = 0; i < 8; i++) {
|
|
474
289
|
if (isWorkspaceRoot(dir)) return dir;
|
|
475
|
-
const parent =
|
|
290
|
+
const parent = dirname(dir);
|
|
476
291
|
if (parent === dir) break;
|
|
477
292
|
dir = parent;
|
|
478
293
|
}
|
|
479
294
|
return null;
|
|
480
295
|
}
|
|
481
296
|
function isWorkspaceRoot(dir) {
|
|
482
|
-
if (
|
|
483
|
-
const pkg = readPackageJsonSafely(
|
|
297
|
+
if (existsSync(join(dir, "pnpm-workspace.yaml"))) return true;
|
|
298
|
+
const pkg = readPackageJsonSafely(join(dir, "package.json"));
|
|
484
299
|
if (!pkg) return false;
|
|
485
300
|
return Boolean(pkg.workspaces);
|
|
486
301
|
}
|
|
@@ -488,8 +303,8 @@ function collectAppsFromGlobs(root) {
|
|
|
488
303
|
const results = [];
|
|
489
304
|
for (const glob of WORKSPACE_GLOB_CANDIDATES) {
|
|
490
305
|
const prefix = glob.replace("/*", "");
|
|
491
|
-
const parentDir =
|
|
492
|
-
if (!
|
|
306
|
+
const parentDir = join(root, prefix);
|
|
307
|
+
if (!existsSync(parentDir)) continue;
|
|
493
308
|
let entries;
|
|
494
309
|
try {
|
|
495
310
|
entries = readdirSync(parentDir);
|
|
@@ -497,7 +312,7 @@ function collectAppsFromGlobs(root) {
|
|
|
497
312
|
continue;
|
|
498
313
|
}
|
|
499
314
|
for (const entry of entries) {
|
|
500
|
-
const pkgPath =
|
|
315
|
+
const pkgPath = join(parentDir, entry, "package.json");
|
|
501
316
|
if (!isFileSafe(pkgPath)) continue;
|
|
502
317
|
const pkg = readPackageJsonSafely(pkgPath);
|
|
503
318
|
if (!pkg) continue;
|
|
@@ -515,14 +330,14 @@ function collectAppsFromGlobs(root) {
|
|
|
515
330
|
function readPackageJsonSafely(path) {
|
|
516
331
|
if (!isFileSafe(path)) return null;
|
|
517
332
|
try {
|
|
518
|
-
return JSON.parse(
|
|
333
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
519
334
|
} catch {
|
|
520
335
|
return null;
|
|
521
336
|
}
|
|
522
337
|
}
|
|
523
338
|
function isFileSafe(path) {
|
|
524
339
|
try {
|
|
525
|
-
return
|
|
340
|
+
return existsSync(path) && statSync(path).isFile();
|
|
526
341
|
} catch {
|
|
527
342
|
return false;
|
|
528
343
|
}
|
|
@@ -591,7 +406,10 @@ async function runInit(options = {}) {
|
|
|
591
406
|
const detected = detectFramework(cwd, pkg);
|
|
592
407
|
const framework = await chooseFramework(detected, options);
|
|
593
408
|
const consoleBase = await resolveConsoleUrl({ cwd });
|
|
594
|
-
const endpoint = resolveCloudEndpoint(
|
|
409
|
+
const endpoint = resolveCloudEndpoint(
|
|
410
|
+
options.endpoint ?? process.env.MUSHI_API_ENDPOINT?.trim() ?? loadConfig().endpoint
|
|
411
|
+
);
|
|
412
|
+
options = { ...options, endpoint };
|
|
595
413
|
const credentials = await acquireCredentials(options, consoleBase, endpoint);
|
|
596
414
|
await verifyCredentials(credentials, options, consoleBase);
|
|
597
415
|
const pm = detectPackageManager(cwd);
|
|
@@ -702,14 +520,14 @@ async function acquireCredentials(options, consoleBase, endpoint) {
|
|
|
702
520
|
p.log.warn("Browser sign-in didn't complete \u2014 switching to manual entry.");
|
|
703
521
|
}
|
|
704
522
|
}
|
|
705
|
-
return collectCredentialsManually(options, consoleBase);
|
|
523
|
+
return collectCredentialsManually(options, consoleBase, endpoint);
|
|
706
524
|
}
|
|
707
525
|
async function runBrowserSignIn(options, endpoint, consoleBase) {
|
|
708
526
|
const startSpin = p.spinner();
|
|
709
527
|
startSpin.start("Starting secure browser sign-in\u2026");
|
|
710
528
|
let session;
|
|
711
529
|
try {
|
|
712
|
-
session = await startDeviceAuth(endpoint);
|
|
530
|
+
session = await startDeviceAuth(endpoint, ensureClientId());
|
|
713
531
|
} catch (err) {
|
|
714
532
|
startSpin.stop("Could not start browser sign-in.");
|
|
715
533
|
p.log.warn(err instanceof Error ? err.message : String(err));
|
|
@@ -807,15 +625,36 @@ async function runBrowserSignIn(options, endpoint, consoleBase) {
|
|
|
807
625
|
if (!projectId || !apiKey) return null;
|
|
808
626
|
return { apiKey, projectId };
|
|
809
627
|
}
|
|
810
|
-
async function collectCredentialsManually(options, consoleBase) {
|
|
628
|
+
async function collectCredentialsManually(options, consoleBase, endpoint) {
|
|
811
629
|
const existing = loadConfig();
|
|
812
|
-
|
|
630
|
+
let savedProjectId = existing.projectId;
|
|
631
|
+
let savedApiKey = existing.apiKey;
|
|
632
|
+
if (!options.projectId && !options.apiKey && savedProjectId && savedApiKey) {
|
|
633
|
+
p.log.info(
|
|
634
|
+
`Found saved credentials from a previous sign-in (project ${sanitizeSecret(savedProjectId).slice(0, 8)}\u2026).`
|
|
635
|
+
);
|
|
636
|
+
const checkSpin = p.spinner();
|
|
637
|
+
checkSpin.start("Checking saved credentials\u2026");
|
|
638
|
+
const check = await apiCall("/v1/sync/whoami", {
|
|
639
|
+
apiKey: sanitizeSecret(savedApiKey),
|
|
640
|
+
projectId: sanitizeSecret(savedProjectId),
|
|
641
|
+
endpoint
|
|
642
|
+
});
|
|
643
|
+
if (check.ok) {
|
|
644
|
+
checkSpin.stop(`Saved credentials still work (${check.data.project_name}).`);
|
|
645
|
+
} else {
|
|
646
|
+
checkSpin.stop("Saved credentials no longer authenticate \u2014 enter fresh ones below.");
|
|
647
|
+
savedProjectId = void 0;
|
|
648
|
+
savedApiKey = void 0;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
const rawProjectId = options.projectId ?? savedProjectId ?? await promptText({
|
|
813
652
|
message: "Project ID",
|
|
814
653
|
placeholder: "e.g. bdafa28d-b153-482f-bd4f-42981f3fd3a4",
|
|
815
654
|
hint: projectIdHint(consoleBase),
|
|
816
655
|
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."
|
|
817
656
|
});
|
|
818
|
-
const rawApiKey = options.apiKey ??
|
|
657
|
+
const rawApiKey = options.apiKey ?? savedApiKey ?? await promptText({
|
|
819
658
|
message: "API key",
|
|
820
659
|
placeholder: "mushi_xxxxxxxxxxxx",
|
|
821
660
|
hint: apiKeyHint(consoleBase),
|
|
@@ -846,10 +685,22 @@ async function verifyCredentials(credentials, options, consoleBase) {
|
|
|
846
685
|
if (!result.ok) {
|
|
847
686
|
spinner2.stop("Credentials could not be verified.");
|
|
848
687
|
p.log.error(result.error?.message ?? "Authentication failed.");
|
|
849
|
-
p.log.
|
|
850
|
-
|
|
688
|
+
p.log.warn("Setup did NOT complete: nothing was installed and no env vars were written.");
|
|
689
|
+
p.log.info(
|
|
690
|
+
[
|
|
691
|
+
"To recover:",
|
|
692
|
+
' \u2022 Re-run `npx mushi-mushi` and choose "Sign in with your browser", or',
|
|
693
|
+
` \u2022 Double-check the Project ID / API key in the console: ${cliSetupDeepLink(consoleBase)}`
|
|
694
|
+
].join("\n")
|
|
695
|
+
);
|
|
851
696
|
throw new Error("Credential verification failed \u2014 fix Project ID / API key and re-run.");
|
|
852
697
|
}
|
|
698
|
+
if (result.data.project_id && result.data.project_id !== credentials.projectId) {
|
|
699
|
+
p.log.warn(
|
|
700
|
+
`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.`
|
|
701
|
+
);
|
|
702
|
+
credentials.projectId = result.data.project_id;
|
|
703
|
+
}
|
|
853
704
|
spinner2.stop(`Connected to ${result.data.project_name}`);
|
|
854
705
|
}
|
|
855
706
|
function sanitizeSecret(raw) {
|
|
@@ -909,10 +760,10 @@ function runCommand(pm, packages, cwd) {
|
|
|
909
760
|
});
|
|
910
761
|
}
|
|
911
762
|
async function writeEnvFile(cwd, apiKey, projectId, framework, endpoint, overwrite) {
|
|
912
|
-
const target = ENV_FILES.find((f) =>
|
|
913
|
-
const targetPath =
|
|
763
|
+
const target = ENV_FILES.find((f) => existsSync2(join2(cwd, f))) ?? ENV_FILES[0];
|
|
764
|
+
const targetPath = join2(cwd, target);
|
|
914
765
|
const newVars = envVarsToWrite(apiKey, projectId, framework, endpoint);
|
|
915
|
-
const existing =
|
|
766
|
+
const existing = existsSync2(targetPath) ? readFileSync2(targetPath, "utf-8") : "";
|
|
916
767
|
if (existing.includes("MUSHI_PROJECT_ID")) {
|
|
917
768
|
let shouldOverwrite = overwrite;
|
|
918
769
|
if (!shouldOverwrite) {
|
|
@@ -971,12 +822,12 @@ function escapeRegexChar(ch) {
|
|
|
971
822
|
return /[-/\\^$+?.()|[\]{}]/.test(ch) ? `\\${ch}` : ch;
|
|
972
823
|
}
|
|
973
824
|
function warnIfMissingFromGitignore(cwd, envFile) {
|
|
974
|
-
const gitignorePath =
|
|
975
|
-
if (!
|
|
825
|
+
const gitignorePath = join2(cwd, ".gitignore");
|
|
826
|
+
if (!existsSync2(gitignorePath)) {
|
|
976
827
|
p.log.warn(`No .gitignore found \u2014 make sure ${envFile} is not committed.`);
|
|
977
828
|
return;
|
|
978
829
|
}
|
|
979
|
-
const content =
|
|
830
|
+
const content = readFileSync2(gitignorePath, "utf-8");
|
|
980
831
|
if (!isEnvFileCoveredByGitignore(content, envFile)) {
|
|
981
832
|
p.log.warn(`${envFile} is not in .gitignore \u2014 add it before committing.`);
|
|
982
833
|
}
|
|
@@ -1018,7 +869,7 @@ async function maybeOfferConnect(credentials, options, _consoleBase) {
|
|
|
1018
869
|
if (p.isCancel(answer) || !answer) return;
|
|
1019
870
|
const endpoint = resolveCloudEndpoint(options.endpoint);
|
|
1020
871
|
try {
|
|
1021
|
-
const { runConnect } = await import("./connect-
|
|
872
|
+
const { runConnect } = await import("./connect-IWFKKXGZ.js");
|
|
1022
873
|
await runConnect({
|
|
1023
874
|
apiKey: credentials.apiKey,
|
|
1024
875
|
projectId: credentials.projectId,
|
package/dist/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mushi-mushi/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.1",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "CLI for Mushi Mushi — `mushi init` wizard installs the right SDK for your framework, plus report triage and pipeline health commands",
|
|
6
6
|
"bin": {
|
|
@@ -47,18 +47,19 @@
|
|
|
47
47
|
"sideEffects": false,
|
|
48
48
|
"keywords": [
|
|
49
49
|
"mushi-mushi",
|
|
50
|
-
"bug-
|
|
51
|
-
"
|
|
52
|
-
"
|
|
53
|
-
"
|
|
54
|
-
"
|
|
55
|
-
"
|
|
56
|
-
"
|
|
57
|
-
"
|
|
50
|
+
"bug-translation",
|
|
51
|
+
"vibe-coding",
|
|
52
|
+
"ai-debugging",
|
|
53
|
+
"ai-triage",
|
|
54
|
+
"auto-fix",
|
|
55
|
+
"mcp",
|
|
56
|
+
"cursor",
|
|
57
|
+
"claude-code",
|
|
58
|
+
"cli",
|
|
58
59
|
"wizard",
|
|
59
60
|
"setup",
|
|
60
|
-
"cli",
|
|
61
61
|
"init",
|
|
62
|
+
"sdk",
|
|
62
63
|
"next.js",
|
|
63
64
|
"nextjs",
|
|
64
65
|
"react",
|
|
@@ -72,10 +73,7 @@
|
|
|
72
73
|
"capacitor",
|
|
73
74
|
"sentry-companion",
|
|
74
75
|
"sentry-alternative",
|
|
75
|
-
"
|
|
76
|
-
"ai-triage",
|
|
77
|
-
"auto-fix",
|
|
78
|
-
"llm-ops"
|
|
76
|
+
"ai-agent"
|
|
79
77
|
],
|
|
80
78
|
"exports": {
|
|
81
79
|
".": {
|