brakit 0.8.5 → 0.8.6

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.
@@ -9,6 +9,91 @@ var __export = (target, all) => {
9
9
  __defProp(target, name, { get: all[name], enumerable: true });
10
10
  };
11
11
 
12
+ // src/constants/limits.ts
13
+ var PROJECT_HASH_LENGTH, SECRET_SCAN_ARRAY_LIMIT, PII_SCAN_ARRAY_LIMIT, MIN_SECRET_VALUE_LENGTH, FULL_RECORD_MIN_FIELDS, LIST_PII_MIN_ITEMS, MAX_OBJECT_SCAN_DEPTH, ISSUE_PRUNE_TTL_MS;
14
+ var init_limits = __esm({
15
+ "src/constants/limits.ts"() {
16
+ "use strict";
17
+ PROJECT_HASH_LENGTH = 8;
18
+ SECRET_SCAN_ARRAY_LIMIT = 5;
19
+ PII_SCAN_ARRAY_LIMIT = 10;
20
+ MIN_SECRET_VALUE_LENGTH = 8;
21
+ FULL_RECORD_MIN_FIELDS = 5;
22
+ LIST_PII_MIN_ITEMS = 2;
23
+ MAX_OBJECT_SCAN_DEPTH = 5;
24
+ ISSUE_PRUNE_TTL_MS = 10 * 60 * 1e3;
25
+ }
26
+ });
27
+
28
+ // src/utils/log.ts
29
+ function brakitDebug(message) {
30
+ if (process.env.DEBUG_BRAKIT) {
31
+ process.stderr.write(`${PREFIX}:debug ${message}
32
+ `);
33
+ }
34
+ }
35
+ var PREFIX;
36
+ var init_log = __esm({
37
+ "src/utils/log.ts"() {
38
+ "use strict";
39
+ PREFIX = "[brakit]";
40
+ }
41
+ });
42
+
43
+ // src/constants/lifecycle.ts
44
+ var VALID_ISSUE_STATES, VALID_AI_FIX_STATUSES, VALID_SECURITY_SEVERITIES;
45
+ var init_lifecycle = __esm({
46
+ "src/constants/lifecycle.ts"() {
47
+ "use strict";
48
+ VALID_ISSUE_STATES = /* @__PURE__ */ new Set(["open", "fixing", "resolved", "stale", "regressed"]);
49
+ VALID_AI_FIX_STATUSES = /* @__PURE__ */ new Set(["fixed", "wont_fix"]);
50
+ VALID_SECURITY_SEVERITIES = /* @__PURE__ */ new Set(["critical", "warning"]);
51
+ }
52
+ });
53
+
54
+ // src/utils/type-guards.ts
55
+ function isNonEmptyString(val) {
56
+ return typeof val === "string" && val.trim().length > 0;
57
+ }
58
+ function getErrorMessage(err) {
59
+ if (err instanceof Error) return err.message;
60
+ if (typeof err === "string") return err;
61
+ return String(err);
62
+ }
63
+ function isValidIssueState(val) {
64
+ return typeof val === "string" && VALID_ISSUE_STATES.has(val);
65
+ }
66
+ function isValidAiFixStatus(val) {
67
+ return typeof val === "string" && VALID_AI_FIX_STATUSES.has(val);
68
+ }
69
+ var init_type_guards = __esm({
70
+ "src/utils/type-guards.ts"() {
71
+ "use strict";
72
+ init_lifecycle();
73
+ init_limits();
74
+ }
75
+ });
76
+
77
+ // src/constants/metrics.ts
78
+ var METRICS_DIR, PORT_FILE;
79
+ var init_metrics = __esm({
80
+ "src/constants/metrics.ts"() {
81
+ "use strict";
82
+ METRICS_DIR = ".brakit";
83
+ PORT_FILE = ".brakit/port";
84
+ }
85
+ });
86
+
87
+ // src/constants/thresholds.ts
88
+ var OVERFETCH_UNWRAP_MIN_SIZE, STALE_ISSUE_TTL_MS;
89
+ var init_thresholds = __esm({
90
+ "src/constants/thresholds.ts"() {
91
+ "use strict";
92
+ OVERFETCH_UNWRAP_MIN_SIZE = 3;
93
+ STALE_ISSUE_TTL_MS = 30 * 60 * 1e3;
94
+ }
95
+ });
96
+
12
97
  // src/constants/routes.ts
13
98
  var DASHBOARD_PREFIX, DASHBOARD_API_REQUESTS, DASHBOARD_API_EVENTS, DASHBOARD_API_FLOWS, DASHBOARD_API_CLEAR, DASHBOARD_API_LOGS, DASHBOARD_API_FETCHES, DASHBOARD_API_ERRORS, DASHBOARD_API_QUERIES, DASHBOARD_API_INGEST, DASHBOARD_API_METRICS, DASHBOARD_API_ACTIVITY, DASHBOARD_API_METRICS_LIVE, DASHBOARD_API_INSIGHTS, DASHBOARD_API_SECURITY, DASHBOARD_API_TAB, DASHBOARD_API_FINDINGS, DASHBOARD_API_FINDINGS_REPORT, VALID_TABS_TUPLE, VALID_TABS;
