brakit 0.8.4 → 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,46 +9,68 @@ var __export = (target, all) => {
9
9
  __defProp(target, name, { get: all[name], enumerable: true });
10
10
  };
11
11
 
12
- // src/constants/routes.ts
13
- var DASHBOARD_API_REQUESTS, DASHBOARD_API_CLEAR, DASHBOARD_API_FETCHES, DASHBOARD_API_ERRORS, DASHBOARD_API_QUERIES, DASHBOARD_API_ACTIVITY, DASHBOARD_API_METRICS_LIVE, DASHBOARD_API_INSIGHTS, DASHBOARD_API_SECURITY, DASHBOARD_API_FINDINGS;
14
- var init_routes = __esm({
15
- "src/constants/routes.ts"() {
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
16
  "use strict";
17
- DASHBOARD_API_REQUESTS = "/__brakit/api/requests";
18
- DASHBOARD_API_CLEAR = "/__brakit/api/clear";
19
- DASHBOARD_API_FETCHES = "/__brakit/api/fetches";
20
- DASHBOARD_API_ERRORS = "/__brakit/api/errors";
21
- DASHBOARD_API_QUERIES = "/__brakit/api/queries";
22
- DASHBOARD_API_ACTIVITY = "/__brakit/api/activity";
23
- DASHBOARD_API_METRICS_LIVE = "/__brakit/api/metrics/live";
24
- DASHBOARD_API_INSIGHTS = "/__brakit/api/insights";
25
- DASHBOARD_API_SECURITY = "/__brakit/api/security";
26
- DASHBOARD_API_FINDINGS = "/__brakit/api/findings";
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;
27
25
  }
28
26
  });
29
27
 
30
- // src/constants/limits.ts
31
- var MAX_INGEST_BYTES;
32
- var init_limits = __esm({
33
- "src/constants/limits.ts"() {
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"() {
34
38
  "use strict";
35
- MAX_INGEST_BYTES = 10 * 1024 * 1024;
39
+ PREFIX = "[brakit]";
36
40
  }
37
41
  });
38
42
 
39
- // src/constants/thresholds.ts
40
- var OVERFETCH_UNWRAP_MIN_SIZE;
41
- var init_thresholds = __esm({
42
- "src/constants/thresholds.ts"() {
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"() {
43
47
  "use strict";
44
- OVERFETCH_UNWRAP_MIN_SIZE = 3;
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"]);
45
51
  }
46
52
  });
47
53
 
48
- // src/constants/transport.ts
49
- var init_transport = __esm({
50
- "src/constants/transport.ts"() {
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"() {
51
71
  "use strict";
72
+ init_lifecycle();
73
+ init_limits();
52
74
  }
53
75
  });
54
76
 
@@ -62,6 +84,61 @@ var init_metrics = __esm({
62
84
  }
63
85
  });
64
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
+
97
+ // src/constants/routes.ts
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;
99
+ var init_routes = __esm({
100
+ "src/constants/routes.ts"() {
101
+ "use strict";
102
+ DASHBOARD_PREFIX = "/__brakit";
103
+ DASHBOARD_API_REQUESTS = `${DASHBOARD_PREFIX}/api/requests`;
104
+ DASHBOARD_API_EVENTS = `${DASHBOARD_PREFIX}/api/events`;
105
+ DASHBOARD_API_FLOWS = `${DASHBOARD_PREFIX}/api/flows`;
106
+ DASHBOARD_API_CLEAR = `${DASHBOARD_PREFIX}/api/clear`;
107
+ DASHBOARD_API_LOGS = `${DASHBOARD_PREFIX}/api/logs`;
108
+ DASHBOARD_API_FETCHES = `${DASHBOARD_PREFIX}/api/fetches`;
109
+ DASHBOARD_API_ERRORS = `${DASHBOARD_PREFIX}/api/errors`;
110
+ DASHBOARD_API_QUERIES = `${DASHBOARD_PREFIX}/api/queries`;
111
+ DASHBOARD_API_INGEST = `${DASHBOARD_PREFIX}/api/ingest`;
112
+ DASHBOARD_API_METRICS = `${DASHBOARD_PREFIX}/api/metrics`;
113
+ DASHBOARD_API_ACTIVITY = `${DASHBOARD_PREFIX}/api/activity`;
114
+ DASHBOARD_API_METRICS_LIVE = `${DASHBOARD_PREFIX}/api/metrics/live`;
115
+ DASHBOARD_API_INSIGHTS = `${DASHBOARD_PREFIX}/api/insights`;
116
+ DASHBOARD_API_SECURITY = `${DASHBOARD_PREFIX}/api/security`;
117
+ DASHBOARD_API_TAB = `${DASHBOARD_PREFIX}/api/tab`;
118
+ DASHBOARD_API_FINDINGS = `${DASHBOARD_PREFIX}/api/findings`;
119
+ DASHBOARD_API_FINDINGS_REPORT = `${DASHBOARD_PREFIX}/api/findings/report`;
120
+ VALID_TABS_TUPLE = [
121
+ "overview",
122
+ "actions",
123
+ "requests",
124
+ "fetches",
125
+ "queries",
126
+ "errors",
127
+ "logs",
128
+ "performance",
129
+ "security"
130
+ ];
131
+ VALID_TABS = new Set(VALID_TABS_TUPLE);
132
+ }
133
+ });
134
+
135
+ // src/constants/transport.ts
136
+ var init_transport = __esm({
137
+ "src/constants/transport.ts"() {
138
+ "use strict";
139
+ }
140
+ });
141
+
65
142
  // src/constants/headers.ts
