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.
- package/README.md +3 -3
- package/dist/api.d.ts +120 -115
- package/dist/api.js +371 -340
- package/dist/bin/brakit.js +457 -332
- package/dist/dashboard.html +60 -59
- package/dist/mcp/server.js +75 -90
- package/dist/runtime/index.js +637 -529
- package/package.json +1 -1
package/dist/bin/brakit.js
CHANGED
|
@@ -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.
|
|
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/
|
|
144
|
-
var
|
|
145
|
-
var
|
|
146
|
-
"src/constants/
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
return this.fetchJson(
|
|
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
|
|
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
|
|
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
|
|
412
|
-
|
|
413
|
-
|
|
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
|
-
|
|
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(
|
|
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 =
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
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:
|
|
457
|
-
severity:
|
|
458
|
-
title:
|
|
459
|
-
endpoint,
|
|
460
|
-
description:
|
|
461
|
-
hint:
|
|
462
|
-
occurrences:
|
|
463
|
-
context: i
|
|
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
|
|
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 && !
|
|
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
|
|
567
|
-
const
|
|
568
|
-
findings = findings.filter((f) =>
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
773
|
-
` Hint: ${finding.
|
|
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.
|
|
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.
|
|
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.
|
|
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 [
|
|
835
|
-
client.
|
|
836
|
-
client.getSecurityFindings(),
|
|
837
|
-
client.getInsights(),
|
|
841
|
+
const [issuesData, metricsData] = await Promise.all([
|
|
842
|
+
client.getIssues(),
|
|
838
843
|
client.getLiveMetrics()
|
|
839
844
|
]);
|
|
840
|
-
const
|
|
841
|
-
const open =
|
|
842
|
-
const resolved =
|
|
843
|
-
const fixing =
|
|
844
|
-
const
|
|
845
|
-
const
|
|
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
|
-
`
|
|
857
|
-
`Performance
|
|
863
|
+
`Security issues: ${securityIssues.length}`,
|
|
864
|
+
`Performance issues: ${perfIssues.length}`,
|
|
858
865
|
"",
|
|
859
|
-
"---
|
|
860
|
-
`Total: ${
|
|
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.
|
|
870
|
-
lines.push(` ${f.
|
|
871
|
-
lines.push(` Fix: ${f.
|
|
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.
|
|
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
|
|
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/
|
|
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 {
|
|
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/
|
|
1156
|
-
|
|
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/
|
|
1190
|
+
// src/store/issue-store.ts
|
|
1171
1191
|
init_log();
|
|
1172
|
-
|
|
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 =
|
|
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 ?
|
|
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(
|
|
1200
|
-
if (await fileExists(
|
|
1201
|
-
if (await fileExists(
|
|
1202
|
-
if (await fileExists(
|
|
1203
|
-
if (await fileExists(
|
|
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(
|
|
1235
|
-
const hasRequirements = await fileExists(
|
|
1236
|
-
const hasSetupPy = await fileExists(
|
|
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(
|
|
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(
|
|
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(
|
|
1276
|
-
if (await fileExists(
|
|
1277
|
-
if (await fileExists(
|
|
1278
|
-
if (await fileExists(
|
|
1279
|
-
if (await fileExists(
|
|
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(
|
|
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(
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
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
|
-
|
|
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,
|
|
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 >=
|
|
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
|
|
1403
|
-
const parsed =
|
|
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
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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
|
|
1787
|
-
const resJson =
|
|
1806
|
+
if (isErrorStatus(r.statusCode)) continue;
|
|
1807
|
+
const resJson = ctx.parsedBodies.response.get(r.id);
|
|
1788
1808
|
if (!resJson) continue;
|
|
1789
|
-
const reqJson =
|
|
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/
|
|
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.
|
|
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
|
-
"
|
|
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(
|
|
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(
|
|
2083
|
+
const hasSrc = await fileExists(join3(rootDir, "src"));
|
|
2048
2084
|
const relPath = hasSrc ? "src/instrumentation.ts" : "instrumentation.ts";
|
|
2049
|
-
const absPath =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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(
|
|
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 =
|
|
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 =
|
|
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(
|
|
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
|
|
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
|
|
2237
|
+
let projects = [];
|
|
2200
2238
|
try {
|
|
2201
|
-
|
|
2202
|
-
|
|
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
|
-
|
|
2208
|
-
|
|
2209
|
-
const
|
|
2210
|
-
if (
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
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
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
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
|
|
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
|
|
2266
|
-
|
|
2267
|
-
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
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 =
|
|
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];
|