brakit 0.8.3 → 0.8.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api.d.ts +26 -9
- package/dist/api.js +221 -112
- package/dist/bin/brakit.js +598 -282
- package/dist/dashboard.html +2652 -0
- package/dist/mcp/server.js +195 -90
- package/dist/runtime/index.js +1045 -386
- package/package.json +3 -2
package/dist/bin/brakit.js
CHANGED
|
@@ -10,29 +10,49 @@ var __export = (target, all) => {
|
|
|
10
10
|
};
|
|
11
11
|
|
|
12
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;
|
|
13
|
+
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
14
|
var init_routes = __esm({
|
|
15
15
|
"src/constants/routes.ts"() {
|
|
16
16
|
"use strict";
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
17
|
+
DASHBOARD_PREFIX = "/__brakit";
|
|
18
|
+
DASHBOARD_API_REQUESTS = `${DASHBOARD_PREFIX}/api/requests`;
|
|
19
|
+
DASHBOARD_API_EVENTS = `${DASHBOARD_PREFIX}/api/events`;
|
|
20
|
+
DASHBOARD_API_FLOWS = `${DASHBOARD_PREFIX}/api/flows`;
|
|
21
|
+
DASHBOARD_API_CLEAR = `${DASHBOARD_PREFIX}/api/clear`;
|
|
22
|
+
DASHBOARD_API_LOGS = `${DASHBOARD_PREFIX}/api/logs`;
|
|
23
|
+
DASHBOARD_API_FETCHES = `${DASHBOARD_PREFIX}/api/fetches`;
|
|
24
|
+
DASHBOARD_API_ERRORS = `${DASHBOARD_PREFIX}/api/errors`;
|
|
25
|
+
DASHBOARD_API_QUERIES = `${DASHBOARD_PREFIX}/api/queries`;
|
|
26
|
+
DASHBOARD_API_INGEST = `${DASHBOARD_PREFIX}/api/ingest`;
|
|
27
|
+
DASHBOARD_API_METRICS = `${DASHBOARD_PREFIX}/api/metrics`;
|
|
28
|
+
DASHBOARD_API_ACTIVITY = `${DASHBOARD_PREFIX}/api/activity`;
|
|
29
|
+
DASHBOARD_API_METRICS_LIVE = `${DASHBOARD_PREFIX}/api/metrics/live`;
|
|
30
|
+
DASHBOARD_API_INSIGHTS = `${DASHBOARD_PREFIX}/api/insights`;
|
|
31
|
+
DASHBOARD_API_SECURITY = `${DASHBOARD_PREFIX}/api/security`;
|
|
32
|
+
DASHBOARD_API_TAB = `${DASHBOARD_PREFIX}/api/tab`;
|
|
33
|
+
DASHBOARD_API_FINDINGS = `${DASHBOARD_PREFIX}/api/findings`;
|
|
34
|
+
DASHBOARD_API_FINDINGS_REPORT = `${DASHBOARD_PREFIX}/api/findings/report`;
|
|
35
|
+
VALID_TABS_TUPLE = [
|
|
36
|
+
"overview",
|
|
37
|
+
"actions",
|
|
38
|
+
"requests",
|
|
39
|
+
"fetches",
|
|
40
|
+
"queries",
|
|
41
|
+
"errors",
|
|
42
|
+
"logs",
|
|
43
|
+
"performance",
|
|
44
|
+
"security"
|
|
45
|
+
];
|
|
46
|
+
VALID_TABS = new Set(VALID_TABS_TUPLE);
|
|
27
47
|
}
|
|
28
48
|
});
|
|
29
49
|
|
|
30
50
|
// src/constants/limits.ts
|
|
31
|
-
var
|
|
51
|
+
var FINDING_ID_HASH_LENGTH;
|
|
32
52
|
var init_limits = __esm({
|
|
33
53
|
"src/constants/limits.ts"() {
|
|
34
54
|
"use strict";
|
|
35
|
-
|
|
55
|
+
FINDING_ID_HASH_LENGTH = 16;
|
|
36
56
|
}
|
|
37
57
|
});
|
|
38
58
|
|
|
@@ -70,19 +90,22 @@ var init_headers = __esm({
|
|
|
70
90
|
});
|
|
71
91
|
|
|
72
92
|
// src/constants/network.ts
|
|
93
|
+
var RECOVERY_WINDOW_MS, PORT_MIN, PORT_MAX;
|
|
73
94
|
var init_network = __esm({
|
|
74
95
|
"src/constants/network.ts"() {
|
|
75
96
|
"use strict";
|
|
97
|
+
RECOVERY_WINDOW_MS = 5 * 60 * 1e3;
|
|
98
|
+
PORT_MIN = 1;
|
|
99
|
+
PORT_MAX = 65535;
|
|
76
100
|
}
|
|
77
101
|
});
|
|
78
102
|
|
|
79
103
|
// src/constants/mcp.ts
|
|
80
|
-
var MCP_SERVER_NAME,
|
|
104
|
+
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
105
|
var init_mcp = __esm({
|
|
82
106
|
"src/constants/mcp.ts"() {
|
|
83
107
|
"use strict";
|
|
84
108
|
MCP_SERVER_NAME = "brakit";
|
|
85
|
-
MCP_SERVER_VERSION = "0.8.3";
|
|
86
109
|
INITIAL_DISCOVERY_TIMEOUT_MS = 5e3;
|
|
87
110
|
LAZY_DISCOVERY_TIMEOUT_MS = 2e3;
|
|
88
111
|
CLIENT_FETCH_TIMEOUT_MS = 1e4;
|
|
@@ -92,6 +115,7 @@ var init_mcp = __esm({
|
|
|
92
115
|
MAX_TIMELINE_EVENTS = 20;
|
|
93
116
|
MAX_RESOLVED_DISPLAY = 5;
|
|
94
117
|
ENRICHMENT_SEVERITY_FILTER = ["critical", "warning"];
|
|
118
|
+
MCP_SERVER_VERSION = "0.8.5";
|
|
95
119
|
}
|
|
96
120
|
});
|
|
97
121
|
|
|
@@ -103,18 +127,27 @@ var init_encoding = __esm({
|
|
|
103
127
|
});
|
|
104
128
|
|
|
105
129
|
// src/constants/severity.ts
|
|
106
|
-
var SEVERITY_CRITICAL, SEVERITY_WARNING, SEVERITY_INFO, SEVERITY_ICON_MAP;
|
|
107
130
|
var init_severity = __esm({
|
|
108
131
|
"src/constants/severity.ts"() {
|
|
109
132
|
"use strict";
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// src/constants/telemetry.ts
|
|
137
|
+
var init_telemetry = __esm({
|
|
138
|
+
"src/constants/telemetry.ts"() {
|
|
139
|
+
"use strict";
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// src/constants/lifecycle.ts
|
|
144
|
+
var VALID_FINDING_STATES, VALID_AI_FIX_STATUSES, VALID_SECURITY_SEVERITIES;
|
|
145
|
+
var init_lifecycle = __esm({
|
|
146
|
+
"src/constants/lifecycle.ts"() {
|
|
147
|
+
"use strict";
|
|
148
|
+
VALID_FINDING_STATES = /* @__PURE__ */ new Set(["open", "fixing", "resolved"]);
|
|
149
|
+
VALID_AI_FIX_STATUSES = /* @__PURE__ */ new Set(["fixed", "wont_fix"]);
|
|
150
|
+
VALID_SECURITY_SEVERITIES = /* @__PURE__ */ new Set(["critical", "warning"]);
|
|
118
151
|
}
|
|
119
152
|
});
|
|
120
153
|
|
|
@@ -132,18 +165,53 @@ var init_constants = __esm({
|
|
|
132
165
|
init_mcp();
|
|
133
166
|
init_encoding();
|
|
134
167
|
init_severity();
|
|
168
|
+
init_telemetry();
|
|
169
|
+
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();
|
|
135
202
|
}
|
|
136
203
|
});
|
|
137
204
|
|
|
138
205
|
// src/store/finding-id.ts
|
|
139
206
|
import { createHash } from "crypto";
|
|
140
|
-
function
|
|
141
|
-
const key = `${
|
|
142
|
-
return createHash("sha256").update(key).digest("hex").slice(0,
|
|
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);
|
|
143
210
|
}
|
|
144
211
|
var init_finding_id = __esm({
|
|
145
212
|
"src/store/finding-id.ts"() {
|
|
146
213
|
"use strict";
|
|
214
|
+
init_limits();
|
|
147
215
|
}
|
|
148
216
|
});
|
|
149
217
|
|
|
@@ -213,6 +281,19 @@ var init_client = __esm({
|
|
|
213
281
|
if (state) url.searchParams.set("state", state);
|
|
214
282
|
return this.fetchJson(url);
|
|
215
283
|
}
|
|
284
|
+
async reportFix(findingId, status, notes) {
|
|
285
|
+
const res = await fetch(`${this.baseUrl}${DASHBOARD_API_FINDINGS_REPORT}`, {
|
|
286
|
+
method: "POST",
|
|
287
|
+
headers: { "Content-Type": "application/json" },
|
|
288
|
+
body: JSON.stringify({ findingId, status, notes }),
|
|
289
|
+
signal: AbortSignal.timeout(CLIENT_FETCH_TIMEOUT_MS)
|
|
290
|
+
});
|
|
291
|
+
if (!res.ok) return false;
|
|
292
|
+
const contentType = res.headers.get("content-type") ?? "";
|
|
293
|
+
if (!contentType.includes("application/json")) return false;
|
|
294
|
+
const body = await res.json();
|
|
295
|
+
return body.ok === true;
|
|
296
|
+
}
|
|
216
297
|
async clearAll() {
|
|
217
298
|
const res = await fetch(`${this.baseUrl}${DASHBOARD_API_CLEAR}`, {
|
|
218
299
|
method: "POST",
|
|
@@ -244,50 +325,56 @@ var init_client = __esm({
|
|
|
244
325
|
});
|
|
245
326
|
|
|
246
327
|
// src/mcp/discovery.ts
|
|
247
|
-
import {
|
|
248
|
-
import { resolve as resolve5, dirname } from "path";
|
|
249
|
-
function readPort(portPath) {
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
328
|
+
import { readFile as readFile6, readdir as readdir2, stat } from "fs/promises";
|
|
329
|
+
import { resolve as resolve5, dirname as dirname2 } from "path";
|
|
330
|
+
async function readPort(portPath) {
|
|
331
|
+
try {
|
|
332
|
+
const raw = (await readFile6(portPath, "utf-8")).trim();
|
|
333
|
+
const port = parseInt(raw, 10);
|
|
334
|
+
return isNaN(port) || port < PORT_MIN || port > PORT_MAX ? null : port;
|
|
335
|
+
} catch {
|
|
336
|
+
return null;
|
|
337
|
+
}
|
|
254
338
|
}
|
|
255
|
-
function portInDir(dir) {
|
|
339
|
+
async function portInDir(dir) {
|
|
256
340
|
return readPort(resolve5(dir, PORT_FILE));
|
|
257
341
|
}
|
|
258
|
-
function portInChildren(dir) {
|
|
342
|
+
async function portInChildren(dir) {
|
|
259
343
|
try {
|
|
260
|
-
|
|
344
|
+
const entries = await readdir2(dir);
|
|
345
|
+
for (const entry of entries) {
|
|
261
346
|
if (entry.startsWith(".") || entry === "node_modules") continue;
|
|
262
347
|
const child = resolve5(dir, entry);
|
|
263
348
|
try {
|
|
264
|
-
if (!
|
|
265
|
-
} catch {
|
|
349
|
+
if (!(await stat(child)).isDirectory()) continue;
|
|
350
|
+
} catch (err) {
|
|
351
|
+
brakitDebug(`discovery: stat failed for ${child}: ${err}`);
|
|
266
352
|
continue;
|
|
267
353
|
}
|
|
268
|
-
const port = portInDir(child);
|
|
354
|
+
const port = await portInDir(child);
|
|
269
355
|
if (port) return port;
|
|
270
356
|
}
|
|
271
|
-
} catch {
|
|
357
|
+
} catch (err) {
|
|
358
|
+
brakitDebug(`discovery: readdir failed for ${dir}: ${err}`);
|
|
272
359
|
}
|
|
273
360
|
return null;
|
|
274
361
|
}
|
|
275
|
-
function searchForPort(startDir) {
|
|
362
|
+
async function searchForPort(startDir) {
|
|
276
363
|
const start = resolve5(startDir);
|
|
277
|
-
const initial = portInDir(start) ?? portInChildren(start);
|
|
364
|
+
const initial = await portInDir(start) ?? await portInChildren(start);
|
|
278
365
|
if (initial) return initial;
|
|
279
|
-
let dir =
|
|
366
|
+
let dir = dirname2(start);
|
|
280
367
|
for (let depth = 0; depth < MAX_DISCOVERY_DEPTH; depth++) {
|
|
281
|
-
const port = portInDir(dir);
|
|
368
|
+
const port = await portInDir(dir) ?? await portInChildren(dir);
|
|
282
369
|
if (port) return port;
|
|
283
|
-
const parent =
|
|
370
|
+
const parent = dirname2(dir);
|
|
284
371
|
if (parent === dir) break;
|
|
285
372
|
dir = parent;
|
|
286
373
|
}
|
|
287
374
|
return null;
|
|
288
375
|
}
|
|
289
|
-
function discoverBrakitPort(cwd) {
|
|
290
|
-
const port = searchForPort(cwd ?? process.cwd());
|
|
376
|
+
async function discoverBrakitPort(cwd) {
|
|
377
|
+
const port = await searchForPort(cwd ?? process.cwd());
|
|
291
378
|
if (!port) {
|
|
292
379
|
throw new Error(
|
|
293
380
|
"Brakit is not running. Start your app with brakit enabled first."
|
|
@@ -299,7 +386,7 @@ async function waitForBrakit(cwd, timeoutMs = 1e4, pollMs = DISCOVERY_POLL_INTER
|
|
|
299
386
|
const deadline = Date.now() + timeoutMs;
|
|
300
387
|
while (Date.now() < deadline) {
|
|
301
388
|
try {
|
|
302
|
-
const result = discoverBrakitPort(cwd);
|
|
389
|
+
const result = await discoverBrakitPort(cwd);
|
|
303
390
|
const res = await fetch(`${result.baseUrl}${DASHBOARD_API_REQUESTS}?limit=1`);
|
|
304
391
|
if (res.ok) return result;
|
|
305
392
|
} catch {
|
|
@@ -314,50 +401,52 @@ var init_discovery = __esm({
|
|
|
314
401
|
"src/mcp/discovery.ts"() {
|
|
315
402
|
"use strict";
|
|
316
403
|
init_constants();
|
|
404
|
+
init_log();
|
|
317
405
|
init_mcp();
|
|
318
406
|
}
|
|
319
407
|
});
|
|
320
408
|
|
|
321
409
|
// src/mcp/enrichment.ts
|
|
322
|
-
import { createHash as createHash2 } from "crypto";
|
|
323
|
-
function computeInsightId(type, endpoint, desc) {
|
|
324
|
-
const key = `${type}:${endpoint}:${desc}`;
|
|
325
|
-
return createHash2("sha256").update(key).digest("hex").slice(0, 16);
|
|
326
|
-
}
|
|
327
410
|
async function enrichFindings(client) {
|
|
328
411
|
const [securityData, insightsData] = await Promise.all([
|
|
329
412
|
client.getSecurityFindings(),
|
|
330
413
|
client.getInsights()
|
|
331
414
|
]);
|
|
332
|
-
const
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
415
|
+
const contexts = await Promise.all(
|
|
416
|
+
securityData.findings.map(async (sf) => {
|
|
417
|
+
try {
|
|
418
|
+
const { path } = parseEndpointKey(sf.finding.endpoint);
|
|
419
|
+
const reqData = await client.getRequests({ search: path, limit: 1 });
|
|
420
|
+
if (reqData.requests.length > 0) {
|
|
421
|
+
const req = reqData.requests[0];
|
|
422
|
+
if (req.id) {
|
|
423
|
+
const activity = await client.getActivity(req.id);
|
|
424
|
+
const queryCount = activity.counts?.queries ?? 0;
|
|
425
|
+
const fetchCount = activity.counts?.fetches ?? 0;
|
|
426
|
+
return `Request took ${req.durationMs}ms. ${queryCount} DB queries, ${fetchCount} fetches.`;
|
|
427
|
+
}
|
|
345
428
|
}
|
|
429
|
+
} catch {
|
|
430
|
+
return "(context unavailable)";
|
|
346
431
|
}
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
432
|
+
return "";
|
|
433
|
+
})
|
|
434
|
+
);
|
|
435
|
+
const enriched = securityData.findings.map((sf, i) => {
|
|
436
|
+
const f = sf.finding;
|
|
437
|
+
return {
|
|
438
|
+
findingId: sf.findingId,
|
|
352
439
|
severity: f.severity,
|
|
353
440
|
title: f.title,
|
|
354
441
|
endpoint: f.endpoint,
|
|
355
442
|
description: f.desc,
|
|
356
443
|
hint: f.hint,
|
|
357
444
|
occurrences: f.count,
|
|
358
|
-
context
|
|
359
|
-
|
|
360
|
-
|
|
445
|
+
context: contexts[i],
|
|
446
|
+
aiStatus: sf.aiStatus,
|
|
447
|
+
aiNotes: sf.aiNotes
|
|
448
|
+
};
|
|
449
|
+
});
|
|
361
450
|
for (const si of insightsData.insights) {
|
|
362
451
|
if (si.state === "resolved") continue;
|
|
363
452
|
const i = si.insight;
|
|
@@ -371,7 +460,9 @@ async function enrichFindings(client) {
|
|
|
371
460
|
description: i.desc,
|
|
372
461
|
hint: i.hint,
|
|
373
462
|
occurrences: 1,
|
|
374
|
-
context: i.detail ?? ""
|
|
463
|
+
context: i.detail ?? "",
|
|
464
|
+
aiStatus: si.aiStatus,
|
|
465
|
+
aiNotes: si.aiNotes
|
|
375
466
|
});
|
|
376
467
|
}
|
|
377
468
|
return enriched;
|
|
@@ -433,13 +524,13 @@ var init_enrichment = __esm({
|
|
|
433
524
|
});
|
|
434
525
|
|
|
435
526
|
// src/mcp/tools/get-findings.ts
|
|
436
|
-
var
|
|
527
|
+
var getFindings;
|
|
437
528
|
var init_get_findings = __esm({
|
|
438
529
|
"src/mcp/tools/get-findings.ts"() {
|
|
439
530
|
"use strict";
|
|
440
531
|
init_enrichment();
|
|
441
|
-
|
|
442
|
-
|
|
532
|
+
init_lifecycle();
|
|
533
|
+
init_type_guards();
|
|
443
534
|
getFindings = {
|
|
444
535
|
name: "get_findings",
|
|
445
536
|
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,10 +552,10 @@ var init_get_findings = __esm({
|
|
|
461
552
|
async handler(client, args) {
|
|
462
553
|
const severity = args.severity;
|
|
463
554
|
const state = args.state;
|
|
464
|
-
if (severity && !
|
|
555
|
+
if (severity && !VALID_SECURITY_SEVERITIES.has(severity)) {
|
|
465
556
|
return { content: [{ type: "text", text: `Invalid severity "${severity}". Use: critical, warning.` }], isError: true };
|
|
466
557
|
}
|
|
467
|
-
if (state && !
|
|
558
|
+
if (state && !isValidFindingState(state)) {
|
|
468
559
|
return { content: [{ type: "text", text: `Invalid state "${state}". Use: open, fixing, resolved.` }], isError: true };
|
|
469
560
|
}
|
|
470
561
|
let findings = await enrichFindings(client);
|
|
@@ -483,10 +574,18 @@ var init_get_findings = __esm({
|
|
|
483
574
|
`];
|
|
484
575
|
for (const f of findings) {
|
|
485
576
|
lines.push(`[${f.severity.toUpperCase()}] ${f.title}`);
|
|
577
|
+
lines.push(` ID: ${f.findingId}`);
|
|
486
578
|
lines.push(` Endpoint: ${f.endpoint}`);
|
|
487
579
|
lines.push(` Issue: ${f.description}`);
|
|
488
580
|
if (f.context) lines.push(` Context: ${f.context}`);
|
|
489
581
|
lines.push(` Fix: ${f.hint}`);
|
|
582
|
+
if (f.aiStatus === "fixed") {
|
|
583
|
+
lines.push(` AI Status: fixed (awaiting verification)`);
|
|
584
|
+
if (f.aiNotes) lines.push(` AI Notes: ${f.aiNotes}`);
|
|
585
|
+
} else if (f.aiStatus === "wont_fix") {
|
|
586
|
+
lines.push(` AI Status: won't fix`);
|
|
587
|
+
if (f.aiNotes) lines.push(` AI Notes: ${f.aiNotes}`);
|
|
588
|
+
}
|
|
490
589
|
lines.push("");
|
|
491
590
|
}
|
|
492
591
|
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
@@ -815,6 +914,61 @@ var init_clear_findings = __esm({
|
|
|
815
914
|
}
|
|
816
915
|
});
|
|
817
916
|
|
|
917
|
+
// src/mcp/tools/report-fix.ts
|
|
918
|
+
var reportFix;
|
|
919
|
+
var init_report_fix = __esm({
|
|
920
|
+
"src/mcp/tools/report-fix.ts"() {
|
|
921
|
+
"use strict";
|
|
922
|
+
init_type_guards();
|
|
923
|
+
reportFix = {
|
|
924
|
+
name: "report_fix",
|
|
925
|
+
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).",
|
|
926
|
+
inputSchema: {
|
|
927
|
+
type: "object",
|
|
928
|
+
properties: {
|
|
929
|
+
finding_id: {
|
|
930
|
+
type: "string",
|
|
931
|
+
description: "The finding ID to report on"
|
|
932
|
+
},
|
|
933
|
+
status: {
|
|
934
|
+
type: "string",
|
|
935
|
+
description: "Whether the fix was applied or can't be fixed",
|
|
936
|
+
enum: ["fixed", "wont_fix"]
|
|
937
|
+
},
|
|
938
|
+
summary: {
|
|
939
|
+
type: "string",
|
|
940
|
+
description: "Brief description of what was done or why it can't be fixed"
|
|
941
|
+
}
|
|
942
|
+
},
|
|
943
|
+
required: ["finding_id", "status", "summary"]
|
|
944
|
+
},
|
|
945
|
+
async handler(client, args) {
|
|
946
|
+
const { finding_id, status, summary } = args;
|
|
947
|
+
if (!isNonEmptyString(finding_id)) {
|
|
948
|
+
return { content: [{ type: "text", text: "finding_id is required." }], isError: true };
|
|
949
|
+
}
|
|
950
|
+
if (!isValidAiFixStatus(status)) {
|
|
951
|
+
return { content: [{ type: "text", text: "status must be 'fixed' or 'wont_fix'." }], isError: true };
|
|
952
|
+
}
|
|
953
|
+
if (!isNonEmptyString(summary)) {
|
|
954
|
+
return { content: [{ type: "text", text: "summary is required." }], isError: true };
|
|
955
|
+
}
|
|
956
|
+
const ok = await client.reportFix(finding_id, status, summary);
|
|
957
|
+
if (!ok) {
|
|
958
|
+
return {
|
|
959
|
+
content: [{ type: "text", text: `Finding ${finding_id} not found. It may have already been resolved.` }],
|
|
960
|
+
isError: true
|
|
961
|
+
};
|
|
962
|
+
}
|
|
963
|
+
const label = status === "fixed" ? "marked as fixed (awaiting verification)" : "marked as won't fix";
|
|
964
|
+
return {
|
|
965
|
+
content: [{ type: "text", text: `Finding ${finding_id} ${label}. Dashboard updated.` }]
|
|
966
|
+
};
|
|
967
|
+
}
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
});
|
|
971
|
+
|
|
818
972
|
// src/mcp/tools/index.ts
|
|
819
973
|
function getToolDefinitions() {
|
|
820
974
|
return [...TOOL_MAP.values()].map((t) => ({
|
|
@@ -843,17 +997,19 @@ var init_tools = __esm({
|
|
|
843
997
|
init_verify_fix();
|
|
844
998
|
init_get_report();
|
|
845
999
|
init_clear_findings();
|
|
1000
|
+
init_report_fix();
|
|
846
1001
|
TOOL_MAP = new Map(
|
|
847
|
-
[getFindings, getEndpoints, getRequestDetail, verifyFix, getReport, clearFindings].map((t) => [t.name, t])
|
|
1002
|
+
[getFindings, getEndpoints, getRequestDetail, verifyFix, getReport, clearFindings, reportFix].map((t) => [t.name, t])
|
|
848
1003
|
);
|
|
849
1004
|
}
|
|
850
1005
|
});
|
|
851
1006
|
|
|
852
1007
|
// src/mcp/prompts.ts
|
|
853
|
-
var PROMPTS, PROMPT_MESSAGES;
|
|
1008
|
+
var SERVER_INSTRUCTIONS, PROMPTS, PROMPT_MESSAGES;
|
|
854
1009
|
var init_prompts = __esm({
|
|
855
1010
|
"src/mcp/prompts.ts"() {
|
|
856
1011
|
"use strict";
|
|
1012
|
+
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.";
|
|
857
1013
|
PROMPTS = [
|
|
858
1014
|
{
|
|
859
1015
|
name: "check-app",
|
|
@@ -865,18 +1021,8 @@ var init_prompts = __esm({
|
|
|
865
1021
|
}
|
|
866
1022
|
];
|
|
867
1023
|
PROMPT_MESSAGES = {
|
|
868
|
-
"check-app":
|
|
869
|
-
|
|
870
|
-
"First get all findings, then get the endpoint summary.",
|
|
871
|
-
"For any critical or warning findings, get the request detail to understand the root cause.",
|
|
872
|
-
"Give me a clear report of what's wrong and offer to fix each issue."
|
|
873
|
-
].join(" "),
|
|
874
|
-
"fix-findings": [
|
|
875
|
-
"Get all open brakit findings.",
|
|
876
|
-
"For each finding, get the request detail to understand the exact issue.",
|
|
877
|
-
"Then find the source code responsible and fix it.",
|
|
878
|
-
"After fixing, ask me to re-trigger the endpoint so you can verify the fix with brakit."
|
|
879
|
-
].join(" ")
|
|
1024
|
+
"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.",
|
|
1025
|
+
"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."
|
|
880
1026
|
};
|
|
881
1027
|
}
|
|
882
1028
|
});
|
|
@@ -904,7 +1050,7 @@ async function startMcpServer() {
|
|
|
904
1050
|
let cachedClient = discovery ? new BrakitClient(discovery.baseUrl) : null;
|
|
905
1051
|
const server = new Server(
|
|
906
1052
|
{ name: MCP_SERVER_NAME, version: MCP_SERVER_VERSION },
|
|
907
|
-
{ capabilities: { tools: {}, prompts: {} } }
|
|
1053
|
+
{ capabilities: { tools: {}, prompts: {} }, instructions: SERVER_INSTRUCTIONS }
|
|
908
1054
|
);
|
|
909
1055
|
server.setRequestHandler(ListPromptsRequestSchema, async () => ({
|
|
910
1056
|
prompts: [...PROMPTS]
|
|
@@ -982,27 +1128,19 @@ import { runMain } from "citty";
|
|
|
982
1128
|
|
|
983
1129
|
// src/cli/commands/install.ts
|
|
984
1130
|
import { defineCommand } from "citty";
|
|
985
|
-
import { resolve as resolve3, join as join2 } from "path";
|
|
986
|
-
import { readFile as
|
|
1131
|
+
import { resolve as resolve3, join as join2, dirname } from "path";
|
|
1132
|
+
import { readFile as readFile4, writeFile as writeFile3 } from "fs/promises";
|
|
987
1133
|
import { execSync } from "child_process";
|
|
1134
|
+
import { existsSync as existsSync5 } from "fs";
|
|
988
1135
|
import pc from "picocolors";
|
|
989
1136
|
|
|
990
1137
|
// src/store/finding-store.ts
|
|
991
|
-
|
|
1138
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
992
1139
|
import { readFileSync as readFileSync2, existsSync as existsSync3 } from "fs";
|
|
993
1140
|
import { resolve as resolve2 } from "path";
|
|
994
1141
|
|
|
995
|
-
// src/utils/atomic-writer.ts
|
|
996
|
-
import {
|
|
997
|
-
writeFileSync as writeFileSync2,
|
|
998
|
-
existsSync as existsSync2,
|
|
999
|
-
mkdirSync as mkdirSync2,
|
|
1000
|
-
renameSync
|
|
1001
|
-
} from "fs";
|
|
1002
|
-
import { writeFile as writeFile2, mkdir, rename } from "fs/promises";
|
|
1003
|
-
|
|
1004
1142
|
// src/utils/fs.ts
|
|
1005
|
-
import { access } from "fs/promises";
|
|
1143
|
+
import { access, readFile, writeFile } from "fs/promises";
|
|
1006
1144
|
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
1007
1145
|
import { resolve } from "path";
|
|
1008
1146
|
async function fileExists(path) {
|
|
@@ -1015,11 +1153,28 @@ async function fileExists(path) {
|
|
|
1015
1153
|
}
|
|
1016
1154
|
|
|
1017
1155
|
// src/store/finding-store.ts
|
|
1156
|
+
init_constants();
|
|
1157
|
+
init_limits();
|
|
1158
|
+
|
|
1159
|
+
// src/utils/atomic-writer.ts
|
|
1160
|
+
import {
|
|
1161
|
+
writeFileSync as writeFileSync2,
|
|
1162
|
+
existsSync as existsSync2,
|
|
1163
|
+
mkdirSync as mkdirSync2,
|
|
1164
|
+
renameSync
|
|
1165
|
+
} from "fs";
|
|
1166
|
+
import { writeFile as writeFile2, mkdir, rename } from "fs/promises";
|
|
1167
|
+
init_log();
|
|
1168
|
+
init_type_guards();
|
|
1169
|
+
|
|
1170
|
+
// src/store/finding-store.ts
|
|
1171
|
+
init_log();
|
|
1018
1172
|
init_finding_id();
|
|
1019
1173
|
|
|
1020
1174
|
// src/detect/project.ts
|
|
1021
|
-
import { readFile as
|
|
1022
|
-
import {
|
|
1175
|
+
import { readFile as readFile3, readdir } from "fs/promises";
|
|
1176
|
+
import { existsSync as existsSync4 } from "fs";
|
|
1177
|
+
import { join, relative } from "path";
|
|
1023
1178
|
var FRAMEWORKS = [
|
|
1024
1179
|
{ name: "nextjs", dep: "next", devCmd: "next dev", bin: "next", defaultPort: 3e3, devArgs: ["dev", "--port"] },
|
|
1025
1180
|
{ name: "remix", dep: "@remix-run/dev", devCmd: "remix dev", bin: "remix", defaultPort: 3e3, devArgs: ["dev"] },
|
|
@@ -1029,22 +1184,14 @@ var FRAMEWORKS = [
|
|
|
1029
1184
|
];
|
|
1030
1185
|
async function detectProject(rootDir) {
|
|
1031
1186
|
const pkgPath = join(rootDir, "package.json");
|
|
1032
|
-
const raw = await
|
|
1187
|
+
const raw = await readFile3(pkgPath, "utf-8");
|
|
1033
1188
|
const pkg = JSON.parse(raw);
|
|
1034
1189
|
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
if (allDeps[f.dep]) {
|
|
1041
|
-
framework = f.name;
|
|
1042
|
-
devCommand = f.devCmd;
|
|
1043
|
-
devBin = join(rootDir, "node_modules", ".bin", f.bin);
|
|
1044
|
-
defaultPort = f.defaultPort;
|
|
1045
|
-
break;
|
|
1046
|
-
}
|
|
1047
|
-
}
|
|
1190
|
+
const framework = detectFrameworkFromDeps(allDeps);
|
|
1191
|
+
const matched = FRAMEWORKS.find((f) => f.name === framework);
|
|
1192
|
+
const devCommand = matched?.devCmd ?? "";
|
|
1193
|
+
const devBin = matched ? join(rootDir, "node_modules", ".bin", matched.bin) : "";
|
|
1194
|
+
const defaultPort = matched?.defaultPort ?? 3e3;
|
|
1048
1195
|
const packageManager = await detectPackageManager(rootDir);
|
|
1049
1196
|
return { framework, devCommand, devBin, defaultPort, packageManager };
|
|
1050
1197
|
}
|
|
@@ -1056,16 +1203,149 @@ async function detectPackageManager(rootDir) {
|
|
|
1056
1203
|
if (await fileExists(join(rootDir, "package-lock.json"))) return "npm";
|
|
1057
1204
|
return "unknown";
|
|
1058
1205
|
}
|
|
1206
|
+
function detectFrameworkFromDeps(allDeps) {
|
|
1207
|
+
for (const f of FRAMEWORKS) {
|
|
1208
|
+
if (allDeps[f.dep]) return f.name;
|
|
1209
|
+
}
|
|
1210
|
+
return "unknown";
|
|
1211
|
+
}
|
|
1212
|
+
var PYTHON_ENTRY_CANDIDATES = [
|
|
1213
|
+
"app.py",
|
|
1214
|
+
"main.py",
|
|
1215
|
+
"wsgi.py",
|
|
1216
|
+
"asgi.py",
|
|
1217
|
+
"server.py",
|
|
1218
|
+
"run.py",
|
|
1219
|
+
"manage.py",
|
|
1220
|
+
"app/__init__.py"
|
|
1221
|
+
];
|
|
1222
|
+
var PYTHON_FRAMEWORK_MAP = {
|
|
1223
|
+
flask: "flask",
|
|
1224
|
+
fastapi: "fastapi",
|
|
1225
|
+
django: "django"
|
|
1226
|
+
};
|
|
1227
|
+
var PYTHON_DEFAULT_PORTS = {
|
|
1228
|
+
flask: 5e3,
|
|
1229
|
+
fastapi: 8e3,
|
|
1230
|
+
django: 8e3,
|
|
1231
|
+
unknown: 8e3
|
|
1232
|
+
};
|
|
1233
|
+
async function detectPythonProject(rootDir) {
|
|
1234
|
+
const hasPyproject = await fileExists(join(rootDir, "pyproject.toml"));
|
|
1235
|
+
const hasRequirements = await fileExists(join(rootDir, "requirements.txt"));
|
|
1236
|
+
const hasSetupPy = await fileExists(join(rootDir, "setup.py"));
|
|
1237
|
+
if (!hasPyproject && !hasRequirements && !hasSetupPy) return null;
|
|
1238
|
+
const framework = await detectPythonFramework(rootDir, hasPyproject, hasRequirements);
|
|
1239
|
+
const packageManager = await detectPythonPackageManager(rootDir);
|
|
1240
|
+
const entryFile = await detectPythonEntry(rootDir);
|
|
1241
|
+
return {
|
|
1242
|
+
framework,
|
|
1243
|
+
packageManager,
|
|
1244
|
+
entryFile,
|
|
1245
|
+
defaultPort: PYTHON_DEFAULT_PORTS[framework] ?? 8e3
|
|
1246
|
+
};
|
|
1247
|
+
}
|
|
1248
|
+
async function detectPythonFramework(rootDir, hasPyproject, hasRequirements) {
|
|
1249
|
+
if (hasPyproject) {
|
|
1250
|
+
try {
|
|
1251
|
+
const content = await readFile3(join(rootDir, "pyproject.toml"), "utf-8");
|
|
1252
|
+
for (const [dep, fw] of Object.entries(PYTHON_FRAMEWORK_MAP)) {
|
|
1253
|
+
if (content.includes(`"${dep}"`) || content.includes(`'${dep}'`) || content.includes(`${dep} `)) {
|
|
1254
|
+
return fw;
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
} catch {
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
if (hasRequirements) {
|
|
1261
|
+
try {
|
|
1262
|
+
const content = await readFile3(join(rootDir, "requirements.txt"), "utf-8");
|
|
1263
|
+
const lines = content.toLowerCase().split("\n");
|
|
1264
|
+
for (const [dep, fw] of Object.entries(PYTHON_FRAMEWORK_MAP)) {
|
|
1265
|
+
if (lines.some((l) => l.startsWith(dep) && (l.length === dep.length || /[=<>~![]/u.test(l[dep.length])))) {
|
|
1266
|
+
return fw;
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
} catch {
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
return "unknown";
|
|
1273
|
+
}
|
|
1274
|
+
async function detectPythonPackageManager(rootDir) {
|
|
1275
|
+
if (await fileExists(join(rootDir, "uv.lock"))) return "uv";
|
|
1276
|
+
if (await fileExists(join(rootDir, "poetry.lock"))) return "poetry";
|
|
1277
|
+
if (await fileExists(join(rootDir, "Pipfile.lock"))) return "pipenv";
|
|
1278
|
+
if (await fileExists(join(rootDir, "Pipfile"))) return "pipenv";
|
|
1279
|
+
if (await fileExists(join(rootDir, "requirements.txt"))) return "pip";
|
|
1280
|
+
try {
|
|
1281
|
+
const content = await readFile3(join(rootDir, "pyproject.toml"), "utf-8");
|
|
1282
|
+
if (content.includes("[tool.poetry]")) return "poetry";
|
|
1283
|
+
if (content.includes("[tool.uv]")) return "uv";
|
|
1284
|
+
} catch {
|
|
1285
|
+
}
|
|
1286
|
+
return "unknown";
|
|
1287
|
+
}
|
|
1288
|
+
async function detectPythonEntry(rootDir) {
|
|
1289
|
+
for (const candidate of PYTHON_ENTRY_CANDIDATES) {
|
|
1290
|
+
if (await fileExists(join(rootDir, candidate))) {
|
|
1291
|
+
return candidate;
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
return null;
|
|
1295
|
+
}
|
|
1296
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
1297
|
+
"node_modules",
|
|
1298
|
+
".git",
|
|
1299
|
+
".brakit",
|
|
1300
|
+
"dist",
|
|
1301
|
+
"build",
|
|
1302
|
+
"__pycache__",
|
|
1303
|
+
".venv",
|
|
1304
|
+
"venv",
|
|
1305
|
+
".next",
|
|
1306
|
+
".nuxt",
|
|
1307
|
+
".output",
|
|
1308
|
+
".cache",
|
|
1309
|
+
"coverage"
|
|
1310
|
+
]);
|
|
1311
|
+
async function scanForProjects(rootDir) {
|
|
1312
|
+
const projects = [];
|
|
1313
|
+
await detectInDir(rootDir, rootDir, projects);
|
|
1314
|
+
try {
|
|
1315
|
+
const entries = await readdir(rootDir, { withFileTypes: true });
|
|
1316
|
+
for (const entry of entries) {
|
|
1317
|
+
if (!entry.isDirectory() || SKIP_DIRS.has(entry.name) || entry.name.startsWith(".")) continue;
|
|
1318
|
+
const childDir = join(rootDir, entry.name);
|
|
1319
|
+
await detectInDir(childDir, rootDir, projects);
|
|
1320
|
+
}
|
|
1321
|
+
} catch {
|
|
1322
|
+
}
|
|
1323
|
+
return projects;
|
|
1324
|
+
}
|
|
1325
|
+
async function detectInDir(dir, rootDir, projects) {
|
|
1326
|
+
const rel = dir === rootDir ? "." : `./${relative(rootDir, dir)}`;
|
|
1327
|
+
if (await fileExists(join(dir, "package.json"))) {
|
|
1328
|
+
try {
|
|
1329
|
+
const node = await detectProject(dir);
|
|
1330
|
+
projects.push({ dir, relDir: rel, type: "node", node });
|
|
1331
|
+
} catch {
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
const python = await detectPythonProject(dir);
|
|
1335
|
+
if (python) {
|
|
1336
|
+
projects.push({ dir, relDir: rel, type: "python", python });
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1059
1339
|
|
|
1060
1340
|
// src/analysis/rules/patterns.ts
|
|
1061
1341
|
var SECRET_KEYS = /^(password|passwd|secret|api_key|apiKey|api_secret|apiSecret|private_key|privateKey|client_secret|clientSecret)$/;
|
|
1062
1342
|
var TOKEN_PARAMS = /^(token|api_key|apiKey|secret|password|access_token|session_id|sessionId)$/;
|
|
1063
1343
|
var SAFE_PARAMS = /^(_rsc|__clerk_handshake|__clerk_db_jwt|callback|code|state|nonce|redirect_uri|utm_|fbclid|gclid)$/;
|
|
1064
|
-
var STACK_TRACE_RE = /at\s+.+\(.+:\d+:\d+\)|at\s+Module\._compile|at\s+Object\.<anonymous>|at\s+processTicksAndRejections
|
|
1344
|
+
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+/;
|
|
1065
1345
|
var DB_CONN_RE = /(postgres|mysql|mongodb|redis):\/\//;
|
|
1066
1346
|
var SQL_FRAGMENT_RE = /\b(SELECT\s+[\w.*]+\s+FROM|INSERT\s+INTO|UPDATE\s+\w+\s+SET|DELETE\s+FROM)\b/i;
|
|
1067
|
-
var SECRET_VAL_RE = /(api_key|apiKey|secret|token)\s*[:=]\s*["']?[A-Za-z0-9_
|
|
1068
|
-
var LOG_SECRET_RE = /(password|secret|token|api_key|apiKey)\s*[:=]\s*["']?[A-Za-z0-9_
|
|
1347
|
+
var SECRET_VAL_RE = /(api_key|apiKey|secret|token)\s*[:=]\s*["']?[A-Za-z0-9_\-.+/]{8,}/;
|
|
1348
|
+
var LOG_SECRET_RE = /(password|secret|token|api_key|apiKey)\s*[:=]\s*["']?[A-Za-z0-9_\-.+/]{8,}/i;
|
|
1069
1349
|
var MASKED_RE = /^\*+$|\[REDACTED\]|\[FILTERED\]|CHANGE_ME|^x{3,}$/i;
|
|
1070
1350
|
var EMAIL_RE = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/;
|
|
1071
1351
|
var INTERNAL_ID_KEYS = /^(id|_id|userId|user_id|createdBy|updatedBy|organizationId|org_id|tenantId|tenant_id)$/;
|
|
@@ -1075,9 +1355,9 @@ var RULE_HINTS = {
|
|
|
1075
1355
|
"token-in-url": "Pass tokens in the Authorization header, not URL query parameters.",
|
|
1076
1356
|
"stack-trace-leak": "Use a custom error handler that returns generic messages in production.",
|
|
1077
1357
|
"error-info-leak": "Sanitize error responses. Return generic messages instead of internal details.",
|
|
1358
|
+
"insecure-cookie": "Set HttpOnly and SameSite flags on all cookies.",
|
|
1078
1359
|
"sensitive-logs": "Redact PII before logging. Never log passwords or tokens.",
|
|
1079
1360
|
"cors-credentials": "Cannot use credentials:true with origin:*. Specify explicit origins.",
|
|
1080
|
-
"insecure-cookie": "Set HttpOnly and SameSite flags on all cookies.",
|
|
1081
1361
|
"response-pii-leak": "API responses should return minimal data. Don't echo back full user records \u2014 select only the fields the client needs."
|
|
1082
1362
|
};
|
|
1083
1363
|
|
|
@@ -1448,48 +1728,47 @@ function hasInternalIds(obj) {
|
|
|
1448
1728
|
}
|
|
1449
1729
|
return false;
|
|
1450
1730
|
}
|
|
1451
|
-
function
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
return { reason: "echo", emailCount: echoed.length };
|
|
1462
|
-
}
|
|
1463
|
-
}
|
|
1464
|
-
}
|
|
1731
|
+
function detectEchoPII(method, reqBody, target) {
|
|
1732
|
+
if (!WRITE_METHODS.has(method) || !reqBody || typeof reqBody !== "object") return null;
|
|
1733
|
+
const reqEmails = findEmails(reqBody);
|
|
1734
|
+
if (reqEmails.length === 0) return null;
|
|
1735
|
+
const resEmails = findEmails(target);
|
|
1736
|
+
const echoed = reqEmails.filter((e) => resEmails.includes(e));
|
|
1737
|
+
if (echoed.length === 0) return null;
|
|
1738
|
+
const inspectObj = Array.isArray(target) && target.length > 0 ? target[0] : target;
|
|
1739
|
+
if (hasInternalIds(inspectObj) || topLevelFieldCount(inspectObj) >= FULL_RECORD_MIN_FIELDS) {
|
|
1740
|
+
return { reason: "echo", emailCount: echoed.length };
|
|
1465
1741
|
}
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1742
|
+
return null;
|
|
1743
|
+
}
|
|
1744
|
+
function detectFullRecordPII(target) {
|
|
1745
|
+
if (!target || typeof target !== "object" || Array.isArray(target)) return null;
|
|
1746
|
+
const fields = topLevelFieldCount(target);
|
|
1747
|
+
if (fields < FULL_RECORD_MIN_FIELDS || !hasInternalIds(target)) return null;
|
|
1748
|
+
const emails = findEmails(target);
|
|
1749
|
+
if (emails.length === 0) return null;
|
|
1750
|
+
return { reason: "full-record", emailCount: emails.length };
|
|
1751
|
+
}
|
|
1752
|
+
function detectListPII(target) {
|
|
1753
|
+
if (!Array.isArray(target) || target.length < LIST_PII_MIN_ITEMS) return null;
|
|
1754
|
+
let itemsWithEmail = 0;
|
|
1755
|
+
for (let i = 0; i < Math.min(target.length, 10); i++) {
|
|
1756
|
+
const item = target[i];
|
|
1757
|
+
if (item && typeof item === "object" && findEmails(item).length > 0) {
|
|
1758
|
+
itemsWithEmail++;
|
|
1473
1759
|
}
|
|
1474
1760
|
}
|
|
1475
|
-
if (
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
if (item && typeof item === "object") {
|
|
1480
|
-
const emails = findEmails(item);
|
|
1481
|
-
if (emails.length > 0) itemsWithEmail++;
|
|
1482
|
-
}
|
|
1483
|
-
}
|
|
1484
|
-
if (itemsWithEmail >= LIST_PII_MIN_ITEMS) {
|
|
1485
|
-
const first = target[0];
|
|
1486
|
-
if (hasInternalIds(first) || topLevelFieldCount(first) >= FULL_RECORD_MIN_FIELDS) {
|
|
1487
|
-
return { reason: "list-pii", emailCount: itemsWithEmail };
|
|
1488
|
-
}
|
|
1489
|
-
}
|
|
1761
|
+
if (itemsWithEmail < LIST_PII_MIN_ITEMS) return null;
|
|
1762
|
+
const first = target[0];
|
|
1763
|
+
if (hasInternalIds(first) || topLevelFieldCount(first) >= FULL_RECORD_MIN_FIELDS) {
|
|
1764
|
+
return { reason: "list-pii", emailCount: itemsWithEmail };
|
|
1490
1765
|
}
|
|
1491
1766
|
return null;
|
|
1492
1767
|
}
|
|
1768
|
+
function detectPII(method, reqBody, resBody) {
|
|
1769
|
+
const target = unwrapResponse(resBody);
|
|
1770
|
+
return detectEchoPII(method, reqBody, target) ?? detectFullRecordPII(target) ?? detectListPII(target);
|
|
1771
|
+
}
|
|
1493
1772
|
var REASON_LABELS = {
|
|
1494
1773
|
echo: "echoes back PII from the request body",
|
|
1495
1774
|
"full-record": "returns a full record with email and internal IDs",
|
|
@@ -1533,6 +1812,9 @@ var responsePiiLeakRule = {
|
|
|
1533
1812
|
}
|
|
1534
1813
|
};
|
|
1535
1814
|
|
|
1815
|
+
// src/analysis/engine.ts
|
|
1816
|
+
init_limits();
|
|
1817
|
+
|
|
1536
1818
|
// src/analysis/group.ts
|
|
1537
1819
|
init_constants();
|
|
1538
1820
|
import { randomUUID } from "crypto";
|
|
@@ -1590,14 +1872,60 @@ init_constants();
|
|
|
1590
1872
|
|
|
1591
1873
|
// src/analysis/insight-tracker.ts
|
|
1592
1874
|
init_endpoint();
|
|
1875
|
+
init_finding_id();
|
|
1593
1876
|
init_thresholds();
|
|
1594
1877
|
|
|
1595
1878
|
// src/index.ts
|
|
1596
|
-
var VERSION = "0.8.
|
|
1879
|
+
var VERSION = "0.8.5";
|
|
1597
1880
|
|
|
1598
1881
|
// src/cli/commands/install.ts
|
|
1882
|
+
init_constants();
|
|
1883
|
+
|
|
1884
|
+
// src/cli/templates.ts
|
|
1599
1885
|
var IMPORT_LINE = `import "brakit";`;
|
|
1600
1886
|
var IMPORT_MARKER = "brakit";
|
|
1887
|
+
var CREATED_FILES = [
|
|
1888
|
+
"src/instrumentation.ts",
|
|
1889
|
+
"instrumentation.ts",
|
|
1890
|
+
"server/plugins/brakit.ts"
|
|
1891
|
+
];
|
|
1892
|
+
var ENTRY_CANDIDATES = [
|
|
1893
|
+
"src/index.ts",
|
|
1894
|
+
"src/server.ts",
|
|
1895
|
+
"src/app.ts",
|
|
1896
|
+
"src/index.js",
|
|
1897
|
+
"src/server.js",
|
|
1898
|
+
"src/app.js",
|
|
1899
|
+
"server.ts",
|
|
1900
|
+
"app.ts",
|
|
1901
|
+
"index.ts",
|
|
1902
|
+
"server.js",
|
|
1903
|
+
"app.js",
|
|
1904
|
+
"index.js"
|
|
1905
|
+
];
|
|
1906
|
+
var BRAKIT_TEMPLATES = {
|
|
1907
|
+
nextjs: [
|
|
1908
|
+
`export async function register() {`,
|
|
1909
|
+
` if (process.env.NODE_ENV !== "production") {`,
|
|
1910
|
+
` try { await import("brakit"); } catch {}`,
|
|
1911
|
+
` }`,
|
|
1912
|
+
`}`
|
|
1913
|
+
].join("\n"),
|
|
1914
|
+
nuxt: `import "brakit";`
|
|
1915
|
+
};
|
|
1916
|
+
var ALL_TEMPLATES = Object.values(BRAKIT_TEMPLATES);
|
|
1917
|
+
function normalize(content) {
|
|
1918
|
+
return content.split("\n").map((line) => line.trim()).filter((line) => line.length > 0).join("\n");
|
|
1919
|
+
}
|
|
1920
|
+
function isExactBrakitTemplate(fileContent) {
|
|
1921
|
+
const normalizedFile = normalize(fileContent);
|
|
1922
|
+
if (!normalizedFile) return false;
|
|
1923
|
+
return ALL_TEMPLATES.some(
|
|
1924
|
+
(template) => normalize(template) === normalizedFile
|
|
1925
|
+
);
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1928
|
+
// src/cli/commands/install.ts
|
|
1601
1929
|
var install_default = defineCommand({
|
|
1602
1930
|
meta: {
|
|
1603
1931
|
name: "brakit install",
|
|
@@ -1614,68 +1942,77 @@ var install_default = defineCommand({
|
|
|
1614
1942
|
},
|
|
1615
1943
|
async run({ args }) {
|
|
1616
1944
|
const rootDir = resolve3(args.dir);
|
|
1617
|
-
const pkgPath = join2(rootDir, "package.json");
|
|
1618
|
-
if (!await fileExists(pkgPath)) {
|
|
1619
|
-
console.error(pc.red(" No project found. Run this from your project directory."));
|
|
1620
|
-
process.exit(1);
|
|
1621
|
-
}
|
|
1622
|
-
let pkg;
|
|
1623
|
-
try {
|
|
1624
|
-
pkg = JSON.parse(await readFile3(pkgPath, "utf-8"));
|
|
1625
|
-
} catch {
|
|
1626
|
-
console.error(pc.red(" Failed to read package.json."));
|
|
1627
|
-
process.exit(1);
|
|
1628
|
-
}
|
|
1629
|
-
if (!pkg.name || typeof pkg.name !== "string") {
|
|
1630
|
-
console.error(pc.red(" No project found. Run this from your project directory."));
|
|
1631
|
-
process.exit(1);
|
|
1632
|
-
}
|
|
1633
|
-
let project;
|
|
1634
|
-
try {
|
|
1635
|
-
project = await detectProject(rootDir);
|
|
1636
|
-
} catch {
|
|
1637
|
-
console.error(pc.red(" Failed to read package.json."));
|
|
1638
|
-
process.exit(1);
|
|
1639
|
-
}
|
|
1640
1945
|
console.log();
|
|
1641
1946
|
console.log(pc.bold(" \u25C6 brakit install"));
|
|
1642
1947
|
console.log();
|
|
1643
|
-
const
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
const result = await setupInstrumentation(rootDir, project.framework);
|
|
1650
|
-
if (result.action === "created") {
|
|
1651
|
-
console.log(pc.green(` \u2713 Created ${result.file}`));
|
|
1652
|
-
if (result.content) {
|
|
1948
|
+
const projects = await scanForProjects(rootDir);
|
|
1949
|
+
const nodeProjects = projects.filter((p) => p.type === "node");
|
|
1950
|
+
const pythonProjects = projects.filter((p) => p.type === "python");
|
|
1951
|
+
if (nodeProjects.length === 0) {
|
|
1952
|
+
if (pythonProjects.length > 0) {
|
|
1953
|
+
console.log(pc.dim(" Python project detected. To add brakit:"));
|
|
1653
1954
|
console.log();
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1955
|
+
console.log(pc.bold(" pip install brakit"));
|
|
1956
|
+
console.log(pc.dim(" Then add to the top of your entry file:"));
|
|
1957
|
+
console.log(pc.bold(" import brakit # noqa: F401"));
|
|
1958
|
+
console.log();
|
|
1959
|
+
} else {
|
|
1960
|
+
console.error(pc.red(" No project found. Run this from your project directory."));
|
|
1657
1961
|
}
|
|
1658
|
-
|
|
1659
|
-
console.log(pc.green(` \u2713 Added import to ${result.file}`));
|
|
1660
|
-
} else if (result.action === "exists") {
|
|
1661
|
-
console.log(pc.dim(` \u2713 ${result.file} already has brakit import`));
|
|
1662
|
-
} else {
|
|
1663
|
-
printManualInstructions(project.framework);
|
|
1962
|
+
process.exit(1);
|
|
1664
1963
|
}
|
|
1964
|
+
for (const p of nodeProjects) {
|
|
1965
|
+
const node = p.node;
|
|
1966
|
+
const suffix = p.relDir === "." ? "" : ` in ${p.relDir}`;
|
|
1967
|
+
const installed = await installPackage(p.dir, node.packageManager);
|
|
1968
|
+
if (installed) {
|
|
1969
|
+
console.log(pc.green(` \u2713 Added brakit to devDependencies${suffix}`));
|
|
1970
|
+
} else {
|
|
1971
|
+
console.log(pc.dim(` \u2713 brakit already in dependencies${suffix}`));
|
|
1972
|
+
}
|
|
1973
|
+
const result = await setupInstrumentation(p.dir, node.framework);
|
|
1974
|
+
const prefix = p.relDir === "." ? "" : `${p.relDir}/`;
|
|
1975
|
+
if (result.action === "created") {
|
|
1976
|
+
console.log(pc.green(` \u2713 Created ${prefix}${result.file}`));
|
|
1977
|
+
} else if (result.action === "prepended") {
|
|
1978
|
+
console.log(pc.green(` \u2713 Added import to ${prefix}${result.file}`));
|
|
1979
|
+
} else if (result.action === "exists") {
|
|
1980
|
+
console.log(pc.dim(` \u2713 ${prefix}${result.file} already has brakit import`));
|
|
1981
|
+
} else {
|
|
1982
|
+
printManualInstructions(node.framework);
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1985
|
+
await ensureGitignoreEntry(rootDir, METRICS_DIR);
|
|
1665
1986
|
const mcpResult = await setupMcp(rootDir);
|
|
1666
1987
|
if (mcpResult === "created" || mcpResult === "updated") {
|
|
1667
1988
|
console.log(pc.green(" \u2713 Configured MCP for Claude Code / Cursor"));
|
|
1668
1989
|
} else if (mcpResult === "exists") {
|
|
1669
1990
|
console.log(pc.dim(" \u2713 MCP already configured"));
|
|
1670
1991
|
}
|
|
1992
|
+
const gitRoot = findGitRoot(rootDir);
|
|
1993
|
+
if (gitRoot && gitRoot !== rootDir) {
|
|
1994
|
+
const parentMcpResult = await setupMcp(gitRoot);
|
|
1995
|
+
if (parentMcpResult === "created" || parentMcpResult === "updated") {
|
|
1996
|
+
console.log(pc.green(" \u2713 Configured MCP at project root"));
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1671
1999
|
console.log();
|
|
2000
|
+
const port = nodeProjects[0].node?.defaultPort ?? 3e3;
|
|
1672
2001
|
console.log(pc.dim(" Start your app and visit:"));
|
|
1673
|
-
console.log(pc.bold(
|
|
2002
|
+
console.log(pc.bold(` http://localhost:${port}/__brakit`));
|
|
2003
|
+
if (pythonProjects.length > 0) {
|
|
2004
|
+
const pyLabel = pythonProjects.map((p) => p.relDir).join(", ");
|
|
2005
|
+
console.log();
|
|
2006
|
+
console.log(pc.dim(` Python backend detected (${pyLabel}). To capture telemetry:`));
|
|
2007
|
+
console.log(pc.bold(" pip install brakit"));
|
|
2008
|
+
console.log(pc.dim(" Then add to the top of your entry file:"));
|
|
2009
|
+
console.log(pc.bold(" import brakit # noqa: F401"));
|
|
2010
|
+
}
|
|
1674
2011
|
console.log();
|
|
1675
2012
|
}
|
|
1676
2013
|
});
|
|
1677
2014
|
async function installPackage(rootDir, pm) {
|
|
1678
|
-
const pkgRaw = await
|
|
2015
|
+
const pkgRaw = await readFile4(join2(rootDir, "package.json"), "utf-8");
|
|
1679
2016
|
const pkg = JSON.parse(pkgRaw);
|
|
1680
2017
|
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
1681
2018
|
if (allDeps["brakit"]) return false;
|
|
@@ -1690,6 +2027,7 @@ async function installPackage(rootDir, pm) {
|
|
|
1690
2027
|
execSync(cmd, { cwd: rootDir, stdio: "pipe" });
|
|
1691
2028
|
} catch {
|
|
1692
2029
|
console.warn(pc.yellow(` \u26A0 Failed to run "${cmd}". Install brakit manually.`));
|
|
2030
|
+
return false;
|
|
1693
2031
|
}
|
|
1694
2032
|
return true;
|
|
1695
2033
|
}
|
|
@@ -1710,20 +2048,13 @@ async function setupNextjs(rootDir) {
|
|
|
1710
2048
|
const relPath = hasSrc ? "src/instrumentation.ts" : "instrumentation.ts";
|
|
1711
2049
|
const absPath = join2(rootDir, relPath);
|
|
1712
2050
|
if (await fileExists(absPath)) {
|
|
1713
|
-
const content2 = await
|
|
2051
|
+
const content2 = await readFile4(absPath, "utf-8");
|
|
1714
2052
|
if (content2.includes(IMPORT_MARKER)) {
|
|
1715
2053
|
return { action: "exists", file: relPath };
|
|
1716
2054
|
}
|
|
1717
2055
|
return { action: "manual", file: relPath };
|
|
1718
2056
|
}
|
|
1719
|
-
const content =
|
|
1720
|
-
`export async function register() {`,
|
|
1721
|
-
` if (process.env.NODE_ENV !== "production") {`,
|
|
1722
|
-
` try { await import("brakit"); } catch {}`,
|
|
1723
|
-
` }`,
|
|
1724
|
-
`}`,
|
|
1725
|
-
``
|
|
1726
|
-
].join("\n");
|
|
2057
|
+
const content = BRAKIT_TEMPLATES.nextjs + "\n";
|
|
1727
2058
|
await writeFile3(absPath, content);
|
|
1728
2059
|
return { action: "created", file: relPath, content };
|
|
1729
2060
|
}
|
|
@@ -1731,14 +2062,13 @@ async function setupNuxt(rootDir) {
|
|
|
1731
2062
|
const relPath = "server/plugins/brakit.ts";
|
|
1732
2063
|
const absPath = join2(rootDir, relPath);
|
|
1733
2064
|
if (await fileExists(absPath)) {
|
|
1734
|
-
const content2 = await
|
|
2065
|
+
const content2 = await readFile4(absPath, "utf-8");
|
|
1735
2066
|
if (content2.includes(IMPORT_MARKER)) {
|
|
1736
2067
|
return { action: "exists", file: relPath };
|
|
1737
2068
|
}
|
|
1738
2069
|
return { action: "manual", file: relPath };
|
|
1739
2070
|
}
|
|
1740
|
-
const content =
|
|
1741
|
-
`;
|
|
2071
|
+
const content = BRAKIT_TEMPLATES.nuxt + "\n";
|
|
1742
2072
|
const dir = join2(rootDir, "server/plugins");
|
|
1743
2073
|
const { mkdirSync: mkdirSync3 } = await import("fs");
|
|
1744
2074
|
mkdirSync3(dir, { recursive: true });
|
|
@@ -1749,7 +2079,7 @@ async function setupPrepend(rootDir, ...candidates) {
|
|
|
1749
2079
|
for (const relPath of candidates) {
|
|
1750
2080
|
const absPath = join2(rootDir, relPath);
|
|
1751
2081
|
if (!await fileExists(absPath)) continue;
|
|
1752
|
-
const content = await
|
|
2082
|
+
const content = await readFile4(absPath, "utf-8");
|
|
1753
2083
|
if (content.includes(IMPORT_MARKER)) {
|
|
1754
2084
|
return { action: "exists", file: relPath };
|
|
1755
2085
|
}
|
|
@@ -1759,23 +2089,9 @@ ${content}`);
|
|
|
1759
2089
|
}
|
|
1760
2090
|
return { action: "manual", file: null };
|
|
1761
2091
|
}
|
|
1762
|
-
var ENTRY_CANDIDATES = [
|
|
1763
|
-
"src/index.ts",
|
|
1764
|
-
"src/server.ts",
|
|
1765
|
-
"src/app.ts",
|
|
1766
|
-
"src/index.js",
|
|
1767
|
-
"src/server.js",
|
|
1768
|
-
"src/app.js",
|
|
1769
|
-
"server.ts",
|
|
1770
|
-
"app.ts",
|
|
1771
|
-
"index.ts",
|
|
1772
|
-
"server.js",
|
|
1773
|
-
"app.js",
|
|
1774
|
-
"index.js"
|
|
1775
|
-
];
|
|
1776
2092
|
async function setupGeneric(rootDir) {
|
|
1777
2093
|
try {
|
|
1778
|
-
const pkgRaw = await
|
|
2094
|
+
const pkgRaw = await readFile4(join2(rootDir, "package.json"), "utf-8");
|
|
1779
2095
|
const pkg = JSON.parse(pkgRaw);
|
|
1780
2096
|
if (pkg.main && typeof pkg.main === "string") {
|
|
1781
2097
|
const result2 = await setupPrepend(rootDir, pkg.main);
|
|
@@ -1795,37 +2111,46 @@ var MCP_CONFIG = {
|
|
|
1795
2111
|
}
|
|
1796
2112
|
}
|
|
1797
2113
|
};
|
|
1798
|
-
async function setupMcp(rootDir) {
|
|
2114
|
+
async function setupMcp(rootDir, config = MCP_CONFIG) {
|
|
1799
2115
|
const mcpPath = join2(rootDir, ".mcp.json");
|
|
1800
2116
|
if (await fileExists(mcpPath)) {
|
|
1801
|
-
const raw = await
|
|
2117
|
+
const raw = await readFile4(mcpPath, "utf-8");
|
|
1802
2118
|
try {
|
|
1803
|
-
const
|
|
1804
|
-
if (
|
|
1805
|
-
|
|
1806
|
-
await writeFile3(mcpPath, JSON.stringify(
|
|
1807
|
-
await
|
|
2119
|
+
const existing = JSON.parse(raw);
|
|
2120
|
+
if (existing?.mcpServers?.brakit) return "exists";
|
|
2121
|
+
existing.mcpServers = { ...existing.mcpServers, ...config.mcpServers };
|
|
2122
|
+
await writeFile3(mcpPath, JSON.stringify(existing, null, 2) + "\n");
|
|
2123
|
+
await ensureGitignoreEntry(rootDir, ".mcp.json");
|
|
1808
2124
|
return "updated";
|
|
1809
2125
|
} catch {
|
|
1810
2126
|
}
|
|
1811
2127
|
}
|
|
1812
|
-
await writeFile3(mcpPath, JSON.stringify(
|
|
1813
|
-
await
|
|
2128
|
+
await writeFile3(mcpPath, JSON.stringify(config, null, 2) + "\n");
|
|
2129
|
+
await ensureGitignoreEntry(rootDir, ".mcp.json");
|
|
1814
2130
|
return "created";
|
|
1815
2131
|
}
|
|
1816
|
-
async function
|
|
2132
|
+
async function ensureGitignoreEntry(rootDir, entry) {
|
|
1817
2133
|
const gitignorePath = join2(rootDir, ".gitignore");
|
|
1818
2134
|
try {
|
|
1819
2135
|
if (await fileExists(gitignorePath)) {
|
|
1820
|
-
const content = await
|
|
1821
|
-
if (content.split("\n").some((l) => l.trim() ===
|
|
1822
|
-
await writeFile3(gitignorePath, content.trimEnd() + "\n
|
|
2136
|
+
const content = await readFile4(gitignorePath, "utf-8");
|
|
2137
|
+
if (content.split("\n").some((l) => l.trim() === entry)) return;
|
|
2138
|
+
await writeFile3(gitignorePath, content.trimEnd() + "\n" + entry + "\n");
|
|
1823
2139
|
} else {
|
|
1824
|
-
await writeFile3(gitignorePath, "
|
|
2140
|
+
await writeFile3(gitignorePath, entry + "\n");
|
|
1825
2141
|
}
|
|
1826
2142
|
} catch {
|
|
1827
2143
|
}
|
|
1828
2144
|
}
|
|
2145
|
+
function findGitRoot(startDir) {
|
|
2146
|
+
let dir = resolve3(startDir);
|
|
2147
|
+
while (true) {
|
|
2148
|
+
if (existsSync5(join2(dir, ".git"))) return dir;
|
|
2149
|
+
const parent = dirname(dir);
|
|
2150
|
+
if (parent === dir) return null;
|
|
2151
|
+
dir = parent;
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
1829
2154
|
function printManualInstructions(framework) {
|
|
1830
2155
|
console.log(pc.yellow(" \u26A0 Could not auto-detect entry file."));
|
|
1831
2156
|
console.log();
|
|
@@ -1846,31 +2171,14 @@ function printManualInstructions(framework) {
|
|
|
1846
2171
|
// src/cli/commands/uninstall.ts
|
|
1847
2172
|
import { defineCommand as defineCommand2 } from "citty";
|
|
1848
2173
|
import { resolve as resolve4, join as join3 } from "path";
|
|
1849
|
-
import { readFile as
|
|
2174
|
+
import { readFile as readFile5, writeFile as writeFile4, unlink, rm } from "fs/promises";
|
|
1850
2175
|
import { execSync as execSync2 } from "child_process";
|
|
1851
2176
|
import pc2 from "picocolors";
|
|
1852
2177
|
init_constants();
|
|
1853
|
-
var IMPORT_LINE2 = `import "brakit";`;
|
|
1854
|
-
var CREATED_FILES = [
|
|
1855
|
-
"src/instrumentation.ts",
|
|
1856
|
-
"instrumentation.ts",
|
|
1857
|
-
"server/plugins/brakit.ts"
|
|
1858
|
-
];
|
|
1859
2178
|
var PREPENDED_FILES = [
|
|
1860
2179
|
"app/entry.server.tsx",
|
|
1861
2180
|
"app/entry.server.ts",
|
|
1862
|
-
|
|
1863
|
-
"src/server.ts",
|
|
1864
|
-
"src/app.ts",
|
|
1865
|
-
"src/index.js",
|
|
1866
|
-
"src/server.js",
|
|
1867
|
-
"src/app.js",
|
|
1868
|
-
"server.ts",
|
|
1869
|
-
"app.ts",
|
|
1870
|
-
"index.ts",
|
|
1871
|
-
"server.js",
|
|
1872
|
-
"app.js",
|
|
1873
|
-
"index.js"
|
|
2181
|
+
...ENTRY_CANDIDATES
|
|
1874
2182
|
];
|
|
1875
2183
|
var uninstall_default = defineCommand2({
|
|
1876
2184
|
meta: {
|
|
@@ -1900,21 +2208,29 @@ var uninstall_default = defineCommand2({
|
|
|
1900
2208
|
for (const relPath of CREATED_FILES) {
|
|
1901
2209
|
const absPath = join3(rootDir, relPath);
|
|
1902
2210
|
if (!await fileExists(absPath)) continue;
|
|
1903
|
-
const content = await
|
|
2211
|
+
const content = await readFile5(absPath, "utf-8");
|
|
1904
2212
|
if (!content.includes("brakit")) continue;
|
|
1905
|
-
|
|
1906
|
-
const allBrakit = lines.every((l) => l.includes("brakit") || l.includes("register") || l.includes("import") || l.includes("export") || l.includes("try") || l.includes("catch") || l.includes("process.env") || l.includes("{") || l.includes("}"));
|
|
1907
|
-
if (allBrakit) {
|
|
2213
|
+
if (isExactBrakitTemplate(content)) {
|
|
1908
2214
|
await unlink(absPath);
|
|
1909
2215
|
console.log(pc2.green(` \u2713 Removed ${relPath}`));
|
|
1910
2216
|
removed = true;
|
|
1911
2217
|
break;
|
|
1912
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
|
+
}
|
|
1913
2229
|
}
|
|
1914
2230
|
if (!removed) {
|
|
1915
2231
|
const candidates = [...PREPENDED_FILES];
|
|
1916
2232
|
try {
|
|
1917
|
-
const pkgRaw = await
|
|
2233
|
+
const pkgRaw = await readFile5(join3(rootDir, "package.json"), "utf-8");
|
|
1918
2234
|
const pkg = JSON.parse(pkgRaw);
|
|
1919
2235
|
if (pkg.main) candidates.unshift(pkg.main);
|
|
1920
2236
|
} catch {
|
|
@@ -1922,9 +2238,9 @@ var uninstall_default = defineCommand2({
|
|
|
1922
2238
|
for (const relPath of candidates) {
|
|
1923
2239
|
const absPath = join3(rootDir, relPath);
|
|
1924
2240
|
if (!await fileExists(absPath)) continue;
|
|
1925
|
-
const content = await
|
|
1926
|
-
if (!content.includes(
|
|
1927
|
-
const updated = content.split("\n").filter((line) => line.trim() !==
|
|
2241
|
+
const content = await readFile5(absPath, "utf-8");
|
|
2242
|
+
if (!content.includes(IMPORT_LINE)) continue;
|
|
2243
|
+
const updated = content.split("\n").filter((line) => line.trim() !== IMPORT_LINE.trim()).join("\n");
|
|
1928
2244
|
await writeFile4(absPath, updated);
|
|
1929
2245
|
console.log(pc2.green(` \u2713 Removed brakit import from ${relPath}`));
|
|
1930
2246
|
removed = true;
|
|
@@ -1958,7 +2274,7 @@ async function removeMcpConfig(rootDir) {
|
|
|
1958
2274
|
const mcpPath = join3(rootDir, ".mcp.json");
|
|
1959
2275
|
if (!await fileExists(mcpPath)) return false;
|
|
1960
2276
|
try {
|
|
1961
|
-
const raw = await
|
|
2277
|
+
const raw = await readFile5(mcpPath, "utf-8");
|
|
1962
2278
|
const config = JSON.parse(raw);
|
|
1963
2279
|
if (!config?.mcpServers?.brakit) return false;
|
|
1964
2280
|
delete config.mcpServers.brakit;
|
|
@@ -1974,7 +2290,7 @@ async function removeMcpConfig(rootDir) {
|
|
|
1974
2290
|
}
|
|
1975
2291
|
async function uninstallPackage(rootDir, pm) {
|
|
1976
2292
|
try {
|
|
1977
|
-
const pkgRaw = await
|
|
2293
|
+
const pkgRaw = await readFile5(join3(rootDir, "package.json"), "utf-8");
|
|
1978
2294
|
const pkg = JSON.parse(pkgRaw);
|
|
1979
2295
|
if (!pkg.devDependencies?.brakit && !pkg.dependencies?.brakit) return false;
|
|
1980
2296
|
} catch {
|
|
@@ -2008,7 +2324,7 @@ async function cleanGitignore(rootDir) {
|
|
|
2008
2324
|
const gitignorePath = join3(rootDir, ".gitignore");
|
|
2009
2325
|
if (!await fileExists(gitignorePath)) return false;
|
|
2010
2326
|
try {
|
|
2011
|
-
const content = await
|
|
2327
|
+
const content = await readFile5(gitignorePath, "utf-8");
|
|
2012
2328
|
const lines = content.split("\n");
|
|
2013
2329
|
const filtered = lines.filter((line) => line.trim() !== METRICS_DIR);
|
|
2014
2330
|
if (filtered.length === lines.length) return false;
|