66
143
  var init_headers = __esm({
67
144
  "src/constants/headers.ts"() {
@@ -70,19 +147,22 @@ var init_headers = __esm({
70
147
  });
71
148
 
72
149
  // src/constants/network.ts
150
+ var RECOVERY_WINDOW_MS, PORT_MIN, PORT_MAX;
73
151
  var init_network = __esm({
74
152
  "src/constants/network.ts"() {
75
153
  "use strict";
154
+ RECOVERY_WINDOW_MS = 5 * 60 * 1e3;
155
+ PORT_MIN = 1;
156
+ PORT_MAX = 65535;
76
157
  }
77
158
  });
78
159
 
79
160
  // src/constants/mcp.ts
80
- var MCP_SERVER_NAME, MCP_SERVER_VERSION, INITIAL_DISCOVERY_TIMEOUT_MS, LAZY_DISCOVERY_TIMEOUT_MS, CLIENT_FETCH_TIMEOUT_MS, HEALTH_CHECK_TIMEOUT_MS, DISCOVERY_POLL_INTERVAL_MS, MAX_DISCOVERY_DEPTH, MAX_TIMELINE_EVENTS, MAX_RESOLVED_DISPLAY, ENRICHMENT_SEVERITY_FILTER;
161
+ var MCP_SERVER_NAME, INITIAL_DISCOVERY_TIMEOUT_MS, LAZY_DISCOVERY_TIMEOUT_MS, CLIENT_FETCH_TIMEOUT_MS, HEALTH_CHECK_TIMEOUT_MS, DISCOVERY_POLL_INTERVAL_MS, MAX_DISCOVERY_DEPTH, MAX_TIMELINE_EVENTS, MAX_RESOLVED_DISPLAY, ENRICHMENT_SEVERITY_FILTER, MCP_SERVER_VERSION;
81
162
  var init_mcp = __esm({
82
163
  "src/constants/mcp.ts"() {
83
164
  "use strict";
84
165
  MCP_SERVER_NAME = "brakit";
85
- MCP_SERVER_VERSION = "0.8.4";
86
166
  INITIAL_DISCOVERY_TIMEOUT_MS = 5e3;
87
167
  LAZY_DISCOVERY_TIMEOUT_MS = 2e3;
88
168
  CLIENT_FETCH_TIMEOUT_MS = 1e4;
@@ -92,6 +172,7 @@ var init_mcp = __esm({
92
172
  MAX_TIMELINE_EVENTS = 20;
93
173
  MAX_RESOLVED_DISPLAY = 5;
94
174
  ENRICHMENT_SEVERITY_FILTER = ["critical", "warning"];
175
+ MCP_SERVER_VERSION = "0.8.6";
95
176
  }
96
177
  });
97
178
 
@@ -103,18 +184,9 @@ var init_encoding = __esm({
103
184
  });
104
185
 
105
186
  // src/constants/severity.ts
106
- var SEVERITY_CRITICAL, SEVERITY_WARNING, SEVERITY_INFO, SEVERITY_ICON_MAP;
107
187
  var init_severity = __esm({
108
188
  "src/constants/severity.ts"() {
109
189
  "use strict";
110
- SEVERITY_CRITICAL = "critical";
111
- SEVERITY_WARNING = "warning";
112
- SEVERITY_INFO = "info";
113
- SEVERITY_ICON_MAP = {
114
- [SEVERITY_CRITICAL]: { icon: "\u2717", cls: "critical" },
115
- [SEVERITY_WARNING]: { icon: "\u26A0", cls: "warning" },
116
- [SEVERITY_INFO]: { icon: "\u2139", cls: "info" }
117
- };
118
190
  }
119
191
  });
120
192
 
@@ -125,6 +197,24 @@ var init_telemetry = __esm({
125
197
  }
126
198
  });
127
199
 
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"() {
204
+ "use strict";
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", "."];
215
+ }
216
+ });
217
+
128
218
  // src/constants/index.ts
129
219
  var init_constants = __esm({
130
220
  "src/constants/index.ts"() {
@@ -140,18 +230,8 @@ var init_constants = __esm({
140
230
  init_encoding();
141
231
  init_severity();
142
232
  init_telemetry();
143
- }
144
- });
145
-
146
- // src/store/finding-id.ts
147
- import { createHash } from "crypto";
148
- function computeFindingId(finding) {
149
- const key = `${finding.rule}:${finding.endpoint}:${finding.desc}`;
150
- return createHash("sha256").update(key).digest("hex").slice(0, 16);
151
- }
152
- var init_finding_id = __esm({
153
- "src/store/finding-id.ts"() {
154
- "use strict";
233
+ init_lifecycle();
234
+ init_cli();
155
235
  }
156
236
  });
157
237
 
@@ -189,11 +269,11 @@ var init_client = __esm({
189
269
  if (params?.offset) url.searchParams.set("offset", String(params.offset));
190
270
  return this.fetchJson(url);
191
271
  }
192
- async getSecurityFindings() {
193
- return this.fetchJson(`${this.baseUrl}${DASHBOARD_API_SECURITY}`);
194
- }
195
- async getInsights() {
196
- 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);
197
277
  }
198
278
  async getQueries(requestId) {
199
279
  const url = new URL(`${this.baseUrl}${DASHBOARD_API_QUERIES}`);
@@ -221,6 +301,19 @@ var init_client = __esm({
221
301
  if (state) url.searchParams.set("state", state);
222
302
  return this.fetchJson(url);
223
303
  }
304
+ async reportFix(findingId, status, notes) {
305
+ const res = await fetch(`${this.baseUrl}${DASHBOARD_API_FINDINGS_REPORT}`, {
306
+ method: "POST",
307
+ headers: { "Content-Type": "application/json" },
308
+ body: JSON.stringify({ findingId, status, notes }),
309
+ signal: AbortSignal.timeout(CLIENT_FETCH_TIMEOUT_MS)
310
+ });
311
+ if (!res.ok) return false;
312
+ const contentType = res.headers.get("content-type") ?? "";
313
+ if (!contentType.includes("application/json")) return false;
314
+ const body = await res.json();
315
+ return body.ok === true;
316
+ }
224
317
  async clearAll() {
225
318
  const res = await fetch(`${this.baseUrl}${DASHBOARD_API_CLEAR}`, {
226
319
  method: "POST",
@@ -252,50 +345,56 @@ var init_client = __esm({
252
345
  });
253
346
 
254
347
  // src/mcp/discovery.ts
255
- import { readFileSync as readFileSync3, existsSync as existsSync5, readdirSync, statSync } from "fs";
256
- import { resolve as resolve5, dirname } from "path";
257
- function readPort(portPath) {
258
- if (!existsSync5(portPath)) return null;
259
- const raw = readFileSync3(portPath, "utf-8").trim();
260
- const port = parseInt(raw, 10);
261
- return isNaN(port) || port < 1 || port > 65535 ? null : port;
262
- }
263
- function portInDir(dir) {
348
+ import { readFile as readFile6, readdir as readdir3, stat } from "fs/promises";
349
+ import { resolve as resolve5, dirname as dirname2 } from "path";
350
+ async function readPort(portPath) {
351
+ try {
352
+ const raw = (await readFile6(portPath, "utf-8")).trim();
353
+ const port = parseInt(raw, 10);
354
+ return isNaN(port) || port < PORT_MIN || port > PORT_MAX ? null : port;
355
+ } catch {
356
+ return null;
357
+ }
358
+ }
359
+ async function portInDir(dir) {
264
360
  return readPort(resolve5(dir, PORT_FILE));
265
361
  }
266
- function portInChildren(dir) {
362
+ async function portInChildren(dir) {
267
363
  try {
268
- for (const entry of readdirSync(dir)) {
364
+ const entries = await readdir3(dir);
365
+ for (const entry of entries) {
269
366
  if (entry.startsWith(".") || entry === "node_modules") continue;
270
367
  const child = resolve5(dir, entry);
271
368
  try {
272
- if (!statSync(child).isDirectory()) continue;
273
- } catch {
369
+ if (!(await stat(child)).isDirectory()) continue;
370
+ } catch (err) {
371
+ brakitDebug(`discovery: stat failed for ${child}: ${err}`);
274
372
  continue;
275
373
  }
276
- const port = portInDir(child);
374
+ const port = await portInDir(child);
277
375
  if (port) return port;
278
376
  }
279
- } catch {
377
+ } catch (err) {
378
+ brakitDebug(`discovery: readdir failed for ${dir}: ${err}`);
280
379
  }
281
380
  return null;
282
381
  }
283
- function searchForPort(startDir) {
382
+ async function searchForPort(startDir) {
284
383
  const start = resolve5(startDir);
285
- const initial = portInDir(start) ?? portInChildren(start);
384
+ const initial = await portInDir(start) ?? await portInChildren(start);
286
385
  if (initial) return initial;
287
- let dir = dirname(start);
386
+ let dir = dirname2(start);
288
387
  for (let depth = 0; depth < MAX_DISCOVERY_DEPTH; depth++) {
289
- const port = portInDir(dir);
388
+ const port = await portInDir(dir) ?? await portInChildren(dir);
290
389
  if (port) return port;
291
- const parent = dirname(dir);
390
+ const parent = dirname2(dir);
292
391
  if (parent === dir) break;
293
392
  dir = parent;
294
393
  }
295
394
  return null;
296
395
  }
297
- function discoverBrakitPort(cwd) {
298
- const port = searchForPort(cwd ?? process.cwd());
396
+ async function discoverBrakitPort(cwd) {
397
+ const port = await searchForPort(cwd ?? process.cwd());
299
398
  if (!port) {
300
399
  throw new Error(
301
400
  "Brakit is not running. Start your app with brakit enabled first."
@@ -307,7 +406,7 @@ async function waitForBrakit(cwd, timeoutMs = 1e4, pollMs = DISCOVERY_POLL_INTER
307
406
  const deadline = Date.now() + timeoutMs;
308
407
  while (Date.now() < deadline) {
309
408
  try {
310
- const result = discoverBrakitPort(cwd);
409
+ const result = await discoverBrakitPort(cwd);
311
410
  const res = await fetch(`${result.baseUrl}${DASHBOARD_API_REQUESTS}?limit=1`);
312
411
  if (res.ok) return result;
313
412
  } catch {
@@ -322,64 +421,54 @@ var init_discovery = __esm({
322
421
  "src/mcp/discovery.ts"() {
323
422
  "use strict";
324
423
  init_constants();
424
+ init_log();
325
425
  init_mcp();
326
426
  }
327
427
  });
328
428
 
329
429
  // src/mcp/enrichment.ts
330
- import { createHash as createHash2 } from "crypto";
331
- function computeInsightId(type, endpoint, desc) {
332
- const key = `${type}:${endpoint}:${desc}`;
333
- return createHash2("sha256").update(key).digest("hex").slice(0, 16);
334
- }
335
430
  async function enrichFindings(client) {
336
- const [securityData, insightsData] = await Promise.all([
337
- client.getSecurityFindings(),
338
- client.getInsights()
339
- ]);
340
- const enriched = [];
341
- for (const f of securityData.findings) {
342
- let context = "";
343
- try {
344
- const { path } = parseEndpointKey(f.endpoint);
345
- const reqData = await client.getRequests({ search: path, limit: 1 });
346
- if (reqData.requests.length > 0) {
347
- const req = reqData.requests[0];
348
- if (req.id) {
349
- const activity = await client.getActivity(req.id);
350
- const queryCount = activity.counts?.queries ?? 0;
351
- const fetchCount = activity.counts?.fetches ?? 0;
352
- context = `Request took ${req.durationMs}ms. ${queryCount} DB queries, ${fetchCount} fetches.`;
431
+ const issuesData = await client.getIssues();
432
+ const issues = issuesData.issues.filter(
433
+ (si) => si.state !== "resolved" && si.state !== "stale"
434
+ );
435
+ const contexts = await Promise.all(
436
+ issues.map(async (si) => {
437
+ const endpoint = si.issue.endpoint;
438
+ if (!endpoint) return si.issue.detail ?? "";
439
+ try {
440
+ const { path } = parseEndpointKey(endpoint);
441
+ const reqData = await client.getRequests({ search: path, limit: 1 });
442
+ if (reqData.requests.length > 0) {
443
+ const req = reqData.requests[0];
444
+ if (req.id) {
445
+ const activity = await client.getActivity(req.id);
446
+ const queryCount = activity.counts?.queries ?? 0;
447
+ const fetchCount = activity.counts?.fetches ?? 0;
448
+ return `Request took ${req.durationMs}ms. ${queryCount} DB queries, ${fetchCount} fetches.`;
449
+ }
353
450
  }
451
+ } catch {
452
+ return "(context unavailable)";
354
453
  }
355
- } catch {
356
- context = "(context unavailable)";
357
- }
358
- enriched.push({
359
- findingId: computeFindingId(f),
360
- severity: f.severity,
361
- title: f.title,
362
- endpoint: f.endpoint,
363
- description: f.desc,
364
- hint: f.hint,
365
- occurrences: f.count,
366
- context
367
- });
368
- }
369
- for (const si of insightsData.insights) {
370
- if (si.state === "resolved") continue;
371
- const i = si.insight;
372
- if (!ENRICHMENT_SEVERITY_FILTER.includes(i.severity)) continue;
373
- const endpoint = i.nav ?? "global";
454
+ return si.issue.detail ?? "";
455
+ })
456
+ );
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;
374
461
  enriched.push({
375
- findingId: computeInsightId(i.type, endpoint, i.desc),
376
- severity: i.severity,
377
- title: i.title,
378
- endpoint,
379
- description: i.desc,
380
- hint: i.hint,
381
- occurrences: 1,
382
- 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],
470
+ aiStatus: si.aiStatus,
471
+ aiNotes: si.aiNotes
383
472
  });
384
473
  }
385
474
  return enriched;
@@ -435,19 +524,18 @@ var init_enrichment = __esm({
435
524
  "src/mcp/enrichment.ts"() {
436
525
  "use strict";
437
526
  init_mcp();
438
- init_finding_id();
439
527
  init_endpoint();
440
528
  }
441
529
  });
442
530
 
443
531
  // src/mcp/tools/get-findings.ts
444
- var VALID_SEVERITIES, VALID_STATES, getFindings;
532
+ var getFindings;
445
533
  var init_get_findings = __esm({
446
534
  "src/mcp/tools/get-findings.ts"() {
447
535
  "use strict";
448
536
  init_enrichment();
449
- VALID_SEVERITIES = /* @__PURE__ */ new Set(["critical", "warning"]);
450
- VALID_STATES = /* @__PURE__ */ new Set(["open", "fixing", "resolved"]);
537
+ init_lifecycle();
538
+ init_type_guards();
451
539
  getFindings = {
452
540
  name: "get_findings",
453
541
  description: "Get all security findings and performance insights from the running app. Returns enriched findings with actionable fix hints, endpoint context, and evidence. Use this to understand what issues exist in the running application.",
@@ -461,28 +549,28 @@ var init_get_findings = __esm({
461
549
  },
462
550
  state: {
463
551
  type: "string",
464
- enum: ["open", "fixing", "resolved"],
465
- description: "Filter by finding state (from finding lifecycle)"
552
+ enum: ["open", "fixing", "resolved", "stale", "regressed"],
553
+ description: "Filter by issue state"
466
554
  }
467
555
  }
468
556
  },
469
557
  async handler(client, args) {
470
558
  const severity = args.severity;
471
559
  const state = args.state;
472
- if (severity && !VALID_SEVERITIES.has(severity)) {
560
+ if (severity && !VALID_SECURITY_SEVERITIES.has(severity)) {
473
561
  return { content: [{ type: "text", text: `Invalid severity "${severity}". Use: critical, warning.` }], isError: true };
474
562
  }
475
- if (state && !VALID_STATES.has(state)) {
476
- 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 };
477
565
  }
478
566
  let findings = await enrichFindings(client);
479
567
  if (severity) {
480
568
  findings = findings.filter((f) => f.severity === severity);
481
569
  }
482
570
  if (state) {
483
- const stateful = await client.getFindings(state);
484
- const statefulIds = new Set(stateful.findings.map((f) => f.findingId));
485
- 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));
486
574
  }
487
575
  if (findings.length === 0) {
488
576
  return { content: [{ type: "text", text: "No findings detected. The application looks healthy." }] };
@@ -491,10 +579,18 @@ var init_get_findings = __esm({
491
579
  `];
492
580
  for (const f of findings) {
493
581
  lines.push(`[${f.severity.toUpperCase()}] ${f.title}`);
582
+ lines.push(` ID: ${f.findingId}`);
494
583
  lines.push(` Endpoint: ${f.endpoint}`);
495
584
  lines.push(` Issue: ${f.description}`);
496
585
  if (f.context) lines.push(` Context: ${f.context}`);
497
586
  lines.push(` Fix: ${f.hint}`);
587
+ if (f.aiStatus === "fixed") {
588
+ lines.push(` AI Status: fixed (awaiting verification)`);
589
+ if (f.aiNotes) lines.push(` AI Notes: ${f.aiNotes}`);
590
+ } else if (f.aiStatus === "wont_fix") {
591
+ lines.push(` AI Status: won't fix`);
592
+ if (f.aiNotes) lines.push(` AI Notes: ${f.aiNotes}`);
593
+ }
498
594
  lines.push("");
499
595
  }
500
596
  return { content: [{ type: "text", text: lines.join("\n") }] };
@@ -653,20 +749,21 @@ var init_verify_fix = __esm({
653
749
  }
654
750
  if (findingId) {
655
751
  const data = await client.getFindings();
656
- const finding = data.findings.find((f) => f.findingId === findingId);
752
+ const finding = data.findings.find((f) => f.issueId === findingId);
657
753
  if (!finding) {
658
754
  return {
659
755
  content: [{
660
756
  type: "text",
661
757
  text: `Finding ${findingId} not found. It may have already been resolved and cleaned up.`
662
- }]
758
+ }],
759
+ isError: true
663
760
  };
664
761
  }
665
762
  if (finding.state === "resolved") {
666
763
  return {
667
764
  content: [{
668
765
  type: "text",
669
- 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.`
670
767
  }]
671
768
  };
672
769
  }
@@ -674,12 +771,12 @@ var init_verify_fix = __esm({
674
771
  content: [{
675
772
  type: "text",
676
773
  text: [
677
- `STILL PRESENT: "${finding.finding.title}" on ${finding.finding.endpoint}`,
774
+ `STILL PRESENT: "${finding.issue.title}" on ${finding.issue.endpoint ?? "global"}`,
678
775
  ` State: ${finding.state}`,
679
776
  ` Last seen: ${new Date(finding.lastSeenAt).toISOString()}`,
680
777
  ` Occurrences: ${finding.occurrences}`,
681
- ` Issue: ${finding.finding.desc}`,
682
- ` Hint: ${finding.finding.hint}`,
778
+ ` Issue: ${finding.issue.desc}`,
779
+ ` Hint: ${finding.issue.hint}`,
683
780
  "",
684
781
  "Make sure the user has triggered the endpoint again after the fix, so Brakit can re-analyze."
685
782
  ].join("\n")
@@ -689,7 +786,7 @@ var init_verify_fix = __esm({
689
786
  if (endpoint) {
690
787
  const data = await client.getFindings();
691
788
  const endpointFindings = data.findings.filter(
692
- (f) => f.finding.endpoint === endpoint || f.finding.endpoint.endsWith(` ${endpoint}`)
789
+ (f) => f.issue.endpoint === endpoint || f.issue.endpoint && f.issue.endpoint.endsWith(` ${endpoint}`)
693
790
  );
694
791
  if (endpointFindings.length === 0) {
695
792
  return {
@@ -699,7 +796,7 @@ var init_verify_fix = __esm({
699
796
  }]
700
797
  };
701
798
  }
702
- const open = endpointFindings.filter((f) => f.state === "open");
799
+ const open = endpointFindings.filter((f) => f.state === "open" || f.state === "regressed");
703
800
  const resolved = endpointFindings.filter((f) => f.state === "resolved");
704
801
  const lines = [
705
802
  `Endpoint: ${endpoint}`,
@@ -708,10 +805,10 @@ var init_verify_fix = __esm({
708
805
  ""
709
806
  ];
710
807
  for (const f of open) {
711
- lines.push(` [${f.finding.severity}] ${f.finding.title}: ${f.finding.desc}`);
808
+ lines.push(` [${f.issue.severity}] ${f.issue.title}: ${f.issue.desc}`);
712
809
  }
713
810
  for (const f of resolved) {
714
- lines.push(` [resolved] ${f.finding.title}`);
811
+ lines.push(` [resolved] ${f.issue.title}`);
715
812
  }
716
813
  return { content: [{ type: "text", text: lines.join("\n") }] };
717
814
  }
@@ -719,7 +816,8 @@ var init_verify_fix = __esm({
719
816
  content: [{
720
817
  type: "text",
721
818
  text: "Please provide either a finding_id or an endpoint to verify."
722
- }]
819
+ }],
820
+ isError: true
723
821
  };
724
822
  }
725
823
  };
@@ -740,51 +838,52 @@ var init_get_report = __esm({
740
838
  properties: {}
741
839
  },
742
840
  async handler(client, _args) {
743
- const [findingsData, securityData, insightsData, metricsData] = await Promise.all([
744
- client.getFindings(),
745
- client.getSecurityFindings(),
746
- client.getInsights(),
841
+ const [issuesData, metricsData] = await Promise.all([
842
+ client.getIssues(),
747
843
  client.getLiveMetrics()
748
844
  ]);
749
- const findings = findingsData.findings;
750
- const open = findings.filter((f) => f.state === "open");
751
- const resolved = findings.filter((f) => f.state === "resolved");
752
- const fixing = findings.filter((f) => f.state === "fixing");
753
- const criticalOpen = open.filter((f) => f.finding.severity === "critical");
754
- 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");
755
854
  const totalRequests = metricsData.endpoints.reduce(
756
855
  (s, ep) => s + ep.summary.totalRequests,
757
856
  0
758
857
  );
759
- const openInsightCount = insightsData.insights.filter((si) => si.state === "open").length;
760
858
  const lines = [
761
859
  "=== Brakit Report ===",
762
860
  "",
763
861
  `Endpoints observed: ${metricsData.endpoints.length}`,
764
862
  `Total requests captured: ${totalRequests}`,
765
- `Active security rules: ${securityData.findings.length} finding(s)`,
766
- `Performance insights: ${openInsightCount} open, ${insightsData.insights.length - openInsightCount} resolved`,
863
+ `Security issues: ${securityIssues.length}`,
864
+ `Performance issues: ${perfIssues.length}`,
767
865
  "",
768
- "--- Finding Summary ---",
769
- `Total: ${findings.length}`,
866
+ "--- Issue Summary ---",
867
+ `Total: ${issues.length}`,
770
868
  ` Open: ${open.length} (${criticalOpen.length} critical, ${warningOpen.length} warning)`,
771
869
  ` In progress: ${fixing.length}`,
772
- ` Resolved: ${resolved.length}`
870
+ ` Resolved: ${resolved.length}`,
871
+ ` Stale: ${stale.length}`
773
872
  ];
774
873
  if (criticalOpen.length > 0) {
775
874
  lines.push("");
776
875
  lines.push("--- Critical Issues (fix first) ---");
777
876
  for (const f of criticalOpen) {
778
- lines.push(` [CRITICAL] ${f.finding.title} \u2014 ${f.finding.endpoint}`);
779
- lines.push(` ${f.finding.desc}`);
780
- 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}`);
781
880
  }
782
881
  }
783
882
  if (resolved.length > 0) {
784
883
  lines.push("");
785
884
  lines.push("--- Recently Resolved ---");
786
885
  for (const f of resolved.slice(0, MAX_RESOLVED_DISPLAY)) {
787
- lines.push(` \u2713 ${f.finding.title} \u2014 ${f.finding.endpoint}`);
886
+ lines.push(` \u2713 ${f.issue.title} \u2014 ${f.issue.endpoint ?? "global"}`);
788
887
  }
789
888
  if (resolved.length > MAX_RESOLVED_DISPLAY) {
790
889
  lines.push(` ... and ${resolved.length - MAX_RESOLVED_DISPLAY} more`);
@@ -823,6 +922,61 @@ var init_clear_findings = __esm({
823
922
  }
824
923
  });
825
924
 
925
+ // src/mcp/tools/report-fix.ts
926
+ var reportFix;
927
+ var init_report_fix = __esm({
928
+ "src/mcp/tools/report-fix.ts"() {
929
+ "use strict";
930
+ init_type_guards();
931
+ reportFix = {
932
+ name: "report_fix",
933
+ description: "Report the result of fixing a brakit finding. Call this after attempting to fix each finding to update the dashboard with the outcome. Use status 'fixed' when you've applied a fix, or 'wont_fix' when the issue can't be resolved (e.g. third-party library, by design).",
934
+ inputSchema: {
935
+ type: "object",
936
+ properties: {
937
+ finding_id: {
938
+ type: "string",
939
+ description: "The finding ID to report on"
940
+ },
941
+ status: {
942
+ type: "string",
943
+ description: "Whether the fix was applied or can't be fixed",
944
+ enum: ["fixed", "wont_fix"]
945
+ },
946
+ summary: {
947
+ type: "string",
948
+ description: "Brief description of what was done or why it can't be fixed"
949
+ }
950
+ },
951
+ required: ["finding_id", "status", "summary"]
952
+ },
953
+ async handler(client, args) {
954
+ const { finding_id, status, summary } = args;
955
+ if (!isNonEmptyString(finding_id)) {
956
+ return { content: [{ type: "text", text: "finding_id is required." }], isError: true };
957
+ }
958
+ if (!isValidAiFixStatus(status)) {
959
+ return { content: [{ type: "text", text: "status must be 'fixed' or 'wont_fix'." }], isError: true };
960
+ }
961
+ if (!isNonEmptyString(summary)) {
962
+ return { content: [{ type: "text", text: "summary is required." }], isError: true };
963
+ }
964
+ const ok = await client.reportFix(finding_id, status, summary);
965
+ if (!ok) {
966
+ return {
967
+ content: [{ type: "text", text: `Finding ${finding_id} not found. It may have already been resolved.` }],
968
+ isError: true
969
+ };
970
+ }
971
+ const label = status === "fixed" ? "marked as fixed (awaiting verification)" : "marked as won't fix";
972
+ return {
973
+ content: [{ type: "text", text: `Finding ${finding_id} ${label}. Dashboard updated.` }]
974
+ };
975
+ }
976
+ };
977
+ }
978
+ });
979
+
826
980
  // src/mcp/tools/index.ts
827
981
  function getToolDefinitions() {
828
982
  return [...TOOL_MAP.values()].map((t) => ({
@@ -851,17 +1005,19 @@ var init_tools = __esm({
851
1005
  init_verify_fix();
852
1006
  init_get_report();
853
1007
  init_clear_findings();
1008
+ init_report_fix();
854
1009
  TOOL_MAP = new Map(
855
- [getFindings, getEndpoints, getRequestDetail, verifyFix, getReport, clearFindings].map((t) => [t.name, t])
1010
+ [getFindings, getEndpoints, getRequestDetail, verifyFix, getReport, clearFindings, reportFix].map((t) => [t.name, t])
856
1011
  );
857
1012
  }
858
1013
  });
859
1014
 
860
1015
  // src/mcp/prompts.ts
861
- var PROMPTS, PROMPT_MESSAGES;
1016
+ var SERVER_INSTRUCTIONS, PROMPTS, PROMPT_MESSAGES;
862
1017
  var init_prompts = __esm({
863
1018
  "src/mcp/prompts.ts"() {
864
1019
  "use strict";
1020
+ SERVER_INSTRUCTIONS = "Brakit is a dev tool that monitors your running app for security vulnerabilities, performance issues, and query problems. IMPORTANT: After fixing any brakit finding, always call the report_fix tool with the finding_id, status ('fixed' or 'wont_fix'), and a brief summary of what you did. This updates the dashboard in real-time so the developer can track AI-driven fixes.";
865
1021
  PROMPTS = [
866
1022
  {
867
1023
  name: "check-app",
@@ -873,18 +1029,8 @@ var init_prompts = __esm({
873
1029
  }
874
1030
  ];
875
1031
  PROMPT_MESSAGES = {
876
- "check-app": [
877
- "Check my running app for security and performance issues using brakit.",
878
- "First get all findings, then get the endpoint summary.",
879
- "For any critical or warning findings, get the request detail to understand the root cause.",
880
- "Give me a clear report of what's wrong and offer to fix each issue."
881
- ].join(" "),
882
- "fix-findings": [
883
- "Get all open brakit findings.",
884
- "For each finding, get the request detail to understand the exact issue.",
885
- "Then find the source code responsible and fix it.",
886
- "After fixing, ask me to re-trigger the endpoint so you can verify the fix with brakit."
887
- ].join(" ")
1032
+ "check-app": "Check my running app for security and performance issues using brakit. First get all findings, then get the endpoint summary. For any critical or warning findings, get the request detail to understand the root cause. Give me a clear report of what's wrong and offer to fix each issue. After fixing any issue, call report_fix with the finding_id, status, and summary.",
1033
+ "fix-findings": "Get all open brakit findings. For each finding:\n1. Get the request detail to understand the exact issue\n2. Find the source code responsible and fix it\n3. Call report_fix with the finding_id, status ('fixed' or 'wont_fix'), and a brief summary of what you did\n4. Move to the next finding\n\nAfter all findings are processed, ask me to re-trigger the endpoints so brakit can verify the fixes."
888
1034
  };
889
1035
  }
890
1036
  });
@@ -912,7 +1058,7 @@ async function startMcpServer() {
912
1058
  let cachedClient = discovery ? new BrakitClient(discovery.baseUrl) : null;
913
1059
  const server = new Server(
914
1060
  { name: MCP_SERVER_NAME, version: MCP_SERVER_VERSION },
915
- { capabilities: { tools: {}, prompts: {} } }
1061
+ { capabilities: { tools: {}, prompts: {} }, instructions: SERVER_INSTRUCTIONS }
916
1062
  );
917
1063
  server.setRequestHandler(ListPromptsRequestSchema, async () => ({
918
1064
  prompts: [...PROMPTS]
@@ -990,29 +1136,31 @@ import { runMain } from "citty";
990
1136
 
991
1137
  // src/cli/commands/install.ts
992
1138
  import { defineCommand } from "citty";
993
- import { resolve as resolve3, join as join2 } from "path";
994
- import { readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
1139
+ import { resolve as resolve3, join as join3, dirname } from "path";
1140
+ import { readFile as readFile4, writeFile as writeFile3 } from "fs/promises";
995
1141
  import { execSync } from "child_process";
1142
+ import { existsSync as existsSync5 } from "fs";
996
1143
  import pc from "picocolors";
997
1144
 
998
- // src/store/finding-store.ts
999
- init_constants();
1000
- import { readFileSync as readFileSync2, existsSync as existsSync3 } from "fs";
1145
+ // src/store/issue-store.ts
1146
+ import { readFile as readFile2 } from "fs/promises";
1147
+ import { readFileSync as readFileSync2, existsSync as existsSync3, unlinkSync } from "fs";
1001
1148
  import { resolve as resolve2 } from "path";
1002
1149
 
1003
- // src/utils/atomic-writer.ts
1004
- import {
1005
- writeFileSync as writeFileSync2,
1006
- existsSync as existsSync2,
1007
- mkdirSync as mkdirSync2,
1008
- renameSync
1009
- } from "fs";
1010
- import { writeFile as writeFile2, mkdir, rename } from "fs/promises";
1011
-
1012
1150
  // src/utils/fs.ts
1013
- import { access } from "fs/promises";
1151
+ init_limits();
1152
+ init_log();
1153
+ init_type_guards();
1154
+ import { access, readFile, writeFile } from "fs/promises";
1014
1155
  import { existsSync, readFileSync, writeFileSync } from "fs";
1015
- 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
+ }
1016
1164
  async function fileExists(path) {
1017
1165
  try {
1018
1166
  await access(path);
@@ -1022,13 +1170,35 @@ async function fileExists(path) {
1022
1170
  }
1023
1171
  }
1024
1172
 
1025
- // src/store/finding-store.ts
1026
- init_finding_id();
1173
+ // src/store/issue-store.ts
1174
+ init_metrics();
1175
+ init_limits();
1176
+ init_thresholds();
1177
+ init_limits();
1178
+
1179
+ // src/utils/atomic-writer.ts
1180
+ import {
1181
+ writeFileSync as writeFileSync2,
1182
+ existsSync as existsSync2,
1183
+ mkdirSync as mkdirSync2,
1184
+ renameSync
1185
+ } from "fs";
1186
+ import { writeFile as writeFile2, mkdir, rename } from "fs/promises";
1187
+ init_log();
1188
+ init_type_guards();
1189
+
1190
+ // src/store/issue-store.ts
1191
+ init_log();
1192
+ init_type_guards();
1193
+
1194
+ // src/utils/issue-id.ts
1195
+ init_limits();
1196
+ import { createHash as createHash2 } from "crypto";
1027
1197
 
1028
1198
  // src/detect/project.ts
1029
- import { readFile as readFile2 } from "fs/promises";
1199
+ import { readFile as readFile3, readdir } from "fs/promises";
1030
1200
  import { existsSync as existsSync4 } from "fs";
1031
- import { join } from "path";
1201
+ import { join as join2, relative } from "path";
1032
1202
  var FRAMEWORKS = [
1033
1203
  { name: "nextjs", dep: "next", devCmd: "next dev", bin: "next", defaultPort: 3e3, devArgs: ["dev", "--port"] },
1034
1204
  { name: "remix", dep: "@remix-run/dev", devCmd: "remix dev", bin: "remix", defaultPort: 3e3, devArgs: ["dev"] },
@@ -1037,24 +1207,24 @@ var FRAMEWORKS = [
1037
1207
  { name: "astro", dep: "astro", devCmd: "astro dev", bin: "astro", defaultPort: 4321, devArgs: ["dev", "--port"] }
1038
1208
  ];
1039
1209
  async function detectProject(rootDir) {
1040
- const pkgPath = join(rootDir, "package.json");
1041
- const raw = await readFile2(pkgPath, "utf-8");
1210
+ const pkgPath = join2(rootDir, "package.json");
1211
+ const raw = await readFile3(pkgPath, "utf-8");
1042
1212
  const pkg = JSON.parse(raw);
1043
1213
  const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
1044
1214
  const framework = detectFrameworkFromDeps(allDeps);
1045
1215
  const matched = FRAMEWORKS.find((f) => f.name === framework);
1046
1216
  const devCommand = matched?.devCmd ?? "";
1047
- const devBin = matched ? join(rootDir, "node_modules", ".bin", matched.bin) : "";
1217
+ const devBin = matched ? join2(rootDir, "node_modules", ".bin", matched.bin) : "";
1048
1218
  const defaultPort = matched?.defaultPort ?? 3e3;
1049
1219
  const packageManager = await detectPackageManager(rootDir);
1050
1220
  return { framework, devCommand, devBin, defaultPort, packageManager };
1051
1221
  }
1052
1222
  async function detectPackageManager(rootDir) {
1053
- if (await fileExists(join(rootDir, "bun.lockb"))) return "bun";
1054
- if (await fileExists(join(rootDir, "bun.lock"))) return "bun";
1055
- if (await fileExists(join(rootDir, "pnpm-lock.yaml"))) return "pnpm";
1056
- if (await fileExists(join(rootDir, "yarn.lock"))) return "yarn";
1057
- 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";
1058
1228
  return "unknown";
1059
1229
  }
1060
1230
  function detectFrameworkFromDeps(allDeps) {
@@ -1063,16 +1233,168 @@ function detectFrameworkFromDeps(allDeps) {
1063
1233
  }
1064
1234
  return "unknown";
1065
1235
  }
1236
+ var PYTHON_ENTRY_CANDIDATES = [
1237
+ "app.py",
1238
+ "main.py",
1239
+ "wsgi.py",
1240
+ "asgi.py",
1241
+ "server.py",
1242
+ "run.py",
1243
+ "manage.py",
1244
+ "app/__init__.py"
1245
+ ];
1246
+ var PYTHON_FRAMEWORK_MAP = {
1247
+ flask: "flask",
1248
+ fastapi: "fastapi",
1249
+ django: "django"
1250
+ };
1251
+ var PYTHON_DEFAULT_PORTS = {
1252
+ flask: 5e3,
1253
+ fastapi: 8e3,
1254
+ django: 8e3,
1255
+ unknown: 8e3
1256
+ };
1257
+ async function detectPythonProject(rootDir) {
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"));
1261
+ if (!hasPyproject && !hasRequirements && !hasSetupPy) return null;
1262
+ const framework = await detectPythonFramework(rootDir, hasPyproject, hasRequirements);
1263
+ const packageManager = await detectPythonPackageManager(rootDir);
1264
+ const entryFile = await detectPythonEntry(rootDir);
1265
+ return {
1266
+ framework,
1267
+ packageManager,
1268
+ entryFile,
1269
+ defaultPort: PYTHON_DEFAULT_PORTS[framework] ?? 8e3
1270
+ };
1271
+ }
1272
+ async function detectPythonFramework(rootDir, hasPyproject, hasRequirements) {
1273
+ if (hasPyproject) {
1274
+ try {
1275
+ const content = await readFile3(join2(rootDir, "pyproject.toml"), "utf-8");
1276
+ for (const [dep, fw] of Object.entries(PYTHON_FRAMEWORK_MAP)) {
1277
+ if (content.includes(`"${dep}"`) || content.includes(`'${dep}'`) || content.includes(`${dep} `)) {
1278
+ return fw;
1279
+ }
1280
+ }
1281
+ } catch {
1282
+ }
1283
+ }
1284
+ if (hasRequirements) {
1285
+ try {
1286
+ const content = await readFile3(join2(rootDir, "requirements.txt"), "utf-8");
1287
+ const lines = content.toLowerCase().split("\n");
1288
+ for (const [dep, fw] of Object.entries(PYTHON_FRAMEWORK_MAP)) {
1289
+ if (lines.some((l) => l.startsWith(dep) && (l.length === dep.length || /[=<>~![]/u.test(l[dep.length])))) {
1290
+ return fw;
1291
+ }
1292
+ }
1293
+ } catch {
1294
+ }
1295
+ }
1296
+ return "unknown";
1297
+ }
1298
+ async function detectPythonPackageManager(rootDir) {
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";
1304
+ try {
1305
+ const content = await readFile3(join2(rootDir, "pyproject.toml"), "utf-8");
1306
+ if (content.includes("[tool.poetry]")) return "poetry";
1307
+ if (content.includes("[tool.uv]")) return "uv";
1308
+ } catch {
1309
+ }
1310
+ return "unknown";
1311
+ }
1312
+ async function detectPythonEntry(rootDir) {
1313
+ for (const candidate of PYTHON_ENTRY_CANDIDATES) {
1314
+ if (await fileExists(join2(rootDir, candidate))) {
1315
+ return candidate;
1316
+ }
1317
+ }
1318
+ return null;
1319
+ }
1320
+ var SKIP_DIRS = /* @__PURE__ */ new Set([
1321
+ "node_modules",
1322
+ ".git",
1323
+ ".brakit",
1324
+ "dist",
1325
+ "build",
1326
+ "__pycache__",
1327
+ ".venv",
1328
+ "venv",
1329
+ ".next",
1330
+ ".nuxt",
1331
+ ".output",
1332
+ ".cache",
1333
+ "coverage"
1334
+ ]);
1335
+ async function scanForProjects(rootDir) {
1336
+ const projects = [];
1337
+ await detectInDir(rootDir, rootDir, projects);
1338
+ try {
1339
+ const entries = await readdir(rootDir, { withFileTypes: true });
1340
+ for (const entry of entries) {
1341
+ if (!entry.isDirectory() || SKIP_DIRS.has(entry.name) || entry.name.startsWith(".")) continue;
1342
+ const childDir = join2(rootDir, entry.name);
1343
+ await detectInDir(childDir, rootDir, projects);
1344
+ }
1345
+ } catch {
1346
+ }
1347
+ return projects;
1348
+ }
1349
+ async function detectInDir(dir, rootDir, projects) {
1350
+ const rel = dir === rootDir ? "." : `./${relative(rootDir, dir)}`;
1351
+ if (await fileExists(join2(dir, "package.json"))) {
1352
+ try {
1353
+ const node = await detectProject(dir);
1354
+ projects.push({ dir, relDir: rel, type: "node", node });
1355
+ } catch {
1356
+ }
1357
+ }
1358
+ const python = await detectPythonProject(dir);
1359
+ if (python) {
1360
+ projects.push({ dir, relDir: rel, type: "python", python });
1361
+ }
1362
+ }
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
+ }
1066
1388
 
1067
1389
  // src/analysis/rules/patterns.ts
1068
1390
  var SECRET_KEYS = /^(password|passwd|secret|api_key|apiKey|api_secret|apiSecret|private_key|privateKey|client_secret|clientSecret)$/;
1069
1391
  var TOKEN_PARAMS = /^(token|api_key|apiKey|secret|password|access_token|session_id|sessionId)$/;
1070
1392
  var SAFE_PARAMS = /^(_rsc|__clerk_handshake|__clerk_db_jwt|callback|code|state|nonce|redirect_uri|utm_|fbclid|gclid)$/;
1071
- var STACK_TRACE_RE = /at\s+.+\(.+:\d+:\d+\)|at\s+Module\._compile|at\s+Object\.<anonymous>|at\s+processTicksAndRejections/;
1393
+ var STACK_TRACE_RE = /at\s+.+\(.+:\d+:\d+\)|at\s+Module\._compile|at\s+Object\.<anonymous>|at\s+processTicksAndRejections|Traceback \(most recent call last\)|File ".+", line \d+/;
1072
1394
  var DB_CONN_RE = /(postgres|mysql|mongodb|redis):\/\//;
1073
1395
  var SQL_FRAGMENT_RE = /\b(SELECT\s+[\w.*]+\s+FROM|INSERT\s+INTO|UPDATE\s+\w+\s+SET|DELETE\s+FROM)\b/i;
1074
- var SECRET_VAL_RE = /(api_key|apiKey|secret|token)\s*[:=]\s*["']?[A-Za-z0-9_\-\.+\/]{8,}/;
1075
- var LOG_SECRET_RE = /(password|secret|token|api_key|apiKey)\s*[:=]\s*["']?[A-Za-z0-9_\-\.+\/]{8,}/i;
1396
+ var SECRET_VAL_RE = /(api_key|apiKey|secret|token)\s*[:=]\s*["']?[A-Za-z0-9_\-.+/]{8,}/;
1397
+ var LOG_SECRET_RE = /(password|secret|token|api_key|apiKey)\s*[:=]\s*["']?[A-Za-z0-9_\-.+/]{8,}/i;
1076
1398
  var MASKED_RE = /^\*+$|\[REDACTED\]|\[FILTERED\]|CHANGE_ME|^x{3,}$/i;
1077
1399
  var EMAIL_RE = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/;
1078
1400
  var INTERNAL_ID_KEYS = /^(id|_id|userId|user_id|createdBy|updatedBy|organizationId|org_id|tenantId|tenant_id)$/;
@@ -1082,37 +1404,41 @@ var RULE_HINTS = {
1082
1404
  "token-in-url": "Pass tokens in the Authorization header, not URL query parameters.",
1083
1405
  "stack-trace-leak": "Use a custom error handler that returns generic messages in production.",
1084
1406
  "error-info-leak": "Sanitize error responses. Return generic messages instead of internal details.",
1407
+ "insecure-cookie": "Set HttpOnly and SameSite flags on all cookies.",
1085
1408
  "sensitive-logs": "Redact PII before logging. Never log passwords or tokens.",
1086
1409
  "cors-credentials": "Cannot use credentials:true with origin:*. Specify explicit origins.",
1087
- "insecure-cookie": "Set HttpOnly and SameSite flags on all cookies.",
1088
1410
  "response-pii-leak": "API responses should return minimal data. Don't echo back full user records \u2014 select only the fields the client needs."
1089
1411
  };
1090
1412
 
1091
1413
  // src/analysis/rules/exposed-secret.ts
1092
- function tryParseJson(body) {
1093
- if (!body) return null;
1094
- try {
1095
- return JSON.parse(body);
1096
- } catch {
1097
- return null;
1098
- }
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;
1099
1422
  }
1100
- function findSecretKeys(obj, prefix) {
1423
+
1424
+ // src/analysis/rules/exposed-secret.ts
1425
+ function findSecretKeys(obj, prefix, depth = 0) {
1101
1426
  const found = [];
1427
+ if (depth >= MAX_OBJECT_SCAN_DEPTH) return found;
1102
1428
  if (!obj || typeof obj !== "object") return found;
1103
1429
  if (Array.isArray(obj)) {
1104
- for (let i = 0; i < Math.min(obj.length, 5); i++) {
1105
- 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));
1106
1432
  }
1107
1433
  return found;
1108
1434
  }
1109
1435
  for (const k of Object.keys(obj)) {
1110
1436
  const val = obj[k];
1111
- 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)) {
1112
1438
  found.push(k);
1113
1439
  }
1114
1440
  if (typeof val === "object" && val !== null) {
1115
- found.push(...findSecretKeys(val, prefix + k + "."));
1441
+ found.push(...findSecretKeys(val, prefix + k + ".", depth + 1));
1116
1442
  }
1117
1443
  }
1118
1444
  return found;
@@ -1126,8 +1452,8 @@ var exposedSecretRule = {
1126
1452
  const findings = [];
1127
1453
  const seen = /* @__PURE__ */ new Map();
1128
1454
  for (const r of ctx.requests) {
1129
- if (r.statusCode >= 400) continue;
1130
- const parsed = tryParseJson(r.responseBody);
1455
+ if (isErrorStatus(r.statusCode)) continue;
1456
+ const parsed = ctx.parsedBodies.response.get(r.id);
1131
1457
  if (!parsed) continue;
1132
1458
  const keys = findSecretKeys(parsed, "");
1133
1459
  if (keys.length === 0) continue;
@@ -1280,7 +1606,7 @@ var errorInfoLeakRule = {
1280
1606
 
1281
1607
  // src/analysis/rules/insecure-cookie.ts
1282
1608
  function isFrameworkResponse(r) {
1283
- if (r.statusCode >= 300 && r.statusCode < 400) return true;
1609
+ if (isRedirect(r.statusCode)) return true;
1284
1610
  if (r.path?.startsWith("/__")) return true;
1285
1611
  if (r.responseHeaders?.["x-middleware-rewrite"]) return true;
1286
1612
  return false;
@@ -1386,49 +1712,16 @@ var corsCredentialsRule = {
1386
1712
  }
1387
1713
  };
1388
1714
 
1389
- // src/utils/response.ts
1390
- init_thresholds();
1391
- function unwrapResponse(parsed) {
1392
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return parsed;
1393
- const obj = parsed;
1394
- const keys = Object.keys(obj);
1395
- if (keys.length > 3) return parsed;
1396
- let best = null;
1397
- let bestSize = 0;
1398
- for (const key of keys) {
1399
- const val = obj[key];
1400
- if (Array.isArray(val) && val.length > bestSize) {
1401
- best = val;
1402
- bestSize = val.length;
1403
- } else if (val && typeof val === "object" && !Array.isArray(val)) {
1404
- const size = Object.keys(val).length;
1405
- if (size > bestSize) {
1406
- best = val;
1407
- bestSize = size;
1408
- }
1409
- }
1410
- }
1411
- return best && bestSize >= OVERFETCH_UNWRAP_MIN_SIZE ? best : parsed;
1412
- }
1413
-
1414
1715
  // src/analysis/rules/response-pii-leak.ts
1716
+ init_limits();
1415
1717
  var WRITE_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH"]);
1416
- var FULL_RECORD_MIN_FIELDS = 5;
1417
- var LIST_PII_MIN_ITEMS = 2;
1418
- function tryParseJson2(body) {
1419
- if (!body) return null;
1420
- try {
1421
- return JSON.parse(body);
1422
- } catch {
1423
- return null;
1424
- }
1425
- }
1426
- function findEmails(obj) {
1718
+ function findEmails(obj, depth = 0) {
1427
1719
  const emails = [];
1720
+ if (depth >= MAX_OBJECT_SCAN_DEPTH) return emails;
1428
1721
  if (!obj || typeof obj !== "object") return emails;
1429
1722
  if (Array.isArray(obj)) {
1430
- for (let i = 0; i < Math.min(obj.length, 10); i++) {
1431
- 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));
1432
1725
  }
1433
1726
  return emails;
1434
1727
  }
@@ -1436,7 +1729,7 @@ function findEmails(obj) {
1436
1729
  if (typeof v === "string" && EMAIL_RE.test(v)) {
1437
1730
  emails.push(v);
1438
1731
  } else if (typeof v === "object" && v !== null) {
1439
- emails.push(...findEmails(v));
1732
+ emails.push(...findEmails(v, depth + 1));
1440
1733
  }
1441
1734
  }
1442
1735
  return emails;
@@ -1455,48 +1748,47 @@ function hasInternalIds(obj) {
1455
1748
  }
1456
1749
  return false;
1457
1750
  }
1458
- function detectPII(method, reqBody, resBody) {
1459
- const target = unwrapResponse(resBody);
1460
- if (WRITE_METHODS.has(method) && reqBody && typeof reqBody === "object") {
1461
- const reqEmails = findEmails(reqBody);
1462
- if (reqEmails.length > 0) {
1463
- const resEmails = findEmails(target);
1464
- const echoed = reqEmails.filter((e) => resEmails.includes(e));
1465
- if (echoed.length > 0) {
1466
- const inspectObj = Array.isArray(target) && target.length > 0 ? target[0] : target;
1467
- if (hasInternalIds(inspectObj) || topLevelFieldCount(inspectObj) >= FULL_RECORD_MIN_FIELDS) {
1468
- return { reason: "echo", emailCount: echoed.length };
1469
- }
1470
- }
1471
- }
1751
+ function detectEchoPII(method, reqBody, target) {
1752
+ if (!WRITE_METHODS.has(method) || !reqBody || typeof reqBody !== "object") return null;
1753
+ const reqEmails = findEmails(reqBody);
1754
+ if (reqEmails.length === 0) return null;
1755
+ const resEmails = findEmails(target);
1756
+ const echoed = reqEmails.filter((e) => resEmails.includes(e));
1757
+ if (echoed.length === 0) return null;
1758
+ const inspectObj = Array.isArray(target) && target.length > 0 ? target[0] : target;
1759
+ if (hasInternalIds(inspectObj) || topLevelFieldCount(inspectObj) >= FULL_RECORD_MIN_FIELDS) {
1760
+ return { reason: "echo", emailCount: echoed.length };
1472
1761
  }
1473
- if (target && typeof target === "object" && !Array.isArray(target)) {
1474
- const fields = topLevelFieldCount(target);
1475
- if (fields >= FULL_RECORD_MIN_FIELDS && hasInternalIds(target)) {
1476
- const emails = findEmails(target);
1477
- if (emails.length > 0) {
1478
- return { reason: "full-record", emailCount: emails.length };
1479
- }
1762
+ return null;
1763
+ }
1764
+ function detectFullRecordPII(target) {
1765
+ if (!target || typeof target !== "object" || Array.isArray(target)) return null;
1766
+ const fields = topLevelFieldCount(target);
1767
+ if (fields < FULL_RECORD_MIN_FIELDS || !hasInternalIds(target)) return null;
1768
+ const emails = findEmails(target);
1769
+ if (emails.length === 0) return null;
1770
+ return { reason: "full-record", emailCount: emails.length };
1771
+ }
1772
+ function detectListPII(target) {
1773
+ if (!Array.isArray(target) || target.length < LIST_PII_MIN_ITEMS) return null;
1774
+ let itemsWithEmail = 0;
1775
+ for (let i = 0; i < Math.min(target.length, PII_SCAN_ARRAY_LIMIT); i++) {
1776
+ const item = target[i];
1777
+ if (item && typeof item === "object" && findEmails(item).length > 0) {
1778
+ itemsWithEmail++;
1480
1779
  }
1481
1780
  }
1482
- if (Array.isArray(target) && target.length >= LIST_PII_MIN_ITEMS) {
1483
- let itemsWithEmail = 0;
1484
- for (let i = 0; i < Math.min(target.length, 10); i++) {
1485
- const item = target[i];
1486
- if (item && typeof item === "object") {
1487
- const emails = findEmails(item);
1488
- if (emails.length > 0) itemsWithEmail++;
1489
- }
1490
- }
1491
- if (itemsWithEmail >= LIST_PII_MIN_ITEMS) {
1492
- const first = target[0];
1493
- if (hasInternalIds(first) || topLevelFieldCount(first) >= FULL_RECORD_MIN_FIELDS) {
1494
- return { reason: "list-pii", emailCount: itemsWithEmail };
1495
- }
1496
- }
1781
+ if (itemsWithEmail < LIST_PII_MIN_ITEMS) return null;
1782
+ const first = target[0];
1783
+ if (hasInternalIds(first) || topLevelFieldCount(first) >= FULL_RECORD_MIN_FIELDS) {
1784
+ return { reason: "list-pii", emailCount: itemsWithEmail };
1497
1785
  }
1498
1786
  return null;
1499
1787
  }
1788
+ function detectPII(method, reqBody, resBody) {
1789
+ const target = unwrapResponse(resBody);
1790
+ return detectEchoPII(method, reqBody, target) ?? detectFullRecordPII(target) ?? detectListPII(target);
1791
+ }
1500
1792
  var REASON_LABELS = {
1501
1793
  echo: "echoes back PII from the request body",
1502
1794
  "full-record": "returns a full record with email and internal IDs",
@@ -1511,10 +1803,10 @@ var responsePiiLeakRule = {
1511
1803
  const findings = [];
1512
1804
  const seen = /* @__PURE__ */ new Map();
1513
1805
  for (const r of ctx.requests) {
1514
- if (r.statusCode >= 400) continue;
1515
- const resJson = tryParseJson2(r.responseBody);
1806
+ if (isErrorStatus(r.statusCode)) continue;
1807
+ const resJson = ctx.parsedBodies.response.get(r.id);
1516
1808
  if (!resJson) continue;
1517
- const reqJson = tryParseJson2(r.requestBody);
1809
+ const reqJson = ctx.parsedBodies.request.get(r.id) ?? null;
1518
1810
  const detection = detectPII(r.method, reqJson, resJson);
1519
1811
  if (!detection) continue;
1520
1812
  const ep = `${r.method} ${r.path}`;
@@ -1540,6 +1832,9 @@ var responsePiiLeakRule = {
1540
1832
  }
1541
1833
  };
1542
1834
 
1835
+ // src/analysis/engine.ts
1836
+ init_limits();
1837
+
1543
1838
  // src/analysis/group.ts
1544
1839
  init_constants();
1545
1840
  import { randomUUID } from "crypto";
@@ -1595,12 +1890,11 @@ init_constants();
1595
1890
  // src/analysis/insights/rules/regression.ts
1596
1891
  init_constants();
1597
1892
 
1598
- // src/analysis/insight-tracker.ts
1893
+ // src/analysis/issue-mappers.ts
1599
1894
  init_endpoint();
1600
- init_thresholds();
1601
1895
 
1602
1896
  // src/index.ts
1603
- var VERSION = "0.8.4";
1897
+ var VERSION = "0.8.6";
1604
1898
 
1605
1899
  // src/cli/commands/install.ts
1606
1900
  init_constants();
@@ -1608,10 +1902,28 @@ init_constants();
1608
1902
  // src/cli/templates.ts
1609
1903
  var IMPORT_LINE = `import "brakit";`;
1610
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
+ }
1611
1920
  var CREATED_FILES = [
1612
1921
  "src/instrumentation.ts",
1922
+ "src/instrumentation.js",
1613
1923
  "instrumentation.ts",
1614
- "server/plugins/brakit.ts"
1924
+ "instrumentation.js",
1925
+ "server/plugins/brakit.ts",
1926
+ "server/plugins/brakit.js"
1615
1927
  ];
1616
1928
  var ENTRY_CANDIDATES = [
1617
1929
  "src/index.ts",
@@ -1628,7 +1940,6 @@ var ENTRY_CANDIDATES = [
1628
1940
  "index.js"
1629
1941
  ];
1630
1942
  var BRAKIT_TEMPLATES = {
1631
- /** Next.js instrumentation.ts — standalone file created by install */
1632
1943
  nextjs: [
1633
1944
  `export async function register() {`,
1634
1945
  ` if (process.env.NODE_ENV !== "production") {`,
@@ -1636,7 +1947,6 @@ var BRAKIT_TEMPLATES = {
1636
1947
  ` }`,
1637
1948
  `}`
1638
1949
  ].join("\n"),
1639
- /** Nuxt server/plugins/brakit.ts — standalone file created by install */
1640
1950
  nuxt: `import "brakit";`
1641
1951
  };
1642
1952
  var ALL_TEMPLATES = Object.values(BRAKIT_TEMPLATES);
@@ -1668,53 +1978,45 @@ var install_default = defineCommand({
1668
1978
  },
1669
1979
  async run({ args }) {
1670
1980
  const rootDir = resolve3(args.dir);
1671
- const pkgPath = join2(rootDir, "package.json");
1672
- if (!await fileExists(pkgPath)) {
1673
- console.error(pc.red(" No project found. Run this from your project directory."));
1674
- process.exit(1);
1675
- }
1676
- let pkg;
1677
- try {
1678
- pkg = JSON.parse(await readFile3(pkgPath, "utf-8"));
1679
- } catch {
1680
- console.error(pc.red(" Failed to read package.json."));
1681
- process.exit(1);
1682
- }
1683
- if (!pkg.name || typeof pkg.name !== "string") {
1684
- console.error(pc.red(" No project found. Run this from your project directory."));
1685
- process.exit(1);
1686
- }
1687
- let project;
1688
- try {
1689
- project = await detectProject(rootDir);
1690
- } catch {
1691
- console.error(pc.red(" Failed to read package.json."));
1692
- process.exit(1);
1693
- }
1694
1981
  console.log();
1695
1982
  console.log(pc.bold(" \u25C6 brakit install"));
1696
1983
  console.log();
1697
- const installed = await installPackage(rootDir, project.packageManager);
1698
- if (installed) {
1699
- console.log(pc.green(" \u2713 Added brakit to devDependencies"));
1700
- } else {
1701
- console.log(pc.dim(" \u2713 brakit already in dependencies"));
1702
- }
1703
- const result = await setupInstrumentation(rootDir, project.framework);
1704
- if (result.action === "created") {
1705
- console.log(pc.green(` \u2713 Created ${result.file}`));
1706
- if (result.content) {
1984
+ const projects = await scanForProjects(rootDir);
1985
+ const nodeProjects = projects.filter((p) => p.type === "node");
1986
+ const pythonProjects = projects.filter((p) => p.type === "python");
1987
+ if (nodeProjects.length === 0) {
1988
+ if (pythonProjects.length > 0) {
1989
+ console.log(pc.dim(" Python project detected. To add brakit:"));
1707
1990
  console.log();
1708
- for (const line of result.content.split("\n")) {
1709
- console.log(pc.dim(` ${line}`));
1710
- }
1991
+ console.log(pc.bold(" pip install brakit"));
1992
+ console.log(pc.dim(" Then add to the top of your entry file:"));
1993
+ console.log(pc.bold(" import brakit # noqa: F401"));
1994
+ console.log();
1995
+ } else {
1996
+ console.error(pc.red(" No project found. Run this from your project directory."));
1997
+ }
1998
+ process.exit(1);
1999
+ }
2000
+ for (const p of nodeProjects) {
2001
+ const node = p.node;
2002
+ const suffix = p.relDir === "." ? "" : ` in ${p.relDir}`;
2003
+ const installed = await installPackage(p.dir, node.packageManager);
2004
+ if (installed) {
2005
+ console.log(pc.green(` \u2713 Added brakit to devDependencies${suffix}`));
2006
+ } else {
2007
+ console.log(pc.dim(` \u2713 brakit already in dependencies${suffix}`));
2008
+ }
2009
+ const result = await setupInstrumentation(p.dir, node.framework);
2010
+ const prefix = p.relDir === "." ? "" : `${p.relDir}/`;
2011
+ if (result.action === "created") {
2012
+ console.log(pc.green(` \u2713 Created ${prefix}${result.file}`));
2013
+ } else if (result.action === "prepended") {
2014
+ console.log(pc.green(` \u2713 Added import to ${prefix}${result.file}`));
2015
+ } else if (result.action === "exists") {
2016
+ console.log(pc.dim(` \u2713 ${prefix}${result.file} already has brakit import`));
2017
+ } else {
2018
+ printManualInstructions(node.framework);
1711
2019
  }
1712
- } else if (result.action === "prepended") {
1713
- console.log(pc.green(` \u2713 Added import to ${result.file}`));
1714
- } else if (result.action === "exists") {
1715
- console.log(pc.dim(` \u2713 ${result.file} already has brakit import`));
1716
- } else {
1717
- printManualInstructions(project.framework);
1718
2020
  }
1719
2021
  await ensureGitignoreEntry(rootDir, METRICS_DIR);
1720
2022
  const mcpResult = await setupMcp(rootDir);
@@ -1723,14 +2025,30 @@ var install_default = defineCommand({
1723
2025
  } else if (mcpResult === "exists") {
1724
2026
  console.log(pc.dim(" \u2713 MCP already configured"));
1725
2027
  }
2028
+ const gitRoot = findGitRoot(rootDir);
2029
+ if (gitRoot && gitRoot !== rootDir) {
2030
+ const parentMcpResult = await setupMcp(gitRoot);
2031
+ if (parentMcpResult === "created" || parentMcpResult === "updated") {
2032
+ console.log(pc.green(" \u2713 Configured MCP at project root"));
2033
+ }
2034
+ }
1726
2035
  console.log();
2036
+ const port = nodeProjects[0].node?.defaultPort ?? 3e3;
1727
2037
  console.log(pc.dim(" Start your app and visit:"));
1728
- console.log(pc.bold(" http://localhost:<port>/__brakit"));
2038
+ console.log(pc.bold(` http://localhost:${port}/__brakit`));
2039
+ if (pythonProjects.length > 0) {
2040
+ const pyLabel = pythonProjects.map((p) => p.relDir).join(", ");
2041
+ console.log();
2042
+ console.log(pc.dim(` Python backend detected (${pyLabel}). To capture telemetry:`));
2043
+ console.log(pc.bold(" pip install brakit"));
2044
+ console.log(pc.dim(" Then add to the top of your entry file:"));
2045
+ console.log(pc.bold(" import brakit # noqa: F401"));
2046
+ }
1729
2047
  console.log();
1730
2048
  }
1731
2049
  });
1732
2050
  async function installPackage(rootDir, pm) {
1733
- const pkgRaw = await readFile3(join2(rootDir, "package.json"), "utf-8");
2051
+ const pkgRaw = await readFile4(join3(rootDir, "package.json"), "utf-8");
1734
2052
  const pkg = JSON.parse(pkgRaw);
1735
2053
  const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
1736
2054
  if (allDeps["brakit"]) return false;
@@ -1762,11 +2080,11 @@ async function setupInstrumentation(rootDir, framework) {
1762
2080
  }
1763
2081
  }
1764
2082
  async function setupNextjs(rootDir) {
1765
- const hasSrc = await fileExists(join2(rootDir, "src"));
2083
+ const hasSrc = await fileExists(join3(rootDir, "src"));
1766
2084
  const relPath = hasSrc ? "src/instrumentation.ts" : "instrumentation.ts";
1767
- const absPath = join2(rootDir, relPath);
2085
+ const absPath = join3(rootDir, relPath);
1768
2086
  if (await fileExists(absPath)) {
1769
- const content2 = await readFile3(absPath, "utf-8");
2087
+ const content2 = await readFile4(absPath, "utf-8");
1770
2088
  if (content2.includes(IMPORT_MARKER)) {
1771
2089
  return { action: "exists", file: relPath };
1772
2090
  }
@@ -1778,16 +2096,16 @@ async function setupNextjs(rootDir) {
1778
2096
  }
1779
2097
  async function setupNuxt(rootDir) {
1780
2098
  const relPath = "server/plugins/brakit.ts";
1781
- const absPath = join2(rootDir, relPath);
2099
+ const absPath = join3(rootDir, relPath);
1782
2100
  if (await fileExists(absPath)) {
1783
- const content2 = await readFile3(absPath, "utf-8");
2101
+ const content2 = await readFile4(absPath, "utf-8");
1784
2102
  if (content2.includes(IMPORT_MARKER)) {
1785
2103
  return { action: "exists", file: relPath };
1786
2104
  }
1787
2105
  return { action: "manual", file: relPath };
1788
2106
  }
1789
2107
  const content = BRAKIT_TEMPLATES.nuxt + "\n";
1790
- const dir = join2(rootDir, "server/plugins");
2108
+ const dir = join3(rootDir, "server/plugins");
1791
2109
  const { mkdirSync: mkdirSync3 } = await import("fs");
1792
2110
  mkdirSync3(dir, { recursive: true });
1793
2111
  await writeFile3(absPath, content);
@@ -1795,9 +2113,9 @@ async function setupNuxt(rootDir) {
1795
2113
  }
1796
2114
  async function setupPrepend(rootDir, ...candidates) {
1797
2115
  for (const relPath of candidates) {
1798
- const absPath = join2(rootDir, relPath);
2116
+ const absPath = join3(rootDir, relPath);
1799
2117
  if (!await fileExists(absPath)) continue;
1800
- const content = await readFile3(absPath, "utf-8");
2118
+ const content = await readFile4(absPath, "utf-8");
1801
2119
  if (content.includes(IMPORT_MARKER)) {
1802
2120
  return { action: "exists", file: relPath };
1803
2121
  }
@@ -1809,7 +2127,7 @@ ${content}`);
1809
2127
  }
1810
2128
  async function setupGeneric(rootDir) {
1811
2129
  try {
1812
- const pkgRaw = await readFile3(join2(rootDir, "package.json"), "utf-8");
2130
+ const pkgRaw = await readFile4(join3(rootDir, "package.json"), "utf-8");
1813
2131
  const pkg = JSON.parse(pkgRaw);
1814
2132
  if (pkg.main && typeof pkg.main === "string") {
1815
2133
  const result2 = await setupPrepend(rootDir, pkg.main);
@@ -1829,29 +2147,29 @@ var MCP_CONFIG = {
1829
2147
  }
1830
2148
  }
1831
2149
  };
1832
- async function setupMcp(rootDir) {
1833
- const mcpPath = join2(rootDir, ".mcp.json");
2150
+ async function setupMcp(rootDir, config = MCP_CONFIG) {
2151
+ const mcpPath = join3(rootDir, ".mcp.json");
1834
2152
  if (await fileExists(mcpPath)) {
1835
- const raw = await readFile3(mcpPath, "utf-8");
2153
+ const raw = await readFile4(mcpPath, "utf-8");
1836
2154
  try {
1837
- const config = JSON.parse(raw);
1838
- if (config?.mcpServers?.brakit) return "exists";
1839
- config.mcpServers = { ...config.mcpServers, ...MCP_CONFIG.mcpServers };
1840
- await writeFile3(mcpPath, JSON.stringify(config, null, 2) + "\n");
2155
+ const existing = JSON.parse(raw);
2156
+ if (existing?.mcpServers?.brakit) return "exists";
2157
+ existing.mcpServers = { ...existing.mcpServers, ...config.mcpServers };
2158
+ await writeFile3(mcpPath, JSON.stringify(existing, null, 2) + "\n");
1841
2159
  await ensureGitignoreEntry(rootDir, ".mcp.json");
1842
2160
  return "updated";
1843
2161
  } catch {
1844
2162
  }
1845
2163
  }
1846
- await writeFile3(mcpPath, JSON.stringify(MCP_CONFIG, null, 2) + "\n");
2164
+ await writeFile3(mcpPath, JSON.stringify(config, null, 2) + "\n");
1847
2165
  await ensureGitignoreEntry(rootDir, ".mcp.json");
1848
2166
  return "created";
1849
2167
  }
1850
2168
  async function ensureGitignoreEntry(rootDir, entry) {
1851
- const gitignorePath = join2(rootDir, ".gitignore");
2169
+ const gitignorePath = join3(rootDir, ".gitignore");
1852
2170
  try {
1853
2171
  if (await fileExists(gitignorePath)) {
1854
- const content = await readFile3(gitignorePath, "utf-8");
2172
+ const content = await readFile4(gitignorePath, "utf-8");
1855
2173
  if (content.split("\n").some((l) => l.trim() === entry)) return;
1856
2174
  await writeFile3(gitignorePath, content.trimEnd() + "\n" + entry + "\n");
1857
2175
  } else {
@@ -1860,6 +2178,15 @@ async function ensureGitignoreEntry(rootDir, entry) {
1860
2178
  } catch {
1861
2179
  }
1862
2180
  }
2181
+ function findGitRoot(startDir) {
2182
+ let dir = resolve3(startDir);
2183
+ while (true) {
2184
+ if (existsSync5(join3(dir, ".git"))) return dir;
2185
+ const parent = dirname(dir);
2186
+ if (parent === dir) return null;
2187
+ dir = parent;
2188
+ }
2189
+ }
1863
2190
  function printManualInstructions(framework) {
1864
2191
  console.log(pc.yellow(" \u26A0 Could not auto-detect entry file."));
1865
2192
  console.log();
@@ -1879,11 +2206,13 @@ function printManualInstructions(framework) {
1879
2206
 
1880
2207
  // src/cli/commands/uninstall.ts
1881
2208
  import { defineCommand as defineCommand2 } from "citty";
1882
- import { resolve as resolve4, join as join3 } from "path";
1883
- import { readFile as readFile4, 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";
1884
2211
  import { execSync as execSync2 } from "child_process";
1885
2212
  import pc2 from "picocolors";
1886
2213
  init_constants();
2214
+ init_log();
2215
+ init_type_guards();
1887
2216
  var PREPENDED_FILES = [
1888
2217
  "app/entry.server.tsx",
1889
2218
  "app/entry.server.ts",
@@ -1905,85 +2234,142 @@ var uninstall_default = defineCommand2({
1905
2234
  },
1906
2235
  async run({ args }) {
1907
2236
  const rootDir = resolve4(args.dir);
1908
- let project = null;
2237
+ let projects = [];
1909
2238
  try {
1910
- project = await detectProject(rootDir);
1911
- } 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" }];
1912
2246
  }
1913
2247
  console.log();
1914
2248
  console.log(pc2.bold(" \u25C6 brakit uninstall"));
1915
2249
  console.log();
1916
- let removed = false;
1917
- for (const relPath of CREATED_FILES) {
1918
- const absPath = join3(rootDir, relPath);
1919
- if (!await fileExists(absPath)) continue;
1920
- const content = await readFile4(absPath, "utf-8");
1921
- if (!content.includes("brakit")) continue;
1922
- if (isExactBrakitTemplate(content)) {
1923
- await unlink(absPath);
1924
- console.log(pc2.green(` \u2713 Removed ${relPath}`));
1925
- removed = true;
1926
- break;
1927
- }
1928
- const lines = content.split("\n");
1929
- const cleaned = lines.filter(
1930
- (line) => !line.includes('import("brakit")') && !line.includes('import "brakit"')
1931
- );
1932
- if (cleaned.length < lines.length) {
1933
- await writeFile4(absPath, cleaned.join("\n"));
1934
- console.log(pc2.green(` \u2713 Removed brakit lines from ${relPath}`));
1935
- removed = true;
1936
- break;
1937
- }
1938
- }
1939
- if (!removed) {
1940
- const candidates = [...PREPENDED_FILES];
1941
- try {
1942
- const pkgRaw = await readFile4(join3(rootDir, "package.json"), "utf-8");
1943
- const pkg = JSON.parse(pkgRaw);
1944
- if (pkg.main) candidates.unshift(pkg.main);
1945
- } 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}.`));
1946
2257
  }
1947
- for (const relPath of candidates) {
1948
- const absPath = join3(rootDir, relPath);
1949
- if (!await fileExists(absPath)) continue;
1950
- const content = await readFile4(absPath, "utf-8");
1951
- if (!content.includes(IMPORT_LINE)) continue;
1952
- const updated = content.split("\n").filter((line) => line.trim() !== IMPORT_LINE.trim()).join("\n");
1953
- await writeFile4(absPath, updated);
1954
- console.log(pc2.green(` \u2713 Removed brakit import from ${relPath}`));
1955
- removed = true;
1956
- 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") {
1957
2262
  }
1958
2263
  }
1959
- if (!removed) {
1960
- console.log(pc2.dim(" No brakit instrumentation files found."));
1961
- }
1962
2264
  const mcpRemoved = await removeMcpConfig(rootDir);
1963
2265
  if (mcpRemoved) {
1964
2266
  console.log(pc2.green(" \u2713 Removed brakit MCP configuration"));
1965
2267
  }
1966
2268
  const dataRemoved = await removeBrakitData(rootDir);
1967
2269
  if (dataRemoved) {
1968
- console.log(pc2.green(" \u2713 Removed .brakit directory"));
2270
+ console.log(pc2.green(" \u2713 Removed .brakit data"));
1969
2271
  }
1970
2272
  const gitignoreCleaned = await cleanGitignore(rootDir);
1971
2273
  if (gitignoreCleaned) {
1972
2274
  console.log(pc2.green(" \u2713 Removed .brakit from .gitignore"));
1973
2275
  }
1974
- const pm = project?.packageManager ?? "npm";
1975
- const uninstalled = await uninstallPackage(rootDir, pm);
1976
- if (uninstalled) {
1977
- 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"));
1978
2279
  }
1979
2280
  console.log();
1980
2281
  }
1981
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
+ }
1982
2368
  async function removeMcpConfig(rootDir) {
1983
- const mcpPath = join3(rootDir, ".mcp.json");
2369
+ const mcpPath = join4(rootDir, ".mcp.json");
1984
2370
  if (!await fileExists(mcpPath)) return false;
1985
2371
  try {
1986
- const raw = await readFile4(mcpPath, "utf-8");
2372
+ const raw = await readFile5(mcpPath, "utf-8");
1987
2373
  const config = JSON.parse(raw);
1988
2374
  if (!config?.mcpServers?.brakit) return false;
1989
2375
  delete config.mcpServers.brakit;
@@ -1993,16 +2379,18 @@ async function removeMcpConfig(rootDir) {
1993
2379
  await writeFile4(mcpPath, JSON.stringify(config, null, 2) + "\n");
1994
2380
  }
1995
2381
  return true;
1996
- } catch {
2382
+ } catch (err) {
2383
+ brakitDebug(`uninstall: MCP config cleanup failed: ${getErrorMessage(err)}`);
1997
2384
  return false;
1998
2385
  }
1999
2386
  }
2000
2387
  async function uninstallPackage(rootDir, pm) {
2001
2388
  try {
2002
- const pkgRaw = await readFile4(join3(rootDir, "package.json"), "utf-8");
2389
+ const pkgRaw = await readFile5(join4(rootDir, "package.json"), "utf-8");
2003
2390
  const pkg = JSON.parse(pkgRaw);
2004
2391
  if (!pkg.devDependencies?.brakit && !pkg.dependencies?.brakit) return false;
2005
- } catch {
2392
+ } catch (err) {
2393
+ brakitDebug(`uninstall: could not read package.json: ${getErrorMessage(err)}`);
2006
2394
  return false;
2007
2395
  }
2008
2396
  const cmds = {
@@ -2014,35 +2402,63 @@ async function uninstallPackage(rootDir, pm) {
2014
2402
  const cmd = cmds[pm] ?? cmds.npm;
2015
2403
  try {
2016
2404
  execSync2(cmd, { cwd: rootDir, stdio: "pipe" });
2405
+ return true;
2017
2406
  } catch {
2018
2407
  console.warn(pc2.yellow(` \u26A0 Failed to run "${cmd}". Remove brakit manually.`));
2408
+ return "failed";
2019
2409
  }
2020
- return true;
2021
2410
  }
2022
2411
  async function removeBrakitData(rootDir) {
2023
- const dataDir = join3(rootDir, METRICS_DIR);
2024
- if (!await fileExists(dataDir)) return false;
2025
- try {
2026
- await rm(dataDir, { recursive: true, force: true });
2027
- return true;
2028
- } catch {
2029
- 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
+ }
2030
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
+ }
2430
+ }
2431
+ return removed;
2031
2432
  }
2032
2433
  async function cleanGitignore(rootDir) {
2033
- const gitignorePath = join3(rootDir, ".gitignore");
2434
+ const gitignorePath = join4(rootDir, ".gitignore");
2034
2435
  if (!await fileExists(gitignorePath)) return false;
2035
2436
  try {
2036
- const content = await readFile4(gitignorePath, "utf-8");
2437
+ const content = await readFile5(gitignorePath, "utf-8");
2037
2438
  const lines = content.split("\n");
2038
2439
  const filtered = lines.filter((line) => line.trim() !== METRICS_DIR);
2039
2440
  if (filtered.length === lines.length) return false;
2040
2441
  await writeFile4(gitignorePath, filtered.join("\n"));
2041
2442
  return true;
2042
- } catch {
2443
+ } catch (err) {
2444
+ brakitDebug(`uninstall: gitignore cleanup failed: ${getErrorMessage(err)}`);
2043
2445
  return false;
2044
2446
  }
2045
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
+ }
2046
2462
 
2047
2463
  // bin/brakit.ts
2048
2464
  var sub = process.argv[2];