14
99
  var init_routes = __esm({
@@ -47,24 +132,6 @@ var init_routes = __esm({
47
132
  }
48
133
  });
49
134
 
50
- // src/constants/limits.ts
51
- var FINDING_ID_HASH_LENGTH;
52
- var init_limits = __esm({
53
- "src/constants/limits.ts"() {
54
- "use strict";
55
- FINDING_ID_HASH_LENGTH = 16;
56
- }
57
- });
58
-
59
- // src/constants/thresholds.ts
60
- var OVERFETCH_UNWRAP_MIN_SIZE;
61
- var init_thresholds = __esm({
62
- "src/constants/thresholds.ts"() {
63
- "use strict";
64
- OVERFETCH_UNWRAP_MIN_SIZE = 3;
65
- }
66
- });
67
-
68
135
  // src/constants/transport.ts
69
136
  var init_transport = __esm({
70
137
  "src/constants/transport.ts"() {
@@ -72,16 +139,6 @@ var init_transport = __esm({
72
139
  }
73
140
  });
74
141
 
75
- // src/constants/metrics.ts
76
- var METRICS_DIR, PORT_FILE;
77
- var init_metrics = __esm({
78
- "src/constants/metrics.ts"() {
79
- "use strict";
80
- METRICS_DIR = ".brakit";
81
- PORT_FILE = ".brakit/port";
82
- }
83
- });
84
-
85
142
  // src/constants/headers.ts
86
143
  var init_headers = __esm({
87
144
  "src/constants/headers.ts"() {
@@ -115,7 +172,7 @@ var init_mcp = __esm({
115
172
  MAX_TIMELINE_EVENTS = 20;
116
173
  MAX_RESOLVED_DISPLAY = 5;
117
174
  ENRICHMENT_SEVERITY_FILTER = ["critical", "warning"];
118
- MCP_SERVER_VERSION = "0.8.5";
175
+ MCP_SERVER_VERSION = "0.8.6";
119
176
  }
120
177
  });
121
178
 
@@ -140,14 +197,21 @@ var init_telemetry = __esm({
140
197
  }
141
198
  });
142
199
 
143
- // src/constants/lifecycle.ts
144
- var VALID_FINDING_STATES, VALID_AI_FIX_STATUSES, VALID_SECURITY_SEVERITIES;
145
- var init_lifecycle = __esm({
146
- "src/constants/lifecycle.ts"() {
200
+ // src/constants/cli.ts
201
+ var SUPPORTED_SOURCE_EXTENSIONS, BUILD_CACHE_DIRS, FALLBACK_SCAN_DIRS;
202
+ var init_cli = __esm({
203
+ "src/constants/cli.ts"() {
147
204
  "use strict";
148
- VALID_FINDING_STATES = /* @__PURE__ */ new Set(["open", "fixing", "resolved"]);
149
- VALID_AI_FIX_STATUSES = /* @__PURE__ */ new Set(["fixed", "wont_fix"]);
150
- VALID_SECURITY_SEVERITIES = /* @__PURE__ */ new Set(["critical", "warning"]);
205
+ SUPPORTED_SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([
206
+ ".ts",
207
+ ".tsx",
208
+ ".js",
209
+ ".jsx",
210
+ ".mjs",
211
+ ".mts"
212
+ ]);
213
+ BUILD_CACHE_DIRS = [".next", ".nuxt", ".output"];
214
+ FALLBACK_SCAN_DIRS = ["src", "."];
151
215
  }
152
216
  });
153
217
 
@@ -167,51 +231,7 @@ var init_constants = __esm({
167
231
  init_severity();
168
232
  init_telemetry();
169
233
  init_lifecycle();
170
- }
171
- });
172
-
173
- // src/utils/log.ts
174
- function brakitDebug(message) {
175
- if (process.env.DEBUG_BRAKIT) {
176
- process.stderr.write(`${PREFIX}:debug ${message}
177
- `);
178
- }
179
- }
180
- var PREFIX;
181
- var init_log = __esm({
182
- "src/utils/log.ts"() {
183
- "use strict";
184
- PREFIX = "[brakit]";
185
- }
186
- });
187
-
188
- // src/utils/type-guards.ts
189
- function isNonEmptyString(val) {
190
- return typeof val === "string" && val.trim().length > 0;
191
- }
192
- function isValidFindingState(val) {
193
- return typeof val === "string" && VALID_FINDING_STATES.has(val);
194
- }
195
- function isValidAiFixStatus(val) {
196
- return typeof val === "string" && VALID_AI_FIX_STATUSES.has(val);
197
- }
198
- var init_type_guards = __esm({
199
- "src/utils/type-guards.ts"() {
200
- "use strict";
201
- init_lifecycle();
202
- }
203
- });
204
-
205
- // src/store/finding-id.ts
206
- import { createHash } from "crypto";
207
- function computeInsightId(type, endpoint, desc) {
208
- const key = `${type}:${endpoint}:${desc}`;
209
- return createHash("sha256").update(key).digest("hex").slice(0, FINDING_ID_HASH_LENGTH);
210
- }
211
- var init_finding_id = __esm({
212
- "src/store/finding-id.ts"() {
213
- "use strict";
214
- init_limits();
234
+ init_cli();
215
235
  }
216
236
  });
217
237
 
@@ -249,11 +269,11 @@ var init_client = __esm({
249
269
  if (params?.offset) url.searchParams.set("offset", String(params.offset));
250
270
  return this.fetchJson(url);
251
271
  }
252
- async getSecurityFindings() {
253
- return this.fetchJson(`${this.baseUrl}${DASHBOARD_API_SECURITY}`);
254
- }
255
- async getInsights() {
256
- return this.fetchJson(`${this.baseUrl}${DASHBOARD_API_INSIGHTS}`);
272
+ async getIssues(params) {
273
+ const url = new URL(`${this.baseUrl}${DASHBOARD_API_INSIGHTS}`);
274
+ if (params?.state) url.searchParams.set("state", params.state);
275
+ if (params?.category) url.searchParams.set("category", params.category);
276
+ return this.fetchJson(url);
257
277
  }
258
278
  async getQueries(requestId) {
259
279
  const url = new URL(`${this.baseUrl}${DASHBOARD_API_QUERIES}`);
@@ -325,7 +345,7 @@ var init_client = __esm({
325
345
  });
326
346
 
327
347
  // src/mcp/discovery.ts
328
- import { readFile as readFile6, readdir as readdir2, stat } from "fs/promises";
348
+ import { readFile as readFile6, readdir as readdir3, stat } from "fs/promises";
329
349
  import { resolve as resolve5, dirname as dirname2 } from "path";
330
350
  async function readPort(portPath) {
331
351
  try {
@@ -341,7 +361,7 @@ async function portInDir(dir) {
341
361
  }
342
362
  async function portInChildren(dir) {
343
363
  try {
344
- const entries = await readdir2(dir);
364
+ const entries = await readdir3(dir);
345
365
  for (const entry of entries) {
346
366
  if (entry.startsWith(".") || entry === "node_modules") continue;
347
367
  const child = resolve5(dir, entry);
@@ -408,14 +428,16 @@ var init_discovery = __esm({
408
428
 
409
429
  // src/mcp/enrichment.ts
410
430
  async function enrichFindings(client) {
411
- const [securityData, insightsData] = await Promise.all([
412
- client.getSecurityFindings(),
413
- client.getInsights()
414
- ]);
431
+ const issuesData = await client.getIssues();
432
+ const issues = issuesData.issues.filter(
433
+ (si) => si.state !== "resolved" && si.state !== "stale"
434
+ );
415
435
  const contexts = await Promise.all(
416
- securityData.findings.map(async (sf) => {
436
+ issues.map(async (si) => {
437
+ const endpoint = si.issue.endpoint;
438
+ if (!endpoint) return si.issue.detail ?? "";
417
439
  try {
418
- const { path } = parseEndpointKey(sf.finding.endpoint);
440
+ const { path } = parseEndpointKey(endpoint);
419
441
  const reqData = await client.getRequests({ search: path, limit: 1 });
420
442
  if (reqData.requests.length > 0) {
421
443
  const req = reqData.requests[0];
@@ -429,38 +451,22 @@ async function enrichFindings(client) {
429
451
  } catch {
430
452
  return "(context unavailable)";
431
453
  }
432
- return "";
454
+ return si.issue.detail ?? "";
433
455
  })
434
456
  );
435
- const enriched = securityData.findings.map((sf, i) => {
436
- const f = sf.finding;
437
- return {
438
- findingId: sf.findingId,
439
- severity: f.severity,
440
- title: f.title,
441
- endpoint: f.endpoint,
442
- description: f.desc,
443
- hint: f.hint,
444
- occurrences: f.count,
445
- context: contexts[i],
446
- aiStatus: sf.aiStatus,
447
- aiNotes: sf.aiNotes
448
- };
449
- });
450
- for (const si of insightsData.insights) {
451
- if (si.state === "resolved") continue;
452
- const i = si.insight;
453
- if (!ENRICHMENT_SEVERITY_FILTER.includes(i.severity)) continue;
454
- const endpoint = i.nav ?? "global";
457
+ const enriched = [];
458
+ for (let i = 0; i < issues.length; i++) {
459
+ const si = issues[i];
460
+ if (!ENRICHMENT_SEVERITY_FILTER.includes(si.issue.severity)) continue;
455
461
  enriched.push({
456
- findingId: computeInsightId(i.type, endpoint, i.desc),
457
- severity: i.severity,
458
- title: i.title,
459
- endpoint,
460
- description: i.desc,
461
- hint: i.hint,
462
- occurrences: 1,
463
- context: i.detail ?? "",
462
+ findingId: si.issueId,
463
+ severity: si.issue.severity,
464
+ title: si.issue.title,
465
+ endpoint: si.issue.endpoint ?? "global",
466
+ description: si.issue.desc,
467
+ hint: si.issue.hint,
468
+ occurrences: si.occurrences,
469
+ context: contexts[i],
464
470
  aiStatus: si.aiStatus,
465
471
  aiNotes: si.aiNotes
466
472
  });
@@ -518,7 +524,6 @@ var init_enrichment = __esm({
518
524
  "src/mcp/enrichment.ts"() {
519
525
  "use strict";
520
526
  init_mcp();
521
- init_finding_id();
522
527
  init_endpoint();
523
528
  }
524
529
  });
@@ -544,8 +549,8 @@ var init_get_findings = __esm({
544
549
  },
545
550
  state: {
546
551
  type: "string",
547
- enum: ["open", "fixing", "resolved"],
548
- description: "Filter by finding state (from finding lifecycle)"
552
+ enum: ["open", "fixing", "resolved", "stale", "regressed"],
553
+ description: "Filter by issue state"
549
554
  }
550
555
  }
551
556
  },
@@ -555,17 +560,17 @@ var init_get_findings = __esm({
555
560
  if (severity && !VALID_SECURITY_SEVERITIES.has(severity)) {
556
561
  return { content: [{ type: "text", text: `Invalid severity "${severity}". Use: critical, warning.` }], isError: true };
557
562
  }
558
- if (state && !isValidFindingState(state)) {
559
- return { content: [{ type: "text", text: `Invalid state "${state}". Use: open, fixing, resolved.` }], isError: true };
563
+ if (state && !isValidIssueState(state)) {
564
+ return { content: [{ type: "text", text: `Invalid state "${state}". Use: open, fixing, resolved, stale, regressed.` }], isError: true };
560
565
  }
561
566
  let findings = await enrichFindings(client);
562
567
  if (severity) {
563
568
  findings = findings.filter((f) => f.severity === severity);
564
569
  }
565
570
  if (state) {
566
- const stateful = await client.getFindings(state);
567
- const statefulIds = new Set(stateful.findings.map((f) => f.findingId));
568
- findings = findings.filter((f) => statefulIds.has(f.findingId));
571
+ const issuesData = await client.getIssues({ state });
572
+ const issueIds = new Set(issuesData.issues.map((i) => i.issueId));
573
+ findings = findings.filter((f) => issueIds.has(f.findingId));
569
574
  }
570
575
  if (findings.length === 0) {
571
576
  return { content: [{ type: "text", text: "No findings detected. The application looks healthy." }] };
@@ -744,20 +749,21 @@ var init_verify_fix = __esm({
744
749
  }
745
750
  if (findingId) {
746
751
  const data = await client.getFindings();
747
- const finding = data.findings.find((f) => f.findingId === findingId);
752
+ const finding = data.findings.find((f) => f.issueId === findingId);
748
753
  if (!finding) {
749
754
  return {
750
755
  content: [{
751
756
  type: "text",
752
757
  text: `Finding ${findingId} not found. It may have already been resolved and cleaned up.`
753
- }]
758
+ }],
759
+ isError: true
754
760
  };
755
761
  }
756
762
  if (finding.state === "resolved") {
757
763
  return {
758
764
  content: [{
759
765
  type: "text",
760
- text: `RESOLVED: "${finding.finding.title}" on ${finding.finding.endpoint} is no longer detected. The fix worked.`
766
+ text: `RESOLVED: "${finding.issue.title}" on ${finding.issue.endpoint ?? "global"} is no longer detected. The fix worked.`
761
767
  }]
762
768
  };
763
769
  }
@@ -765,12 +771,12 @@ var init_verify_fix = __esm({
765
771
  content: [{
766
772
  type: "text",
767
773
  text: [
768
- `STILL PRESENT: "${finding.finding.title}" on ${finding.finding.endpoint}`,
774
+ `STILL PRESENT: "${finding.issue.title}" on ${finding.issue.endpoint ?? "global"}`,
769
775
  ` State: ${finding.state}`,
770
776
  ` Last seen: ${new Date(finding.lastSeenAt).toISOString()}`,
771
777
  ` Occurrences: ${finding.occurrences}`,
772
- ` Issue: ${finding.finding.desc}`,
773
- ` Hint: ${finding.finding.hint}`,
778
+ ` Issue: ${finding.issue.desc}`,
779
+ ` Hint: ${finding.issue.hint}`,
774
780
  "",
775
781
  "Make sure the user has triggered the endpoint again after the fix, so Brakit can re-analyze."
776
782
  ].join("\n")
@@ -780,7 +786,7 @@ var init_verify_fix = __esm({
780
786
  if (endpoint) {
781
787
  const data = await client.getFindings();
782
788
  const endpointFindings = data.findings.filter(
783
- (f) => f.finding.endpoint === endpoint || f.finding.endpoint.endsWith(` ${endpoint}`)
789
+ (f) => f.issue.endpoint === endpoint || f.issue.endpoint && f.issue.endpoint.endsWith(` ${endpoint}`)
784
790
  );
785
791
  if (endpointFindings.length === 0) {
786
792
  return {
@@ -790,7 +796,7 @@ var init_verify_fix = __esm({
790
796
  }]
791
797
  };
792
798
  }
793
- const open = endpointFindings.filter((f) => f.state === "open");
799
+ const open = endpointFindings.filter((f) => f.state === "open" || f.state === "regressed");
794
800
  const resolved = endpointFindings.filter((f) => f.state === "resolved");
795
801
  const lines = [
796
802
  `Endpoint: ${endpoint}`,
@@ -799,10 +805,10 @@ var init_verify_fix = __esm({
799
805
  ""
800
806
  ];
801
807
  for (const f of open) {
802
- lines.push(` [${f.finding.severity}] ${f.finding.title}: ${f.finding.desc}`);
808
+ lines.push(` [${f.issue.severity}] ${f.issue.title}: ${f.issue.desc}`);
803
809
  }
804
810
  for (const f of resolved) {
805
- lines.push(` [resolved] ${f.finding.title}`);
811
+ lines.push(` [resolved] ${f.issue.title}`);
806
812
  }
807
813
  return { content: [{ type: "text", text: lines.join("\n") }] };
808
814
  }
@@ -810,7 +816,8 @@ var init_verify_fix = __esm({
810
816
  content: [{
811
817
  type: "text",
812
818
  text: "Please provide either a finding_id or an endpoint to verify."
813
- }]
819
+ }],
820
+ isError: true
814
821
  };
815
822
  }
816
823
  };
@@ -831,51 +838,52 @@ var init_get_report = __esm({
831
838
  properties: {}
832
839
  },
833
840
  async handler(client, _args) {
834
- const [findingsData, securityData, insightsData, metricsData] = await Promise.all([
835
- client.getFindings(),
836
- client.getSecurityFindings(),
837
- client.getInsights(),
841
+ const [issuesData, metricsData] = await Promise.all([
842
+ client.getIssues(),
838
843
  client.getLiveMetrics()
839
844
  ]);
840
- const findings = findingsData.findings;
841
- const open = findings.filter((f) => f.state === "open");
842
- const resolved = findings.filter((f) => f.state === "resolved");
843
- const fixing = findings.filter((f) => f.state === "fixing");
844
- const criticalOpen = open.filter((f) => f.finding.severity === "critical");
845
- const warningOpen = open.filter((f) => f.finding.severity === "warning");
845
+ const issues = issuesData.issues;
846
+ const open = issues.filter((f) => f.state === "open" || f.state === "regressed");
847
+ const resolved = issues.filter((f) => f.state === "resolved");
848
+ const fixing = issues.filter((f) => f.state === "fixing");
849
+ const stale = issues.filter((f) => f.state === "stale");
850
+ const criticalOpen = open.filter((f) => f.issue.severity === "critical");
851
+ const warningOpen = open.filter((f) => f.issue.severity === "warning");
852
+ const securityIssues = issues.filter((f) => f.category === "security");
853
+ const perfIssues = issues.filter((f) => f.category === "performance");
846
854
  const totalRequests = metricsData.endpoints.reduce(
847
855
  (s, ep) => s + ep.summary.totalRequests,
848
856
  0
849
857
  );
850
- const openInsightCount = insightsData.insights.filter((si) => si.state === "open").length;
851
858
  const lines = [
852
859
  "=== Brakit Report ===",
853
860
  "",
854
861
  `Endpoints observed: ${metricsData.endpoints.length}`,
855
862
  `Total requests captured: ${totalRequests}`,
856
- `Active security rules: ${securityData.findings.length} finding(s)`,
857
- `Performance insights: ${openInsightCount} open, ${insightsData.insights.length - openInsightCount} resolved`,
863
+ `Security issues: ${securityIssues.length}`,
864
+ `Performance issues: ${perfIssues.length}`,
858
865
  "",
859
- "--- Finding Summary ---",
860
- `Total: ${findings.length}`,
866
+ "--- Issue Summary ---",
867
+ `Total: ${issues.length}`,
861
868
  ` Open: ${open.length} (${criticalOpen.length} critical, ${warningOpen.length} warning)`,
862
869
  ` In progress: ${fixing.length}`,
863
- ` Resolved: ${resolved.length}`
870
+ ` Resolved: ${resolved.length}`,
871
+ ` Stale: ${stale.length}`
864
872
  ];
865
873
  if (criticalOpen.length > 0) {
866
874
  lines.push("");
867
875
  lines.push("--- Critical Issues (fix first) ---");
868
876
  for (const f of criticalOpen) {
869
- lines.push(` [CRITICAL] ${f.finding.title} \u2014 ${f.finding.endpoint}`);
870
- lines.push(` ${f.finding.desc}`);
871
- lines.push(` Fix: ${f.finding.hint}`);
877
+ lines.push(` [CRITICAL] ${f.issue.title} \u2014 ${f.issue.endpoint ?? "global"}`);
878
+ lines.push(` ${f.issue.desc}`);
879
+ lines.push(` Fix: ${f.issue.hint}`);
872
880
  }
873
881
  }
874
882
  if (resolved.length > 0) {
875
883
  lines.push("");
876
884
  lines.push("--- Recently Resolved ---");
877
885
  for (const f of resolved.slice(0, MAX_RESOLVED_DISPLAY)) {
878
- lines.push(` \u2713 ${f.finding.title} \u2014 ${f.finding.endpoint}`);
886
+ lines.push(` \u2713 ${f.issue.title} \u2014 ${f.issue.endpoint ?? "global"}`);
879
887
  }
880
888
  if (resolved.length > MAX_RESOLVED_DISPLAY) {
881
889
  lines.push(` ... and ${resolved.length - MAX_RESOLVED_DISPLAY} more`);
@@ -1128,21 +1136,31 @@ import { runMain } from "citty";
1128
1136
 
1129
1137
  // src/cli/commands/install.ts
1130
1138
  import { defineCommand } from "citty";
1131
- import { resolve as resolve3, join as join2, dirname } from "path";
1139
+ import { resolve as resolve3, join as join3, dirname } from "path";
1132
1140
  import { readFile as readFile4, writeFile as writeFile3 } from "fs/promises";
1133
1141
  import { execSync } from "child_process";
1134
1142
  import { existsSync as existsSync5 } from "fs";
1135
1143
  import pc from "picocolors";
1136
1144
 
1137
- // src/store/finding-store.ts
1145
+ // src/store/issue-store.ts
1138
1146
  import { readFile as readFile2 } from "fs/promises";
1139
- import { readFileSync as readFileSync2, existsSync as existsSync3 } from "fs";
1147
+ import { readFileSync as readFileSync2, existsSync as existsSync3, unlinkSync } from "fs";
1140
1148
  import { resolve as resolve2 } from "path";
1141
1149
 
1142
1150
  // src/utils/fs.ts
1151
+ init_limits();
1152
+ init_log();
1153
+ init_type_guards();
1143
1154
  import { access, readFile, writeFile } from "fs/promises";
1144
1155
  import { existsSync, readFileSync, writeFileSync } from "fs";
1145
- import { resolve } from "path";
1156
+ import { createHash } from "crypto";
1157
+ import { homedir } from "os";
1158
+ import { resolve, join } from "path";
1159
+ function getProjectDataDir(projectRoot) {
1160
+ const absolute = resolve(projectRoot);
1161
+ const hash = createHash("sha256").update(absolute).digest("hex").slice(0, PROJECT_HASH_LENGTH);
1162
+ return join(homedir(), ".brakit", "projects", hash);
1163
+ }
1146
1164
  async function fileExists(path) {
1147
1165
  try {
1148
1166
  await access(path);
@@ -1152,8 +1170,10 @@ async function fileExists(path) {
1152
1170
  }
1153
1171
  }
1154
1172
 
1155
- // src/store/finding-store.ts
1156
- init_constants();
1173
+ // src/store/issue-store.ts
1174
+ init_metrics();
1175
+ init_limits();
1176
+ init_thresholds();
1157
1177
  init_limits();
1158
1178
 
1159
1179
  // src/utils/atomic-writer.ts
@@ -1167,14 +1187,18 @@ import { writeFile as writeFile2, mkdir, rename } from "fs/promises";
1167
1187
  init_log();
1168
1188
  init_type_guards();
1169
1189
 
1170
- // src/store/finding-store.ts
1190
+ // src/store/issue-store.ts
1171
1191
  init_log();
1172
- init_finding_id();
1192
+ init_type_guards();
1193
+
1194
+ // src/utils/issue-id.ts
1195
+ init_limits();
1196
+ import { createHash as createHash2 } from "crypto";
1173
1197
 
1174
1198
  // src/detect/project.ts
1175
1199
  import { readFile as readFile3, readdir } from "fs/promises";
1176
1200
  import { existsSync as existsSync4 } from "fs";
1177
- import { join, relative } from "path";
1201
+ import { join as join2, relative } from "path";
1178
1202
  var FRAMEWORKS = [
1179
1203
  { name: "nextjs", dep: "next", devCmd: "next dev", bin: "next", defaultPort: 3e3, devArgs: ["dev", "--port"] },
1180
1204
  { name: "remix", dep: "@remix-run/dev", devCmd: "remix dev", bin: "remix", defaultPort: 3e3, devArgs: ["dev"] },
@@ -1183,24 +1207,24 @@ var FRAMEWORKS = [
1183
1207
  { name: "astro", dep: "astro", devCmd: "astro dev", bin: "astro", defaultPort: 4321, devArgs: ["dev", "--port"] }
1184
1208
  ];
1185
1209
  async function detectProject(rootDir) {
1186
- const pkgPath = join(rootDir, "package.json");
1210
+ const pkgPath = join2(rootDir, "package.json");
1187
1211
  const raw = await readFile3(pkgPath, "utf-8");
1188
1212
  const pkg = JSON.parse(raw);
1189
1213
  const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
1190
1214
  const framework = detectFrameworkFromDeps(allDeps);
1191
1215
  const matched = FRAMEWORKS.find((f) => f.name === framework);
1192
1216
  const devCommand = matched?.devCmd ?? "";
1193
- const devBin = matched ? join(rootDir, "node_modules", ".bin", matched.bin) : "";
1217
+ const devBin = matched ? join2(rootDir, "node_modules", ".bin", matched.bin) : "";
1194
1218
  const defaultPort = matched?.defaultPort ?? 3e3;
1195
1219
  const packageManager = await detectPackageManager(rootDir);
1196
1220
  return { framework, devCommand, devBin, defaultPort, packageManager };
1197
1221
  }
1198
1222
  async function detectPackageManager(rootDir) {
1199
- if (await fileExists(join(rootDir, "bun.lockb"))) return "bun";
1200
- if (await fileExists(join(rootDir, "bun.lock"))) return "bun";
1201
- if (await fileExists(join(rootDir, "pnpm-lock.yaml"))) return "pnpm";
1202
- if (await fileExists(join(rootDir, "yarn.lock"))) return "yarn";
1203
- if (await fileExists(join(rootDir, "package-lock.json"))) return "npm";
1223
+ if (await fileExists(join2(rootDir, "bun.lockb"))) return "bun";
1224
+ if (await fileExists(join2(rootDir, "bun.lock"))) return "bun";
1225
+ if (await fileExists(join2(rootDir, "pnpm-lock.yaml"))) return "pnpm";
1226
+ if (await fileExists(join2(rootDir, "yarn.lock"))) return "yarn";
1227
+ if (await fileExists(join2(rootDir, "package-lock.json"))) return "npm";
1204
1228
  return "unknown";
1205
1229
  }
1206
1230
  function detectFrameworkFromDeps(allDeps) {
@@ -1231,9 +1255,9 @@ var PYTHON_DEFAULT_PORTS = {
1231
1255
  unknown: 8e3
1232
1256
  };
1233
1257
  async function detectPythonProject(rootDir) {
1234
- const hasPyproject = await fileExists(join(rootDir, "pyproject.toml"));
1235
- const hasRequirements = await fileExists(join(rootDir, "requirements.txt"));
1236
- const hasSetupPy = await fileExists(join(rootDir, "setup.py"));
1258
+ const hasPyproject = await fileExists(join2(rootDir, "pyproject.toml"));
1259
+ const hasRequirements = await fileExists(join2(rootDir, "requirements.txt"));
1260
+ const hasSetupPy = await fileExists(join2(rootDir, "setup.py"));
1237
1261
  if (!hasPyproject && !hasRequirements && !hasSetupPy) return null;
1238
1262
  const framework = await detectPythonFramework(rootDir, hasPyproject, hasRequirements);
1239
1263
  const packageManager = await detectPythonPackageManager(rootDir);
@@ -1248,7 +1272,7 @@ async function detectPythonProject(rootDir) {
1248
1272
  async function detectPythonFramework(rootDir, hasPyproject, hasRequirements) {
1249
1273
  if (hasPyproject) {
1250
1274
  try {
1251
- const content = await readFile3(join(rootDir, "pyproject.toml"), "utf-8");
1275
+ const content = await readFile3(join2(rootDir, "pyproject.toml"), "utf-8");
1252
1276
  for (const [dep, fw] of Object.entries(PYTHON_FRAMEWORK_MAP)) {
1253
1277
  if (content.includes(`"${dep}"`) || content.includes(`'${dep}'`) || content.includes(`${dep} `)) {
1254
1278
  return fw;
@@ -1259,7 +1283,7 @@ async function detectPythonFramework(rootDir, hasPyproject, hasRequirements) {
1259
1283
  }
1260
1284
  if (hasRequirements) {
1261
1285
  try {
1262
- const content = await readFile3(join(rootDir, "requirements.txt"), "utf-8");
1286
+ const content = await readFile3(join2(rootDir, "requirements.txt"), "utf-8");
1263
1287
  const lines = content.toLowerCase().split("\n");
1264
1288
  for (const [dep, fw] of Object.entries(PYTHON_FRAMEWORK_MAP)) {
1265
1289
  if (lines.some((l) => l.startsWith(dep) && (l.length === dep.length || /[=<>~![]/u.test(l[dep.length])))) {
@@ -1272,13 +1296,13 @@ async function detectPythonFramework(rootDir, hasPyproject, hasRequirements) {
1272
1296
  return "unknown";
1273
1297
  }
1274
1298
  async function detectPythonPackageManager(rootDir) {
1275
- if (await fileExists(join(rootDir, "uv.lock"))) return "uv";
1276
- if (await fileExists(join(rootDir, "poetry.lock"))) return "poetry";
1277
- if (await fileExists(join(rootDir, "Pipfile.lock"))) return "pipenv";
1278
- if (await fileExists(join(rootDir, "Pipfile"))) return "pipenv";
1279
- if (await fileExists(join(rootDir, "requirements.txt"))) return "pip";
1299
+ if (await fileExists(join2(rootDir, "uv.lock"))) return "uv";
1300
+ if (await fileExists(join2(rootDir, "poetry.lock"))) return "poetry";
1301
+ if (await fileExists(join2(rootDir, "Pipfile.lock"))) return "pipenv";
1302
+ if (await fileExists(join2(rootDir, "Pipfile"))) return "pipenv";
1303
+ if (await fileExists(join2(rootDir, "requirements.txt"))) return "pip";
1280
1304
  try {
1281
- const content = await readFile3(join(rootDir, "pyproject.toml"), "utf-8");
1305
+ const content = await readFile3(join2(rootDir, "pyproject.toml"), "utf-8");
1282
1306
  if (content.includes("[tool.poetry]")) return "poetry";
1283
1307
  if (content.includes("[tool.uv]")) return "uv";
1284
1308
  } catch {
@@ -1287,7 +1311,7 @@ async function detectPythonPackageManager(rootDir) {
1287
1311
  }
1288
1312
  async function detectPythonEntry(rootDir) {
1289
1313
  for (const candidate of PYTHON_ENTRY_CANDIDATES) {
1290
- if (await fileExists(join(rootDir, candidate))) {
1314
+ if (await fileExists(join2(rootDir, candidate))) {
1291
1315
  return candidate;
1292
1316
  }
1293
1317
  }
@@ -1315,7 +1339,7 @@ async function scanForProjects(rootDir) {
1315
1339
  const entries = await readdir(rootDir, { withFileTypes: true });
1316
1340
  for (const entry of entries) {
1317
1341
  if (!entry.isDirectory() || SKIP_DIRS.has(entry.name) || entry.name.startsWith(".")) continue;
1318
- const childDir = join(rootDir, entry.name);
1342
+ const childDir = join2(rootDir, entry.name);
1319
1343
  await detectInDir(childDir, rootDir, projects);
1320
1344
  }
1321
1345
  } catch {
@@ -1324,7 +1348,7 @@ async function scanForProjects(rootDir) {
1324
1348
  }
1325
1349
  async function detectInDir(dir, rootDir, projects) {
1326
1350
  const rel = dir === rootDir ? "." : `./${relative(rootDir, dir)}`;
1327
- if (await fileExists(join(dir, "package.json"))) {
1351
+ if (await fileExists(join2(dir, "package.json"))) {
1328
1352
  try {
1329
1353
  const node = await detectProject(dir);
1330
1354
  projects.push({ dir, relDir: rel, type: "node", node });
@@ -1337,6 +1361,31 @@ async function detectInDir(dir, rootDir, projects) {
1337
1361
  }
1338
1362
  }
1339
1363
 
1364
+ // src/utils/response.ts
1365
+ init_thresholds();
1366
+ function unwrapResponse(parsed) {
1367
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return parsed;
1368
+ const obj = parsed;
1369
+ const keys = Object.keys(obj);
1370
+ if (keys.length > 3) return parsed;
1371
+ let best = null;
1372
+ let bestSize = 0;
1373
+ for (const key of keys) {
1374
+ const val = obj[key];
1375
+ if (Array.isArray(val) && val.length > bestSize) {
1376
+ best = val;
1377
+ bestSize = val.length;
1378
+ } else if (val && typeof val === "object" && !Array.isArray(val)) {
1379
+ const size = Object.keys(val).length;
1380
+ if (size > bestSize) {
1381
+ best = val;
1382
+ bestSize = size;
1383
+ }
1384
+ }
1385
+ }
1386
+ return best && bestSize >= OVERFETCH_UNWRAP_MIN_SIZE ? best : parsed;
1387
+ }
1388
+
1340
1389
  // src/analysis/rules/patterns.ts
1341
1390
  var SECRET_KEYS = /^(password|passwd|secret|api_key|apiKey|api_secret|apiSecret|private_key|privateKey|client_secret|clientSecret)$/;
1342
1391
  var TOKEN_PARAMS = /^(token|api_key|apiKey|secret|password|access_token|session_id|sessionId)$/;
@@ -1362,30 +1411,34 @@ var RULE_HINTS = {
1362
1411
  };
1363
1412
 
1364
1413
  // src/analysis/rules/exposed-secret.ts
1365
- function tryParseJson(body) {
1366
- if (!body) return null;
1367
- try {
1368
- return JSON.parse(body);
1369
- } catch {
1370
- return null;
1371
- }
1414
+ init_limits();
1415
+
1416
+ // src/utils/http-status.ts
1417
+ function isErrorStatus(code) {
1418
+ return code >= 400;
1419
+ }
1420
+ function isRedirect(code) {
1421
+ return code >= 300 && code < 400;
1372
1422
  }
1373
- function findSecretKeys(obj, prefix) {
1423
+
1424
+ // src/analysis/rules/exposed-secret.ts
1425
+ function findSecretKeys(obj, prefix, depth = 0) {
1374
1426
  const found = [];
1427
+ if (depth >= MAX_OBJECT_SCAN_DEPTH) return found;
1375
1428
  if (!obj || typeof obj !== "object") return found;
1376
1429
  if (Array.isArray(obj)) {
1377
- for (let i = 0; i < Math.min(obj.length, 5); i++) {
1378
- found.push(...findSecretKeys(obj[i], prefix));
1430
+ for (let i = 0; i < Math.min(obj.length, SECRET_SCAN_ARRAY_LIMIT); i++) {
1431
+ found.push(...findSecretKeys(obj[i], prefix, depth + 1));
1379
1432
  }
1380
1433
  return found;
1381
1434
  }
1382
1435
  for (const k of Object.keys(obj)) {
1383
1436
  const val = obj[k];
1384
- if (SECRET_KEYS.test(k) && typeof val === "string" && val.length >= 8 && !MASKED_RE.test(val)) {
1437
+ if (SECRET_KEYS.test(k) && typeof val === "string" && val.length >= MIN_SECRET_VALUE_LENGTH && !MASKED_RE.test(val)) {
1385
1438
  found.push(k);
1386
1439
  }
1387
1440
  if (typeof val === "object" && val !== null) {
1388
- found.push(...findSecretKeys(val, prefix + k + "."));
1441
+ found.push(...findSecretKeys(val, prefix + k + ".", depth + 1));
1389
1442
  }
1390
1443
  }
1391
1444
  return found;
@@ -1399,8 +1452,8 @@ var exposedSecretRule = {
1399
1452
  const findings = [];
1400
1453
  const seen = /* @__PURE__ */ new Map();
1401
1454
  for (const r of ctx.requests) {
1402
- if (r.statusCode >= 400) continue;
1403
- const parsed = tryParseJson(r.responseBody);
1455
+ if (isErrorStatus(r.statusCode)) continue;
1456
+ const parsed = ctx.parsedBodies.response.get(r.id);
1404
1457
  if (!parsed) continue;
1405
1458
  const keys = findSecretKeys(parsed, "");
1406
1459
  if (keys.length === 0) continue;
@@ -1553,7 +1606,7 @@ var errorInfoLeakRule = {
1553
1606
 
1554
1607
  // src/analysis/rules/insecure-cookie.ts
1555
1608
  function isFrameworkResponse(r) {
1556
- if (r.statusCode >= 300 && r.statusCode < 400) return true;
1609
+ if (isRedirect(r.statusCode)) return true;
1557
1610
  if (r.path?.startsWith("/__")) return true;
1558
1611
  if (r.responseHeaders?.["x-middleware-rewrite"]) return true;
1559
1612
  return false;
@@ -1659,49 +1712,16 @@ var corsCredentialsRule = {
1659
1712
  }
1660
1713
  };
1661
1714
 
1662
- // src/utils/response.ts
1663
- init_thresholds();
1664
- function unwrapResponse(parsed) {
1665
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return parsed;
1666
- const obj = parsed;
1667
- const keys = Object.keys(obj);
1668
- if (keys.length > 3) return parsed;
1669
- let best = null;
1670
- let bestSize = 0;
1671
- for (const key of keys) {
1672
- const val = obj[key];
1673
- if (Array.isArray(val) && val.length > bestSize) {
1674
- best = val;
1675
- bestSize = val.length;
1676
- } else if (val && typeof val === "object" && !Array.isArray(val)) {
1677
- const size = Object.keys(val).length;
1678
- if (size > bestSize) {
1679
- best = val;
1680
- bestSize = size;
1681
- }
1682
- }
1683
- }
1684
- return best && bestSize >= OVERFETCH_UNWRAP_MIN_SIZE ? best : parsed;
1685
- }
1686
-
1687
1715
  // src/analysis/rules/response-pii-leak.ts
1716
+ init_limits();
1688
1717
  var WRITE_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH"]);
1689
- var FULL_RECORD_MIN_FIELDS = 5;
1690
- var LIST_PII_MIN_ITEMS = 2;
1691
- function tryParseJson2(body) {
1692
- if (!body) return null;
1693
- try {
1694
- return JSON.parse(body);
1695
- } catch {
1696
- return null;
1697
- }
1698
- }
1699
- function findEmails(obj) {
1718
+ function findEmails(obj, depth = 0) {
1700
1719
  const emails = [];
1720
+ if (depth >= MAX_OBJECT_SCAN_DEPTH) return emails;
1701
1721
  if (!obj || typeof obj !== "object") return emails;
1702
1722
  if (Array.isArray(obj)) {
1703
- for (let i = 0; i < Math.min(obj.length, 10); i++) {
1704
- emails.push(...findEmails(obj[i]));
1723
+ for (let i = 0; i < Math.min(obj.length, PII_SCAN_ARRAY_LIMIT); i++) {
1724
+ emails.push(...findEmails(obj[i], depth + 1));
1705
1725
  }
1706
1726
  return emails;
1707
1727
  }
@@ -1709,7 +1729,7 @@ function findEmails(obj) {
1709
1729
  if (typeof v === "string" && EMAIL_RE.test(v)) {
1710
1730
  emails.push(v);
1711
1731
  } else if (typeof v === "object" && v !== null) {
1712
- emails.push(...findEmails(v));
1732
+ emails.push(...findEmails(v, depth + 1));
1713
1733
  }
1714
1734
  }
1715
1735
  return emails;
@@ -1752,7 +1772,7 @@ function detectFullRecordPII(target) {
1752
1772
  function detectListPII(target) {
1753
1773
  if (!Array.isArray(target) || target.length < LIST_PII_MIN_ITEMS) return null;
1754
1774
  let itemsWithEmail = 0;
1755
- for (let i = 0; i < Math.min(target.length, 10); i++) {
1775
+ for (let i = 0; i < Math.min(target.length, PII_SCAN_ARRAY_LIMIT); i++) {
1756
1776
  const item = target[i];
1757
1777
  if (item && typeof item === "object" && findEmails(item).length > 0) {
1758
1778
  itemsWithEmail++;
@@ -1783,10 +1803,10 @@ var responsePiiLeakRule = {
1783
1803
  const findings = [];
1784
1804
  const seen = /* @__PURE__ */ new Map();
1785
1805
  for (const r of ctx.requests) {
1786
- if (r.statusCode >= 400) continue;
1787
- const resJson = tryParseJson2(r.responseBody);
1806
+ if (isErrorStatus(r.statusCode)) continue;
1807
+ const resJson = ctx.parsedBodies.response.get(r.id);
1788
1808
  if (!resJson) continue;
1789
- const reqJson = tryParseJson2(r.requestBody);
1809
+ const reqJson = ctx.parsedBodies.request.get(r.id) ?? null;
1790
1810
  const detection = detectPII(r.method, reqJson, resJson);
1791
1811
  if (!detection) continue;
1792
1812
  const ep = `${r.method} ${r.path}`;
@@ -1870,13 +1890,11 @@ init_constants();
1870
1890
  // src/analysis/insights/rules/regression.ts
1871
1891
  init_constants();
1872
1892
 
1873
- // src/analysis/insight-tracker.ts
1893
+ // src/analysis/issue-mappers.ts
1874
1894
  init_endpoint();
1875
- init_finding_id();
1876
- init_thresholds();
1877
1895
 
1878
1896
  // src/index.ts
1879
- var VERSION = "0.8.5";
1897
+ var VERSION = "0.8.6";
1880
1898
 
1881
1899
  // src/cli/commands/install.ts
1882
1900
  init_constants();
@@ -1884,10 +1902,28 @@ init_constants();
1884
1902
  // src/cli/templates.ts
1885
1903
  var IMPORT_LINE = `import "brakit";`;
1886
1904
  var IMPORT_MARKER = "brakit";
1905
+ var BRAKIT_IMPORT_PATTERNS = [
1906
+ 'import("brakit")',
1907
+ 'import "brakit"',
1908
+ "import 'brakit'",
1909
+ 'require("brakit")',
1910
+ "require('brakit')"
1911
+ ];
1912
+ function containsBrakitImport(content) {
1913
+ return BRAKIT_IMPORT_PATTERNS.some((p) => content.includes(p));
1914
+ }
1915
+ function removeBrakitImportLines(lines) {
1916
+ return lines.filter(
1917
+ (line) => !BRAKIT_IMPORT_PATTERNS.some((p) => line.includes(p))
1918
+ );
1919
+ }
1887
1920
  var CREATED_FILES = [
1888
1921
  "src/instrumentation.ts",
1922
+ "src/instrumentation.js",
1889
1923
  "instrumentation.ts",
1890
- "server/plugins/brakit.ts"
1924
+ "instrumentation.js",
1925
+ "server/plugins/brakit.ts",
1926
+ "server/plugins/brakit.js"
1891
1927
  ];
1892
1928
  var ENTRY_CANDIDATES = [
1893
1929
  "src/index.ts",
@@ -2012,7 +2048,7 @@ var install_default = defineCommand({
2012
2048
  }
2013
2049
  });
2014
2050
  async function installPackage(rootDir, pm) {
2015
- const pkgRaw = await readFile4(join2(rootDir, "package.json"), "utf-8");
2051
+ const pkgRaw = await readFile4(join3(rootDir, "package.json"), "utf-8");
2016
2052
  const pkg = JSON.parse(pkgRaw);
2017
2053
  const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
2018
2054
  if (allDeps["brakit"]) return false;
@@ -2044,9 +2080,9 @@ async function setupInstrumentation(rootDir, framework) {
2044
2080
  }
2045
2081
  }
2046
2082
  async function setupNextjs(rootDir) {
2047
- const hasSrc = await fileExists(join2(rootDir, "src"));
2083
+ const hasSrc = await fileExists(join3(rootDir, "src"));
2048
2084
  const relPath = hasSrc ? "src/instrumentation.ts" : "instrumentation.ts";
2049
- const absPath = join2(rootDir, relPath);
2085
+ const absPath = join3(rootDir, relPath);
2050
2086
  if (await fileExists(absPath)) {
2051
2087
  const content2 = await readFile4(absPath, "utf-8");
2052
2088
  if (content2.includes(IMPORT_MARKER)) {
@@ -2060,7 +2096,7 @@ async function setupNextjs(rootDir) {
2060
2096
  }
2061
2097
  async function setupNuxt(rootDir) {
2062
2098
  const relPath = "server/plugins/brakit.ts";
2063
- const absPath = join2(rootDir, relPath);
2099
+ const absPath = join3(rootDir, relPath);
2064
2100
  if (await fileExists(absPath)) {
2065
2101
  const content2 = await readFile4(absPath, "utf-8");
2066
2102
  if (content2.includes(IMPORT_MARKER)) {
@@ -2069,7 +2105,7 @@ async function setupNuxt(rootDir) {
2069
2105
  return { action: "manual", file: relPath };
2070
2106
  }
2071
2107
  const content = BRAKIT_TEMPLATES.nuxt + "\n";
2072
- const dir = join2(rootDir, "server/plugins");
2108
+ const dir = join3(rootDir, "server/plugins");
2073
2109
  const { mkdirSync: mkdirSync3 } = await import("fs");
2074
2110
  mkdirSync3(dir, { recursive: true });
2075
2111
  await writeFile3(absPath, content);
@@ -2077,7 +2113,7 @@ async function setupNuxt(rootDir) {
2077
2113
  }
2078
2114
  async function setupPrepend(rootDir, ...candidates) {
2079
2115
  for (const relPath of candidates) {
2080
- const absPath = join2(rootDir, relPath);
2116
+ const absPath = join3(rootDir, relPath);
2081
2117
  if (!await fileExists(absPath)) continue;
2082
2118
  const content = await readFile4(absPath, "utf-8");
2083
2119
  if (content.includes(IMPORT_MARKER)) {
@@ -2091,7 +2127,7 @@ ${content}`);
2091
2127
  }
2092
2128
  async function setupGeneric(rootDir) {
2093
2129
  try {
2094
- const pkgRaw = await readFile4(join2(rootDir, "package.json"), "utf-8");
2130
+ const pkgRaw = await readFile4(join3(rootDir, "package.json"), "utf-8");
2095
2131
  const pkg = JSON.parse(pkgRaw);
2096
2132
  if (pkg.main && typeof pkg.main === "string") {
2097
2133
  const result2 = await setupPrepend(rootDir, pkg.main);
@@ -2112,7 +2148,7 @@ var MCP_CONFIG = {
2112
2148
  }
2113
2149
  };
2114
2150
  async function setupMcp(rootDir, config = MCP_CONFIG) {
2115
- const mcpPath = join2(rootDir, ".mcp.json");
2151
+ const mcpPath = join3(rootDir, ".mcp.json");
2116
2152
  if (await fileExists(mcpPath)) {
2117
2153
  const raw = await readFile4(mcpPath, "utf-8");
2118
2154
  try {
@@ -2130,7 +2166,7 @@ async function setupMcp(rootDir, config = MCP_CONFIG) {
2130
2166
  return "created";
2131
2167
  }
2132
2168
  async function ensureGitignoreEntry(rootDir, entry) {
2133
- const gitignorePath = join2(rootDir, ".gitignore");
2169
+ const gitignorePath = join3(rootDir, ".gitignore");
2134
2170
  try {
2135
2171
  if (await fileExists(gitignorePath)) {
2136
2172
  const content = await readFile4(gitignorePath, "utf-8");
@@ -2145,7 +2181,7 @@ async function ensureGitignoreEntry(rootDir, entry) {
2145
2181
  function findGitRoot(startDir) {
2146
2182
  let dir = resolve3(startDir);
2147
2183
  while (true) {
2148
- if (existsSync5(join2(dir, ".git"))) return dir;
2184
+ if (existsSync5(join3(dir, ".git"))) return dir;
2149
2185
  const parent = dirname(dir);
2150
2186
  if (parent === dir) return null;
2151
2187
  dir = parent;
@@ -2170,11 +2206,13 @@ function printManualInstructions(framework) {
2170
2206
 
2171
2207
  // src/cli/commands/uninstall.ts
2172
2208
  import { defineCommand as defineCommand2 } from "citty";
2173
- import { resolve as resolve4, join as join3 } from "path";
2174
- import { readFile as readFile5, writeFile as writeFile4, unlink, rm } from "fs/promises";
2209
+ import { resolve as resolve4, join as join4, relative as relative2 } from "path";
2210
+ import { readFile as readFile5, writeFile as writeFile4, unlink, rm, readdir as readdir2 } from "fs/promises";
2175
2211
  import { execSync as execSync2 } from "child_process";
2176
2212
  import pc2 from "picocolors";
2177
2213
  init_constants();
2214
+ init_log();
2215
+ init_type_guards();
2178
2216
  var PREPENDED_FILES = [
2179
2217
  "app/entry.server.tsx",
2180
2218
  "app/entry.server.ts",
@@ -2196,82 +2234,139 @@ var uninstall_default = defineCommand2({
2196
2234
  },
2197
2235
  async run({ args }) {
2198
2236
  const rootDir = resolve4(args.dir);
2199
- let project = null;
2237
+ let projects = [];
2200
2238
  try {
2201
- project = await detectProject(rootDir);
2202
- } catch {
2239
+ const scanned = await scanForProjects(rootDir);
2240
+ projects = scanned.filter((p) => p.type === "node" && p.node).map((p) => ({ dir: p.dir, pm: p.node.packageManager }));
2241
+ } catch (err) {
2242
+ brakitDebug(`uninstall: project scan failed: ${getErrorMessage(err)}`);
2243
+ }
2244
+ if (projects.length === 0) {
2245
+ projects = [{ dir: rootDir, pm: "npm" }];
2203
2246
  }
2204
2247
  console.log();
2205
2248
  console.log(pc2.bold(" \u25C6 brakit uninstall"));
2206
2249
  console.log();
2207
- let removed = false;
2208
- for (const relPath of CREATED_FILES) {
2209
- const absPath = join3(rootDir, relPath);
2210
- if (!await fileExists(absPath)) continue;
2211
- const content = await readFile5(absPath, "utf-8");
2212
- if (!content.includes("brakit")) continue;
2213
- if (isExactBrakitTemplate(content)) {
2214
- await unlink(absPath);
2215
- console.log(pc2.green(` \u2713 Removed ${relPath}`));
2216
- removed = true;
2217
- break;
2218
- }
2219
- const lines = content.split("\n");
2220
- const cleaned = lines.filter(
2221
- (line) => !line.includes('import("brakit")') && !line.includes('import "brakit"')
2222
- );
2223
- if (cleaned.length < lines.length) {
2224
- await writeFile4(absPath, cleaned.join("\n"));
2225
- console.log(pc2.green(` \u2713 Removed brakit lines from ${relPath}`));
2226
- removed = true;
2227
- break;
2228
- }
2229
- }
2230
- if (!removed) {
2231
- const candidates = [...PREPENDED_FILES];
2232
- try {
2233
- const pkgRaw = await readFile5(join3(rootDir, "package.json"), "utf-8");
2234
- const pkg = JSON.parse(pkgRaw);
2235
- if (pkg.main) candidates.unshift(pkg.main);
2236
- } catch {
2250
+ for (const project of projects) {
2251
+ const suffix = projects.length > 1 ? ` in ${relative2(rootDir, project.dir) || "."}` : "";
2252
+ const removed = await removeInstrumentation(project.dir);
2253
+ if (removed) {
2254
+ console.log(pc2.green(` \u2713 ${removed}${suffix}`));
2255
+ } else {
2256
+ console.log(pc2.dim(` No brakit instrumentation files found${suffix}.`));
2237
2257
  }
2238
- for (const relPath of candidates) {
2239
- const absPath = join3(rootDir, relPath);
2240
- if (!await fileExists(absPath)) continue;
2241
- const content = await readFile5(absPath, "utf-8");
2242
- if (!content.includes(IMPORT_LINE)) continue;
2243
- const updated = content.split("\n").filter((line) => line.trim() !== IMPORT_LINE.trim()).join("\n");
2244
- await writeFile4(absPath, updated);
2245
- console.log(pc2.green(` \u2713 Removed brakit import from ${relPath}`));
2246
- removed = true;
2247
- break;
2258
+ const uninstalled = await uninstallPackage(project.dir, project.pm);
2259
+ if (uninstalled === true) {
2260
+ console.log(pc2.green(` \u2713 Removed brakit from devDependencies${suffix}`));
2261
+ } else if (uninstalled === "failed") {
2248
2262
  }
2249
2263
  }
2250
- if (!removed) {
2251
- console.log(pc2.dim(" No brakit instrumentation files found."));
2252
- }
2253
2264
  const mcpRemoved = await removeMcpConfig(rootDir);
2254
2265
  if (mcpRemoved) {
2255
2266
  console.log(pc2.green(" \u2713 Removed brakit MCP configuration"));
2256
2267
  }
2257
2268
  const dataRemoved = await removeBrakitData(rootDir);
2258
2269
  if (dataRemoved) {
2259
- console.log(pc2.green(" \u2713 Removed .brakit directory"));
2270
+ console.log(pc2.green(" \u2713 Removed .brakit data"));
2260
2271
  }
2261
2272
  const gitignoreCleaned = await cleanGitignore(rootDir);
2262
2273
  if (gitignoreCleaned) {
2263
2274
  console.log(pc2.green(" \u2713 Removed .brakit from .gitignore"));
2264
2275
  }
2265
- const pm = project?.packageManager ?? "npm";
2266
- const uninstalled = await uninstallPackage(rootDir, pm);
2267
- if (uninstalled) {
2268
- console.log(pc2.green(" \u2713 Removed brakit from devDependencies"));
2276
+ const cacheCleared = await clearBuildCaches(rootDir);
2277
+ if (cacheCleared) {
2278
+ console.log(pc2.green(" \u2713 Cleared build cache"));
2269
2279
  }
2270
2280
  console.log();
2271
2281
  }
2272
2282
  });
2283
+ async function removeInstrumentation(projectDir) {
2284
+ for (const relPath of CREATED_FILES) {
2285
+ const result2 = await tryRemoveBrakitFromFile(projectDir, relPath);
2286
+ if (result2) return result2;
2287
+ }
2288
+ const candidates = [...PREPENDED_FILES];
2289
+ try {
2290
+ const pkgRaw = await readFile5(join4(projectDir, "package.json"), "utf-8");
2291
+ const pkg = JSON.parse(pkgRaw);
2292
+ if (pkg.main) candidates.unshift(pkg.main);
2293
+ } catch (err) {
2294
+ brakitDebug(`uninstall: no package.json main: ${getErrorMessage(err)}`);
2295
+ }
2296
+ for (const relPath of candidates) {
2297
+ const result2 = await tryRemoveImportLine(projectDir, relPath);
2298
+ if (result2) return result2;
2299
+ }
2300
+ const result = await fallbackSearchAndRemove(projectDir);
2301
+ if (result) return result;
2302
+ return null;
2303
+ }
2304
+ async function tryRemoveBrakitFromFile(projectDir, relPath) {
2305
+ const absPath = join4(projectDir, relPath);
2306
+ if (!await fileExists(absPath)) return null;
2307
+ const content = await readFile5(absPath, "utf-8");
2308
+ if (!content.includes("brakit")) return null;
2309
+ if (isExactBrakitTemplate(content)) {
2310
+ await unlink(absPath);
2311
+ return `Removed ${relPath}`;
2312
+ }
2313
+ const lines = content.split("\n");
2314
+ const cleaned = removeBrakitImportLines(lines);
2315
+ if (cleaned.length < lines.length) {
2316
+ await writeFile4(absPath, cleaned.join("\n"));
2317
+ return `Removed brakit lines from ${relPath}`;
2318
+ }
2319
+ return null;
2320
+ }
2321
+ async function tryRemoveImportLine(projectDir, relPath) {
2322
+ const absPath = join4(projectDir, relPath);
2323
+ if (!await fileExists(absPath)) return null;
2324
+ const content = await readFile5(absPath, "utf-8");
2325
+ if (!content.includes(IMPORT_LINE)) return null;
2326
+ const updated = content.split("\n").filter((line) => line.trim() !== IMPORT_LINE.trim()).join("\n");
2327
+ await writeFile4(absPath, updated);
2328
+ return `Removed brakit import from ${relPath}`;
2329
+ }
2330
+ async function fallbackSearchAndRemove(projectDir) {
2331
+ const dirsToScan = FALLBACK_SCAN_DIRS;
2332
+ for (const dir of dirsToScan) {
2333
+ const absDir = join4(projectDir, dir);
2334
+ if (!await fileExists(absDir)) continue;
2335
+ let entries;
2336
+ try {
2337
+ entries = await readdir2(absDir);
2338
+ } catch (err) {
2339
+ brakitDebug(`uninstall: could not read ${absDir}: ${getErrorMessage(err)}`);
2340
+ continue;
2341
+ }
2342
+ for (const entry of entries) {
2343
+ const ext = entry.slice(entry.lastIndexOf("."));
2344
+ if (!SUPPORTED_SOURCE_EXTENSIONS.has(ext)) continue;
2345
+ const relPath = dir === "." ? entry : `${dir}/${entry}`;
2346
+ const absPath = join4(projectDir, relPath);
2347
+ try {
2348
+ const content = await readFile5(absPath, "utf-8");
2349
+ if (!containsBrakitImport(content)) continue;
2350
+ if (isExactBrakitTemplate(content)) {
2351
+ await unlink(absPath);
2352
+ return `Removed ${relPath}`;
2353
+ }
2354
+ const lines = content.split("\n");
2355
+ const cleaned = removeBrakitImportLines(lines);
2356
+ if (cleaned.length < lines.length) {
2357
+ await writeFile4(absPath, cleaned.join("\n"));
2358
+ return `Removed brakit import from ${relPath}`;
2359
+ }
2360
+ } catch (err) {
2361
+ brakitDebug(`uninstall: fallback scan failed for ${relPath}: ${getErrorMessage(err)}`);
2362
+ continue;
2363
+ }
2364
+ }
2365
+ }
2366
+ return null;
2367
+ }
2273
2368
  async function removeMcpConfig(rootDir) {
2274
- const mcpPath = join3(rootDir, ".mcp.json");
2369
+ const mcpPath = join4(rootDir, ".mcp.json");
2275
2370
  if (!await fileExists(mcpPath)) return false;
2276
2371
  try {
2277
2372
  const raw = await readFile5(mcpPath, "utf-8");
@@ -2284,16 +2379,18 @@ async function removeMcpConfig(rootDir) {
2284
2379
  await writeFile4(mcpPath, JSON.stringify(config, null, 2) + "\n");
2285
2380
  }
2286
2381
  return true;
2287
- } catch {
2382
+ } catch (err) {
2383
+ brakitDebug(`uninstall: MCP config cleanup failed: ${getErrorMessage(err)}`);
2288
2384
  return false;
2289
2385
  }
2290
2386
  }
2291
2387
  async function uninstallPackage(rootDir, pm) {
2292
2388
  try {
2293
- const pkgRaw = await readFile5(join3(rootDir, "package.json"), "utf-8");
2389
+ const pkgRaw = await readFile5(join4(rootDir, "package.json"), "utf-8");
2294
2390
  const pkg = JSON.parse(pkgRaw);
2295
2391
  if (!pkg.devDependencies?.brakit && !pkg.dependencies?.brakit) return false;
2296
- } catch {
2392
+ } catch (err) {
2393
+ brakitDebug(`uninstall: could not read package.json: ${getErrorMessage(err)}`);
2297
2394
  return false;
2298
2395
  }
2299
2396
  const cmds = {
@@ -2305,23 +2402,36 @@ async function uninstallPackage(rootDir, pm) {
2305
2402
  const cmd = cmds[pm] ?? cmds.npm;
2306
2403
  try {
2307
2404
  execSync2(cmd, { cwd: rootDir, stdio: "pipe" });
2405
+ return true;
2308
2406
  } catch {
2309
2407
  console.warn(pc2.yellow(` \u26A0 Failed to run "${cmd}". Remove brakit manually.`));
2408
+ return "failed";
2310
2409
  }
2311
- return true;
2312
2410
  }
2313
2411
  async function removeBrakitData(rootDir) {
2314
- const dataDir = join3(rootDir, METRICS_DIR);
2315
- if (!await fileExists(dataDir)) return false;
2316
- try {
2317
- await rm(dataDir, { recursive: true, force: true });
2318
- return true;
2319
- } catch {
2320
- return false;
2412
+ let removed = false;
2413
+ const projectDir = join4(rootDir, METRICS_DIR);
2414
+ if (await fileExists(projectDir)) {
2415
+ try {
2416
+ await rm(projectDir, { recursive: true, force: true });
2417
+ removed = true;
2418
+ } catch (err) {
2419
+ brakitDebug(`uninstall: could not remove ${projectDir}: ${getErrorMessage(err)}`);
2420
+ }
2421
+ }
2422
+ const homeDataDir = getProjectDataDir(rootDir);
2423
+ if (await fileExists(homeDataDir)) {
2424
+ try {
2425
+ await rm(homeDataDir, { recursive: true, force: true });
2426
+ removed = true;
2427
+ } catch (err) {
2428
+ brakitDebug(`uninstall: could not remove ${homeDataDir}: ${getErrorMessage(err)}`);
2429
+ }
2321
2430
  }
2431
+ return removed;
2322
2432
  }
2323
2433
  async function cleanGitignore(rootDir) {
2324
- const gitignorePath = join3(rootDir, ".gitignore");
2434
+ const gitignorePath = join4(rootDir, ".gitignore");
2325
2435
  if (!await fileExists(gitignorePath)) return false;
2326
2436
  try {
2327
2437
  const content = await readFile5(gitignorePath, "utf-8");
@@ -2330,10 +2440,25 @@ async function cleanGitignore(rootDir) {
2330
2440
  if (filtered.length === lines.length) return false;
2331
2441
  await writeFile4(gitignorePath, filtered.join("\n"));
2332
2442
  return true;
2333
- } catch {
2443
+ } catch (err) {
2444
+ brakitDebug(`uninstall: gitignore cleanup failed: ${getErrorMessage(err)}`);
2334
2445
  return false;
2335
2446
  }
2336
2447
  }
2448
+ async function clearBuildCaches(rootDir) {
2449
+ let cleared = false;
2450
+ for (const dir of BUILD_CACHE_DIRS) {
2451
+ const absDir = join4(rootDir, dir);
2452
+ if (!await fileExists(absDir)) continue;
2453
+ try {
2454
+ await rm(absDir, { recursive: true, force: true });
2455
+ cleared = true;
2456
+ } catch (err) {
2457
+ brakitDebug(`uninstall: could not clear cache ${absDir}: ${getErrorMessage(err)}`);
2458
+ }
2459
+ }
2460
+ return cleared;
2461
+ }
2337
2462
 
2338
2463
  // bin/brakit.ts
2339
2464
  var sub = process.argv[2];