@mushi-mushi/cli 0.19.0 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-6LM2KW4S.js +6 -0
- package/dist/index.js +426 -152
- package/dist/init.js +300 -61
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-T4XX5FMH.js +0 -6
package/dist/index.js
CHANGED
|
@@ -110,7 +110,7 @@ function getAbortSignal(external) {
|
|
|
110
110
|
}
|
|
111
111
|
|
|
112
112
|
// src/version.ts
|
|
113
|
-
var MUSHI_CLI_VERSION = true ? "0.
|
|
113
|
+
var MUSHI_CLI_VERSION = true ? "0.20.0" : "0.0.0-dev";
|
|
114
114
|
|
|
115
115
|
// src/init.ts
|
|
116
116
|
import * as p from "@clack/prompts";
|
|
@@ -126,7 +126,10 @@ var HOSTED_CONSOLE_BASE = "https://kensaur.us/mushi-mushi/admin";
|
|
|
126
126
|
var LOCAL_ADMIN_URL = "http://localhost:6464";
|
|
127
127
|
var LOCAL_PROBE_MS = 800;
|
|
128
128
|
function normalizeConsoleBase(url) {
|
|
129
|
-
|
|
129
|
+
const trimmed = url.trim();
|
|
130
|
+
let end = trimmed.length;
|
|
131
|
+
while (end > 0 && trimmed.charCodeAt(end - 1) === 47) end--;
|
|
132
|
+
return trimmed.slice(0, end);
|
|
130
133
|
}
|
|
131
134
|
function consoleUrl(base, route) {
|
|
132
135
|
const normalizedRoute = route.startsWith("/") ? route : `/${route}`;
|
|
@@ -201,10 +204,34 @@ function reportsUrl(base, reportId) {
|
|
|
201
204
|
return reportId ? consoleUrl(base, `/reports/${reportId}`) : consoleUrl(base, "/reports");
|
|
202
205
|
}
|
|
203
206
|
async function openInBrowser(url) {
|
|
204
|
-
|
|
205
|
-
|
|
207
|
+
let parsed;
|
|
208
|
+
try {
|
|
209
|
+
parsed = new URL(url);
|
|
210
|
+
} catch {
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
const safeUrl = parsed.toString();
|
|
217
|
+
const { spawn: spawn2 } = await import("child_process");
|
|
218
|
+
const [command, args] = process.platform === "win32" ? (
|
|
219
|
+
// Open via rundll32's FileProtocolHandler rather than `cmd /c start`.
|
|
220
|
+
// `start` is a cmd builtin, so the URL would be re-parsed by cmd.exe and
|
|
221
|
+
// characters like `&` / `|` / `^` could still break out — exactly the
|
|
222
|
+
// injection surface CodeQL flags. rundll32 never invokes a shell: the URL
|
|
223
|
+
// is handed to the default browser as one opaque argument.
|
|
224
|
+
["rundll32", ["url.dll,FileProtocolHandler", safeUrl]]
|
|
225
|
+
) : process.platform === "darwin" ? ["open", [safeUrl]] : ["xdg-open", [safeUrl]];
|
|
206
226
|
await new Promise((resolve3) => {
|
|
207
|
-
|
|
227
|
+
try {
|
|
228
|
+
const child = spawn2(command, [...args], { stdio: "ignore", shell: false });
|
|
229
|
+
child.on("error", () => resolve3());
|
|
230
|
+
child.on("exit", () => resolve3());
|
|
231
|
+
child.unref();
|
|
232
|
+
} catch {
|
|
233
|
+
resolve3();
|
|
234
|
+
}
|
|
208
235
|
});
|
|
209
236
|
}
|
|
210
237
|
|
|
@@ -329,6 +356,147 @@ function pad(s, width) {
|
|
|
329
356
|
return s.length >= width ? s : s + " ".repeat(width - s.length);
|
|
330
357
|
}
|
|
331
358
|
|
|
359
|
+
// src/device-auth.ts
|
|
360
|
+
var DEVICE_FETCH_TIMEOUT_MS = 15e3;
|
|
361
|
+
function trimTrailingSlash(endpoint) {
|
|
362
|
+
let end = endpoint.length;
|
|
363
|
+
while (end > 0 && endpoint.charCodeAt(end - 1) === 47) end--;
|
|
364
|
+
return endpoint.slice(0, end);
|
|
365
|
+
}
|
|
366
|
+
async function deviceFetch(url, init = {}) {
|
|
367
|
+
const controller = new AbortController();
|
|
368
|
+
const timer = setTimeout(() => controller.abort(), DEVICE_FETCH_TIMEOUT_MS);
|
|
369
|
+
try {
|
|
370
|
+
return await fetch(url, { ...init, signal: controller.signal });
|
|
371
|
+
} finally {
|
|
372
|
+
clearTimeout(timer);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
async function startDeviceAuth(endpoint) {
|
|
376
|
+
const base = trimTrailingSlash(endpoint);
|
|
377
|
+
const res = await deviceFetch(`${base}/v1/cli/auth/device/start`, {
|
|
378
|
+
method: "POST",
|
|
379
|
+
headers: { "Content-Type": "application/json" }
|
|
380
|
+
});
|
|
381
|
+
const json = await res.json().catch(() => null);
|
|
382
|
+
if (!res.ok || !json?.ok || !json.data) {
|
|
383
|
+
throw new Error(json?.error?.message ?? `Could not start browser sign-in (HTTP ${res.status}).`);
|
|
384
|
+
}
|
|
385
|
+
return json.data;
|
|
386
|
+
}
|
|
387
|
+
async function pollDeviceToken(endpoint, deviceCode) {
|
|
388
|
+
const base = trimTrailingSlash(endpoint);
|
|
389
|
+
try {
|
|
390
|
+
const res = await deviceFetch(`${base}/v1/cli/auth/device/token`, {
|
|
391
|
+
method: "POST",
|
|
392
|
+
headers: { "Content-Type": "application/json" },
|
|
393
|
+
body: JSON.stringify({ device_code: deviceCode })
|
|
394
|
+
});
|
|
395
|
+
const json = await res.json().catch(() => ({}));
|
|
396
|
+
if (res.ok && json.ok && json.data?.cli_token) {
|
|
397
|
+
return { status: "approved", cliToken: json.data.cli_token, userId: json.data.user_id };
|
|
398
|
+
}
|
|
399
|
+
switch (json.error) {
|
|
400
|
+
case "authorization_pending":
|
|
401
|
+
return { status: "pending" };
|
|
402
|
+
case "access_denied":
|
|
403
|
+
return { status: "denied" };
|
|
404
|
+
case "expired_token":
|
|
405
|
+
return { status: "expired" };
|
|
406
|
+
default:
|
|
407
|
+
return {
|
|
408
|
+
status: "error",
|
|
409
|
+
message: json.error_description ?? json.error ?? `HTTP ${res.status}`,
|
|
410
|
+
// 5xx is a server hiccup the next poll may clear; a 4xx is a definitive
|
|
411
|
+
// rejection (already claimed / invalid_grant) that won't fix itself.
|
|
412
|
+
retryable: res.status >= 500
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
} catch (err) {
|
|
416
|
+
return { status: "error", message: err instanceof Error ? err.message : String(err), retryable: true };
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
var defaultSleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
420
|
+
async function waitForCliToken(endpoint, session, opts = {}) {
|
|
421
|
+
const sleep = opts.sleep ?? defaultSleep;
|
|
422
|
+
const now = opts.now ?? Date.now;
|
|
423
|
+
const intervalMs = (session.interval || 5) * 1e3;
|
|
424
|
+
const deadline = now() + (session.expires_in || 600) * 1e3;
|
|
425
|
+
const maxConsecutiveErrors = opts.maxConsecutiveErrors ?? 5;
|
|
426
|
+
let consecutiveErrors = 0;
|
|
427
|
+
while (now() < deadline) {
|
|
428
|
+
await sleep(intervalMs);
|
|
429
|
+
const outcome = await pollDeviceToken(endpoint, session.device_code);
|
|
430
|
+
switch (outcome.status) {
|
|
431
|
+
case "approved":
|
|
432
|
+
return outcome.cliToken;
|
|
433
|
+
case "pending":
|
|
434
|
+
consecutiveErrors = 0;
|
|
435
|
+
opts.onPending?.();
|
|
436
|
+
continue;
|
|
437
|
+
case "denied":
|
|
438
|
+
throw new Error("Login was denied in the browser.");
|
|
439
|
+
case "expired":
|
|
440
|
+
throw new Error("The login code expired. Run sign-in again.");
|
|
441
|
+
case "error":
|
|
442
|
+
if (!outcome.retryable) {
|
|
443
|
+
throw new Error(outcome.message);
|
|
444
|
+
}
|
|
445
|
+
consecutiveErrors += 1;
|
|
446
|
+
if (consecutiveErrors >= maxConsecutiveErrors) {
|
|
447
|
+
throw new Error(outcome.message);
|
|
448
|
+
}
|
|
449
|
+
opts.onTransientError?.(outcome.message, consecutiveErrors);
|
|
450
|
+
continue;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
throw new Error("Login timed out before approval. Run sign-in again.");
|
|
454
|
+
}
|
|
455
|
+
function cliAuthHeaders(cliToken) {
|
|
456
|
+
return {
|
|
457
|
+
"Content-Type": "application/json",
|
|
458
|
+
Authorization: `Bearer ${cliToken}`
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
async function listProjects(endpoint, cliToken) {
|
|
462
|
+
const base = trimTrailingSlash(endpoint);
|
|
463
|
+
try {
|
|
464
|
+
const res = await deviceFetch(`${base}/v1/cli/projects`, { headers: cliAuthHeaders(cliToken) });
|
|
465
|
+
const json = await res.json().catch(() => null);
|
|
466
|
+
if (res.ok && json?.ok) return json.data?.projects ?? [];
|
|
467
|
+
} catch {
|
|
468
|
+
}
|
|
469
|
+
return [];
|
|
470
|
+
}
|
|
471
|
+
async function createProject(endpoint, cliToken, name) {
|
|
472
|
+
const base = trimTrailingSlash(endpoint);
|
|
473
|
+
const res = await deviceFetch(`${base}/v1/cli/projects`, {
|
|
474
|
+
method: "POST",
|
|
475
|
+
headers: cliAuthHeaders(cliToken),
|
|
476
|
+
body: JSON.stringify({ name })
|
|
477
|
+
});
|
|
478
|
+
const json = await res.json().catch(() => null);
|
|
479
|
+
if (!res.ok || !json?.ok || !json.data) {
|
|
480
|
+
throw new Error(json?.error?.message ?? `Could not create project (HTTP ${res.status}).`);
|
|
481
|
+
}
|
|
482
|
+
const { id, name: created, slug, apiKey } = json.data;
|
|
483
|
+
return { id, name: created, slug, apiKey: apiKey ?? null };
|
|
484
|
+
}
|
|
485
|
+
async function mintProjectKey(endpoint, cliToken, projectId) {
|
|
486
|
+
const base = trimTrailingSlash(endpoint);
|
|
487
|
+
try {
|
|
488
|
+
const res = await deviceFetch(`${base}/v1/cli/projects/${projectId}/keys`, {
|
|
489
|
+
method: "POST",
|
|
490
|
+
headers: cliAuthHeaders(cliToken),
|
|
491
|
+
body: JSON.stringify({})
|
|
492
|
+
});
|
|
493
|
+
const json = await res.json().catch(() => null);
|
|
494
|
+
if (res.ok && json?.ok) return json.data?.key ?? null;
|
|
495
|
+
} catch {
|
|
496
|
+
}
|
|
497
|
+
return null;
|
|
498
|
+
}
|
|
499
|
+
|
|
332
500
|
// src/freshness.ts
|
|
333
501
|
var REGISTRY = "https://registry.npmjs.org";
|
|
334
502
|
var DEFAULT_TIMEOUT_MS = 2e3;
|
|
@@ -510,8 +678,8 @@ async function runInit(options = {}) {
|
|
|
510
678
|
const detected = detectFramework(cwd, pkg);
|
|
511
679
|
const framework = await chooseFramework(detected, options);
|
|
512
680
|
const consoleBase = await resolveConsoleUrl({ cwd });
|
|
513
|
-
|
|
514
|
-
const credentials = await
|
|
681
|
+
const endpoint = resolveCloudEndpoint(options.endpoint);
|
|
682
|
+
const credentials = await acquireCredentials(options, consoleBase, endpoint);
|
|
515
683
|
await verifyCredentials(credentials, options, consoleBase);
|
|
516
684
|
const pm = detectPackageManager(cwd);
|
|
517
685
|
const packagesToInstall = framework.needsWebPackage ? [framework.packageName, "@mushi-mushi/web"] : [framework.packageName];
|
|
@@ -520,7 +688,6 @@ async function runInit(options = {}) {
|
|
|
520
688
|
} else {
|
|
521
689
|
p.log.info(`Skipped install. Run \`${installCommand(pm, packagesToInstall)}\` yourself.`);
|
|
522
690
|
}
|
|
523
|
-
const endpoint = resolveCloudEndpoint(options.endpoint);
|
|
524
691
|
writeEnvFile(cwd, credentials.apiKey, credentials.projectId, framework);
|
|
525
692
|
persistCliConfig(credentials.apiKey, credentials.projectId, endpoint);
|
|
526
693
|
const enableRewards = await maybeEnableRewards(options);
|
|
@@ -567,63 +734,17 @@ async function chooseFramework(detected, options) {
|
|
|
567
734
|
}
|
|
568
735
|
return FRAMEWORKS[confirmed];
|
|
569
736
|
}
|
|
570
|
-
async function
|
|
571
|
-
if (options.
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
message: "Do you already have a Mushi project?",
|
|
577
|
-
options: [
|
|
578
|
-
{
|
|
579
|
-
value: "create",
|
|
580
|
-
label: "No \u2014 open the console to create one",
|
|
581
|
-
hint: "Recommended for first-time setup"
|
|
582
|
-
},
|
|
583
|
-
{
|
|
584
|
-
value: "have",
|
|
585
|
-
label: "Yes \u2014 I have Project ID + API key",
|
|
586
|
-
hint: "Paste values from the admin console"
|
|
587
|
-
},
|
|
588
|
-
{
|
|
589
|
-
value: "login",
|
|
590
|
-
label: "Use mushi login first",
|
|
591
|
-
hint: "Run `mushi login` in another terminal, then re-run this wizard"
|
|
592
|
-
}
|
|
593
|
-
]
|
|
594
|
-
});
|
|
595
|
-
if (p.isCancel(choice)) {
|
|
596
|
-
p.cancel("Aborted.");
|
|
597
|
-
process.exit(0);
|
|
598
|
-
}
|
|
599
|
-
if (choice === "login") {
|
|
600
|
-
p.log.info("Run `mushi login` to save credentials, then re-run `mushi init`.");
|
|
601
|
-
p.cancel("Aborted.");
|
|
602
|
-
process.exit(0);
|
|
603
|
-
}
|
|
604
|
-
if (choice !== "create") return;
|
|
605
|
-
const setupUrl = cliSetupDeepLink(consoleBase);
|
|
606
|
-
p.log.info(`Opening the Mushi console:
|
|
607
|
-
${setupUrl}`);
|
|
608
|
-
await openInBrowser(setupUrl);
|
|
609
|
-
p.log.message("In the console:");
|
|
610
|
-
p.log.message(" 1. Enter a project name and click Create");
|
|
611
|
-
p.log.message(" 2. Copy the Project ID from the success panel");
|
|
612
|
-
p.log.message(" 3. Generate an API key (Verify tab \u2014 report:write scope)");
|
|
613
|
-
p.log.message(" 4. Return here and paste both values");
|
|
614
|
-
const ready = await p.confirm({
|
|
615
|
-
message: "Created your project and ready to paste credentials?",
|
|
616
|
-
initialValue: false
|
|
617
|
-
});
|
|
618
|
-
if (p.isCancel(ready) || !ready) {
|
|
619
|
-
p.log.warn(`Finish in the console first: ${setupUrl}`);
|
|
737
|
+
async function acquireCredentials(options, consoleBase, endpoint) {
|
|
738
|
+
if (options.projectId && options.apiKey) {
|
|
739
|
+
return {
|
|
740
|
+
projectId: sanitizeSecret(options.projectId),
|
|
741
|
+
apiKey: sanitizeSecret(options.apiKey)
|
|
742
|
+
};
|
|
620
743
|
}
|
|
621
|
-
}
|
|
622
|
-
async function collectCredentials(options, consoleBase) {
|
|
623
744
|
const existing = loadConfig();
|
|
624
745
|
if (!options.projectId && !options.apiKey && existing.projectId && existing.apiKey) {
|
|
625
746
|
const reuse = options.yes ? true : await p.confirm({
|
|
626
|
-
message: "Use
|
|
747
|
+
message: "Use the Mushi credentials saved from your last sign-in?",
|
|
627
748
|
initialValue: true
|
|
628
749
|
});
|
|
629
750
|
if (p.isCancel(reuse)) {
|
|
@@ -637,6 +758,124 @@ async function collectCredentials(options, consoleBase) {
|
|
|
637
758
|
};
|
|
638
759
|
}
|
|
639
760
|
}
|
|
761
|
+
if (options.yes) {
|
|
762
|
+
const creds = await runBrowserSignIn(options, endpoint);
|
|
763
|
+
if (creds) return creds;
|
|
764
|
+
p.log.warn("Browser sign-in didn't complete \u2014 switching to manual entry.");
|
|
765
|
+
} else {
|
|
766
|
+
const method = await p.select({
|
|
767
|
+
message: "Connect this app to Mushi",
|
|
768
|
+
initialValue: "browser",
|
|
769
|
+
options: [
|
|
770
|
+
{
|
|
771
|
+
value: "browser",
|
|
772
|
+
label: "Sign in with your browser",
|
|
773
|
+
hint: "Recommended \u2014 no copy-paste, creates the project + key for you"
|
|
774
|
+
},
|
|
775
|
+
{
|
|
776
|
+
value: "manual",
|
|
777
|
+
label: "Paste a Project ID + API key",
|
|
778
|
+
hint: "Self-hosted or expert setup"
|
|
779
|
+
}
|
|
780
|
+
]
|
|
781
|
+
});
|
|
782
|
+
if (p.isCancel(method)) {
|
|
783
|
+
p.cancel("Aborted.");
|
|
784
|
+
process.exit(0);
|
|
785
|
+
}
|
|
786
|
+
if (method === "browser") {
|
|
787
|
+
const creds = await runBrowserSignIn(options, endpoint);
|
|
788
|
+
if (creds) return creds;
|
|
789
|
+
p.log.warn("Browser sign-in didn't complete \u2014 switching to manual entry.");
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
return collectCredentialsManually(options, consoleBase);
|
|
793
|
+
}
|
|
794
|
+
async function runBrowserSignIn(options, endpoint) {
|
|
795
|
+
const startSpin = p.spinner();
|
|
796
|
+
startSpin.start("Starting secure browser sign-in\u2026");
|
|
797
|
+
let session;
|
|
798
|
+
try {
|
|
799
|
+
session = await startDeviceAuth(endpoint);
|
|
800
|
+
} catch (err) {
|
|
801
|
+
startSpin.stop("Could not start browser sign-in.");
|
|
802
|
+
p.log.warn(err instanceof Error ? err.message : String(err));
|
|
803
|
+
return null;
|
|
804
|
+
}
|
|
805
|
+
startSpin.stop("Browser sign-in ready.");
|
|
806
|
+
p.log.step(`Confirmation code: ${session.user_code}`);
|
|
807
|
+
p.log.info(`Opening ${session.verification_uri}`);
|
|
808
|
+
try {
|
|
809
|
+
await openInBrowser(session.verification_uri);
|
|
810
|
+
} catch {
|
|
811
|
+
}
|
|
812
|
+
p.log.message("Approve the request in your browser to continue. (Ctrl+C to cancel)");
|
|
813
|
+
const waitSpin = p.spinner();
|
|
814
|
+
waitSpin.start("Waiting for you to approve in the browser\u2026");
|
|
815
|
+
let cliToken;
|
|
816
|
+
try {
|
|
817
|
+
cliToken = await waitForCliToken(endpoint, session);
|
|
818
|
+
} catch (err) {
|
|
819
|
+
waitSpin.stop("Browser sign-in didn't complete.");
|
|
820
|
+
p.log.warn(err instanceof Error ? err.message : String(err));
|
|
821
|
+
return null;
|
|
822
|
+
}
|
|
823
|
+
waitSpin.stop("Approved.");
|
|
824
|
+
let projectId = options.projectId ? sanitizeSecret(options.projectId) : void 0;
|
|
825
|
+
let apiKey;
|
|
826
|
+
if (!projectId) {
|
|
827
|
+
let projects = [];
|
|
828
|
+
try {
|
|
829
|
+
projects = await listProjects(endpoint, cliToken);
|
|
830
|
+
} catch {
|
|
831
|
+
}
|
|
832
|
+
const NEW = "__new__";
|
|
833
|
+
const choice = await p.select({
|
|
834
|
+
message: "Choose a project",
|
|
835
|
+
initialValue: projects[0]?.id ?? NEW,
|
|
836
|
+
options: [
|
|
837
|
+
...projects.map((pr) => ({ value: pr.id, label: pr.name, hint: pr.id.slice(0, 8) })),
|
|
838
|
+
{ value: NEW, label: "Create a new project", hint: "mints an SDK key automatically" }
|
|
839
|
+
]
|
|
840
|
+
});
|
|
841
|
+
if (p.isCancel(choice)) {
|
|
842
|
+
p.cancel("Aborted.");
|
|
843
|
+
process.exit(0);
|
|
844
|
+
}
|
|
845
|
+
if (choice === NEW) {
|
|
846
|
+
const name = await p.text({
|
|
847
|
+
message: "Project name",
|
|
848
|
+
placeholder: "My app",
|
|
849
|
+
validate: (v) => v && v.trim().length > 0 ? void 0 : "Required"
|
|
850
|
+
});
|
|
851
|
+
if (p.isCancel(name)) {
|
|
852
|
+
p.cancel("Aborted.");
|
|
853
|
+
process.exit(0);
|
|
854
|
+
}
|
|
855
|
+
const createSpin = p.spinner();
|
|
856
|
+
createSpin.start(`Creating "${name.trim()}"\u2026`);
|
|
857
|
+
try {
|
|
858
|
+
const created = await createProject(endpoint, cliToken, name.trim());
|
|
859
|
+
projectId = created.id;
|
|
860
|
+
apiKey = created.apiKey ?? void 0;
|
|
861
|
+
createSpin.stop(`Created project "${created.name}".`);
|
|
862
|
+
} catch (err) {
|
|
863
|
+
createSpin.stop("Could not create the project.");
|
|
864
|
+
p.log.warn(err instanceof Error ? err.message : String(err));
|
|
865
|
+
return null;
|
|
866
|
+
}
|
|
867
|
+
} else {
|
|
868
|
+
projectId = choice;
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
if (projectId && !apiKey) {
|
|
872
|
+
apiKey = await mintProjectKey(endpoint, cliToken, projectId) ?? void 0;
|
|
873
|
+
}
|
|
874
|
+
if (!projectId || !apiKey) return null;
|
|
875
|
+
return { apiKey, projectId };
|
|
876
|
+
}
|
|
877
|
+
async function collectCredentialsManually(options, consoleBase) {
|
|
878
|
+
const existing = loadConfig();
|
|
640
879
|
const rawProjectId = options.projectId ?? existing.projectId ?? await promptText({
|
|
641
880
|
message: "Project ID",
|
|
642
881
|
placeholder: "e.g. bdafa28d-b153-482f-bd4f-42981f3fd3a4",
|
|
@@ -1151,15 +1390,7 @@ Examples:
|
|
|
1151
1390
|
console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
1152
1391
|
let deviceData;
|
|
1153
1392
|
try {
|
|
1154
|
-
|
|
1155
|
-
method: "POST",
|
|
1156
|
-
headers: { "Content-Type": "application/json" }
|
|
1157
|
-
});
|
|
1158
|
-
const json = await res.json();
|
|
1159
|
-
if (!res.ok || !json.ok || !json.data) {
|
|
1160
|
-
throw new Error(json.error?.message ?? `HTTP ${res.status}`);
|
|
1161
|
-
}
|
|
1162
|
-
deviceData = json.data;
|
|
1393
|
+
deviceData = await startDeviceAuth(endpoint);
|
|
1163
1394
|
} catch (err) {
|
|
1164
1395
|
process.stderr.write(`
|
|
1165
1396
|
error: Could not start login session: ${err instanceof Error ? err.message : String(err)}
|
|
@@ -1185,42 +1416,41 @@ error: Could not start login session: ${err instanceof Error ? err.message : Str
|
|
|
1185
1416
|
let cliToken = null;
|
|
1186
1417
|
const pollIntervalMs = (deviceData.interval ?? 5) * 1e3;
|
|
1187
1418
|
const deadline = Date.now() + (deviceData.expires_in ?? 600) * 1e3;
|
|
1419
|
+
const MAX_CONSECUTIVE_ERRORS = 5;
|
|
1420
|
+
let consecutiveErrors = 0;
|
|
1188
1421
|
while (Date.now() < deadline) {
|
|
1189
1422
|
await new Promise((r) => setTimeout(r, pollIntervalMs));
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
console.log("");
|
|
1208
|
-
process.stderr.write("\nerror: Login denied in the browser.\n");
|
|
1209
|
-
process.exit(1);
|
|
1210
|
-
}
|
|
1211
|
-
if (errCode === "expired_token") {
|
|
1212
|
-
console.log("");
|
|
1213
|
-
process.stderr.write("\nerror: Login code expired. Run mushi login again.\n");
|
|
1214
|
-
process.exit(1);
|
|
1423
|
+
const outcome = await pollDeviceToken(endpoint, deviceData.device_code);
|
|
1424
|
+
if (outcome.status === "approved") {
|
|
1425
|
+
cliToken = outcome.cliToken;
|
|
1426
|
+
break;
|
|
1427
|
+
}
|
|
1428
|
+
if (outcome.status === "pending") {
|
|
1429
|
+
consecutiveErrors = 0;
|
|
1430
|
+
process.stdout.write(".");
|
|
1431
|
+
continue;
|
|
1432
|
+
}
|
|
1433
|
+
if (outcome.status === "error") {
|
|
1434
|
+
if (outcome.retryable) {
|
|
1435
|
+
consecutiveErrors += 1;
|
|
1436
|
+
if (consecutiveErrors < MAX_CONSECUTIVE_ERRORS) {
|
|
1437
|
+
process.stdout.write("\xB7");
|
|
1438
|
+
continue;
|
|
1439
|
+
}
|
|
1215
1440
|
}
|
|
1216
|
-
throw new Error(pollJson.error_description ?? errCode ?? `HTTP ${pollRes.status}`);
|
|
1217
|
-
} catch (err) {
|
|
1218
1441
|
console.log("");
|
|
1219
1442
|
process.stderr.write(`
|
|
1220
|
-
error:
|
|
1443
|
+
error: ${outcome.message}
|
|
1221
1444
|
`);
|
|
1222
1445
|
process.exit(1);
|
|
1223
1446
|
}
|
|
1447
|
+
console.log("");
|
|
1448
|
+
if (outcome.status === "denied") {
|
|
1449
|
+
process.stderr.write("\nerror: Login denied in the browser.\n");
|
|
1450
|
+
} else if (outcome.status === "expired") {
|
|
1451
|
+
process.stderr.write("\nerror: Login code expired. Run mushi login again.\n");
|
|
1452
|
+
}
|
|
1453
|
+
process.exit(1);
|
|
1224
1454
|
}
|
|
1225
1455
|
if (!cliToken) {
|
|
1226
1456
|
console.log("");
|
|
@@ -1229,17 +1459,7 @@ error: Poll failed: ${err instanceof Error ? err.message : String(err)}
|
|
|
1229
1459
|
}
|
|
1230
1460
|
console.log("");
|
|
1231
1461
|
console.log(" \u2713 Approved!");
|
|
1232
|
-
const
|
|
1233
|
-
"Content-Type": "application/json",
|
|
1234
|
-
"Authorization": `Bearer ${cliToken}`
|
|
1235
|
-
};
|
|
1236
|
-
let projectsList = [];
|
|
1237
|
-
try {
|
|
1238
|
-
const listRes = await fetch(`${endpoint}/v1/cli/projects`, { headers: cliHeaders });
|
|
1239
|
-
const listJson = await listRes.json();
|
|
1240
|
-
if (listRes.ok && listJson.ok) projectsList = listJson.data?.projects ?? [];
|
|
1241
|
-
} catch {
|
|
1242
|
-
}
|
|
1462
|
+
const projectsList = await listProjects(endpoint, cliToken);
|
|
1243
1463
|
let chosenProjectId = projectId;
|
|
1244
1464
|
let chosenProjectName;
|
|
1245
1465
|
if (!chosenProjectId) {
|
|
@@ -1271,18 +1491,10 @@ error: Poll failed: ${err instanceof Error ? err.message : String(err)}
|
|
|
1271
1491
|
process.exit(2);
|
|
1272
1492
|
}
|
|
1273
1493
|
try {
|
|
1274
|
-
const
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
});
|
|
1279
|
-
const createJson = await createRes.json();
|
|
1280
|
-
if (!createRes.ok || !createJson.ok || !createJson.data) {
|
|
1281
|
-
throw new Error(createJson.error?.message ?? `HTTP ${createRes.status}`);
|
|
1282
|
-
}
|
|
1283
|
-
chosenProjectId = createJson.data.id;
|
|
1284
|
-
chosenProjectName = createJson.data.name;
|
|
1285
|
-
apiKey = createJson.data.apiKey ?? void 0;
|
|
1494
|
+
const created = await createProject(endpoint, cliToken, newName.trim());
|
|
1495
|
+
chosenProjectId = created.id;
|
|
1496
|
+
chosenProjectName = created.name;
|
|
1497
|
+
apiKey = created.apiKey ?? void 0;
|
|
1286
1498
|
console.log(` \u2713 Created project "${chosenProjectName}"`);
|
|
1287
1499
|
} catch (err) {
|
|
1288
1500
|
process.stderr.write(`
|
|
@@ -1296,15 +1508,7 @@ error: Could not create project: ${err instanceof Error ? err.message : String(e
|
|
|
1296
1508
|
}
|
|
1297
1509
|
if (!apiKey && chosenProjectId) {
|
|
1298
1510
|
try {
|
|
1299
|
-
|
|
1300
|
-
method: "POST",
|
|
1301
|
-
headers: cliHeaders,
|
|
1302
|
-
body: JSON.stringify({})
|
|
1303
|
-
});
|
|
1304
|
-
const keyJson = await keyRes.json();
|
|
1305
|
-
if (keyRes.ok && keyJson.ok) {
|
|
1306
|
-
apiKey = keyJson.data?.key ?? void 0;
|
|
1307
|
-
}
|
|
1511
|
+
apiKey = await mintProjectKey(endpoint, cliToken, chosenProjectId) ?? void 0;
|
|
1308
1512
|
} catch {
|
|
1309
1513
|
}
|
|
1310
1514
|
}
|
|
@@ -2145,56 +2349,124 @@ Examples:
|
|
|
2145
2349
|
// src/commands/project.ts
|
|
2146
2350
|
function registerProjectCommands(program2) {
|
|
2147
2351
|
const project = program2.command("project").description("Project management");
|
|
2148
|
-
project.command("create").description("Create a
|
|
2149
|
-
|
|
2150
|
-
and writes the following to the current directory:
|
|
2352
|
+
project.command("create").description("Create or select a Mushi project via browser sign-in, then write config files").option("--name <name>", "Project name for a new project (skips the prompt)").option("--no-browser", "Print the verification URL instead of opening the browser").option("--endpoint <url>", "Override API endpoint (self-hosted)").addHelpText("after", `
|
|
2353
|
+
Signs you in through the browser (no copy-paste), creates or selects a project,
|
|
2354
|
+
mints a report:write SDK key, and writes the following to the current directory:
|
|
2151
2355
|
.env.local \u2014 MUSHI_API_KEY, MUSHI_PROJECT_ID, MUSHI_API_ENDPOINT
|
|
2152
2356
|
.cursor/mcp.json \u2014 pre-filled mcpServers.mushi block for Cursor
|
|
2153
2357
|
|
|
2154
2358
|
Typical first-time flow:
|
|
2155
2359
|
npx mushi-mushi project create
|
|
2156
|
-
# Browser opens \u2192
|
|
2360
|
+
# Browser opens \u2192 click Approve \u2192 pick or create a project
|
|
2157
2361
|
# CLI writes .env.local and .cursor/mcp.json
|
|
2158
2362
|
# mushi whoami to confirm`).action(async (opts) => {
|
|
2159
2363
|
const { writeFile } = await import("fs/promises");
|
|
2160
2364
|
const { existsSync: existsSync4 } = await import("fs");
|
|
2161
2365
|
const nodePath = await import("path");
|
|
2162
|
-
const
|
|
2163
|
-
const
|
|
2164
|
-
|
|
2366
|
+
const savedConfig = loadConfig();
|
|
2367
|
+
const endpoint = resolveCloudEndpoint(
|
|
2368
|
+
opts.endpoint ?? process.env.MUSHI_API_ENDPOINT?.trim() ?? savedConfig.endpoint
|
|
2369
|
+
);
|
|
2370
|
+
const consoleBase = await resolveConsoleUrl();
|
|
2165
2371
|
console.log("");
|
|
2166
2372
|
console.log(" Mushi project create");
|
|
2167
2373
|
console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
2168
2374
|
console.log("");
|
|
2375
|
+
let session;
|
|
2376
|
+
try {
|
|
2377
|
+
session = await startDeviceAuth(endpoint);
|
|
2378
|
+
} catch (err) {
|
|
2379
|
+
process.stderr.write(`
|
|
2380
|
+
error: Could not start browser sign-in: ${err instanceof Error ? err.message : String(err)}
|
|
2381
|
+
`);
|
|
2382
|
+
process.stderr.write(" Fallback: mushi login --api-key <key> --project-id <uuid>\n");
|
|
2383
|
+
process.exit(1);
|
|
2384
|
+
}
|
|
2385
|
+
console.log(` Confirmation code: ${session.user_code}`);
|
|
2386
|
+
console.log("");
|
|
2169
2387
|
if (opts.browser !== false) {
|
|
2170
|
-
console.log("
|
|
2388
|
+
console.log(" Opening the Mushi console in your browser\u2026");
|
|
2171
2389
|
try {
|
|
2172
|
-
|
|
2173
|
-
const openCmd = process.platform === "win32" ? `start "" "${signUpUrl}"` : process.platform === "darwin" ? `open "${signUpUrl}"` : `xdg-open "${signUpUrl}"`;
|
|
2174
|
-
exec(openCmd);
|
|
2390
|
+
await openInBrowser(session.verification_uri);
|
|
2175
2391
|
} catch {
|
|
2176
2392
|
}
|
|
2177
|
-
} else {
|
|
2178
|
-
console.log(` 1. Sign up or log in at: ${signUpUrl}`);
|
|
2179
2393
|
}
|
|
2394
|
+
console.log(` If the browser didn't open: ${session.verification_uri}`);
|
|
2180
2395
|
console.log("");
|
|
2181
|
-
console.log("
|
|
2182
|
-
|
|
2396
|
+
console.log(" Waiting for you to approve in the browser\u2026 (Ctrl+C to cancel)");
|
|
2397
|
+
let cliToken;
|
|
2398
|
+
try {
|
|
2399
|
+
cliToken = await waitForCliToken(endpoint, session, {
|
|
2400
|
+
onPending: () => process.stdout.write("."),
|
|
2401
|
+
onTransientError: () => process.stdout.write("\xB7")
|
|
2402
|
+
});
|
|
2403
|
+
} catch (err) {
|
|
2404
|
+
console.log("");
|
|
2405
|
+
process.stderr.write(`
|
|
2406
|
+
error: ${err instanceof Error ? err.message : String(err)}
|
|
2407
|
+
`);
|
|
2408
|
+
process.exit(1);
|
|
2409
|
+
}
|
|
2183
2410
|
console.log("");
|
|
2411
|
+
console.log(" \u2713 Approved!");
|
|
2412
|
+
const projectsList = await listProjects(endpoint, cliToken);
|
|
2184
2413
|
const { createInterface } = await import("readline");
|
|
2185
2414
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
2186
2415
|
const ask = (q) => new Promise((resolve3) => rl.question(q, (a) => resolve3(a.trim())));
|
|
2187
|
-
|
|
2188
|
-
|
|
2416
|
+
let chosen;
|
|
2417
|
+
let projectId;
|
|
2418
|
+
let projectName;
|
|
2419
|
+
let apiKey;
|
|
2420
|
+
if (!opts.name && projectsList.length > 0) {
|
|
2421
|
+
console.log("");
|
|
2422
|
+
console.log(" Your projects:");
|
|
2423
|
+
projectsList.forEach((pr, i) => {
|
|
2424
|
+
console.log(` ${i + 1}. ${pr.name} (${pr.id})`);
|
|
2425
|
+
});
|
|
2426
|
+
console.log(` ${projectsList.length + 1}. Create a new project`);
|
|
2427
|
+
console.log("");
|
|
2428
|
+
const choice = await ask(` Pick a project [1-${projectsList.length + 1}]: `);
|
|
2429
|
+
const num = parseInt(choice, 10);
|
|
2430
|
+
if (num >= 1 && num <= projectsList.length) {
|
|
2431
|
+
chosen = projectsList[num - 1];
|
|
2432
|
+
projectId = chosen.id;
|
|
2433
|
+
projectName = chosen.name;
|
|
2434
|
+
}
|
|
2435
|
+
}
|
|
2436
|
+
if (!projectId) {
|
|
2437
|
+
const newName = opts.name?.trim() || await ask(" Project name: ");
|
|
2438
|
+
if (!newName) {
|
|
2439
|
+
rl.close();
|
|
2440
|
+
process.stderr.write("\nerror: Project name is required.\n");
|
|
2441
|
+
process.exit(2);
|
|
2442
|
+
}
|
|
2443
|
+
try {
|
|
2444
|
+
const created = await createProject(endpoint, cliToken, newName);
|
|
2445
|
+
projectId = created.id;
|
|
2446
|
+
projectName = created.name;
|
|
2447
|
+
apiKey = created.apiKey ?? void 0;
|
|
2448
|
+
console.log(` \u2713 Created project "${projectName}"`);
|
|
2449
|
+
} catch (err) {
|
|
2450
|
+
rl.close();
|
|
2451
|
+
process.stderr.write(`
|
|
2452
|
+
error: Could not create project: ${err instanceof Error ? err.message : String(err)}
|
|
2453
|
+
`);
|
|
2454
|
+
process.exit(1);
|
|
2455
|
+
}
|
|
2456
|
+
}
|
|
2189
2457
|
rl.close();
|
|
2458
|
+
if (projectId && !apiKey) {
|
|
2459
|
+
apiKey = await mintProjectKey(endpoint, cliToken, projectId) ?? void 0;
|
|
2460
|
+
}
|
|
2190
2461
|
if (!projectId || !apiKey) {
|
|
2191
|
-
process.stderr.write("\nerror:
|
|
2192
|
-
process.exit(
|
|
2462
|
+
process.stderr.write("\nerror: Could not obtain an SDK key for the project. Run `mushi login` and try again.\n");
|
|
2463
|
+
process.exit(1);
|
|
2193
2464
|
}
|
|
2194
2465
|
const config = loadConfig();
|
|
2195
2466
|
config.apiKey = apiKey;
|
|
2196
2467
|
config.endpoint = endpoint;
|
|
2197
2468
|
config.projectId = projectId;
|
|
2469
|
+
config.consoleUrl = consoleBase;
|
|
2198
2470
|
saveConfig(config);
|
|
2199
2471
|
const cwd = process.cwd();
|
|
2200
2472
|
const envPath = nodePath.join(cwd, ".env.local");
|
|
@@ -3240,7 +3512,7 @@ Examples:
|
|
|
3240
3512
|
}
|
|
3241
3513
|
if (result.plan.entries.length === 0) process.exit(1);
|
|
3242
3514
|
});
|
|
3243
|
-
program2.command("connect").description("Save credentials, merge env vars, wire Cursor MCP, optionally wait for SDK heartbeat").option("--api-key <key>", "Mushi API key (mushi_\u2026) \u2014 or set MUSHI_API_KEY to keep it out of shell history").requiredOption("--project-id <id>", "Project UUID").requiredOption("--endpoint <url>", "Supabase edge function URL").option("--cwd <path>", "Target repo").option("--write-env", "
|
|
3515
|
+
program2.command("connect").description("Save credentials, merge env vars, wire Cursor MCP, optionally wait for SDK heartbeat").option("--api-key <key>", "Mushi API key (mushi_\u2026) \u2014 or set MUSHI_API_KEY to keep it out of shell history").requiredOption("--project-id <id>", "Project UUID").requiredOption("--endpoint <url>", "Supabase edge function URL").option("--cwd <path>", "Target repo").option("--write-env", "Force-write SDK env vars to .env.local (default; overrides --no-env)").option("--wire-ide", "Force-wire Cursor MCP into .cursor/mcp.json (default; overrides --no-ide)").option("--no-env", "Skip writing .env.local").option("--no-ide", "Skip writing .cursor/mcp.json").option("--wait", "Poll ingest-setup until SDK heartbeat lands").option("--wait-timeout <sec>", "Max seconds for --wait", "120").option("--json", "Machine-readable output").addHelpText("after", `
|
|
3244
3516
|
Examples:
|
|
3245
3517
|
MUSHI_API_KEY=mushi_xxx mushi connect --project-id <uuid> --endpoint https://<ref>.supabase.co/functions/v1/api --wait
|
|
3246
3518
|
mushi connect --api-key mushi_xxx --project-id <uuid> --endpoint <url> --no-ide`).action(async (opts) => {
|
|
@@ -3254,8 +3526,10 @@ Examples:
|
|
|
3254
3526
|
projectId: opts.projectId,
|
|
3255
3527
|
endpoint: opts.endpoint,
|
|
3256
3528
|
cwd: opts.cwd,
|
|
3257
|
-
|
|
3258
|
-
|
|
3529
|
+
// Explicit --write-env / --wire-ide force the action on even if --no-env /
|
|
3530
|
+
// --no-ide was also passed; otherwise writing is the default.
|
|
3531
|
+
writeEnv: opts.writeEnv === true ? true : opts.env !== false,
|
|
3532
|
+
wireIde: opts.wireIde === true ? true : opts.ide !== false,
|
|
3259
3533
|
wait: opts.wait,
|
|
3260
3534
|
waitTimeoutSec: parseInt(opts.waitTimeout, 10) || 120,
|
|
3261
3535
|
json: opts.json
|
package/dist/init.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
MUSHI_CLI_VERSION
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-6LM2KW4S.js";
|
|
4
4
|
import {
|
|
5
5
|
TEST_REPORT_FETCH_TIMEOUT_MS,
|
|
6
6
|
loadConfig,
|
|
@@ -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
|
|
|
@@ -211,6 +238,147 @@ 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
|
+
while (now() < deadline) {
|
|
310
|
+
await sleep(intervalMs);
|
|
311
|
+
const outcome = await pollDeviceToken(endpoint, session.device_code);
|
|
312
|
+
switch (outcome.status) {
|
|
313
|
+
case "approved":
|
|
314
|
+
return outcome.cliToken;
|
|
315
|
+
case "pending":
|
|
316
|
+
consecutiveErrors = 0;
|
|
317
|
+
opts.onPending?.();
|
|
318
|
+
continue;
|
|
319
|
+
case "denied":
|
|
320
|
+
throw new Error("Login was denied in the browser.");
|
|
321
|
+
case "expired":
|
|
322
|
+
throw new Error("The login code expired. Run sign-in again.");
|
|
323
|
+
case "error":
|
|
324
|
+
if (!outcome.retryable) {
|
|
325
|
+
throw new Error(outcome.message);
|
|
326
|
+
}
|
|
327
|
+
consecutiveErrors += 1;
|
|
328
|
+
if (consecutiveErrors >= maxConsecutiveErrors) {
|
|
329
|
+
throw new Error(outcome.message);
|
|
330
|
+
}
|
|
331
|
+
opts.onTransientError?.(outcome.message, consecutiveErrors);
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
throw new Error("Login timed out before approval. Run sign-in again.");
|
|
336
|
+
}
|
|
337
|
+
function cliAuthHeaders(cliToken) {
|
|
338
|
+
return {
|
|
339
|
+
"Content-Type": "application/json",
|
|
340
|
+
Authorization: `Bearer ${cliToken}`
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
async function listProjects(endpoint, cliToken) {
|
|
344
|
+
const base = trimTrailingSlash(endpoint);
|
|
345
|
+
try {
|
|
346
|
+
const res = await deviceFetch(`${base}/v1/cli/projects`, { headers: cliAuthHeaders(cliToken) });
|
|
347
|
+
const json = await res.json().catch(() => null);
|
|
348
|
+
if (res.ok && json?.ok) return json.data?.projects ?? [];
|
|
349
|
+
} catch {
|
|
350
|
+
}
|
|
351
|
+
return [];
|
|
352
|
+
}
|
|
353
|
+
async function createProject(endpoint, cliToken, name) {
|
|
354
|
+
const base = trimTrailingSlash(endpoint);
|
|
355
|
+
const res = await deviceFetch(`${base}/v1/cli/projects`, {
|
|
356
|
+
method: "POST",
|
|
357
|
+
headers: cliAuthHeaders(cliToken),
|
|
358
|
+
body: JSON.stringify({ name })
|
|
359
|
+
});
|
|
360
|
+
const json = await res.json().catch(() => null);
|
|
361
|
+
if (!res.ok || !json?.ok || !json.data) {
|
|
362
|
+
throw new Error(json?.error?.message ?? `Could not create project (HTTP ${res.status}).`);
|
|
363
|
+
}
|
|
364
|
+
const { id, name: created, slug, apiKey } = json.data;
|
|
365
|
+
return { id, name: created, slug, apiKey: apiKey ?? null };
|
|
366
|
+
}
|
|
367
|
+
async function mintProjectKey(endpoint, cliToken, projectId) {
|
|
368
|
+
const base = trimTrailingSlash(endpoint);
|
|
369
|
+
try {
|
|
370
|
+
const res = await deviceFetch(`${base}/v1/cli/projects/${projectId}/keys`, {
|
|
371
|
+
method: "POST",
|
|
372
|
+
headers: cliAuthHeaders(cliToken),
|
|
373
|
+
body: JSON.stringify({})
|
|
374
|
+
});
|
|
375
|
+
const json = await res.json().catch(() => null);
|
|
376
|
+
if (res.ok && json?.ok) return json.data?.key ?? null;
|
|
377
|
+
} catch {
|
|
378
|
+
}
|
|
379
|
+
return null;
|
|
380
|
+
}
|
|
381
|
+
|
|
214
382
|
// src/freshness.ts
|
|
215
383
|
var REGISTRY = "https://registry.npmjs.org";
|
|
216
384
|
var DEFAULT_TIMEOUT_MS = 2e3;
|
|
@@ -392,8 +560,8 @@ async function runInit(options = {}) {
|
|
|
392
560
|
const detected = detectFramework(cwd, pkg);
|
|
393
561
|
const framework = await chooseFramework(detected, options);
|
|
394
562
|
const consoleBase = await resolveConsoleUrl({ cwd });
|
|
395
|
-
|
|
396
|
-
const credentials = await
|
|
563
|
+
const endpoint = resolveCloudEndpoint(options.endpoint);
|
|
564
|
+
const credentials = await acquireCredentials(options, consoleBase, endpoint);
|
|
397
565
|
await verifyCredentials(credentials, options, consoleBase);
|
|
398
566
|
const pm = detectPackageManager(cwd);
|
|
399
567
|
const packagesToInstall = framework.needsWebPackage ? [framework.packageName, "@mushi-mushi/web"] : [framework.packageName];
|
|
@@ -402,7 +570,6 @@ async function runInit(options = {}) {
|
|
|
402
570
|
} else {
|
|
403
571
|
p.log.info(`Skipped install. Run \`${installCommand(pm, packagesToInstall)}\` yourself.`);
|
|
404
572
|
}
|
|
405
|
-
const endpoint = resolveCloudEndpoint(options.endpoint);
|
|
406
573
|
writeEnvFile(cwd, credentials.apiKey, credentials.projectId, framework);
|
|
407
574
|
persistCliConfig(credentials.apiKey, credentials.projectId, endpoint);
|
|
408
575
|
const enableRewards = await maybeEnableRewards(options);
|
|
@@ -449,63 +616,17 @@ async function chooseFramework(detected, options) {
|
|
|
449
616
|
}
|
|
450
617
|
return FRAMEWORKS[confirmed];
|
|
451
618
|
}
|
|
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}`);
|
|
619
|
+
async function acquireCredentials(options, consoleBase, endpoint) {
|
|
620
|
+
if (options.projectId && options.apiKey) {
|
|
621
|
+
return {
|
|
622
|
+
projectId: sanitizeSecret(options.projectId),
|
|
623
|
+
apiKey: sanitizeSecret(options.apiKey)
|
|
624
|
+
};
|
|
502
625
|
}
|
|
503
|
-
}
|
|
504
|
-
async function collectCredentials(options, consoleBase) {
|
|
505
626
|
const existing = loadConfig();
|
|
506
627
|
if (!options.projectId && !options.apiKey && existing.projectId && existing.apiKey) {
|
|
507
628
|
const reuse = options.yes ? true : await p.confirm({
|
|
508
|
-
message: "Use
|
|
629
|
+
message: "Use the Mushi credentials saved from your last sign-in?",
|
|
509
630
|
initialValue: true
|
|
510
631
|
});
|
|
511
632
|
if (p.isCancel(reuse)) {
|
|
@@ -519,6 +640,124 @@ async function collectCredentials(options, consoleBase) {
|
|
|
519
640
|
};
|
|
520
641
|
}
|
|
521
642
|
}
|
|
643
|
+
if (options.yes) {
|
|
644
|
+
const creds = await runBrowserSignIn(options, endpoint);
|
|
645
|
+
if (creds) return creds;
|
|
646
|
+
p.log.warn("Browser sign-in didn't complete \u2014 switching to manual entry.");
|
|
647
|
+
} else {
|
|
648
|
+
const method = await p.select({
|
|
649
|
+
message: "Connect this app to Mushi",
|
|
650
|
+
initialValue: "browser",
|
|
651
|
+
options: [
|
|
652
|
+
{
|
|
653
|
+
value: "browser",
|
|
654
|
+
label: "Sign in with your browser",
|
|
655
|
+
hint: "Recommended \u2014 no copy-paste, creates the project + key for you"
|
|
656
|
+
},
|
|
657
|
+
{
|
|
658
|
+
value: "manual",
|
|
659
|
+
label: "Paste a Project ID + API key",
|
|
660
|
+
hint: "Self-hosted or expert setup"
|
|
661
|
+
}
|
|
662
|
+
]
|
|
663
|
+
});
|
|
664
|
+
if (p.isCancel(method)) {
|
|
665
|
+
p.cancel("Aborted.");
|
|
666
|
+
process.exit(0);
|
|
667
|
+
}
|
|
668
|
+
if (method === "browser") {
|
|
669
|
+
const creds = await runBrowserSignIn(options, endpoint);
|
|
670
|
+
if (creds) return creds;
|
|
671
|
+
p.log.warn("Browser sign-in didn't complete \u2014 switching to manual entry.");
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
return collectCredentialsManually(options, consoleBase);
|
|
675
|
+
}
|
|
676
|
+
async function runBrowserSignIn(options, endpoint) {
|
|
677
|
+
const startSpin = p.spinner();
|
|
678
|
+
startSpin.start("Starting secure browser sign-in\u2026");
|
|
679
|
+
let session;
|
|
680
|
+
try {
|
|
681
|
+
session = await startDeviceAuth(endpoint);
|
|
682
|
+
} catch (err) {
|
|
683
|
+
startSpin.stop("Could not start browser sign-in.");
|
|
684
|
+
p.log.warn(err instanceof Error ? err.message : String(err));
|
|
685
|
+
return null;
|
|
686
|
+
}
|
|
687
|
+
startSpin.stop("Browser sign-in ready.");
|
|
688
|
+
p.log.step(`Confirmation code: ${session.user_code}`);
|
|
689
|
+
p.log.info(`Opening ${session.verification_uri}`);
|
|
690
|
+
try {
|
|
691
|
+
await openInBrowser(session.verification_uri);
|
|
692
|
+
} catch {
|
|
693
|
+
}
|
|
694
|
+
p.log.message("Approve the request in your browser to continue. (Ctrl+C to cancel)");
|
|
695
|
+
const waitSpin = p.spinner();
|
|
696
|
+
waitSpin.start("Waiting for you to approve in the browser\u2026");
|
|
697
|
+
let cliToken;
|
|
698
|
+
try {
|
|
699
|
+
cliToken = await waitForCliToken(endpoint, session);
|
|
700
|
+
} catch (err) {
|
|
701
|
+
waitSpin.stop("Browser sign-in didn't complete.");
|
|
702
|
+
p.log.warn(err instanceof Error ? err.message : String(err));
|
|
703
|
+
return null;
|
|
704
|
+
}
|
|
705
|
+
waitSpin.stop("Approved.");
|
|
706
|
+
let projectId = options.projectId ? sanitizeSecret(options.projectId) : void 0;
|
|
707
|
+
let apiKey;
|
|
708
|
+
if (!projectId) {
|
|
709
|
+
let projects = [];
|
|
710
|
+
try {
|
|
711
|
+
projects = await listProjects(endpoint, cliToken);
|
|
712
|
+
} catch {
|
|
713
|
+
}
|
|
714
|
+
const NEW = "__new__";
|
|
715
|
+
const choice = await p.select({
|
|
716
|
+
message: "Choose a project",
|
|
717
|
+
initialValue: projects[0]?.id ?? NEW,
|
|
718
|
+
options: [
|
|
719
|
+
...projects.map((pr) => ({ value: pr.id, label: pr.name, hint: pr.id.slice(0, 8) })),
|
|
720
|
+
{ value: NEW, label: "Create a new project", hint: "mints an SDK key automatically" }
|
|
721
|
+
]
|
|
722
|
+
});
|
|
723
|
+
if (p.isCancel(choice)) {
|
|
724
|
+
p.cancel("Aborted.");
|
|
725
|
+
process.exit(0);
|
|
726
|
+
}
|
|
727
|
+
if (choice === NEW) {
|
|
728
|
+
const name = await p.text({
|
|
729
|
+
message: "Project name",
|
|
730
|
+
placeholder: "My app",
|
|
731
|
+
validate: (v) => v && v.trim().length > 0 ? void 0 : "Required"
|
|
732
|
+
});
|
|
733
|
+
if (p.isCancel(name)) {
|
|
734
|
+
p.cancel("Aborted.");
|
|
735
|
+
process.exit(0);
|
|
736
|
+
}
|
|
737
|
+
const createSpin = p.spinner();
|
|
738
|
+
createSpin.start(`Creating "${name.trim()}"\u2026`);
|
|
739
|
+
try {
|
|
740
|
+
const created = await createProject(endpoint, cliToken, name.trim());
|
|
741
|
+
projectId = created.id;
|
|
742
|
+
apiKey = created.apiKey ?? void 0;
|
|
743
|
+
createSpin.stop(`Created project "${created.name}".`);
|
|
744
|
+
} catch (err) {
|
|
745
|
+
createSpin.stop("Could not create the project.");
|
|
746
|
+
p.log.warn(err instanceof Error ? err.message : String(err));
|
|
747
|
+
return null;
|
|
748
|
+
}
|
|
749
|
+
} else {
|
|
750
|
+
projectId = choice;
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
if (projectId && !apiKey) {
|
|
754
|
+
apiKey = await mintProjectKey(endpoint, cliToken, projectId) ?? void 0;
|
|
755
|
+
}
|
|
756
|
+
if (!projectId || !apiKey) return null;
|
|
757
|
+
return { apiKey, projectId };
|
|
758
|
+
}
|
|
759
|
+
async function collectCredentialsManually(options, consoleBase) {
|
|
760
|
+
const existing = loadConfig();
|
|
522
761
|
const rawProjectId = options.projectId ?? existing.projectId ?? await promptText({
|
|
523
762
|
message: "Project ID",
|
|
524
763
|
placeholder: "e.g. bdafa28d-b153-482f-bd4f-42981f3fd3a4",
|
package/dist/version.js
CHANGED
package/package.json
CHANGED