@mushi-mushi/cli 0.19.0 → 0.21.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/CONTRIBUTING.md +14 -2
- package/SECURITY.md +1 -1
- package/dist/{chunk-7Q44KWGN.js → chunk-JAUKBLPK.js} +15 -9
- package/dist/chunk-L7KBJ2XY.js +6 -0
- package/dist/{chunk-D5IDNBJO.js → chunk-R2HPR2FV.js} +15 -9
- package/dist/{connect-IC7I76Z2.js → connect-2RM43XHO.js} +1 -1
- package/dist/{connect-W2Q6N7K3.js → connect-JIEIWTF3.js} +1 -1
- package/dist/detect.d.ts +1 -1
- package/dist/detect.js +1 -1
- package/dist/index.js +888 -203
- package/dist/init.js +394 -78
- package/dist/version.js +1 -1
- package/package.json +3 -3
- package/dist/chunk-T4XX5FMH.js +0 -6
package/dist/init.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
MUSHI_CLI_VERSION
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-L7KBJ2XY.js";
|
|
4
4
|
import {
|
|
5
5
|
TEST_REPORT_FETCH_TIMEOUT_MS,
|
|
6
6
|
loadConfig,
|
|
@@ -15,13 +15,13 @@ import {
|
|
|
15
15
|
envVarsToWrite,
|
|
16
16
|
installCommand,
|
|
17
17
|
readPackageJson
|
|
18
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-JAUKBLPK.js";
|
|
19
19
|
|
|
20
20
|
// src/init.ts
|
|
21
21
|
import * as p from "@clack/prompts";
|
|
22
22
|
import { spawn } from "child_process";
|
|
23
23
|
import { randomUUID } from "crypto";
|
|
24
|
-
import { appendFileSync, existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
|
|
24
|
+
import { appendFileSync, existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync } from "fs";
|
|
25
25
|
import { join as join3 } from "path";
|
|
26
26
|
|
|
27
27
|
// src/console-url.ts
|
|
@@ -31,7 +31,10 @@ var HOSTED_CONSOLE_BASE = "https://kensaur.us/mushi-mushi/admin";
|
|
|
31
31
|
var LOCAL_ADMIN_URL = "http://localhost:6464";
|
|
32
32
|
var LOCAL_PROBE_MS = 800;
|
|
33
33
|
function normalizeConsoleBase(url) {
|
|
34
|
-
|
|
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);
|
|
35
38
|
}
|
|
36
39
|
function consoleUrl(base, route) {
|
|
37
40
|
const normalizedRoute = route.startsWith("/") ? route : `/${route}`;
|
|
@@ -106,10 +109,34 @@ function reportsUrl(base, reportId) {
|
|
|
106
109
|
return reportId ? consoleUrl(base, `/reports/${reportId}`) : consoleUrl(base, "/reports");
|
|
107
110
|
}
|
|
108
111
|
async function openInBrowser(url) {
|
|
109
|
-
|
|
110
|
-
|
|
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]];
|
|
111
131
|
await new Promise((resolve2) => {
|
|
112
|
-
|
|
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
|
+
}
|
|
113
140
|
});
|
|
114
141
|
}
|
|
115
142
|
|
|
@@ -180,13 +207,13 @@ async function apiCall(path, config, options = {}) {
|
|
|
180
207
|
}
|
|
181
208
|
if (!res.ok && typeof body === "object" && body !== null && !("ok" in body)) {
|
|
182
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})`;
|
|
183
213
|
return {
|
|
184
214
|
ok: false,
|
|
185
215
|
httpStatus: res.status,
|
|
186
|
-
error: {
|
|
187
|
-
code: b["code"] ?? `HTTP_${res.status}`,
|
|
188
|
-
message: b["message"] ?? `Request failed (${res.status})`
|
|
189
|
-
}
|
|
216
|
+
error: { code: errCode, message: errMsg }
|
|
190
217
|
};
|
|
191
218
|
}
|
|
192
219
|
return body;
|
|
@@ -211,6 +238,151 @@ async function apiCall(path, config, options = {}) {
|
|
|
211
238
|
}
|
|
212
239
|
}
|
|
213
240
|
|
|
241
|
+
// src/device-auth.ts
|
|
242
|
+
var DEVICE_FETCH_TIMEOUT_MS = 15e3;
|
|
243
|
+
function trimTrailingSlash(endpoint) {
|
|
244
|
+
let end = endpoint.length;
|
|
245
|
+
while (end > 0 && endpoint.charCodeAt(end - 1) === 47) end--;
|
|
246
|
+
return endpoint.slice(0, end);
|
|
247
|
+
}
|
|
248
|
+
async function deviceFetch(url, init = {}) {
|
|
249
|
+
const controller = new AbortController();
|
|
250
|
+
const timer = setTimeout(() => controller.abort(), DEVICE_FETCH_TIMEOUT_MS);
|
|
251
|
+
try {
|
|
252
|
+
return await fetch(url, { ...init, signal: controller.signal });
|
|
253
|
+
} finally {
|
|
254
|
+
clearTimeout(timer);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
async function startDeviceAuth(endpoint) {
|
|
258
|
+
const base = trimTrailingSlash(endpoint);
|
|
259
|
+
const res = await deviceFetch(`${base}/v1/cli/auth/device/start`, {
|
|
260
|
+
method: "POST",
|
|
261
|
+
headers: { "Content-Type": "application/json" }
|
|
262
|
+
});
|
|
263
|
+
const json = await res.json().catch(() => null);
|
|
264
|
+
if (!res.ok || !json?.ok || !json.data) {
|
|
265
|
+
throw new Error(json?.error?.message ?? `Could not start browser sign-in (HTTP ${res.status}).`);
|
|
266
|
+
}
|
|
267
|
+
return json.data;
|
|
268
|
+
}
|
|
269
|
+
async function pollDeviceToken(endpoint, deviceCode) {
|
|
270
|
+
const base = trimTrailingSlash(endpoint);
|
|
271
|
+
try {
|
|
272
|
+
const res = await deviceFetch(`${base}/v1/cli/auth/device/token`, {
|
|
273
|
+
method: "POST",
|
|
274
|
+
headers: { "Content-Type": "application/json" },
|
|
275
|
+
body: JSON.stringify({ device_code: deviceCode })
|
|
276
|
+
});
|
|
277
|
+
const json = await res.json().catch(() => ({}));
|
|
278
|
+
if (res.ok && json.ok && json.data?.cli_token) {
|
|
279
|
+
return { status: "approved", cliToken: json.data.cli_token, userId: json.data.user_id };
|
|
280
|
+
}
|
|
281
|
+
switch (json.error) {
|
|
282
|
+
case "authorization_pending":
|
|
283
|
+
return { status: "pending" };
|
|
284
|
+
case "access_denied":
|
|
285
|
+
return { status: "denied" };
|
|
286
|
+
case "expired_token":
|
|
287
|
+
return { status: "expired" };
|
|
288
|
+
default:
|
|
289
|
+
return {
|
|
290
|
+
status: "error",
|
|
291
|
+
message: json.error_description ?? json.error ?? `HTTP ${res.status}`,
|
|
292
|
+
// 5xx is a server hiccup the next poll may clear; a 4xx is a definitive
|
|
293
|
+
// rejection (already claimed / invalid_grant) that won't fix itself.
|
|
294
|
+
retryable: res.status >= 500
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
} catch (err) {
|
|
298
|
+
return { status: "error", message: err instanceof Error ? err.message : String(err), retryable: true };
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
var defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
302
|
+
async function waitForCliToken(endpoint, session, opts = {}) {
|
|
303
|
+
const sleep = opts.sleep ?? defaultSleep;
|
|
304
|
+
const now = opts.now ?? Date.now;
|
|
305
|
+
const intervalMs = (session.interval || 5) * 1e3;
|
|
306
|
+
const deadline = now() + (session.expires_in || 600) * 1e3;
|
|
307
|
+
const maxConsecutiveErrors = opts.maxConsecutiveErrors ?? 5;
|
|
308
|
+
let consecutiveErrors = 0;
|
|
309
|
+
let firstPoll = true;
|
|
310
|
+
while (now() < deadline) {
|
|
311
|
+
if (!firstPoll) {
|
|
312
|
+
await sleep(intervalMs);
|
|
313
|
+
}
|
|
314
|
+
firstPoll = false;
|
|
315
|
+
const outcome = await pollDeviceToken(endpoint, session.device_code);
|
|
316
|
+
switch (outcome.status) {
|
|
317
|
+
case "approved":
|
|
318
|
+
return outcome.cliToken;
|
|
319
|
+
case "pending":
|
|
320
|
+
consecutiveErrors = 0;
|
|
321
|
+
opts.onPending?.();
|
|
322
|
+
continue;
|
|
323
|
+
case "denied":
|
|
324
|
+
throw new Error("Login was denied in the browser.");
|
|
325
|
+
case "expired":
|
|
326
|
+
throw new Error("The login code expired. Run sign-in again.");
|
|
327
|
+
case "error":
|
|
328
|
+
if (!outcome.retryable) {
|
|
329
|
+
throw new Error(outcome.message);
|
|
330
|
+
}
|
|
331
|
+
consecutiveErrors += 1;
|
|
332
|
+
if (consecutiveErrors >= maxConsecutiveErrors) {
|
|
333
|
+
throw new Error(outcome.message);
|
|
334
|
+
}
|
|
335
|
+
opts.onTransientError?.(outcome.message, consecutiveErrors);
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
throw new Error("Login timed out before approval. Run sign-in again.");
|
|
340
|
+
}
|
|
341
|
+
function cliAuthHeaders(cliToken) {
|
|
342
|
+
return {
|
|
343
|
+
"Content-Type": "application/json",
|
|
344
|
+
Authorization: `Bearer ${cliToken}`
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
async function listProjects(endpoint, cliToken) {
|
|
348
|
+
const base = trimTrailingSlash(endpoint);
|
|
349
|
+
try {
|
|
350
|
+
const res = await deviceFetch(`${base}/v1/cli/projects`, { headers: cliAuthHeaders(cliToken) });
|
|
351
|
+
const json = await res.json().catch(() => null);
|
|
352
|
+
if (res.ok && json?.ok) return json.data?.projects ?? [];
|
|
353
|
+
} catch {
|
|
354
|
+
}
|
|
355
|
+
return [];
|
|
356
|
+
}
|
|
357
|
+
async function createProject(endpoint, cliToken, name) {
|
|
358
|
+
const base = trimTrailingSlash(endpoint);
|
|
359
|
+
const res = await deviceFetch(`${base}/v1/cli/projects`, {
|
|
360
|
+
method: "POST",
|
|
361
|
+
headers: cliAuthHeaders(cliToken),
|
|
362
|
+
body: JSON.stringify({ name })
|
|
363
|
+
});
|
|
364
|
+
const json = await res.json().catch(() => null);
|
|
365
|
+
if (!res.ok || !json?.ok || !json.data) {
|
|
366
|
+
throw new Error(json?.error?.message ?? `Could not create project (HTTP ${res.status}).`);
|
|
367
|
+
}
|
|
368
|
+
const { id, name: created, slug, apiKey } = json.data;
|
|
369
|
+
return { id, name: created, slug, apiKey: apiKey ?? null };
|
|
370
|
+
}
|
|
371
|
+
async function mintProjectKey(endpoint, cliToken, projectId) {
|
|
372
|
+
const base = trimTrailingSlash(endpoint);
|
|
373
|
+
try {
|
|
374
|
+
const res = await deviceFetch(`${base}/v1/cli/projects/${projectId}/keys`, {
|
|
375
|
+
method: "POST",
|
|
376
|
+
headers: cliAuthHeaders(cliToken),
|
|
377
|
+
body: JSON.stringify({})
|
|
378
|
+
});
|
|
379
|
+
const json = await res.json().catch(() => null);
|
|
380
|
+
if (res.ok && json?.ok) return json.data?.key ?? null;
|
|
381
|
+
} catch {
|
|
382
|
+
}
|
|
383
|
+
return null;
|
|
384
|
+
}
|
|
385
|
+
|
|
214
386
|
// src/freshness.ts
|
|
215
387
|
var REGISTRY = "https://registry.npmjs.org";
|
|
216
388
|
var DEFAULT_TIMEOUT_MS = 2e3;
|
|
@@ -367,6 +539,33 @@ function getFrameworkFromPkg(pkg) {
|
|
|
367
539
|
return void 0;
|
|
368
540
|
}
|
|
369
541
|
|
|
542
|
+
// src/auth-ui.ts
|
|
543
|
+
var LINE = "\u2500".repeat(58);
|
|
544
|
+
function printAuthBanner(userCode, verificationUri) {
|
|
545
|
+
console.log("");
|
|
546
|
+
console.log(` \u250C${LINE}\u2510`);
|
|
547
|
+
console.log(` \u2502 Browser sign-in \u2014 follow these 3 steps \u2502`);
|
|
548
|
+
console.log(` \u251C${LINE}\u2524`);
|
|
549
|
+
console.log(` \u2502 \u2502`);
|
|
550
|
+
console.log(` \u2502 1. A browser tab will open (or visit the URL below) \u2502`);
|
|
551
|
+
console.log(` \u2502 2. Sign in if prompted, then click "Approve" \u2502`);
|
|
552
|
+
console.log(` \u2502 3. Come back here \u2014 setup continues automatically \u2502`);
|
|
553
|
+
console.log(` \u2502 \u2502`);
|
|
554
|
+
console.log(` \u251C${LINE}\u2524`);
|
|
555
|
+
console.log(` \u2502 Verification code (show in browser \u2014 NOT terminal): \u2502`);
|
|
556
|
+
console.log(` \u2502 \u2502`);
|
|
557
|
+
console.log(` \u2502 ${userCode.padEnd(52)}\u2502`);
|
|
558
|
+
console.log(` \u2502 \u2502`);
|
|
559
|
+
console.log(` \u2502 \u26A0 Do NOT paste or type this code in your terminal \u2502`);
|
|
560
|
+
console.log(` \u2514${LINE}\u2518`);
|
|
561
|
+
console.log("");
|
|
562
|
+
console.log(` If the browser didn't open, visit this URL and approve:`);
|
|
563
|
+
console.log(` ${verificationUri}`);
|
|
564
|
+
console.log("");
|
|
565
|
+
console.log(" \u23F3 Waiting for browser approval\u2026 (Ctrl+C to cancel)");
|
|
566
|
+
console.log("");
|
|
567
|
+
}
|
|
568
|
+
|
|
370
569
|
// src/init.ts
|
|
371
570
|
var ENV_FILES = [".env.local", ".env"];
|
|
372
571
|
var PROJECT_ID_PATTERN = /^(?:proj_[A-Za-z0-9_-]{10,}|[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/i;
|
|
@@ -392,8 +591,8 @@ async function runInit(options = {}) {
|
|
|
392
591
|
const detected = detectFramework(cwd, pkg);
|
|
393
592
|
const framework = await chooseFramework(detected, options);
|
|
394
593
|
const consoleBase = await resolveConsoleUrl({ cwd });
|
|
395
|
-
|
|
396
|
-
const credentials = await
|
|
594
|
+
const endpoint = resolveCloudEndpoint(options.endpoint);
|
|
595
|
+
const credentials = await acquireCredentials(options, consoleBase, endpoint);
|
|
397
596
|
await verifyCredentials(credentials, options, consoleBase);
|
|
398
597
|
const pm = detectPackageManager(cwd);
|
|
399
598
|
const packagesToInstall = framework.needsWebPackage ? [framework.packageName, "@mushi-mushi/web"] : [framework.packageName];
|
|
@@ -402,8 +601,7 @@ async function runInit(options = {}) {
|
|
|
402
601
|
} else {
|
|
403
602
|
p.log.info(`Skipped install. Run \`${installCommand(pm, packagesToInstall)}\` yourself.`);
|
|
404
603
|
}
|
|
405
|
-
|
|
406
|
-
writeEnvFile(cwd, credentials.apiKey, credentials.projectId, framework);
|
|
604
|
+
await writeEnvFile(cwd, credentials.apiKey, credentials.projectId, framework, endpoint, Boolean(options.yes));
|
|
407
605
|
persistCliConfig(credentials.apiKey, credentials.projectId, endpoint);
|
|
408
606
|
const enableRewards = await maybeEnableRewards(options);
|
|
409
607
|
printNextSteps(framework, consoleBase, enableRewards);
|
|
@@ -449,63 +647,17 @@ async function chooseFramework(detected, options) {
|
|
|
449
647
|
}
|
|
450
648
|
return FRAMEWORKS[confirmed];
|
|
451
649
|
}
|
|
452
|
-
async function
|
|
453
|
-
if (options.
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
message: "Do you already have a Mushi project?",
|
|
459
|
-
options: [
|
|
460
|
-
{
|
|
461
|
-
value: "create",
|
|
462
|
-
label: "No \u2014 open the console to create one",
|
|
463
|
-
hint: "Recommended for first-time setup"
|
|
464
|
-
},
|
|
465
|
-
{
|
|
466
|
-
value: "have",
|
|
467
|
-
label: "Yes \u2014 I have Project ID + API key",
|
|
468
|
-
hint: "Paste values from the admin console"
|
|
469
|
-
},
|
|
470
|
-
{
|
|
471
|
-
value: "login",
|
|
472
|
-
label: "Use mushi login first",
|
|
473
|
-
hint: "Run `mushi login` in another terminal, then re-run this wizard"
|
|
474
|
-
}
|
|
475
|
-
]
|
|
476
|
-
});
|
|
477
|
-
if (p.isCancel(choice)) {
|
|
478
|
-
p.cancel("Aborted.");
|
|
479
|
-
process.exit(0);
|
|
480
|
-
}
|
|
481
|
-
if (choice === "login") {
|
|
482
|
-
p.log.info("Run `mushi login` to save credentials, then re-run `mushi init`.");
|
|
483
|
-
p.cancel("Aborted.");
|
|
484
|
-
process.exit(0);
|
|
485
|
-
}
|
|
486
|
-
if (choice !== "create") return;
|
|
487
|
-
const setupUrl = cliSetupDeepLink(consoleBase);
|
|
488
|
-
p.log.info(`Opening the Mushi console:
|
|
489
|
-
${setupUrl}`);
|
|
490
|
-
await openInBrowser(setupUrl);
|
|
491
|
-
p.log.message("In the console:");
|
|
492
|
-
p.log.message(" 1. Enter a project name and click Create");
|
|
493
|
-
p.log.message(" 2. Copy the Project ID from the success panel");
|
|
494
|
-
p.log.message(" 3. Generate an API key (Verify tab \u2014 report:write scope)");
|
|
495
|
-
p.log.message(" 4. Return here and paste both values");
|
|
496
|
-
const ready = await p.confirm({
|
|
497
|
-
message: "Created your project and ready to paste credentials?",
|
|
498
|
-
initialValue: false
|
|
499
|
-
});
|
|
500
|
-
if (p.isCancel(ready) || !ready) {
|
|
501
|
-
p.log.warn(`Finish in the console first: ${setupUrl}`);
|
|
650
|
+
async function acquireCredentials(options, consoleBase, endpoint) {
|
|
651
|
+
if (options.projectId && options.apiKey) {
|
|
652
|
+
return {
|
|
653
|
+
projectId: sanitizeSecret(options.projectId),
|
|
654
|
+
apiKey: sanitizeSecret(options.apiKey)
|
|
655
|
+
};
|
|
502
656
|
}
|
|
503
|
-
}
|
|
504
|
-
async function collectCredentials(options, consoleBase) {
|
|
505
657
|
const existing = loadConfig();
|
|
506
658
|
if (!options.projectId && !options.apiKey && existing.projectId && existing.apiKey) {
|
|
507
659
|
const reuse = options.yes ? true : await p.confirm({
|
|
508
|
-
message: "Use
|
|
660
|
+
message: "Use the Mushi credentials saved from your last sign-in?",
|
|
509
661
|
initialValue: true
|
|
510
662
|
});
|
|
511
663
|
if (p.isCancel(reuse)) {
|
|
@@ -519,6 +671,144 @@ async function collectCredentials(options, consoleBase) {
|
|
|
519
671
|
};
|
|
520
672
|
}
|
|
521
673
|
}
|
|
674
|
+
if (options.yes) {
|
|
675
|
+
const creds = await runBrowserSignIn(options, endpoint, consoleBase);
|
|
676
|
+
if (creds) return creds;
|
|
677
|
+
p.log.warn("Browser sign-in didn't complete \u2014 switching to manual entry.");
|
|
678
|
+
} else {
|
|
679
|
+
const method = await p.select({
|
|
680
|
+
message: "Connect this app to Mushi",
|
|
681
|
+
initialValue: "browser",
|
|
682
|
+
options: [
|
|
683
|
+
{
|
|
684
|
+
value: "browser",
|
|
685
|
+
label: "Sign in with your browser",
|
|
686
|
+
hint: "Recommended \u2014 no copy-paste, creates the project + key for you"
|
|
687
|
+
},
|
|
688
|
+
{
|
|
689
|
+
value: "manual",
|
|
690
|
+
label: "Paste a Project ID + API key",
|
|
691
|
+
hint: "Self-hosted or expert setup \u2014 run npx mushi-mushi (not mushi setup)"
|
|
692
|
+
}
|
|
693
|
+
]
|
|
694
|
+
});
|
|
695
|
+
if (p.isCancel(method)) {
|
|
696
|
+
p.cancel("Aborted.");
|
|
697
|
+
process.exit(0);
|
|
698
|
+
}
|
|
699
|
+
if (method === "browser") {
|
|
700
|
+
const creds = await runBrowserSignIn(options, endpoint, consoleBase);
|
|
701
|
+
if (creds) return creds;
|
|
702
|
+
p.log.warn("Browser sign-in didn't complete \u2014 switching to manual entry.");
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
return collectCredentialsManually(options, consoleBase);
|
|
706
|
+
}
|
|
707
|
+
async function runBrowserSignIn(options, endpoint, consoleBase) {
|
|
708
|
+
const startSpin = p.spinner();
|
|
709
|
+
startSpin.start("Starting secure browser sign-in\u2026");
|
|
710
|
+
let session;
|
|
711
|
+
try {
|
|
712
|
+
session = await startDeviceAuth(endpoint);
|
|
713
|
+
} catch (err) {
|
|
714
|
+
startSpin.stop("Could not start browser sign-in.");
|
|
715
|
+
p.log.warn(err instanceof Error ? err.message : String(err));
|
|
716
|
+
return null;
|
|
717
|
+
}
|
|
718
|
+
startSpin.stop("Browser sign-in ready.");
|
|
719
|
+
try {
|
|
720
|
+
await openInBrowser(session.verification_uri);
|
|
721
|
+
} catch {
|
|
722
|
+
}
|
|
723
|
+
printAuthBanner(session.user_code, session.verification_uri);
|
|
724
|
+
const waitSpin = p.spinner();
|
|
725
|
+
waitSpin.start("Waiting for you to approve in the browser\u2026");
|
|
726
|
+
let cliToken;
|
|
727
|
+
try {
|
|
728
|
+
cliToken = await waitForCliToken(endpoint, session);
|
|
729
|
+
} catch (err) {
|
|
730
|
+
waitSpin.stop("Browser sign-in didn't complete.");
|
|
731
|
+
p.log.warn(err instanceof Error ? err.message : String(err));
|
|
732
|
+
return null;
|
|
733
|
+
}
|
|
734
|
+
waitSpin.stop("Approved.");
|
|
735
|
+
let projectId = options.projectId ? sanitizeSecret(options.projectId) : void 0;
|
|
736
|
+
let apiKey;
|
|
737
|
+
if (!projectId) {
|
|
738
|
+
let projects = [];
|
|
739
|
+
const fetchSpin = p.spinner();
|
|
740
|
+
fetchSpin.start("Loading your projects\u2026");
|
|
741
|
+
try {
|
|
742
|
+
projects = await listProjects(endpoint, cliToken);
|
|
743
|
+
fetchSpin.stop(projects.length > 0 ? `Found ${projects.length} project(s).` : "No projects yet.");
|
|
744
|
+
} catch (err) {
|
|
745
|
+
fetchSpin.stop("Could not load projects \u2014 you can still create a new one.");
|
|
746
|
+
p.log.warn(err instanceof Error ? err.message : String(err));
|
|
747
|
+
}
|
|
748
|
+
const NEW = "__new__";
|
|
749
|
+
const choice = await p.select({
|
|
750
|
+
message: "Choose a project",
|
|
751
|
+
initialValue: projects[0]?.id ?? NEW,
|
|
752
|
+
options: [
|
|
753
|
+
...projects.map((pr) => ({ value: pr.id, label: pr.name, hint: pr.id.slice(0, 8) })),
|
|
754
|
+
{ value: NEW, label: "Create a new project", hint: "mints an SDK key automatically" }
|
|
755
|
+
]
|
|
756
|
+
});
|
|
757
|
+
if (p.isCancel(choice)) {
|
|
758
|
+
p.cancel("Aborted.");
|
|
759
|
+
process.exit(0);
|
|
760
|
+
}
|
|
761
|
+
if (choice === NEW) {
|
|
762
|
+
const name = await p.text({
|
|
763
|
+
message: "Project name",
|
|
764
|
+
placeholder: "My app",
|
|
765
|
+
validate: (v) => v && v.trim().length > 0 ? void 0 : "Required"
|
|
766
|
+
});
|
|
767
|
+
if (p.isCancel(name)) {
|
|
768
|
+
p.cancel("Aborted.");
|
|
769
|
+
process.exit(0);
|
|
770
|
+
}
|
|
771
|
+
const createSpin = p.spinner();
|
|
772
|
+
createSpin.start(`Creating "${name.trim()}"\u2026`);
|
|
773
|
+
try {
|
|
774
|
+
const created = await createProject(endpoint, cliToken, name.trim());
|
|
775
|
+
projectId = created.id;
|
|
776
|
+
apiKey = created.apiKey ?? void 0;
|
|
777
|
+
createSpin.stop(`Created project "${created.name}".`);
|
|
778
|
+
} catch (err) {
|
|
779
|
+
createSpin.stop("Could not create the project.");
|
|
780
|
+
p.log.warn(err instanceof Error ? err.message : String(err));
|
|
781
|
+
return null;
|
|
782
|
+
}
|
|
783
|
+
} else {
|
|
784
|
+
projectId = choice;
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
if (projectId && !apiKey) {
|
|
788
|
+
const keySpin = p.spinner();
|
|
789
|
+
keySpin.start("Minting SDK key\u2026");
|
|
790
|
+
try {
|
|
791
|
+
const minted = await mintProjectKey(endpoint, cliToken, projectId);
|
|
792
|
+
apiKey = minted ?? void 0;
|
|
793
|
+
if (!apiKey) {
|
|
794
|
+
keySpin.stop("Could not mint an API key.");
|
|
795
|
+
p.log.warn(
|
|
796
|
+
`Open the console Verify tab to generate a key manually: ${consoleUrl(consoleBase, "/onboarding?tab=verify")}`
|
|
797
|
+
);
|
|
798
|
+
return null;
|
|
799
|
+
}
|
|
800
|
+
keySpin.stop("SDK key ready.");
|
|
801
|
+
} catch (err) {
|
|
802
|
+
keySpin.stop("Could not mint an API key.");
|
|
803
|
+
p.log.warn(err instanceof Error ? err.message : String(err));
|
|
804
|
+
return null;
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
if (!projectId || !apiKey) return null;
|
|
808
|
+
return { apiKey, projectId };
|
|
809
|
+
}
|
|
810
|
+
async function collectCredentialsManually(options, consoleBase) {
|
|
811
|
+
const existing = loadConfig();
|
|
522
812
|
const rawProjectId = options.projectId ?? existing.projectId ?? await promptText({
|
|
523
813
|
message: "Project ID",
|
|
524
814
|
placeholder: "e.g. bdafa28d-b153-482f-bd4f-42981f3fd3a4",
|
|
@@ -618,13 +908,37 @@ function runCommand(pm, packages, cwd) {
|
|
|
618
908
|
});
|
|
619
909
|
});
|
|
620
910
|
}
|
|
621
|
-
function writeEnvFile(cwd, apiKey, projectId, framework) {
|
|
911
|
+
async function writeEnvFile(cwd, apiKey, projectId, framework, endpoint, overwrite) {
|
|
622
912
|
const target = ENV_FILES.find((f) => existsSync3(join3(cwd, f))) ?? ENV_FILES[0];
|
|
623
913
|
const targetPath = join3(cwd, target);
|
|
624
|
-
const newVars = envVarsToWrite(apiKey, projectId, framework);
|
|
914
|
+
const newVars = envVarsToWrite(apiKey, projectId, framework, endpoint);
|
|
625
915
|
const existing = existsSync3(targetPath) ? readFileSync3(targetPath, "utf-8") : "";
|
|
626
916
|
if (existing.includes("MUSHI_PROJECT_ID")) {
|
|
627
|
-
|
|
917
|
+
let shouldOverwrite = overwrite;
|
|
918
|
+
if (!shouldOverwrite) {
|
|
919
|
+
const answer = await p.confirm({
|
|
920
|
+
message: `Existing MUSHI_* vars found in ${target}. Update with new credentials?`,
|
|
921
|
+
initialValue: true
|
|
922
|
+
});
|
|
923
|
+
if (p.isCancel(answer)) {
|
|
924
|
+
p.log.info(`Kept existing env vars in ${target}.`);
|
|
925
|
+
return;
|
|
926
|
+
}
|
|
927
|
+
shouldOverwrite = Boolean(answer);
|
|
928
|
+
}
|
|
929
|
+
if (!shouldOverwrite) {
|
|
930
|
+
p.log.info(`Kept existing env vars in ${target}. Re-run and confirm to overwrite.`);
|
|
931
|
+
return;
|
|
932
|
+
}
|
|
933
|
+
const MUSHI_LINE_RE = /^(NEXT_PUBLIC_|NUXT_PUBLIC_|VITE_|EXPO_PUBLIC_)?MUSHI_[A-Z_]+=.*/gm;
|
|
934
|
+
const stripped = existing.replace(MUSHI_LINE_RE, "").replace(/\n{3,}/g, "\n\n").trimEnd();
|
|
935
|
+
const prefix2 = stripped.length > 0 ? "\n" : "";
|
|
936
|
+
writeFileSync(targetPath, `${stripped}${prefix2}
|
|
937
|
+
# Mushi Mushi
|
|
938
|
+
${newVars}
|
|
939
|
+
`);
|
|
940
|
+
p.log.success(`Updated MUSHI_* env vars in ${target}`);
|
|
941
|
+
warnIfMissingFromGitignore(cwd, target);
|
|
628
942
|
return;
|
|
629
943
|
}
|
|
630
944
|
const prefix = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
|
|
@@ -685,12 +999,14 @@ import { MushiRewardsBadge } from '@mushi-mushi/react';
|
|
|
685
999
|
p.log.info(`Enable rewards in your project settings at ${consoleUrl(consoleBase, "/rewards")}`);
|
|
686
1000
|
p.log.info("Users will earn points for bug reports, screen navigation, and app activity.");
|
|
687
1001
|
}
|
|
688
|
-
p.
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
1002
|
+
p.note(
|
|
1003
|
+
[
|
|
1004
|
+
" [ ] 1. Paste the init snippet above into your app entry file",
|
|
1005
|
+
" [ ] 2. Start your dev server",
|
|
1006
|
+
" [ ] 3. Run: mushi connect --write-env --wire-ide --wait",
|
|
1007
|
+
` [ ] 4. Open the Verify tab to send a test report: ${consoleUrl(consoleBase, "/onboarding?tab=verify")}`
|
|
1008
|
+
].join("\n"),
|
|
1009
|
+
"Next steps:"
|
|
694
1010
|
);
|
|
695
1011
|
}
|
|
696
1012
|
async function maybeOfferConnect(credentials, options, _consoleBase) {
|
|
@@ -702,7 +1018,7 @@ async function maybeOfferConnect(credentials, options, _consoleBase) {
|
|
|
702
1018
|
if (p.isCancel(answer) || !answer) return;
|
|
703
1019
|
const endpoint = resolveCloudEndpoint(options.endpoint);
|
|
704
1020
|
try {
|
|
705
|
-
const { runConnect } = await import("./connect-
|
|
1021
|
+
const { runConnect } = await import("./connect-2RM43XHO.js");
|
|
706
1022
|
await runConnect({
|
|
707
1023
|
apiKey: credentials.apiKey,
|
|
708
1024
|
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.21.0",
|
|
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": {
|
|
@@ -18,13 +18,13 @@
|
|
|
18
18
|
"vitest": "^4.1.8",
|
|
19
19
|
"@mushi-mushi/eslint-config": "0.0.0"
|
|
20
20
|
},
|
|
21
|
-
"author": "Kenji Sakuramoto",
|
|
21
|
+
"author": "Kenji Sakuramoto (https://bsky.app/profile/mushimushi.dev)",
|
|
22
22
|
"repository": {
|
|
23
23
|
"type": "git",
|
|
24
24
|
"url": "https://github.com/kensaurus/mushi-mushi.git",
|
|
25
25
|
"directory": "packages/cli"
|
|
26
26
|
},
|
|
27
|
-
"homepage": "https://
|
|
27
|
+
"homepage": "https://kensaur.us/mushi-mushi",
|
|
28
28
|
"bugs": {
|
|
29
29
|
"url": "https://github.com/kensaurus/mushi-mushi/issues"
|
|
30
30
|
},
|