@mushi-mushi/cli 0.6.1 → 0.8.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 +27 -0
- package/README.md +409 -59
- package/SECURITY.md +167 -4
- package/dist/chunk-KUQZQFOL.js +6 -0
- package/dist/index.js +867 -167
- package/dist/init.js +70 -23
- package/dist/version.js +1 -1
- package/package.json +6 -6
- package/dist/chunk-ZL3BTW32.js +0 -6
package/dist/index.js
CHANGED
|
@@ -10,13 +10,20 @@ import { homedir } from "os";
|
|
|
10
10
|
var CONFIG_PATH = join(homedir(), ".mushirc");
|
|
11
11
|
var SECURE_FILE_MODE = 384;
|
|
12
12
|
function loadConfig(path = CONFIG_PATH) {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
13
|
+
let file = {};
|
|
14
|
+
if (existsSync(path)) {
|
|
15
|
+
tightenPermissions(path);
|
|
16
|
+
try {
|
|
17
|
+
file = JSON.parse(readFileSync(path, "utf-8"));
|
|
18
|
+
} catch {
|
|
19
|
+
}
|
|
19
20
|
}
|
|
21
|
+
const fromEnv = {
|
|
22
|
+
...process.env["MUSHI_API_KEY"] ? { apiKey: process.env["MUSHI_API_KEY"] } : {},
|
|
23
|
+
...process.env["MUSHI_PROJECT_ID"] ? { projectId: process.env["MUSHI_PROJECT_ID"] } : {},
|
|
24
|
+
...process.env["MUSHI_API_ENDPOINT"] ? { endpoint: process.env["MUSHI_API_ENDPOINT"] } : {}
|
|
25
|
+
};
|
|
26
|
+
return { ...file, ...fromEnv };
|
|
20
27
|
}
|
|
21
28
|
function saveConfig(config, path = CONFIG_PATH) {
|
|
22
29
|
writeFileSync(path, JSON.stringify(config, null, 2), { mode: SECURE_FILE_MODE });
|
|
@@ -275,7 +282,6 @@ function collectDeps(pkg) {
|
|
|
275
282
|
}
|
|
276
283
|
|
|
277
284
|
// src/endpoint.ts
|
|
278
|
-
var DEFAULT_ENDPOINT = "https://api.mushimushi.dev";
|
|
279
285
|
var TEST_REPORT_TIMEOUT_MS = 1e4;
|
|
280
286
|
var TEST_REPORT_FETCH_TIMEOUT_MS = TEST_REPORT_TIMEOUT_MS;
|
|
281
287
|
function assertEndpoint(url) {
|
|
@@ -293,10 +299,14 @@ function assertEndpoint(url) {
|
|
|
293
299
|
return parsed.origin + (parsed.pathname === "/" ? "" : parsed.pathname);
|
|
294
300
|
}
|
|
295
301
|
function normalizeEndpoint(url) {
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
302
|
+
if (!url) {
|
|
303
|
+
throw new Error(
|
|
304
|
+
"No API endpoint configured. Run `mushi init` or set MUSHI_API_ENDPOINT. Set endpoint to your Supabase edge function URL, e.g. https://xyz.supabase.co/functions/v1/api"
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
let end = url.length;
|
|
308
|
+
while (end > 0 && url.charCodeAt(end - 1) === 47) end--;
|
|
309
|
+
return url.slice(0, end);
|
|
300
310
|
}
|
|
301
311
|
|
|
302
312
|
// src/freshness.ts
|
|
@@ -454,11 +464,11 @@ function getFrameworkFromPkg(pkg) {
|
|
|
454
464
|
}
|
|
455
465
|
|
|
456
466
|
// src/version.ts
|
|
457
|
-
var MUSHI_CLI_VERSION = true ? "0.
|
|
467
|
+
var MUSHI_CLI_VERSION = true ? "0.8.0" : "0.0.0-dev";
|
|
458
468
|
|
|
459
469
|
// src/init.ts
|
|
460
470
|
var ENV_FILES = [".env.local", ".env"];
|
|
461
|
-
var PROJECT_ID_PATTERN = /^proj_[A-Za-z0-9_-]{10,}
|
|
471
|
+
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;
|
|
462
472
|
var API_KEY_PATTERN = /^mushi_[A-Za-z0-9_-]{10,}$/;
|
|
463
473
|
async function runInit(options = {}) {
|
|
464
474
|
const cwd = options.cwd ?? process.cwd();
|
|
@@ -490,7 +500,8 @@ async function runInit(options = {}) {
|
|
|
490
500
|
}
|
|
491
501
|
writeEnvFile(cwd, credentials.apiKey, credentials.projectId, framework);
|
|
492
502
|
persistCliConfig(credentials.apiKey, credentials.projectId);
|
|
493
|
-
|
|
503
|
+
const enableRewards = await maybeEnableRewards(options);
|
|
504
|
+
printNextSteps(framework, credentials.apiKey, credentials.projectId, enableRewards);
|
|
494
505
|
await maybeSendTestReport(credentials, options);
|
|
495
506
|
p.outro("Setup complete. Happy bug squashing \u{1F41B}");
|
|
496
507
|
}
|
|
@@ -502,7 +513,7 @@ function ensureInteractiveOrBailOut(options) {
|
|
|
502
513
|
);
|
|
503
514
|
if (hasAllFlags) return;
|
|
504
515
|
process.stderr.write(
|
|
505
|
-
"mushi-mushi: non-interactive terminal detected.\nPass all of --yes (or --framework), --project-id, and --api-key to run unattended.\nExample: npx mushi-mushi --yes --project-id
|
|
516
|
+
"mushi-mushi: non-interactive terminal detected.\nPass all of --yes (or --framework), --project-id, and --api-key to run unattended.\nExample: npx mushi-mushi --yes --project-id xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx --api-key mushi_xxx\nYour project ID is the UUID shown in the Projects page of the Mushi admin console.\n"
|
|
506
517
|
);
|
|
507
518
|
process.exit(1);
|
|
508
519
|
}
|
|
@@ -536,21 +547,22 @@ async function collectCredentials(options) {
|
|
|
536
547
|
const existing = loadConfig();
|
|
537
548
|
const rawProjectId = options.projectId ?? existing.projectId ?? await promptText({
|
|
538
549
|
message: "Project ID",
|
|
539
|
-
placeholder: "
|
|
540
|
-
hint: "
|
|
541
|
-
validate: (v) => PROJECT_ID_PATTERN.test(v) ? void 0 : "Expected
|
|
550
|
+
placeholder: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
|
|
551
|
+
hint: "Where to find it: https://kensaur.us/mushi-mushi/projects \u2192 click your project \u2192 copy the UUID below the project name.",
|
|
552
|
+
validate: (v) => PROJECT_ID_PATTERN.test(v) ? void 0 : "Expected a UUID (xxxxxxxx-xxxx-...) \u2014 copy it from the Mushi admin console Projects page."
|
|
542
553
|
});
|
|
543
554
|
const rawApiKey = options.apiKey ?? existing.apiKey ?? await promptText({
|
|
544
555
|
message: "API key",
|
|
545
556
|
placeholder: "mushi_xxxxxxxxxxxx",
|
|
546
|
-
hint: "Treat
|
|
557
|
+
hint: "Where to find it: https://kensaur.us/mushi-mushi/settings \u2192 API Keys tab. Treat it like a password \u2014 env file only, never commit it.",
|
|
547
558
|
validate: (v) => API_KEY_PATTERN.test(v) ? void 0 : "Expected format: mushi_ followed by 10+ alphanumeric characters"
|
|
548
559
|
});
|
|
549
560
|
const projectId = sanitizeSecret(rawProjectId);
|
|
550
561
|
const apiKey = sanitizeSecret(rawApiKey);
|
|
551
562
|
if (!PROJECT_ID_PATTERN.test(projectId)) {
|
|
552
563
|
throw new Error(
|
|
553
|
-
`Invalid project ID.
|
|
564
|
+
`Invalid project ID. Got: ${redact(projectId)}
|
|
565
|
+
Expected a UUID (e.g. 542b34e0-019e-41fe-b900-7b637717bb86) \u2014 copy it from the Projects page in the Mushi console at https://kensaur.us/mushi-mushi/projects`
|
|
554
566
|
);
|
|
555
567
|
}
|
|
556
568
|
if (!API_KEY_PATTERN.test(apiKey)) {
|
|
@@ -568,6 +580,7 @@ function redact(value) {
|
|
|
568
580
|
return `${value.slice(0, 4)}\u2026${value.slice(-2)}`;
|
|
569
581
|
}
|
|
570
582
|
async function promptText(opts) {
|
|
583
|
+
if (opts.hint) p.log.info(opts.hint);
|
|
571
584
|
const value = await p.text({
|
|
572
585
|
message: opts.message,
|
|
573
586
|
placeholder: opts.placeholder,
|
|
@@ -584,18 +597,17 @@ async function promptText(opts) {
|
|
|
584
597
|
p.cancel("Aborted.");
|
|
585
598
|
process.exit(0);
|
|
586
599
|
}
|
|
587
|
-
if (opts.hint) p.log.info(opts.hint);
|
|
588
600
|
return value;
|
|
589
601
|
}
|
|
590
602
|
async function installPackages(pm, packages, cwd) {
|
|
591
603
|
const command = installCommand(pm, packages);
|
|
592
|
-
const
|
|
593
|
-
|
|
604
|
+
const spinner3 = p.spinner();
|
|
605
|
+
spinner3.start(`Installing ${packages.join(", ")} via ${pm}\u2026`);
|
|
594
606
|
try {
|
|
595
607
|
await runCommand(pm, packages, cwd);
|
|
596
|
-
|
|
608
|
+
spinner3.stop(`Installed ${packages.join(", ")}`);
|
|
597
609
|
} catch (err) {
|
|
598
|
-
|
|
610
|
+
spinner3.stop(`Install failed \u2014 run \`${command}\` manually.`);
|
|
599
611
|
p.log.error(err instanceof Error ? err.name + ": " + err.message : String(err));
|
|
600
612
|
}
|
|
601
613
|
}
|
|
@@ -669,13 +681,34 @@ function persistCliConfig(apiKey, projectId) {
|
|
|
669
681
|
const existing = loadConfig();
|
|
670
682
|
saveConfig({ ...existing, apiKey, projectId });
|
|
671
683
|
}
|
|
672
|
-
function printNextSteps(framework, apiKey, projectId) {
|
|
684
|
+
function printNextSteps(framework, apiKey, projectId, enableRewards = false) {
|
|
673
685
|
p.note(framework.snippet(apiKey, projectId), "Add this to your app:");
|
|
686
|
+
if (enableRewards) {
|
|
687
|
+
const badgeSnippet = framework.id === "react" ? `// Add to your user menu or profile UI:
|
|
688
|
+
import { MushiRewardsBadge } from '@mushi-mushi/react';
|
|
689
|
+
|
|
690
|
+
// Inside your component:
|
|
691
|
+
<MushiRewardsBadge showPoints />` : `// Add to your user menu:
|
|
692
|
+
// import { MushiRewardsBadge } from '@mushi-mushi/react';
|
|
693
|
+
// <MushiRewardsBadge showPoints />`;
|
|
694
|
+
p.note(badgeSnippet, "Rewards badge snippet:");
|
|
695
|
+
p.log.info("Enable rewards in your project settings at https://kensaur.us/mushi-mushi/rewards");
|
|
696
|
+
p.log.info("Users will earn points for bug reports, screen navigation, and app activity.");
|
|
697
|
+
}
|
|
674
698
|
p.log.message("Verify the install:");
|
|
675
699
|
p.log.message(" \u2022 Start your dev server");
|
|
676
700
|
p.log.message(" \u2022 Look for the \u{1F41B} button in the bottom-right corner (or shake on mobile)");
|
|
677
701
|
p.log.message(" \u2022 Submit a test report \u2014 it should appear at https://kensaur.us/mushi-mushi/reports");
|
|
678
702
|
}
|
|
703
|
+
async function maybeEnableRewards(options) {
|
|
704
|
+
if (options.yes) return false;
|
|
705
|
+
const answer = await p.confirm({
|
|
706
|
+
message: "Enable Mushi Rewards? (users earn points for bug reports + app activity)",
|
|
707
|
+
initialValue: false
|
|
708
|
+
});
|
|
709
|
+
if (p.isCancel(answer)) return false;
|
|
710
|
+
return Boolean(answer);
|
|
711
|
+
}
|
|
679
712
|
async function maybeSendTestReport(credentials, options) {
|
|
680
713
|
if (options.sendTestReport === false) return;
|
|
681
714
|
let shouldSend;
|
|
@@ -690,8 +723,15 @@ async function maybeSendTestReport(credentials, options) {
|
|
|
690
723
|
shouldSend = answer;
|
|
691
724
|
}
|
|
692
725
|
if (!shouldSend) return;
|
|
693
|
-
|
|
694
|
-
|
|
726
|
+
if (!options.endpoint) {
|
|
727
|
+
p.note(
|
|
728
|
+
"No endpoint configured \u2014 skipping test report.\nSet endpoint to your Supabase edge function URL, e.g. https://xyz.supabase.co/functions/v1/api",
|
|
729
|
+
"Skipped"
|
|
730
|
+
);
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
const spinner3 = p.spinner();
|
|
734
|
+
spinner3.start("Sending test report\u2026");
|
|
695
735
|
const endpoint = normalizeEndpoint(options.endpoint);
|
|
696
736
|
const controller = new AbortController();
|
|
697
737
|
const timer = setTimeout(() => controller.abort(), TEST_REPORT_FETCH_TIMEOUT_MS);
|
|
@@ -723,17 +763,24 @@ async function maybeSendTestReport(credentials, options) {
|
|
|
723
763
|
})
|
|
724
764
|
});
|
|
725
765
|
if (!res.ok) {
|
|
726
|
-
|
|
766
|
+
spinner3.stop(`Test report rejected (HTTP ${res.status}).`);
|
|
727
767
|
p.log.warn(
|
|
728
768
|
res.status === 401 || res.status === 403 ? "Credentials did not authenticate \u2014 double-check the project ID and API key." : "Skipping test report. You can retry with `mushi test`."
|
|
729
769
|
);
|
|
730
770
|
return;
|
|
731
771
|
}
|
|
732
|
-
|
|
733
|
-
|
|
772
|
+
spinner3.stop("Test report sent.");
|
|
773
|
+
let reportId;
|
|
774
|
+
try {
|
|
775
|
+
const body = await res.json();
|
|
776
|
+
reportId = body.data?.reportId;
|
|
777
|
+
} catch {
|
|
778
|
+
}
|
|
779
|
+
const reportPath = reportId ? `/reports/${reportId}` : "/reports";
|
|
780
|
+
p.log.success(`View it at https://kensaur.us/mushi-mushi/admin${reportPath}`);
|
|
734
781
|
} catch (err) {
|
|
735
782
|
const aborted = err instanceof Error && err.name === "AbortError";
|
|
736
|
-
|
|
783
|
+
spinner3.stop(aborted ? "Timed out reaching the Mushi API." : "Could not reach the Mushi API.");
|
|
737
784
|
p.log.warn(err instanceof Error ? err.message : String(err));
|
|
738
785
|
} finally {
|
|
739
786
|
clearTimeout(timer);
|
|
@@ -828,7 +875,7 @@ var MIGRATE_CATALOG = [
|
|
|
828
875
|
category: "competitor",
|
|
829
876
|
status: "published",
|
|
830
877
|
detectionLabel: pkgs[0],
|
|
831
|
-
match: (d) => pkgs.some((
|
|
878
|
+
match: (d) => pkgs.some((p3) => d.has(p3))
|
|
832
879
|
}))
|
|
833
880
|
];
|
|
834
881
|
function titleForCompetitor(slug) {
|
|
@@ -860,10 +907,10 @@ function depsFromPackageJson(pkg) {
|
|
|
860
907
|
}
|
|
861
908
|
function runMigrate(opts = {}) {
|
|
862
909
|
const cwd = opts.cwd ?? process.cwd();
|
|
863
|
-
const
|
|
910
|
+
const log3 = opts.log ?? ((s) => console.log(s));
|
|
864
911
|
const pkg = readPackageJson(cwd);
|
|
865
912
|
if (!pkg) {
|
|
866
|
-
|
|
913
|
+
log3(
|
|
867
914
|
opts.json ? JSON.stringify({ ok: false, error: "no-package-json", cwd, matches: [] }, null, 2) : `No package.json found in ${cwd}. Run \`mushi migrate\` from your project root.`
|
|
868
915
|
);
|
|
869
916
|
return { matches: [] };
|
|
@@ -871,7 +918,7 @@ function runMigrate(opts = {}) {
|
|
|
871
918
|
const deps = depsFromPackageJson(pkg);
|
|
872
919
|
const matches = detectMigrations(deps);
|
|
873
920
|
if (opts.json) {
|
|
874
|
-
|
|
921
|
+
log3(
|
|
875
922
|
JSON.stringify(
|
|
876
923
|
{
|
|
877
924
|
ok: true,
|
|
@@ -891,40 +938,302 @@ function runMigrate(opts = {}) {
|
|
|
891
938
|
return { matches };
|
|
892
939
|
}
|
|
893
940
|
if (matches.length === 0) {
|
|
894
|
-
|
|
895
|
-
|
|
941
|
+
log3("No migrations suggested for this project.");
|
|
942
|
+
log3(`Browse the full catalog: ${DOCS_BASE}/migrations`);
|
|
896
943
|
return { matches };
|
|
897
944
|
}
|
|
898
|
-
|
|
899
|
-
|
|
945
|
+
log3(`Suggested migration${matches.length > 1 ? "s" : ""} for this project:`);
|
|
946
|
+
log3("");
|
|
900
947
|
for (const { guide, url } of matches) {
|
|
901
|
-
|
|
902
|
-
if (guide.detectionLabel)
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
948
|
+
log3(` \u2022 ${guide.title}`);
|
|
949
|
+
if (guide.detectionLabel) log3(` detected: ${guide.detectionLabel}`);
|
|
950
|
+
log3(` ${guide.summary}`);
|
|
951
|
+
log3(` ${url}`);
|
|
952
|
+
log3("");
|
|
906
953
|
}
|
|
907
|
-
|
|
954
|
+
log3(`Browse the full catalog: ${DOCS_BASE}/migrations`);
|
|
908
955
|
return { matches };
|
|
909
956
|
}
|
|
910
957
|
|
|
958
|
+
// src/sourcemaps.ts
|
|
959
|
+
import { createReadStream } from "fs";
|
|
960
|
+
import { readFile, readdir } from "fs/promises";
|
|
961
|
+
import { createHash } from "crypto";
|
|
962
|
+
import { join as join5, relative, basename } from "path";
|
|
963
|
+
import * as p2 from "@clack/prompts";
|
|
964
|
+
async function findMapFiles(dir) {
|
|
965
|
+
const results = [];
|
|
966
|
+
async function walk(current) {
|
|
967
|
+
const entries = await readdir(current, { withFileTypes: true });
|
|
968
|
+
for (const e of entries) {
|
|
969
|
+
const full = join5(current, e.name);
|
|
970
|
+
if (e.isDirectory()) {
|
|
971
|
+
await walk(full);
|
|
972
|
+
} else if (e.isFile() && (e.name.endsWith(".js.map") || e.name.endsWith(".css.map"))) {
|
|
973
|
+
results.push(full);
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
await walk(dir);
|
|
978
|
+
return results;
|
|
979
|
+
}
|
|
980
|
+
function fileHash(path) {
|
|
981
|
+
return new Promise((resolve2, reject) => {
|
|
982
|
+
const hash = createHash("sha256");
|
|
983
|
+
const stream = createReadStream(path);
|
|
984
|
+
stream.on("data", (chunk) => hash.update(chunk));
|
|
985
|
+
stream.on("end", () => resolve2(hash.digest("hex")));
|
|
986
|
+
stream.on("error", reject);
|
|
987
|
+
});
|
|
988
|
+
}
|
|
989
|
+
async function uploadFile(filePath, release, endpoint, apiKey) {
|
|
990
|
+
const sha256 = await fileHash(filePath);
|
|
991
|
+
try {
|
|
992
|
+
const checkRes = await fetch(
|
|
993
|
+
`${endpoint}/v1/sourcemaps?sha256=${encodeURIComponent(sha256)}&release=${encodeURIComponent(release)}`,
|
|
994
|
+
{ headers: { Authorization: `Bearer ${apiKey}`, "X-Mushi-Api-Key": apiKey } }
|
|
995
|
+
);
|
|
996
|
+
if (checkRes.ok) {
|
|
997
|
+
const json = await checkRes.json();
|
|
998
|
+
if (json.exists) return { ok: true, skipped: true };
|
|
999
|
+
}
|
|
1000
|
+
} catch {
|
|
1001
|
+
}
|
|
1002
|
+
const contents = await readFile(filePath);
|
|
1003
|
+
const form = new FormData();
|
|
1004
|
+
form.append("file", new Blob([contents]), basename(filePath));
|
|
1005
|
+
form.append("filename", relative(process.cwd(), filePath).replaceAll("\\", "/"));
|
|
1006
|
+
form.append("release", release);
|
|
1007
|
+
form.append("sha256", sha256);
|
|
1008
|
+
const res = await fetch(`${endpoint}/v1/sourcemaps`, {
|
|
1009
|
+
method: "POST",
|
|
1010
|
+
headers: { Authorization: `Bearer ${apiKey}`, "X-Mushi-Api-Key": apiKey },
|
|
1011
|
+
body: form
|
|
1012
|
+
});
|
|
1013
|
+
if (!res.ok) {
|
|
1014
|
+
const text2 = await res.text().catch(() => "");
|
|
1015
|
+
return {
|
|
1016
|
+
ok: false,
|
|
1017
|
+
skipped: false,
|
|
1018
|
+
reason: `HTTP ${res.status}: ${text2.slice(0, 120)}`
|
|
1019
|
+
};
|
|
1020
|
+
}
|
|
1021
|
+
return { ok: true, skipped: false };
|
|
1022
|
+
}
|
|
1023
|
+
async function runSourcemapsUpload(opts) {
|
|
1024
|
+
const endpoint = opts.endpoint ?? process.env["MUSHI_API_ENDPOINT"] ?? "";
|
|
1025
|
+
const apiKey = opts.apiKey ?? process.env["MUSHI_API_KEY"] ?? "";
|
|
1026
|
+
if (!opts.dryRun && !endpoint) {
|
|
1027
|
+
p2.log.error(
|
|
1028
|
+
"No API endpoint configured. Pass --endpoint <url>, set MUSHI_API_ENDPOINT,\n or run `mushi config endpoint <url>` to persist it. For Supabase self-hosting,\n this is your edge-functions URL, e.g. https://xyz.supabase.co/functions/v1/api"
|
|
1029
|
+
);
|
|
1030
|
+
process.exit(1);
|
|
1031
|
+
}
|
|
1032
|
+
if (!opts.dryRun && !apiKey) {
|
|
1033
|
+
p2.log.error("No API key \u2014 set MUSHI_API_KEY or pass --api-key <key>");
|
|
1034
|
+
process.exit(1);
|
|
1035
|
+
}
|
|
1036
|
+
if (!opts.silent) p2.intro(`sourcemaps upload \xB7 release ${opts.release}`);
|
|
1037
|
+
const spin = p2.spinner();
|
|
1038
|
+
spin.start(`Scanning ${opts.dir} for .map files\u2026`);
|
|
1039
|
+
let files;
|
|
1040
|
+
try {
|
|
1041
|
+
files = await findMapFiles(opts.dir);
|
|
1042
|
+
} catch (err) {
|
|
1043
|
+
spin.stop("Scan failed");
|
|
1044
|
+
p2.log.error(
|
|
1045
|
+
`Cannot read ${opts.dir}: ${err instanceof Error ? err.message : String(err)}`
|
|
1046
|
+
);
|
|
1047
|
+
process.exit(1);
|
|
1048
|
+
}
|
|
1049
|
+
spin.stop(
|
|
1050
|
+
`Found ${files.length} map file${files.length === 1 ? "" : "s"}`
|
|
1051
|
+
);
|
|
1052
|
+
if (files.length === 0) {
|
|
1053
|
+
p2.log.warn("No .js.map or .css.map files found \u2014 nothing to upload.");
|
|
1054
|
+
return;
|
|
1055
|
+
}
|
|
1056
|
+
if (opts.dryRun) {
|
|
1057
|
+
p2.log.info("Dry run \u2014 files that would be uploaded:");
|
|
1058
|
+
for (const f of files) {
|
|
1059
|
+
p2.log.message(` ${relative(process.cwd(), f).replaceAll("\\", "/")}`);
|
|
1060
|
+
}
|
|
1061
|
+
p2.outro(`${files.length} file${files.length === 1 ? "" : "s"} would be uploaded`);
|
|
1062
|
+
return;
|
|
1063
|
+
}
|
|
1064
|
+
let uploaded = 0;
|
|
1065
|
+
let skipped = 0;
|
|
1066
|
+
let failed = 0;
|
|
1067
|
+
for (const filePath of files) {
|
|
1068
|
+
const rel = relative(process.cwd(), filePath).replaceAll("\\", "/");
|
|
1069
|
+
const fs = p2.spinner();
|
|
1070
|
+
fs.start(rel);
|
|
1071
|
+
const result = await uploadFile(filePath, opts.release, endpoint, apiKey);
|
|
1072
|
+
if (result.skipped) {
|
|
1073
|
+
fs.stop(`\u21A9 ${rel} (already uploaded)`);
|
|
1074
|
+
skipped++;
|
|
1075
|
+
} else if (result.ok) {
|
|
1076
|
+
fs.stop(`\u2713 ${rel}`);
|
|
1077
|
+
uploaded++;
|
|
1078
|
+
} else {
|
|
1079
|
+
fs.stop(`\u2717 ${rel} \u2014 ${result.reason ?? "unknown error"}`);
|
|
1080
|
+
failed++;
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
const total = files.length;
|
|
1084
|
+
const parts = [
|
|
1085
|
+
`Uploaded ${uploaded} / ${total} file${total === 1 ? "" : "s"}`,
|
|
1086
|
+
skipped > 0 ? `(${skipped} already existed)` : "",
|
|
1087
|
+
failed > 0 ? `\u2014 ${failed} failed` : ""
|
|
1088
|
+
].filter(Boolean);
|
|
1089
|
+
const summary = parts.join(" ");
|
|
1090
|
+
if (!opts.silent) {
|
|
1091
|
+
if (failed > 0) {
|
|
1092
|
+
p2.log.error(summary);
|
|
1093
|
+
} else {
|
|
1094
|
+
p2.outro(summary);
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
if (failed > 0) process.exit(1);
|
|
1098
|
+
}
|
|
1099
|
+
|
|
911
1100
|
// src/index.ts
|
|
1101
|
+
var API_TIMEOUT_MS = 15e3;
|
|
912
1102
|
async function apiCall(path, config, options = {}) {
|
|
913
|
-
const endpoint = config.endpoint
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
1103
|
+
const endpoint = config.endpoint;
|
|
1104
|
+
if (!endpoint) {
|
|
1105
|
+
return {
|
|
1106
|
+
ok: false,
|
|
1107
|
+
error: {
|
|
1108
|
+
code: "NO_ENDPOINT",
|
|
1109
|
+
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"
|
|
1110
|
+
}
|
|
1111
|
+
};
|
|
1112
|
+
}
|
|
1113
|
+
const controller = new AbortController();
|
|
1114
|
+
const timer = setTimeout(() => controller.abort(), API_TIMEOUT_MS);
|
|
1115
|
+
try {
|
|
1116
|
+
const res = await fetch(`${endpoint}${path}`, {
|
|
1117
|
+
...options,
|
|
1118
|
+
signal: controller.signal,
|
|
1119
|
+
headers: {
|
|
1120
|
+
"Content-Type": "application/json",
|
|
1121
|
+
// `apiKeyAuth` reads X-Mushi-Api-Key; the Authorization header is a
|
|
1122
|
+
// fallback accepted by some older middleware. Both are sent.
|
|
1123
|
+
"Authorization": `Bearer ${config.apiKey ?? ""}`,
|
|
1124
|
+
"X-Mushi-Api-Key": config.apiKey ?? "",
|
|
1125
|
+
"X-Mushi-Project": config.projectId ?? "",
|
|
1126
|
+
"X-Mushi-Cli-Version": MUSHI_CLI_VERSION,
|
|
1127
|
+
...options.headers
|
|
1128
|
+
}
|
|
1129
|
+
});
|
|
1130
|
+
clearTimeout(timer);
|
|
1131
|
+
let body;
|
|
1132
|
+
const contentType = res.headers.get("content-type") ?? "";
|
|
1133
|
+
if (contentType.includes("application/json")) {
|
|
1134
|
+
try {
|
|
1135
|
+
body = await res.json();
|
|
1136
|
+
} catch {
|
|
1137
|
+
body = null;
|
|
1138
|
+
}
|
|
1139
|
+
} else {
|
|
1140
|
+
const text2 = await res.text();
|
|
1141
|
+
try {
|
|
1142
|
+
body = JSON.parse(text2);
|
|
1143
|
+
} catch {
|
|
1144
|
+
body = {
|
|
1145
|
+
ok: false,
|
|
1146
|
+
error: {
|
|
1147
|
+
code: `HTTP_${res.status}`,
|
|
1148
|
+
message: text2.trim().slice(0, 300) || `HTTP ${res.status}`
|
|
1149
|
+
}
|
|
1150
|
+
};
|
|
1151
|
+
}
|
|
922
1152
|
}
|
|
923
|
-
|
|
924
|
-
|
|
1153
|
+
if (!res.ok && typeof body === "object" && body !== null && !("ok" in body)) {
|
|
1154
|
+
const b = body;
|
|
1155
|
+
return {
|
|
1156
|
+
ok: false,
|
|
1157
|
+
httpStatus: res.status,
|
|
1158
|
+
error: {
|
|
1159
|
+
code: b["code"] ?? `HTTP_${res.status}`,
|
|
1160
|
+
message: b["message"] ?? `Request failed (${res.status})`
|
|
1161
|
+
}
|
|
1162
|
+
};
|
|
1163
|
+
}
|
|
1164
|
+
return body;
|
|
1165
|
+
} catch (err) {
|
|
1166
|
+
clearTimeout(timer);
|
|
1167
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
1168
|
+
return {
|
|
1169
|
+
ok: false,
|
|
1170
|
+
error: {
|
|
1171
|
+
code: "TIMEOUT",
|
|
1172
|
+
message: `Request timed out after ${API_TIMEOUT_MS / 1e3}s. Check your network or endpoint.`
|
|
1173
|
+
}
|
|
1174
|
+
};
|
|
1175
|
+
}
|
|
1176
|
+
return {
|
|
1177
|
+
ok: false,
|
|
1178
|
+
error: {
|
|
1179
|
+
code: "NETWORK_ERROR",
|
|
1180
|
+
message: err instanceof Error ? err.message : String(err)
|
|
1181
|
+
}
|
|
1182
|
+
};
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
function die(result, exitCode = 1) {
|
|
1186
|
+
const { code, message } = result.error;
|
|
1187
|
+
const status = result.httpStatus ? ` [${result.httpStatus}]` : "";
|
|
1188
|
+
process.stderr.write(`error${status}: ${code} \u2014 ${message}
|
|
1189
|
+
`);
|
|
1190
|
+
process.exit(exitCode);
|
|
1191
|
+
}
|
|
1192
|
+
function requireConfig(opts = {}) {
|
|
1193
|
+
const config = loadConfig();
|
|
1194
|
+
if (!config.apiKey) {
|
|
1195
|
+
process.stderr.write(
|
|
1196
|
+
"error: API key not configured.\n Run: mushi login --api-key <key> --endpoint <url>\n Or: export MUSHI_API_KEY=<key>\n"
|
|
1197
|
+
);
|
|
1198
|
+
process.exit(2);
|
|
1199
|
+
}
|
|
1200
|
+
if (!config.endpoint) {
|
|
1201
|
+
process.stderr.write(
|
|
1202
|
+
"error: API endpoint not configured.\n Run: mushi login --endpoint https://<ref>.supabase.co/functions/v1/api\n Or: export MUSHI_API_ENDPOINT=<url>\n"
|
|
1203
|
+
);
|
|
1204
|
+
process.exit(2);
|
|
1205
|
+
}
|
|
1206
|
+
if (opts.needsProject && !config.projectId) {
|
|
1207
|
+
process.stderr.write(
|
|
1208
|
+
"error: Project ID not configured.\n Run: mushi login --project-id <uuid>\n Or: export MUSHI_PROJECT_ID=<uuid>\n Find your project ID: https://kensaur.us/mushi-mushi/projects\n"
|
|
1209
|
+
);
|
|
1210
|
+
process.exit(2);
|
|
1211
|
+
}
|
|
1212
|
+
return config;
|
|
1213
|
+
}
|
|
1214
|
+
function fmtDate(iso) {
|
|
1215
|
+
if (!iso) return "\u2014";
|
|
1216
|
+
return new Date(iso).toLocaleString(void 0, { dateStyle: "short", timeStyle: "short" });
|
|
1217
|
+
}
|
|
1218
|
+
function pad(s, width) {
|
|
1219
|
+
return s.length >= width ? s : s + " ".repeat(width - s.length);
|
|
925
1220
|
}
|
|
926
|
-
var program = new Command().name("mushi").description("Mushi Mushi CLI \u2014 set up the SDK, manage bug reports, monitor pipeline").version(MUSHI_CLI_VERSION)
|
|
927
|
-
|
|
1221
|
+
var program = new Command().name("mushi").description("Mushi Mushi CLI \u2014 set up the SDK, manage bug reports, monitor pipeline").version(MUSHI_CLI_VERSION).addHelpText("after", `
|
|
1222
|
+
Environment variables:
|
|
1223
|
+
MUSHI_API_KEY Project API key (from Settings \u2192 API Keys in the console)
|
|
1224
|
+
MUSHI_PROJECT_ID Project UUID (from the Projects page in the console)
|
|
1225
|
+
MUSHI_API_ENDPOINT Supabase edge function URL
|
|
1226
|
+
e.g. https://<ref>.supabase.co/functions/v1/api
|
|
1227
|
+
|
|
1228
|
+
Exit codes:
|
|
1229
|
+
0 success
|
|
1230
|
+
1 API / runtime error
|
|
1231
|
+
2 configuration error (missing credentials or endpoint)
|
|
1232
|
+
3 not found (resource does not exist)
|
|
1233
|
+
|
|
1234
|
+
Console: https://kensaur.us/mushi-mushi/
|
|
1235
|
+
Docs: https://github.com/kensaurus/mushi-mushi`);
|
|
1236
|
+
program.command("init").description("Set up the Mushi Mushi SDK in this project (auto-detects framework)").option("--project-id <id>", "Skip the prompt \u2014 pass UUID from the Projects page").option("--api-key <key>", "Skip the prompt \u2014 pass the API key (CI only)").option("--framework <id>", "Force a framework (next, react, vue, nuxt, svelte, sveltekit, angular, expo, react-native, capacitor, vanilla)").option("--skip-install", "Print the install command instead of running it").option("-y, --yes", "Accept detected framework without prompting").option("--cwd <path>", "Run the wizard in a different directory").option("--endpoint <url>", "Override the Mushi API endpoint (self-hosted)").option("--skip-test-report", 'Skip the end-of-wizard "send a test report" prompt').action(async (opts) => {
|
|
928
1237
|
await runInit({
|
|
929
1238
|
projectId: opts.projectId,
|
|
930
1239
|
apiKey: opts.apiKey,
|
|
@@ -936,112 +1245,503 @@ program.command("init").description("Set up the Mushi Mushi SDK in this project
|
|
|
936
1245
|
sendTestReport: opts.skipTestReport ? false : void 0
|
|
937
1246
|
});
|
|
938
1247
|
});
|
|
939
|
-
program.command("migrate").description(
|
|
940
|
-
"Suggest the most relevant Mushi Mushi migration guide based on your package.json"
|
|
941
|
-
).option("--cwd <path>", "Run from a different directory").option("--json", "Machine-readable JSON output").action((opts) => {
|
|
1248
|
+
program.command("migrate").description("Suggest the most relevant Mushi Mushi migration guide based on your package.json").option("--cwd <path>", "Run from a different directory").option("--json", "Machine-readable JSON output").action((opts) => {
|
|
942
1249
|
const { matches } = runMigrate({ cwd: opts.cwd, json: opts.json });
|
|
943
1250
|
if (matches.length === 0) process.exit(1);
|
|
944
1251
|
});
|
|
945
|
-
program.command("login").description("
|
|
1252
|
+
program.command("login").description("Save API credentials to ~/.mushirc (mode 0o600)").requiredOption("--api-key <key>", "Mushi API key (mushi_...)").option("--endpoint <url>", "Supabase edge function URL").option("--project-id <id>", "Project UUID (from the Projects page)").addHelpText("after", `
|
|
1253
|
+
Examples:
|
|
1254
|
+
mushi login --api-key mushi_xxx --endpoint https://xyz.supabase.co/functions/v1/api
|
|
1255
|
+
mushi login --api-key mushi_xxx --project-id 542b34e0-019e-41fe-b900-7b637717bb86`).action((opts) => {
|
|
946
1256
|
const config = loadConfig();
|
|
947
1257
|
config.apiKey = opts.apiKey;
|
|
948
1258
|
if (opts.endpoint) config.endpoint = assertEndpoint(opts.endpoint);
|
|
949
1259
|
if (opts.projectId) config.projectId = opts.projectId;
|
|
950
1260
|
saveConfig(config);
|
|
951
|
-
console.log("
|
|
952
|
-
|
|
953
|
-
program.command("status").description("Show project stats").action(async () => {
|
|
954
|
-
const config = loadConfig();
|
|
955
|
-
if (!config.apiKey) {
|
|
956
|
-
console.error("Run `mushi login` first");
|
|
957
|
-
process.exit(1);
|
|
958
|
-
}
|
|
959
|
-
const data = await apiCall("/v1/admin/stats", config);
|
|
960
|
-
console.log(JSON.stringify(data, null, 2));
|
|
1261
|
+
console.log("\u2713 Credentials saved to ~/.mushirc (mode 0o600)");
|
|
1262
|
+
console.log(" Run 'mushi whoami' to verify the connection.");
|
|
961
1263
|
});
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
}
|
|
969
|
-
const params = new URLSearchParams();
|
|
970
|
-
params.set("limit", opts.limit);
|
|
971
|
-
if (opts.status) params.set("status", opts.status);
|
|
972
|
-
const data = await apiCall(`/v1/admin/reports?${params}`, config);
|
|
1264
|
+
program.command("whoami").description("Verify API key and display project info").option("--json", "Machine-readable JSON output").addHelpText("after", `
|
|
1265
|
+
Verifies that MUSHI_API_KEY is valid and shows which project it belongs to.
|
|
1266
|
+
Useful after 'mushi login' to confirm credentials are correct.`).action(async (opts) => {
|
|
1267
|
+
const config = requireConfig();
|
|
1268
|
+
const result = await apiCall("/v1/sync/whoami", config);
|
|
1269
|
+
if (!result.ok) die(result);
|
|
973
1270
|
if (opts.json) {
|
|
974
|
-
console.log(JSON.stringify(data, null, 2));
|
|
1271
|
+
console.log(JSON.stringify(result.data, null, 2));
|
|
975
1272
|
} else {
|
|
976
|
-
const
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
}
|
|
1273
|
+
const d = result.data;
|
|
1274
|
+
console.log(`\u2713 Authenticated`);
|
|
1275
|
+
console.log(` Project: ${d.project_name} (${d.project_id})`);
|
|
1276
|
+
console.log(` Endpoint: ${config.endpoint}`);
|
|
1277
|
+
console.log(` Reports: ${d.stats.total_reports} total \xB7 ${d.stats.open_reports} open`);
|
|
980
1278
|
}
|
|
981
1279
|
});
|
|
982
|
-
|
|
983
|
-
const config =
|
|
984
|
-
|
|
985
|
-
|
|
1280
|
+
program.command("ping").description("Check connectivity to the Mushi backend").option("--json", "Machine-readable JSON output").action(async (opts) => {
|
|
1281
|
+
const config = requireConfig();
|
|
1282
|
+
const t0 = Date.now();
|
|
1283
|
+
try {
|
|
1284
|
+
const controller = new AbortController();
|
|
1285
|
+
const timer = setTimeout(() => controller.abort(), API_TIMEOUT_MS);
|
|
1286
|
+
const res = await fetch(`${config.endpoint}/health`, { signal: controller.signal });
|
|
1287
|
+
clearTimeout(timer);
|
|
1288
|
+
const latency = Date.now() - t0;
|
|
1289
|
+
if (opts.json) {
|
|
1290
|
+
console.log(JSON.stringify({ ok: res.ok, status: res.status, latency_ms: latency }));
|
|
1291
|
+
} else {
|
|
1292
|
+
const symbol = res.ok ? "\u2713" : "\u2717";
|
|
1293
|
+
console.log(`${symbol} ${res.ok ? "OK" : "FAIL"} \u2014 ${res.status} (${latency}ms)`);
|
|
1294
|
+
if (!res.ok) process.exit(1);
|
|
1295
|
+
}
|
|
1296
|
+
} catch (err) {
|
|
1297
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1298
|
+
if (opts.json) {
|
|
1299
|
+
console.log(JSON.stringify({ ok: false, error: msg, latency_ms: Date.now() - t0 }));
|
|
1300
|
+
} else {
|
|
1301
|
+
process.stderr.write(`\u2717 Unreachable: ${msg}
|
|
1302
|
+
`);
|
|
1303
|
+
}
|
|
986
1304
|
process.exit(1);
|
|
987
1305
|
}
|
|
988
|
-
const data = await apiCall(`/v1/admin/reports/${id}`, config);
|
|
989
|
-
console.log(JSON.stringify(data, null, 2));
|
|
990
1306
|
});
|
|
991
|
-
|
|
992
|
-
const config =
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
1307
|
+
program.command("status").description("Show project stats: report counts by severity and status").option("--json", "Machine-readable JSON output").action(async (opts) => {
|
|
1308
|
+
const config = requireConfig();
|
|
1309
|
+
const result = await apiCall("/v1/sync/stats", config);
|
|
1310
|
+
if (!result.ok) die(result);
|
|
1311
|
+
if (opts.json) {
|
|
1312
|
+
console.log(JSON.stringify(result.data, null, 2));
|
|
1313
|
+
return;
|
|
996
1314
|
}
|
|
997
|
-
const
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1315
|
+
const d = result.data;
|
|
1316
|
+
console.log(`Project: ${d.project_name}`);
|
|
1317
|
+
console.log("");
|
|
1318
|
+
console.log("Reports by status:");
|
|
1319
|
+
for (const [status, count] of Object.entries(d.by_status)) {
|
|
1320
|
+
console.log(` ${pad(status, 14)} ${count}`);
|
|
1321
|
+
}
|
|
1322
|
+
console.log("");
|
|
1323
|
+
console.log("Reports by severity:");
|
|
1324
|
+
for (const [severity, count] of Object.entries(d.by_severity)) {
|
|
1325
|
+
console.log(` ${pad(severity, 14)} ${count}`);
|
|
1326
|
+
}
|
|
1327
|
+
console.log("");
|
|
1328
|
+
console.log(`Fixes: ${d.fixes_count} total \xB7 ${d.fixes_merged} merged`);
|
|
1329
|
+
console.log(`Lessons: ${d.lessons_count} active rules`);
|
|
1002
1330
|
});
|
|
1003
|
-
program.command("config").description("View or update CLI config").argument("[key]", "Config key to set").argument("[value]", "
|
|
1331
|
+
program.command("config").description("View or update CLI config (stored in ~/.mushirc)").argument("[key]", "Config key to set: apiKey | endpoint | projectId").argument("[value]", "New value").addHelpText("after", `
|
|
1332
|
+
Keys:
|
|
1333
|
+
apiKey \u2014 Mushi API key (mushi_...)
|
|
1334
|
+
endpoint \u2014 Supabase edge function URL
|
|
1335
|
+
projectId \u2014 Project UUID
|
|
1336
|
+
|
|
1337
|
+
Examples:
|
|
1338
|
+
mushi config # show all config
|
|
1339
|
+
mushi config apiKey mushi_xxx # set API key
|
|
1340
|
+
mushi config endpoint https://... # set endpoint
|
|
1341
|
+
mushi config projectId <uuid> # set project`).action((key, value) => {
|
|
1004
1342
|
const config = loadConfig();
|
|
1343
|
+
const ALLOWED_KEYS = /* @__PURE__ */ new Set(["apiKey", "endpoint", "projectId"]);
|
|
1005
1344
|
if (key && value) {
|
|
1345
|
+
if (!ALLOWED_KEYS.has(key)) {
|
|
1346
|
+
process.stderr.write(`error: unknown config key "${key}". Allowed: ${[...ALLOWED_KEYS].join(", ")}
|
|
1347
|
+
`);
|
|
1348
|
+
process.exit(2);
|
|
1349
|
+
}
|
|
1006
1350
|
const safeValue = key === "endpoint" ? assertEndpoint(value) : value;
|
|
1007
1351
|
config[key] = safeValue;
|
|
1008
1352
|
saveConfig(config);
|
|
1009
|
-
console.log(
|
|
1353
|
+
console.log(`\u2713 Set ${key}`);
|
|
1010
1354
|
} else {
|
|
1011
|
-
|
|
1355
|
+
const safe = { ...config, apiKey: config.apiKey ? `${config.apiKey.slice(0, 10)}\u2026` : void 0 };
|
|
1356
|
+
console.log(JSON.stringify(safe, null, 2));
|
|
1012
1357
|
}
|
|
1013
1358
|
});
|
|
1014
1359
|
var deploy = program.command("deploy").description("Deployment management");
|
|
1015
|
-
deploy.command("check").description("Check edge function health").action(async () => {
|
|
1016
|
-
const config =
|
|
1017
|
-
|
|
1018
|
-
console.error("Run `mushi login` first");
|
|
1019
|
-
process.exit(1);
|
|
1020
|
-
}
|
|
1021
|
-
const endpoint = config.endpoint ?? "https://api.mushimushi.dev";
|
|
1360
|
+
deploy.command("check").description("Check edge function health and measure latency").option("--json", "Machine-readable JSON output").action(async (opts) => {
|
|
1361
|
+
const config = requireConfig();
|
|
1362
|
+
const t0 = Date.now();
|
|
1022
1363
|
try {
|
|
1023
|
-
const
|
|
1024
|
-
|
|
1364
|
+
const controller = new AbortController();
|
|
1365
|
+
const timer = setTimeout(() => controller.abort(), API_TIMEOUT_MS);
|
|
1366
|
+
const res = await fetch(`${config.endpoint}/health`, { signal: controller.signal });
|
|
1367
|
+
clearTimeout(timer);
|
|
1368
|
+
const latency = Date.now() - t0;
|
|
1369
|
+
const body = res.headers.get("content-type")?.includes("json") ? await res.json().catch(() => ({})) : {};
|
|
1370
|
+
if (opts.json) {
|
|
1371
|
+
console.log(JSON.stringify({ ok: res.ok, status: res.status, latency_ms: latency, ...body }));
|
|
1372
|
+
} else {
|
|
1373
|
+
console.log(`Health: ${res.status === 200 ? "OK" : "FAIL"} (${res.status}) \u2014 ${latency}ms`);
|
|
1374
|
+
if (body["version"]) console.log(` Version: ${body["version"]}`);
|
|
1375
|
+
if (body["region"]) console.log(` Region: ${body["region"]}`);
|
|
1376
|
+
}
|
|
1377
|
+
if (!res.ok) process.exit(1);
|
|
1025
1378
|
} catch (err) {
|
|
1026
|
-
|
|
1379
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1380
|
+
if (opts.json) {
|
|
1381
|
+
console.log(JSON.stringify({ ok: false, error: msg }));
|
|
1382
|
+
} else {
|
|
1383
|
+
process.stderr.write(`error: ${msg}
|
|
1384
|
+
`);
|
|
1385
|
+
}
|
|
1386
|
+
process.exit(1);
|
|
1027
1387
|
}
|
|
1028
1388
|
});
|
|
1029
|
-
program.command("
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1389
|
+
var reports = program.command("reports").description("Manage bug reports");
|
|
1390
|
+
reports.command("list").description("List recent reports for the current project").option("--limit <n>", "Max results (1\u2013100)", "20").option("--status <status>", "Filter by status: new|triaged|in_progress|resolved|dismissed").option("--severity <severity>", "Filter by severity: critical|high|medium|low").option("--search <query>", "Full-text search in summary and description").option("--json", "Machine-readable JSON output").addHelpText("after", `
|
|
1391
|
+
Examples:
|
|
1392
|
+
mushi reports list
|
|
1393
|
+
mushi reports list --status new --severity critical
|
|
1394
|
+
mushi reports list --search "button not working" --limit 5 --json`).action(async (opts) => {
|
|
1395
|
+
const config = requireConfig();
|
|
1396
|
+
const limit = Math.min(Math.max(1, parseInt(opts.limit) || 20), 100);
|
|
1397
|
+
const params = new URLSearchParams({ limit: String(limit) });
|
|
1398
|
+
if (opts.status) params.set("status", opts.status);
|
|
1399
|
+
if (opts.severity) params.set("severity", opts.severity);
|
|
1400
|
+
if (opts.search) params.set("search", opts.search);
|
|
1401
|
+
const result = await apiCall(`/v1/sync/reports?${params}`, config);
|
|
1402
|
+
if (!result.ok) die(result);
|
|
1403
|
+
if (opts.json) {
|
|
1404
|
+
console.log(JSON.stringify(result.data, null, 2));
|
|
1405
|
+
return;
|
|
1034
1406
|
}
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1407
|
+
const rows = result.data.reports;
|
|
1408
|
+
if (rows.length === 0) {
|
|
1409
|
+
console.log("No reports found.");
|
|
1410
|
+
return;
|
|
1411
|
+
}
|
|
1412
|
+
console.log(`${pad("ID", 38)} ${pad("SEV", 9)} ${pad("STATUS", 12)} ${pad("CREATED", 17)} SUMMARY`);
|
|
1413
|
+
console.log("\u2500".repeat(110));
|
|
1414
|
+
for (const r of rows) {
|
|
1415
|
+
const sev = r.severity ?? "unset";
|
|
1416
|
+
const status = r.status ?? "new";
|
|
1417
|
+
const summary = (r.summary ?? r.description ?? "").slice(0, 50);
|
|
1418
|
+
console.log(`${pad(r.id, 38)} ${pad(sev, 9)} ${pad(status, 12)} ${pad(fmtDate(r.created_at), 17)} ${summary}`);
|
|
1419
|
+
}
|
|
1420
|
+
if (result.data.total > rows.length) {
|
|
1421
|
+
console.log(`
|
|
1422
|
+
\u2026 ${result.data.total - rows.length} more. Use --limit to see more.`);
|
|
1423
|
+
}
|
|
1424
|
+
});
|
|
1425
|
+
reports.command("show <id>").description("Show full details for a single report").option("--json", "Machine-readable JSON output").action(async (id, opts) => {
|
|
1426
|
+
const config = requireConfig();
|
|
1427
|
+
const result = await apiCall(`/v1/sync/reports/${id}`, config);
|
|
1428
|
+
if (!result.ok) {
|
|
1429
|
+
if (result.httpStatus === 404 || result.error.code === "NOT_FOUND") {
|
|
1430
|
+
process.stderr.write(`error: report "${id}" not found
|
|
1431
|
+
`);
|
|
1432
|
+
process.exit(3);
|
|
1433
|
+
}
|
|
1434
|
+
die(result);
|
|
1435
|
+
}
|
|
1436
|
+
if (opts.json) {
|
|
1437
|
+
console.log(JSON.stringify(result.data, null, 2));
|
|
1438
|
+
return;
|
|
1439
|
+
}
|
|
1440
|
+
const r = result.data;
|
|
1441
|
+
console.log(`Report: ${r.id}`);
|
|
1442
|
+
console.log(` Status: ${r.status ?? "new"}`);
|
|
1443
|
+
console.log(` Severity: ${r.severity ?? "unset"}`);
|
|
1444
|
+
console.log(` Category: ${r.category ?? "\u2014"}`);
|
|
1445
|
+
console.log(` Created: ${fmtDate(r.created_at)}`);
|
|
1446
|
+
if (r.summary) console.log(` Summary: ${r.summary}`);
|
|
1447
|
+
if (r.description) {
|
|
1448
|
+
console.log(` Description:`);
|
|
1449
|
+
console.log(` ${r.description.replace(/\n/g, "\n ")}`);
|
|
1450
|
+
}
|
|
1451
|
+
if (r.environment?.url) console.log(` URL: ${r.environment.url}`);
|
|
1452
|
+
if (r.component) console.log(` Component: ${r.component}`);
|
|
1453
|
+
if (r.sentry_event_id) console.log(` Sentry: ${r.sentry_event_id}`);
|
|
1454
|
+
if (r.fix_id) console.log(` Fix: ${r.fix_id}`);
|
|
1455
|
+
if (r.tags && Object.keys(r.tags).length > 0) {
|
|
1456
|
+
console.log(` Tags: ${JSON.stringify(r.tags)}`);
|
|
1457
|
+
}
|
|
1458
|
+
});
|
|
1459
|
+
reports.command("triage <id>").description("Update the status and/or severity of a report").option("--status <status>", "New status: new|triaged|in_progress|resolved|dismissed").option("--severity <severity>", "New severity: critical|high|medium|low").option("--note <text>", "Internal triage note").option("--json", "Machine-readable JSON output").addHelpText("after", `
|
|
1460
|
+
Examples:
|
|
1461
|
+
mushi reports triage <id> --status triaged --severity high
|
|
1462
|
+
mushi reports triage <id> --status in_progress --note "assigned to @alice"`).action(async (id, opts) => {
|
|
1463
|
+
const config = requireConfig();
|
|
1464
|
+
const body = {};
|
|
1465
|
+
if (opts.status) body["status"] = opts.status;
|
|
1466
|
+
if (opts.severity) body["severity"] = opts.severity;
|
|
1467
|
+
if (opts.note) body["note"] = opts.note;
|
|
1468
|
+
if (Object.keys(body).length === 0) {
|
|
1469
|
+
process.stderr.write("error: provide at least one of --status, --severity, or --note\n");
|
|
1470
|
+
process.exit(2);
|
|
1471
|
+
}
|
|
1472
|
+
const result = await apiCall(`/v1/sync/reports/${id}`, config, {
|
|
1473
|
+
method: "PATCH",
|
|
1474
|
+
body: JSON.stringify(body)
|
|
1475
|
+
});
|
|
1476
|
+
if (!result.ok) {
|
|
1477
|
+
if (result.httpStatus === 404 || result.error.code === "NOT_FOUND") {
|
|
1478
|
+
process.stderr.write(`error: report "${id}" not found
|
|
1479
|
+
`);
|
|
1480
|
+
process.exit(3);
|
|
1481
|
+
}
|
|
1482
|
+
die(result);
|
|
1483
|
+
}
|
|
1484
|
+
if (opts.json) {
|
|
1485
|
+
console.log(JSON.stringify(result.data, null, 2));
|
|
1486
|
+
} else {
|
|
1487
|
+
console.log(`\u2713 Updated report ${id}`);
|
|
1488
|
+
if (opts.status) console.log(` Status: ${opts.status}`);
|
|
1489
|
+
if (opts.severity) console.log(` Severity: ${opts.severity}`);
|
|
1490
|
+
if (opts.note) console.log(` Note: ${opts.note}`);
|
|
1491
|
+
}
|
|
1492
|
+
});
|
|
1493
|
+
reports.command("resolve <id>").description("Mark a report as resolved (shorthand for triage --status resolved)").option("--note <text>", "Resolution note").option("--json", "Machine-readable JSON output").action(async (id, opts) => {
|
|
1494
|
+
const config = requireConfig();
|
|
1495
|
+
const body = { status: "resolved" };
|
|
1496
|
+
if (opts.note) body["note"] = opts.note;
|
|
1497
|
+
const result = await apiCall(`/v1/sync/reports/${id}`, config, {
|
|
1498
|
+
method: "PATCH",
|
|
1499
|
+
body: JSON.stringify(body)
|
|
1500
|
+
});
|
|
1501
|
+
if (!result.ok) {
|
|
1502
|
+
if (result.httpStatus === 404 || result.error.code === "NOT_FOUND") {
|
|
1503
|
+
process.stderr.write(`error: report "${id}" not found
|
|
1504
|
+
`);
|
|
1505
|
+
process.exit(3);
|
|
1506
|
+
}
|
|
1507
|
+
die(result);
|
|
1508
|
+
}
|
|
1509
|
+
if (opts.json) {
|
|
1510
|
+
console.log(JSON.stringify(result.data, null, 2));
|
|
1511
|
+
} else {
|
|
1512
|
+
console.log(`\u2713 Resolved report ${id}`);
|
|
1513
|
+
if (opts.note) console.log(` Note: ${opts.note}`);
|
|
1514
|
+
}
|
|
1515
|
+
});
|
|
1516
|
+
reports.command("reopen <id>").description("Reopen a resolved or dismissed report").option("--note <text>", "Note explaining the reopen").option("--json", "Machine-readable JSON output").action(async (id, opts) => {
|
|
1517
|
+
const config = requireConfig();
|
|
1518
|
+
const body = { status: "new" };
|
|
1519
|
+
if (opts.note) body["note"] = opts.note;
|
|
1520
|
+
const result = await apiCall(`/v1/sync/reports/${id}`, config, {
|
|
1521
|
+
method: "PATCH",
|
|
1522
|
+
body: JSON.stringify(body)
|
|
1523
|
+
});
|
|
1524
|
+
if (!result.ok) {
|
|
1525
|
+
if (result.httpStatus === 404 || result.error.code === "NOT_FOUND") {
|
|
1526
|
+
process.stderr.write(`error: report "${id}" not found
|
|
1527
|
+
`);
|
|
1528
|
+
process.exit(3);
|
|
1529
|
+
}
|
|
1530
|
+
die(result);
|
|
1531
|
+
}
|
|
1532
|
+
if (opts.json) {
|
|
1533
|
+
console.log(JSON.stringify(result.data, null, 2));
|
|
1534
|
+
} else {
|
|
1535
|
+
console.log(`\u2713 Reopened report ${id}`);
|
|
1536
|
+
}
|
|
1537
|
+
});
|
|
1538
|
+
reports.command("dismiss <id>").description("Dismiss a report (not a real bug / out of scope)").option("--note <text>", "Reason for dismissal").option("--json", "Machine-readable JSON output").action(async (id, opts) => {
|
|
1539
|
+
const config = requireConfig();
|
|
1540
|
+
const body = { status: "dismissed" };
|
|
1541
|
+
if (opts.note) body["note"] = opts.note;
|
|
1542
|
+
const result = await apiCall(`/v1/sync/reports/${id}`, config, {
|
|
1543
|
+
method: "PATCH",
|
|
1544
|
+
body: JSON.stringify(body)
|
|
1545
|
+
});
|
|
1546
|
+
if (!result.ok) {
|
|
1547
|
+
if (result.httpStatus === 404 || result.error.code === "NOT_FOUND") {
|
|
1548
|
+
process.stderr.write(`error: report "${id}" not found
|
|
1549
|
+
`);
|
|
1550
|
+
process.exit(3);
|
|
1551
|
+
}
|
|
1552
|
+
die(result);
|
|
1553
|
+
}
|
|
1554
|
+
if (opts.json) {
|
|
1555
|
+
console.log(JSON.stringify(result.data, null, 2));
|
|
1556
|
+
} else {
|
|
1557
|
+
console.log(`\u2713 Dismissed report ${id}`);
|
|
1558
|
+
}
|
|
1559
|
+
});
|
|
1560
|
+
reports.command("search <query>").description("Search reports by keyword in summary and description").option("--limit <n>", "Max results (1\u201350)", "10").option("--status <status>", "Filter by status").option("--json", "Machine-readable JSON output").addHelpText("after", `
|
|
1561
|
+
Examples:
|
|
1562
|
+
mushi reports search "login button"
|
|
1563
|
+
mushi reports search "404 error" --status new --limit 20`).action(async (query, opts) => {
|
|
1564
|
+
const config = requireConfig();
|
|
1565
|
+
const limit = Math.min(Math.max(1, parseInt(opts.limit) || 10), 50);
|
|
1566
|
+
const params = new URLSearchParams({ search: query, limit: String(limit) });
|
|
1567
|
+
if (opts.status) params.set("status", opts.status);
|
|
1568
|
+
const result = await apiCall(`/v1/sync/reports?${params}`, config);
|
|
1569
|
+
if (!result.ok) die(result);
|
|
1570
|
+
if (opts.json) {
|
|
1571
|
+
console.log(JSON.stringify(result.data, null, 2));
|
|
1572
|
+
return;
|
|
1573
|
+
}
|
|
1574
|
+
const rows = result.data.reports;
|
|
1575
|
+
if (rows.length === 0) {
|
|
1576
|
+
console.log(`No reports matching "${query}".`);
|
|
1577
|
+
return;
|
|
1578
|
+
}
|
|
1579
|
+
console.log(`${rows.length} result${rows.length === 1 ? "" : "s"} for "${query}":`);
|
|
1580
|
+
console.log("");
|
|
1581
|
+
for (const r of rows) {
|
|
1582
|
+
console.log(` ${r.id}`);
|
|
1583
|
+
console.log(` ${r.severity ?? "unset"} \xB7 ${r.status ?? "new"} \xB7 ${fmtDate(r.created_at)}`);
|
|
1584
|
+
const text2 = r.summary ?? r.description ?? "";
|
|
1585
|
+
if (text2) console.log(` ${text2.slice(0, 80)}`);
|
|
1586
|
+
console.log("");
|
|
1587
|
+
}
|
|
1588
|
+
});
|
|
1589
|
+
var lessons = program.command("lessons").description("Manage learned mistake rules");
|
|
1590
|
+
lessons.command("list").description("List active lessons (mistake rules) for the current project").option("--severity <sev>", "Filter: info|warn|critical").option("--limit <n>", "Max results (1\u2013200)", "50").option("--json", "Machine-readable JSON output").addHelpText("after", `
|
|
1591
|
+
Lessons are mistake rules extracted from past bug reports by the clustering
|
|
1592
|
+
pipeline. They are injected into AI code-review context via the MCP server.`).action(async (opts) => {
|
|
1593
|
+
const config = requireConfig();
|
|
1594
|
+
const limit = Math.min(Math.max(1, parseInt(opts.limit) || 50), 200);
|
|
1595
|
+
const params = new URLSearchParams({ limit: String(limit) });
|
|
1596
|
+
if (opts.severity) params.set("severity", opts.severity);
|
|
1597
|
+
const result = await apiCall(`/v1/sync/lessons?${params}`, config);
|
|
1598
|
+
if (!result.ok) die(result);
|
|
1599
|
+
if (opts.json) {
|
|
1600
|
+
console.log(JSON.stringify(result.data, null, 2));
|
|
1601
|
+
return;
|
|
1602
|
+
}
|
|
1603
|
+
const rows = result.data;
|
|
1604
|
+
if (!Array.isArray(rows) || rows.length === 0) {
|
|
1605
|
+
console.log("No active lessons yet. Reports are clustered nightly.");
|
|
1606
|
+
return;
|
|
1607
|
+
}
|
|
1608
|
+
console.log(`${pad("SEV", 9)} ${pad("FREQ", 6)} RULE`);
|
|
1609
|
+
console.log("\u2500".repeat(90));
|
|
1610
|
+
for (const l of rows) {
|
|
1611
|
+
const sev = l.severity ?? "info";
|
|
1612
|
+
const freq = String(l.frequency ?? 0);
|
|
1613
|
+
const rule = (l.rule_text ?? "").slice(0, 70);
|
|
1614
|
+
console.log(`${pad(sev, 9)} ${pad(freq, 6)} ${rule}`);
|
|
1615
|
+
}
|
|
1616
|
+
console.log(`
|
|
1617
|
+
${rows.length} active lesson${rows.length === 1 ? "" : "s"}`);
|
|
1618
|
+
});
|
|
1619
|
+
lessons.command("show <id>").description("Show full detail for a single lesson (rule text, anti-pattern, source reports)").option("--json", "Machine-readable JSON output").action(async (id, opts) => {
|
|
1620
|
+
const config = requireConfig();
|
|
1621
|
+
const result = await apiCall(`/v1/sync/lessons/${id}`, config);
|
|
1622
|
+
if (!result.ok) {
|
|
1623
|
+
if (result.httpStatus === 404 || result.error.code === "NOT_FOUND") {
|
|
1624
|
+
process.stderr.write(`error: lesson "${id}" not found
|
|
1625
|
+
`);
|
|
1626
|
+
process.exit(3);
|
|
1627
|
+
}
|
|
1628
|
+
die(result);
|
|
1629
|
+
}
|
|
1630
|
+
if (opts.json) {
|
|
1631
|
+
console.log(JSON.stringify(result.data, null, 2));
|
|
1632
|
+
return;
|
|
1633
|
+
}
|
|
1634
|
+
const l = result.data;
|
|
1635
|
+
console.log(`Lesson: ${l.id}`);
|
|
1636
|
+
console.log(` Severity: ${l.severity}`);
|
|
1637
|
+
console.log(` Frequency: ${l.frequency} reports`);
|
|
1638
|
+
if (l.last_reinforced_at) console.log(` Updated: ${fmtDate(l.last_reinforced_at)}`);
|
|
1639
|
+
console.log("");
|
|
1640
|
+
console.log(`Rule:`);
|
|
1641
|
+
console.log(` ${l.rule_text}`);
|
|
1642
|
+
if (l.anti_pattern) {
|
|
1643
|
+
console.log("");
|
|
1644
|
+
console.log(`Anti-pattern:`);
|
|
1645
|
+
console.log(` ${l.anti_pattern}`);
|
|
1646
|
+
}
|
|
1647
|
+
if (l.summary_paragraph) {
|
|
1648
|
+
console.log("");
|
|
1649
|
+
console.log(`Summary:`);
|
|
1650
|
+
console.log(` ${l.summary_paragraph}`);
|
|
1651
|
+
}
|
|
1652
|
+
});
|
|
1653
|
+
program.command("sync-lessons").description("Pull promoted lessons from Mushi and write .mushi/lessons.json into this repo").option("--cwd <path>", "Target directory (default: current working dir)").option("--dry-run", "Print the JSON that would be written without writing anything").option("--json", "Machine-readable output: { ok, path, count }").addHelpText("after", `
|
|
1654
|
+
Used in CI to keep .mushi/lessons.json up to date so the Mushi MCP server
|
|
1655
|
+
and Cursor rules can inject the latest project-specific mistake rules into
|
|
1656
|
+
AI code review context.
|
|
1657
|
+
|
|
1658
|
+
Typical CI usage:
|
|
1659
|
+
MUSHI_API_KEY=$KEY MUSHI_PROJECT_ID=$PID MUSHI_API_ENDPOINT=$URL \\
|
|
1660
|
+
npx @mushi-mushi/cli sync-lessons --cwd .`).action(async (opts) => {
|
|
1661
|
+
const { writeFile, mkdir } = await import("fs/promises");
|
|
1662
|
+
const nodePath = await import("path");
|
|
1663
|
+
const config = requireConfig();
|
|
1664
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
1665
|
+
const target = nodePath.join(cwd, ".mushi", "lessons.json");
|
|
1666
|
+
const result = await apiCall("/v1/sync/lessons?limit=500", config);
|
|
1667
|
+
if (!result.ok) die(result);
|
|
1668
|
+
const rows = Array.isArray(result.data) ? result.data : [];
|
|
1669
|
+
const lessons2 = rows.map((l) => ({
|
|
1670
|
+
id: l.id,
|
|
1671
|
+
rule: l.rule_text,
|
|
1672
|
+
anti_pattern: l.anti_pattern ?? void 0,
|
|
1673
|
+
severity: l.severity,
|
|
1674
|
+
frequency: l.frequency,
|
|
1675
|
+
last_reinforced: l.last_reinforced_at?.slice(0, 10) ?? "",
|
|
1676
|
+
cluster_id: l.cluster_id ?? void 0
|
|
1677
|
+
}));
|
|
1678
|
+
const output = {
|
|
1679
|
+
schema_version: "1",
|
|
1680
|
+
project_id: config.projectId ?? "",
|
|
1681
|
+
generated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1682
|
+
lessons: lessons2
|
|
1683
|
+
};
|
|
1684
|
+
if (opts.dryRun) {
|
|
1685
|
+
console.log(JSON.stringify(output, null, 2));
|
|
1686
|
+
return;
|
|
1687
|
+
}
|
|
1688
|
+
await mkdir(nodePath.dirname(target), { recursive: true });
|
|
1689
|
+
await writeFile(target, JSON.stringify(output, null, 2) + "\n", "utf8");
|
|
1690
|
+
if (opts.json) {
|
|
1691
|
+
console.log(JSON.stringify({ ok: true, path: target, count: lessons2.length }));
|
|
1692
|
+
} else {
|
|
1693
|
+
console.log(`\u2713 Wrote ${lessons2.length} lesson${lessons2.length === 1 ? "" : "s"} to ${target}`);
|
|
1694
|
+
}
|
|
1695
|
+
});
|
|
1696
|
+
program.command("test").description("Submit a synthetic test report to verify the ingestion pipeline end-to-end").option("--json", "Machine-readable JSON output").action(async (opts) => {
|
|
1697
|
+
const config = requireConfig();
|
|
1698
|
+
const result = await apiCall("/v1/reports", config, {
|
|
1699
|
+
method: "POST",
|
|
1700
|
+
body: JSON.stringify({
|
|
1701
|
+
projectId: config.projectId,
|
|
1702
|
+
description: "CLI test report \u2014 verifying ingestion pipeline",
|
|
1703
|
+
category: "other",
|
|
1704
|
+
reporterToken: `cli-test-${Date.now()}`,
|
|
1705
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1706
|
+
environment: {
|
|
1707
|
+
url: "cli://test",
|
|
1708
|
+
userAgent: `mushi-cli/${MUSHI_CLI_VERSION}`,
|
|
1709
|
+
platform: process.platform,
|
|
1710
|
+
language: "en",
|
|
1711
|
+
viewport: { width: 0, height: 0 },
|
|
1712
|
+
referrer: "",
|
|
1713
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1714
|
+
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone
|
|
1715
|
+
}
|
|
1716
|
+
})
|
|
1717
|
+
});
|
|
1718
|
+
if (!result.ok) die(result);
|
|
1719
|
+
if (opts.json) {
|
|
1720
|
+
console.log(JSON.stringify(result.data, null, 2));
|
|
1721
|
+
} else {
|
|
1722
|
+
const d = result.data;
|
|
1723
|
+
console.log(`\u2713 Test report submitted`);
|
|
1724
|
+
console.log(` ID: ${d.reportId}`);
|
|
1725
|
+
console.log(` Status: ${d.status}`);
|
|
1726
|
+
console.log(` View: https://kensaur.us/mushi-mushi/reports/${d.reportId}`);
|
|
1038
1727
|
}
|
|
1039
|
-
|
|
1728
|
+
});
|
|
1729
|
+
program.command("index <path>").description("Walk a local repo and upload code chunks to the Mushi RAG indexer").option("--language <lang>", "Limit to one language: ts, tsx, js, py, go, rs").option("--dry-run", "Show what would be uploaded without sending").option("--json", "Machine-readable summary: { files, bytes }").addHelpText("after", `
|
|
1730
|
+
Uploads source code into the Mushi vector index so the fix-worker can
|
|
1731
|
+
retrieve relevant context when generating patches. Only needed for private
|
|
1732
|
+
repos that cannot be auto-indexed via GitHub App.
|
|
1733
|
+
|
|
1734
|
+
Examples:
|
|
1735
|
+
mushi index ./src
|
|
1736
|
+
mushi index ./src --language ts --dry-run`).action(async (path, opts) => {
|
|
1737
|
+
const config = requireConfig({ needsProject: true });
|
|
1738
|
+
const { readdir: readdir2, readFile: readFile2, stat } = await import("fs/promises");
|
|
1040
1739
|
const nodePath = await import("path");
|
|
1041
1740
|
const SKIP = /node_modules|\.git|dist|build|\.next|\.turbo|coverage/;
|
|
1042
1741
|
const EXTS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".py", ".go", ".rs"]);
|
|
1742
|
+
const MAX_FILE_BYTES = 5e5;
|
|
1043
1743
|
async function* walk(dir) {
|
|
1044
|
-
const entries = await
|
|
1744
|
+
const entries = await readdir2(dir, { withFileTypes: true });
|
|
1045
1745
|
for (const e of entries) {
|
|
1046
1746
|
const full = nodePath.join(dir, e.name);
|
|
1047
1747
|
if (SKIP.test(full)) continue;
|
|
@@ -1051,60 +1751,60 @@ program.command("index <path>").description("Walk a local repo and upload code c
|
|
|
1051
1751
|
}
|
|
1052
1752
|
let count = 0;
|
|
1053
1753
|
let bytes = 0;
|
|
1754
|
+
let errors = 0;
|
|
1054
1755
|
const root = nodePath.resolve(path);
|
|
1055
1756
|
for await (const file of walk(root)) {
|
|
1056
1757
|
const lang = nodePath.extname(file).slice(1);
|
|
1057
1758
|
if (opts.language && opts.language !== lang) continue;
|
|
1058
1759
|
const stats = await stat(file);
|
|
1059
|
-
if (stats.size >
|
|
1060
|
-
|
|
1061
|
-
|
|
1760
|
+
if (stats.size > MAX_FILE_BYTES) {
|
|
1761
|
+
if (!opts.json) process.stdout.write(` skip ${nodePath.relative(root, file)} (>${MAX_FILE_BYTES / 1e3}KB)
|
|
1762
|
+
`);
|
|
1763
|
+
continue;
|
|
1764
|
+
}
|
|
1765
|
+
const source = await readFile2(file, "utf8");
|
|
1766
|
+
const relative2 = nodePath.relative(root, file).replaceAll("\\", "/");
|
|
1062
1767
|
count++;
|
|
1063
1768
|
bytes += source.length;
|
|
1064
1769
|
if (opts.dryRun) {
|
|
1065
|
-
|
|
1770
|
+
if (!opts.json) process.stdout.write(` ${relative2} (${source.length} bytes)
|
|
1771
|
+
`);
|
|
1066
1772
|
continue;
|
|
1067
1773
|
}
|
|
1068
|
-
const
|
|
1774
|
+
const result = await apiCall("/v1/sync/codebase/upload", config, {
|
|
1069
1775
|
method: "POST",
|
|
1070
|
-
body: JSON.stringify({
|
|
1071
|
-
projectId: config.projectId,
|
|
1072
|
-
filePath: relative,
|
|
1073
|
-
source
|
|
1074
|
-
})
|
|
1776
|
+
body: JSON.stringify({ projectId: config.projectId, filePath: relative2, source })
|
|
1075
1777
|
});
|
|
1076
|
-
if (!
|
|
1077
|
-
|
|
1778
|
+
if (!result.ok) {
|
|
1779
|
+
errors++;
|
|
1780
|
+
process.stderr.write(` FAIL ${relative2}: ${result.error.message}
|
|
1078
1781
|
`);
|
|
1782
|
+
} else if (!opts.json) {
|
|
1783
|
+
process.stdout.write(` ok ${relative2} \u2192 ${result.data.chunks} chunks
|
|
1784
|
+
`);
|
|
1785
|
+
}
|
|
1079
1786
|
}
|
|
1080
|
-
|
|
1081
|
-
});
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
process.exit(1);
|
|
1787
|
+
if (opts.json) {
|
|
1788
|
+
console.log(JSON.stringify({ ok: errors === 0, files: count, bytes, errors }));
|
|
1789
|
+
} else {
|
|
1790
|
+
const kb = (bytes / 1024).toFixed(1);
|
|
1791
|
+
console.log(`
|
|
1792
|
+
Indexed ${count} files (${kb} KB) into project ${config.projectId}${errors ? ` \u2014 ${errors} failed` : ""}`);
|
|
1087
1793
|
}
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
referrer: "",
|
|
1103
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1104
|
-
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone
|
|
1105
|
-
}
|
|
1106
|
-
})
|
|
1794
|
+
if (errors > 0) process.exit(1);
|
|
1795
|
+
});
|
|
1796
|
+
var sourcemaps = program.command("sourcemaps").description("Source map management");
|
|
1797
|
+
sourcemaps.command("upload").description("Upload source maps to Mushi (idempotent, SHA256-keyed) for stack trace symbolication").requiredOption("--release <version>", "Release identifier, e.g. 1.0.0 or a git SHA").option("--dir <path>", "Directory containing .map files", "./dist").option("--dry-run", "List files that would be uploaded without uploading").option("-e, --endpoint <url>", "API endpoint (overrides MUSHI_API_ENDPOINT)").option("--api-key <key>", "API key (overrides MUSHI_API_KEY)").option("--silent", "Suppress progress output").addHelpText("after", `
|
|
1798
|
+
Examples:
|
|
1799
|
+
mushi sourcemaps upload --release 1.0.0
|
|
1800
|
+
mushi sourcemaps upload --release $(git rev-parse --short HEAD) --dir ./dist`).action(async (opts) => {
|
|
1801
|
+
await runSourcemapsUpload({
|
|
1802
|
+
release: opts.release,
|
|
1803
|
+
dir: opts.dir,
|
|
1804
|
+
dryRun: opts.dryRun,
|
|
1805
|
+
endpoint: opts.endpoint,
|
|
1806
|
+
apiKey: opts.apiKey,
|
|
1807
|
+
silent: opts.silent
|
|
1107
1808
|
});
|
|
1108
|
-
console.log("Test report submitted:", JSON.stringify(data, null, 2));
|
|
1109
1809
|
});
|
|
1110
1810
|
program.parse();
|