@launchsecure/launch-kit 0.0.2 → 0.0.4
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/chart-client/assets/index-BPR5akxH.js +323 -0
- package/dist/chart-client/assets/index-DhNl1aFF.css +1 -0
- package/dist/chart-client/index.html +21 -0
- package/dist/client/assets/{index-CcHIoRl6.js → index-D9e81rsq.js} +68 -63
- package/dist/client/assets/index-nR-HgoHH.css +32 -0
- package/dist/client/index.html +2 -2
- package/dist/server/chart-serve.js +1764 -0
- package/dist/server/cli.js +672 -42
- package/dist/server/graph-mcp-entry.js +1281 -256
- package/package.json +7 -3
- package/dist/client/assets/index-C8GAsRGO.css +0 -32
|
@@ -1,22 +1,111 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __esm = (fn, res) => function __init() {
|
|
10
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
11
|
+
};
|
|
12
|
+
var __export = (target, all) => {
|
|
13
|
+
for (var name in all)
|
|
14
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
15
|
+
};
|
|
16
|
+
var __copyProps = (to, from, except, desc) => {
|
|
17
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
18
|
+
for (let key of __getOwnPropNames(from))
|
|
19
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
20
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
21
|
+
}
|
|
22
|
+
return to;
|
|
23
|
+
};
|
|
24
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
25
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
26
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
27
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
28
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
29
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
30
|
+
mod
|
|
31
|
+
));
|
|
3
32
|
|
|
4
|
-
// src/server/
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
33
|
+
// src/server/lockfile.ts
|
|
34
|
+
function lockDir() {
|
|
35
|
+
return (0, import_node_path.join)((0, import_node_os.homedir)(), ".launchsecure");
|
|
36
|
+
}
|
|
37
|
+
function lockPath() {
|
|
38
|
+
return (0, import_node_path.join)(lockDir(), "launch-chart.lock");
|
|
39
|
+
}
|
|
40
|
+
function readLock() {
|
|
41
|
+
const p = lockPath();
|
|
42
|
+
if (!(0, import_node_fs.existsSync)(p)) return null;
|
|
43
|
+
try {
|
|
44
|
+
const data = JSON.parse((0, import_node_fs.readFileSync)(p, "utf-8"));
|
|
45
|
+
if (typeof data.pid !== "number" || typeof data.port !== "number") return null;
|
|
46
|
+
return data;
|
|
47
|
+
} catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function isPidAlive(pid) {
|
|
52
|
+
try {
|
|
53
|
+
process.kill(pid, 0);
|
|
54
|
+
return true;
|
|
55
|
+
} catch {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function getListenerPid(port) {
|
|
60
|
+
try {
|
|
61
|
+
const out = (0, import_node_child_process.execFileSync)("lsof", ["-nP", "-iTCP:" + port, "-sTCP:LISTEN", "-t"], {
|
|
62
|
+
encoding: "utf-8",
|
|
63
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
64
|
+
timeout: 500
|
|
65
|
+
}).trim();
|
|
66
|
+
if (!out) return null;
|
|
67
|
+
const pid = parseInt(out.split("\n")[0], 10);
|
|
68
|
+
return Number.isFinite(pid) ? pid : null;
|
|
69
|
+
} catch {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function getLiveLock() {
|
|
74
|
+
const lock = readLock();
|
|
75
|
+
if (!lock) return null;
|
|
76
|
+
const listenerPid = getListenerPid(lock.port);
|
|
77
|
+
const live = listenerPid !== null ? listenerPid === lock.pid : isPidAlive(lock.pid);
|
|
78
|
+
if (!live) {
|
|
79
|
+
try {
|
|
80
|
+
(0, import_node_fs.unlinkSync)(lockPath());
|
|
81
|
+
} catch {
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
return lock;
|
|
86
|
+
}
|
|
87
|
+
function writeLock(data) {
|
|
88
|
+
(0, import_node_fs.mkdirSync)(lockDir(), { recursive: true });
|
|
89
|
+
(0, import_node_fs.writeFileSync)(lockPath(), JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
90
|
+
}
|
|
91
|
+
function clearLock() {
|
|
92
|
+
try {
|
|
93
|
+
(0, import_node_fs.unlinkSync)(lockPath());
|
|
94
|
+
} catch {
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
var import_node_child_process, import_node_fs, import_node_os, import_node_path;
|
|
98
|
+
var init_lockfile = __esm({
|
|
99
|
+
"src/server/lockfile.ts"() {
|
|
100
|
+
"use strict";
|
|
101
|
+
import_node_child_process = require("node:child_process");
|
|
102
|
+
import_node_fs = require("node:fs");
|
|
103
|
+
import_node_os = require("node:os");
|
|
104
|
+
import_node_path = require("node:path");
|
|
105
|
+
}
|
|
106
|
+
});
|
|
15
107
|
|
|
16
108
|
// src/server/graph/core/ast-helpers.ts
|
|
17
|
-
var import_node_fs = require("node:fs");
|
|
18
|
-
var import_node_path = require("node:path");
|
|
19
|
-
var tsModule;
|
|
20
109
|
function getTs() {
|
|
21
110
|
if (!tsModule) {
|
|
22
111
|
tsModule = require("typescript");
|
|
@@ -25,8 +114,8 @@ function getTs() {
|
|
|
25
114
|
}
|
|
26
115
|
function parseFile(absPath) {
|
|
27
116
|
const ts = getTs();
|
|
28
|
-
const content = (0,
|
|
29
|
-
const ext = (0,
|
|
117
|
+
const content = (0, import_node_fs2.readFileSync)(absPath, "utf-8");
|
|
118
|
+
const ext = (0, import_node_path2.extname)(absPath);
|
|
30
119
|
const scriptKind = ext === ".tsx" ? ts.ScriptKind.TSX : ext === ".ts" ? ts.ScriptKind.TS : ext === ".jsx" ? ts.ScriptKind.JSX : ts.ScriptKind.JS;
|
|
31
120
|
const sourceFile = ts.createSourceFile(
|
|
32
121
|
absPath,
|
|
@@ -45,6 +134,8 @@ function parseFile(absPath) {
|
|
|
45
134
|
const reExports = [];
|
|
46
135
|
const jsxElements = /* @__PURE__ */ new Set();
|
|
47
136
|
const navigations = [];
|
|
137
|
+
const fetchCalls = [];
|
|
138
|
+
const includeConcat = process.env.LAUNCH_CHART_INCLUDE_CONCAT_FETCHES === "1";
|
|
48
139
|
function addExport(name2, kind) {
|
|
49
140
|
if (!exportsSet.has(name2)) {
|
|
50
141
|
exportsSet.add(name2);
|
|
@@ -67,6 +158,33 @@ function parseFile(absPath) {
|
|
|
67
158
|
}
|
|
68
159
|
return null;
|
|
69
160
|
}
|
|
161
|
+
function looksLikeUrl(s) {
|
|
162
|
+
return s.startsWith("/") || /^(https?:)?\/\//i.test(s);
|
|
163
|
+
}
|
|
164
|
+
function templateStartsWithSlash(expr) {
|
|
165
|
+
const head = expr.head.text;
|
|
166
|
+
return head.startsWith("/");
|
|
167
|
+
}
|
|
168
|
+
function extractUrlFromFetchArg(arg) {
|
|
169
|
+
if (ts.isStringLiteral(arg) || ts.isNoSubstitutionTemplateLiteral(arg)) {
|
|
170
|
+
if (!looksLikeUrl(arg.text)) return null;
|
|
171
|
+
return { url: arg.text, isTemplate: false };
|
|
172
|
+
}
|
|
173
|
+
if (ts.isTemplateExpression(arg)) {
|
|
174
|
+
if (!templateStartsWithSlash(arg)) return null;
|
|
175
|
+
return { url: arg.getText(sourceFile), isTemplate: true };
|
|
176
|
+
}
|
|
177
|
+
if (includeConcat && ts.isBinaryExpression(arg) && arg.operatorToken.kind === ts.SyntaxKind.PlusToken) {
|
|
178
|
+
let leftmost = arg;
|
|
179
|
+
while (ts.isBinaryExpression(leftmost) && leftmost.operatorToken.kind === ts.SyntaxKind.PlusToken) {
|
|
180
|
+
leftmost = leftmost.left;
|
|
181
|
+
}
|
|
182
|
+
if ((ts.isStringLiteral(leftmost) || ts.isNoSubstitutionTemplateLiteral(leftmost)) && leftmost.text.startsWith("/")) {
|
|
183
|
+
return { url: arg.getText(sourceFile), isTemplate: false, isConcat: true };
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
70
188
|
function visit(node) {
|
|
71
189
|
if (ts.isImportDeclaration(node)) {
|
|
72
190
|
const moduleSpec = node.moduleSpecifier;
|
|
@@ -90,6 +208,8 @@ function parseFile(absPath) {
|
|
|
90
208
|
}
|
|
91
209
|
if (names.length > 0 || isTypeOnly) {
|
|
92
210
|
imports.push({ names, specifier, isTypeOnly, typeNames });
|
|
211
|
+
} else if (!clause) {
|
|
212
|
+
imports.push({ names: [], specifier, isTypeOnly: false, typeNames: /* @__PURE__ */ new Set() });
|
|
93
213
|
}
|
|
94
214
|
}
|
|
95
215
|
}
|
|
@@ -103,6 +223,19 @@ function parseFile(absPath) {
|
|
|
103
223
|
reExports.push({ name: exportedName, from: fromSpec });
|
|
104
224
|
}
|
|
105
225
|
}
|
|
226
|
+
} else if (!node.exportClause && fromSpec) {
|
|
227
|
+
reExports.push({ name: "*", from: fromSpec, isWildcard: true });
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
if (ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.ImportKeyword) {
|
|
231
|
+
const arg = node.arguments[0];
|
|
232
|
+
if (arg && ts.isStringLiteral(arg)) {
|
|
233
|
+
imports.push({
|
|
234
|
+
names: [],
|
|
235
|
+
specifier: arg.text,
|
|
236
|
+
isTypeOnly: false,
|
|
237
|
+
typeNames: /* @__PURE__ */ new Set()
|
|
238
|
+
});
|
|
106
239
|
}
|
|
107
240
|
}
|
|
108
241
|
if (ts.isExportAssignment(node) && !node.isExportEquals) {
|
|
@@ -164,6 +297,36 @@ function parseFile(absPath) {
|
|
|
164
297
|
}
|
|
165
298
|
}
|
|
166
299
|
}
|
|
300
|
+
if (ts.isCallExpression(node) && node.arguments.length > 0) {
|
|
301
|
+
const expr = node.expression;
|
|
302
|
+
const firstArg = node.arguments[0];
|
|
303
|
+
if (ts.isIdentifier(expr) && expr.text === "fetch") {
|
|
304
|
+
const extracted = extractUrlFromFetchArg(firstArg);
|
|
305
|
+
if (extracted) {
|
|
306
|
+
fetchCalls.push({
|
|
307
|
+
url: extracted.url,
|
|
308
|
+
isTemplate: extracted.isTemplate,
|
|
309
|
+
...extracted.isConcat ? { isConcat: true } : {},
|
|
310
|
+
kind: "fetch"
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
if (ts.isPropertyAccessExpression(expr) && ts.isIdentifier(expr.name)) {
|
|
315
|
+
const methodName = expr.name.text;
|
|
316
|
+
if (HTTP_METHODS.has(methodName)) {
|
|
317
|
+
const extracted = extractUrlFromFetchArg(firstArg);
|
|
318
|
+
if (extracted) {
|
|
319
|
+
fetchCalls.push({
|
|
320
|
+
method: methodName.toUpperCase(),
|
|
321
|
+
url: extracted.url,
|
|
322
|
+
isTemplate: extracted.isTemplate,
|
|
323
|
+
...extracted.isConcat ? { isConcat: true } : {},
|
|
324
|
+
kind: "client-method"
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
167
330
|
if (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) {
|
|
168
331
|
const tagName = node.tagName;
|
|
169
332
|
if (ts.isIdentifier(tagName) && tagName.text === "Link") {
|
|
@@ -213,24 +376,14 @@ function parseFile(absPath) {
|
|
|
213
376
|
imports,
|
|
214
377
|
reExports,
|
|
215
378
|
jsxElements,
|
|
216
|
-
navigations
|
|
379
|
+
navigations,
|
|
380
|
+
fetchCalls
|
|
217
381
|
};
|
|
218
382
|
}
|
|
219
|
-
var MUTATION_METHODS = /* @__PURE__ */ new Set([
|
|
220
|
-
"create",
|
|
221
|
-
"createMany",
|
|
222
|
-
"createManyAndReturn",
|
|
223
|
-
"update",
|
|
224
|
-
"updateMany",
|
|
225
|
-
"updateManyAndReturn",
|
|
226
|
-
"upsert",
|
|
227
|
-
"delete",
|
|
228
|
-
"deleteMany"
|
|
229
|
-
]);
|
|
230
383
|
function extractDbCalls(absPath) {
|
|
231
384
|
const ts = getTs();
|
|
232
|
-
const content = (0,
|
|
233
|
-
const ext = (0,
|
|
385
|
+
const content = (0, import_node_fs2.readFileSync)(absPath, "utf-8");
|
|
386
|
+
const ext = (0, import_node_path2.extname)(absPath);
|
|
234
387
|
const scriptKind = ext === ".tsx" ? ts.ScriptKind.TSX : ts.ScriptKind.TS;
|
|
235
388
|
const sourceFile = ts.createSourceFile(absPath, content, ts.ScriptTarget.Latest, true, scriptKind);
|
|
236
389
|
const calls = [];
|
|
@@ -259,8 +412,8 @@ function extractDbCalls(absPath) {
|
|
|
259
412
|
}
|
|
260
413
|
function extractAuthWrappers(absPath) {
|
|
261
414
|
const ts = getTs();
|
|
262
|
-
const content = (0,
|
|
263
|
-
const ext = (0,
|
|
415
|
+
const content = (0, import_node_fs2.readFileSync)(absPath, "utf-8");
|
|
416
|
+
const ext = (0, import_node_path2.extname)(absPath);
|
|
264
417
|
const scriptKind = ext === ".tsx" ? ts.ScriptKind.TSX : ts.ScriptKind.TS;
|
|
265
418
|
const sourceFile = ts.createSourceFile(absPath, content, ts.ScriptTarget.Latest, true, scriptKind);
|
|
266
419
|
const wrappers = /* @__PURE__ */ new Set();
|
|
@@ -276,55 +429,125 @@ function extractAuthWrappers(absPath) {
|
|
|
276
429
|
visit(sourceFile);
|
|
277
430
|
return wrappers;
|
|
278
431
|
}
|
|
432
|
+
var import_node_fs2, import_node_path2, tsModule, HTTP_METHODS, MUTATION_METHODS;
|
|
433
|
+
var init_ast_helpers = __esm({
|
|
434
|
+
"src/server/graph/core/ast-helpers.ts"() {
|
|
435
|
+
"use strict";
|
|
436
|
+
import_node_fs2 = require("node:fs");
|
|
437
|
+
import_node_path2 = require("node:path");
|
|
438
|
+
HTTP_METHODS = /* @__PURE__ */ new Set(["get", "post", "put", "patch", "delete", "head", "options"]);
|
|
439
|
+
MUTATION_METHODS = /* @__PURE__ */ new Set([
|
|
440
|
+
"create",
|
|
441
|
+
"createMany",
|
|
442
|
+
"createManyAndReturn",
|
|
443
|
+
"update",
|
|
444
|
+
"updateMany",
|
|
445
|
+
"updateManyAndReturn",
|
|
446
|
+
"upsert",
|
|
447
|
+
"delete",
|
|
448
|
+
"deleteMany"
|
|
449
|
+
]);
|
|
450
|
+
}
|
|
451
|
+
});
|
|
279
452
|
|
|
280
453
|
// src/server/graph/parsers/ui/react-nextjs.ts
|
|
281
|
-
var RENDER_TYPES = /* @__PURE__ */ new Set(["component", "ui", "layout", "context"]);
|
|
282
454
|
function walk(dir, exts) {
|
|
283
455
|
const results = [];
|
|
284
|
-
if (!(0,
|
|
285
|
-
for (const entry of (0,
|
|
286
|
-
const full = (0,
|
|
456
|
+
if (!(0, import_node_fs3.existsSync)(dir)) return results;
|
|
457
|
+
for (const entry of (0, import_node_fs3.readdirSync)(dir, { withFileTypes: true })) {
|
|
458
|
+
const full = (0, import_node_path3.join)(dir, entry.name);
|
|
287
459
|
if (entry.isDirectory()) {
|
|
288
460
|
results.push(...walk(full, exts));
|
|
289
|
-
} else if (exts.includes((0,
|
|
461
|
+
} else if (exts.includes((0, import_node_path3.extname)(entry.name))) {
|
|
290
462
|
results.push(full);
|
|
291
463
|
}
|
|
292
464
|
}
|
|
293
465
|
return results;
|
|
294
466
|
}
|
|
467
|
+
function walkWithIgnore(dir, exts, ignoreDirs) {
|
|
468
|
+
const results = [];
|
|
469
|
+
if (!(0, import_node_fs3.existsSync)(dir)) return results;
|
|
470
|
+
for (const entry of (0, import_node_fs3.readdirSync)(dir, { withFileTypes: true })) {
|
|
471
|
+
if (entry.isDirectory()) {
|
|
472
|
+
if (ignoreDirs.has(entry.name)) continue;
|
|
473
|
+
results.push(...walkWithIgnore((0, import_node_path3.join)(dir, entry.name), exts, ignoreDirs));
|
|
474
|
+
} else if (exts.includes((0, import_node_path3.extname)(entry.name))) {
|
|
475
|
+
results.push((0, import_node_path3.join)(dir, entry.name));
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
return results;
|
|
479
|
+
}
|
|
295
480
|
function toNodeId(srcDir, absPath) {
|
|
296
|
-
return (0,
|
|
481
|
+
return (0, import_node_path3.relative)(srcDir, absPath).replace(/\\/g, "/");
|
|
297
482
|
}
|
|
298
483
|
function resolveImport(srcDir, specifier) {
|
|
299
484
|
if (!specifier.startsWith("@/")) return null;
|
|
300
485
|
const rel = specifier.slice(2);
|
|
301
|
-
const base = (0,
|
|
302
|
-
for (const c of [base, base + ".ts", base + ".tsx", (0,
|
|
303
|
-
if ((0,
|
|
486
|
+
const base = (0, import_node_path3.join)(srcDir, rel);
|
|
487
|
+
for (const c of [base, base + ".ts", base + ".tsx", (0, import_node_path3.join)(base, "index.ts"), (0, import_node_path3.join)(base, "index.tsx")]) {
|
|
488
|
+
if ((0, import_node_fs3.existsSync)(c) && (0, import_node_fs3.statSync)(c).isFile()) return c;
|
|
304
489
|
}
|
|
305
490
|
return null;
|
|
306
491
|
}
|
|
307
492
|
function resolveRelativeImport(fromFile, specifier) {
|
|
308
|
-
const base = (0,
|
|
309
|
-
for (const c of [base, base + ".ts", base + ".tsx", (0,
|
|
310
|
-
if ((0,
|
|
493
|
+
const base = (0, import_node_path3.join)((0, import_node_path3.dirname)(fromFile), specifier);
|
|
494
|
+
for (const c of [base, base + ".ts", base + ".tsx", (0, import_node_path3.join)(base, "index.ts"), (0, import_node_path3.join)(base, "index.tsx")]) {
|
|
495
|
+
if ((0, import_node_fs3.existsSync)(c) && (0, import_node_fs3.statSync)(c).isFile()) return c;
|
|
311
496
|
}
|
|
312
497
|
return null;
|
|
313
498
|
}
|
|
499
|
+
function resolveBarrelMap(barrelAbsPath, parsedByPath, memo, visiting) {
|
|
500
|
+
const cached = memo.get(barrelAbsPath);
|
|
501
|
+
if (cached) return cached;
|
|
502
|
+
if (visiting.has(barrelAbsPath)) return /* @__PURE__ */ new Map();
|
|
503
|
+
visiting.add(barrelAbsPath);
|
|
504
|
+
const parsed = parsedByPath.get(barrelAbsPath);
|
|
505
|
+
const map = /* @__PURE__ */ new Map();
|
|
506
|
+
if (!parsed) {
|
|
507
|
+
visiting.delete(barrelAbsPath);
|
|
508
|
+
memo.set(barrelAbsPath, map);
|
|
509
|
+
return map;
|
|
510
|
+
}
|
|
511
|
+
for (const re of parsed.reExports) {
|
|
512
|
+
if (!re.from.startsWith(".")) continue;
|
|
513
|
+
const resolved = resolveRelativeImport(barrelAbsPath, re.from);
|
|
514
|
+
if (!resolved) continue;
|
|
515
|
+
if (re.isWildcard) {
|
|
516
|
+
const targetBn = (0, import_node_path3.basename)(resolved);
|
|
517
|
+
const targetIsBarrel = targetBn === "index.ts" || targetBn === "index.tsx";
|
|
518
|
+
if (targetIsBarrel) {
|
|
519
|
+
const nested = resolveBarrelMap(resolved, parsedByPath, memo, visiting);
|
|
520
|
+
for (const [name, target] of nested) {
|
|
521
|
+
if (!map.has(name)) map.set(name, target);
|
|
522
|
+
}
|
|
523
|
+
} else {
|
|
524
|
+
const targetParsed = parsedByPath.get(resolved);
|
|
525
|
+
if (targetParsed) {
|
|
526
|
+
for (const exp of targetParsed.exports) {
|
|
527
|
+
if (!map.has(exp)) map.set(exp, resolved);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
} else {
|
|
532
|
+
if (!map.has(re.name)) map.set(re.name, resolved);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
visiting.delete(barrelAbsPath);
|
|
536
|
+
memo.set(barrelAbsPath, map);
|
|
537
|
+
return map;
|
|
538
|
+
}
|
|
314
539
|
function buildAllBarrelMaps(srcDir, parsedByPath) {
|
|
315
540
|
const barrels = /* @__PURE__ */ new Map();
|
|
541
|
+
const memo = /* @__PURE__ */ new Map();
|
|
316
542
|
for (const [absPath, parsed] of parsedByPath) {
|
|
317
|
-
const bn = (0,
|
|
543
|
+
const bn = (0, import_node_path3.basename)(absPath);
|
|
318
544
|
if (bn !== "index.ts" && bn !== "index.tsx") continue;
|
|
319
545
|
if (parsed.reExports.length === 0) continue;
|
|
320
|
-
const
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
const resolved = resolveRelativeImport(absPath, re.from);
|
|
325
|
-
if (resolved) map.set(re.name, resolved);
|
|
546
|
+
const map = resolveBarrelMap(absPath, parsedByPath, memo, /* @__PURE__ */ new Set());
|
|
547
|
+
if (map.size > 0) {
|
|
548
|
+
const barrelId = (0, import_node_path3.relative)(srcDir, (0, import_node_path3.dirname)(absPath)).replace(/\\/g, "/");
|
|
549
|
+
barrels.set(barrelId, map);
|
|
326
550
|
}
|
|
327
|
-
if (map.size > 0) barrels.set(barrelId, map);
|
|
328
551
|
}
|
|
329
552
|
return barrels;
|
|
330
553
|
}
|
|
@@ -381,7 +604,7 @@ function extractRoute(id) {
|
|
|
381
604
|
return route || "/";
|
|
382
605
|
}
|
|
383
606
|
function nameFromFilename(absPath) {
|
|
384
|
-
return (0,
|
|
607
|
+
return (0, import_node_path3.basename)(absPath, (0, import_node_path3.extname)(absPath)).replace(/[-_](\w)/g, (_, c) => c.toUpperCase()).replace(/^(\w)/, (_, c) => c.toUpperCase());
|
|
385
608
|
}
|
|
386
609
|
function resolveTemplateLiteralRoute(template, routeToNodeId) {
|
|
387
610
|
const parameterized = template.replace(/\$\{([^}]+)\}/g, (_, expr) => {
|
|
@@ -462,6 +685,105 @@ function matchRouteToPage(route, routeToNodeId) {
|
|
|
462
685
|
if (routeToNodeId.has(normalized)) return routeToNodeId.get(normalized);
|
|
463
686
|
return null;
|
|
464
687
|
}
|
|
688
|
+
function loadApiRoutes(rootDir) {
|
|
689
|
+
const apiJsonPath = (0, import_node_path3.join)(rootDir, ".launchsecure", "graphs", "api.json");
|
|
690
|
+
if (!(0, import_node_fs3.existsSync)(apiJsonPath)) return [];
|
|
691
|
+
try {
|
|
692
|
+
const parsed = JSON.parse((0, import_node_fs3.readFileSync)(apiJsonPath, "utf-8"));
|
|
693
|
+
const routes = [];
|
|
694
|
+
for (const n of parsed.nodes ?? []) {
|
|
695
|
+
const path3 = n.path;
|
|
696
|
+
if (!path3 || typeof path3 !== "string") continue;
|
|
697
|
+
routes.push({
|
|
698
|
+
path: path3,
|
|
699
|
+
nodeId: n.id,
|
|
700
|
+
segments: path3.split("/").filter(Boolean)
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
return routes;
|
|
704
|
+
} catch {
|
|
705
|
+
return [];
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
function buildApiPathMap(routes) {
|
|
709
|
+
const map = /* @__PURE__ */ new Map();
|
|
710
|
+
for (const r of routes) {
|
|
711
|
+
if (!map.has(r.path)) map.set(r.path, r.nodeId);
|
|
712
|
+
}
|
|
713
|
+
return map;
|
|
714
|
+
}
|
|
715
|
+
function normalizeFetchUrl(raw) {
|
|
716
|
+
let s = raw.replace(/^`|`$/g, "");
|
|
717
|
+
const qIdx = s.indexOf("?");
|
|
718
|
+
if (qIdx >= 0) s = s.slice(0, qIdx);
|
|
719
|
+
const hIdx = s.indexOf("#");
|
|
720
|
+
if (hIdx >= 0) s = s.slice(0, hIdx);
|
|
721
|
+
let hadInterpolation = false;
|
|
722
|
+
s = s.replace(/\$\{([^}]+)\}/g, (_, expr) => {
|
|
723
|
+
hadInterpolation = true;
|
|
724
|
+
const cleaned = expr.trim();
|
|
725
|
+
const last = cleaned.split(".").pop() ?? cleaned;
|
|
726
|
+
const name = last.replace(/[^\w]/g, "") || "param";
|
|
727
|
+
return ":" + name;
|
|
728
|
+
});
|
|
729
|
+
s = s.replace(/\/+/g, "/");
|
|
730
|
+
if (s.length > 1 && s.endsWith("/")) s = s.slice(0, -1);
|
|
731
|
+
return { path: s || "/", hadInterpolation };
|
|
732
|
+
}
|
|
733
|
+
function scoreApiRouteMatch(candidate, known) {
|
|
734
|
+
if (candidate.length !== known.length) return -1;
|
|
735
|
+
let score = 0;
|
|
736
|
+
for (let i = 0; i < candidate.length; i++) {
|
|
737
|
+
const a = candidate[i];
|
|
738
|
+
const b = known[i];
|
|
739
|
+
if (a === b) {
|
|
740
|
+
score += 3;
|
|
741
|
+
continue;
|
|
742
|
+
}
|
|
743
|
+
if (a.startsWith(":") && b.startsWith(":")) {
|
|
744
|
+
score += 2;
|
|
745
|
+
continue;
|
|
746
|
+
}
|
|
747
|
+
if (a.startsWith(":") || b.startsWith(":")) {
|
|
748
|
+
score += 1;
|
|
749
|
+
continue;
|
|
750
|
+
}
|
|
751
|
+
return -1;
|
|
752
|
+
}
|
|
753
|
+
return score;
|
|
754
|
+
}
|
|
755
|
+
function resolveFetchCall(call, apiPathMap, apiRoutes) {
|
|
756
|
+
const raw = call.url;
|
|
757
|
+
if (/^(https?:)?\/\//i.test(raw)) {
|
|
758
|
+
return { kind: "external", normalizedUrl: raw };
|
|
759
|
+
}
|
|
760
|
+
if (call.isConcat) {
|
|
761
|
+
return { kind: "dynamic", normalizedUrl: raw };
|
|
762
|
+
}
|
|
763
|
+
const { path: path3, hadInterpolation } = normalizeFetchUrl(raw);
|
|
764
|
+
if (!path3.startsWith("/")) {
|
|
765
|
+
return { kind: "unresolved", normalizedUrl: path3 };
|
|
766
|
+
}
|
|
767
|
+
const segs = path3.split("/").filter(Boolean);
|
|
768
|
+
if (hadInterpolation && segs.length > 0 && segs[0].startsWith(":")) {
|
|
769
|
+
return { kind: "dynamic", normalizedUrl: path3 };
|
|
770
|
+
}
|
|
771
|
+
const exact = apiPathMap.get(path3);
|
|
772
|
+
if (exact) return { kind: "resolved", nodeId: exact, normalizedUrl: path3 };
|
|
773
|
+
let bestScore = -1;
|
|
774
|
+
let bestId = null;
|
|
775
|
+
for (const r of apiRoutes) {
|
|
776
|
+
const score = scoreApiRouteMatch(segs, r.segments);
|
|
777
|
+
if (score > bestScore) {
|
|
778
|
+
bestScore = score;
|
|
779
|
+
bestId = r.nodeId;
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
if (bestId && bestScore > 0) {
|
|
783
|
+
return { kind: "resolved", nodeId: bestId, normalizedUrl: path3 };
|
|
784
|
+
}
|
|
785
|
+
return { kind: "unresolved", normalizedUrl: path3 };
|
|
786
|
+
}
|
|
465
787
|
function extractEdges(srcDir, absPath, sourceId, parsed, nodeIdSet, nodeTypeMap, barrelMaps, routeToNodeId) {
|
|
466
788
|
const edges = [];
|
|
467
789
|
const flagged = [];
|
|
@@ -561,26 +883,26 @@ function extractEdges(srcDir, absPath, sourceId, parsed, nodeIdSet, nodeTypeMap,
|
|
|
561
883
|
return { edges, flagged };
|
|
562
884
|
}
|
|
563
885
|
function detect(rootDir) {
|
|
564
|
-
return (0,
|
|
886
|
+
return (0, import_node_fs3.existsSync)((0, import_node_path3.join)(rootDir, "src", "app")) && (0, import_node_fs3.existsSync)((0, import_node_path3.join)(rootDir, "next.config.ts")) || (0, import_node_fs3.existsSync)((0, import_node_path3.join)(rootDir, "next.config.js")) || (0, import_node_fs3.existsSync)((0, import_node_path3.join)(rootDir, "next.config.mjs"));
|
|
565
887
|
}
|
|
566
888
|
function generate(rootDir) {
|
|
567
|
-
const srcDir = (0,
|
|
568
|
-
const appFiles = walk((0,
|
|
569
|
-
(f) =>
|
|
889
|
+
const srcDir = (0, import_node_path3.join)(rootDir, "src");
|
|
890
|
+
const appFiles = walk((0, import_node_path3.join)(srcDir, "app"), [".tsx", ".ts"]).filter(
|
|
891
|
+
(f) => (0, import_node_path3.basename)(f) !== "route.ts" && (0, import_node_path3.basename)(f) !== "route.tsx"
|
|
570
892
|
);
|
|
571
|
-
const clientFiles = walk((0,
|
|
572
|
-
const serverFiles = walk((0,
|
|
573
|
-
(f) => (0,
|
|
893
|
+
const clientFiles = walk((0, import_node_path3.join)(srcDir, "client"), [".tsx", ".ts"]);
|
|
894
|
+
const serverFiles = walk((0, import_node_path3.join)(srcDir, "server"), [".ts", ".tsx"]).filter(
|
|
895
|
+
(f) => (0, import_node_path3.basename)(f) !== "route.ts" && (0, import_node_path3.basename)(f) !== "route.tsx"
|
|
574
896
|
);
|
|
575
|
-
const libFiles = walk((0,
|
|
576
|
-
const configFiles = walk((0,
|
|
897
|
+
const libFiles = walk((0, import_node_path3.join)(srcDir, "lib"), [".ts", ".tsx"]);
|
|
898
|
+
const configFiles = walk((0, import_node_path3.join)(srcDir, "config"), [".ts", ".tsx"]);
|
|
577
899
|
const allDiscovered = [...appFiles, ...clientFiles, ...serverFiles, ...libFiles, ...configFiles];
|
|
578
900
|
const parsedByPath = /* @__PURE__ */ new Map();
|
|
579
901
|
for (const absPath of allDiscovered) {
|
|
580
902
|
parsedByPath.set(absPath, parseFile(absPath));
|
|
581
903
|
}
|
|
582
904
|
const barrelMaps = buildAllBarrelMaps(srcDir, parsedByPath);
|
|
583
|
-
const fileSet = allDiscovered.filter((f) => !(0,
|
|
905
|
+
const fileSet = allDiscovered.filter((f) => !(0, import_node_path3.basename)(f).startsWith("index."));
|
|
584
906
|
const nodes = [];
|
|
585
907
|
const nodeIdSet = /* @__PURE__ */ new Set();
|
|
586
908
|
const nodeTypeMap = /* @__PURE__ */ new Map();
|
|
@@ -599,6 +921,7 @@ function generate(rootDir) {
|
|
|
599
921
|
}
|
|
600
922
|
const allEdges = [];
|
|
601
923
|
const allFlagged = [];
|
|
924
|
+
const crossRefs = [];
|
|
602
925
|
for (const absPath of fileSet) {
|
|
603
926
|
const sourceId = toNodeId(srcDir, absPath);
|
|
604
927
|
const parsed = parsedByPath.get(absPath);
|
|
@@ -615,6 +938,139 @@ function generate(rootDir) {
|
|
|
615
938
|
allEdges.push(...edges);
|
|
616
939
|
allFlagged.push(...flagged);
|
|
617
940
|
}
|
|
941
|
+
const apiRoutes = loadApiRoutes(rootDir);
|
|
942
|
+
const apiPathMap = buildApiPathMap(apiRoutes);
|
|
943
|
+
const includeExternalFetches = process.env.LAUNCH_CHART_INCLUDE_EXTERNAL_FETCHES === "1";
|
|
944
|
+
const fetchSeen = /* @__PURE__ */ new Set();
|
|
945
|
+
let fetchResolvedCount = 0;
|
|
946
|
+
let fetchDynamicCount = 0;
|
|
947
|
+
let fetchUnresolvedCount = 0;
|
|
948
|
+
let fetchExternalCount = 0;
|
|
949
|
+
for (const absPath of fileSet) {
|
|
950
|
+
const sourceId = toNodeId(srcDir, absPath);
|
|
951
|
+
const parsed = parsedByPath.get(absPath);
|
|
952
|
+
if (parsed.fetchCalls.length === 0) continue;
|
|
953
|
+
for (const call of parsed.fetchCalls) {
|
|
954
|
+
const result = resolveFetchCall(call, apiPathMap, apiRoutes);
|
|
955
|
+
const methodTag = call.method ?? (call.kind === "fetch" ? "GET?" : "?");
|
|
956
|
+
if (result.kind === "resolved" && result.nodeId) {
|
|
957
|
+
const key = `${sourceId}\u2192${result.nodeId}\u2192calls_api`;
|
|
958
|
+
if (fetchSeen.has(key)) continue;
|
|
959
|
+
fetchSeen.add(key);
|
|
960
|
+
crossRefs.push({
|
|
961
|
+
source: sourceId,
|
|
962
|
+
target: result.nodeId,
|
|
963
|
+
type: "calls_api",
|
|
964
|
+
layer: "api"
|
|
965
|
+
});
|
|
966
|
+
fetchResolvedCount++;
|
|
967
|
+
continue;
|
|
968
|
+
}
|
|
969
|
+
if (result.kind === "dynamic") {
|
|
970
|
+
fetchDynamicCount++;
|
|
971
|
+
allFlagged.push({
|
|
972
|
+
source: sourceId,
|
|
973
|
+
target: "DYNAMIC",
|
|
974
|
+
type: "calls_api",
|
|
975
|
+
label: call.isConcat ? `${methodTag} fetch with concat: ${call.url}` : `${methodTag} fetch with template: ${call.url}`,
|
|
976
|
+
confidence: call.isConcat ? "low" : "medium"
|
|
977
|
+
});
|
|
978
|
+
continue;
|
|
979
|
+
}
|
|
980
|
+
if (result.kind === "external") {
|
|
981
|
+
fetchExternalCount++;
|
|
982
|
+
if (!includeExternalFetches) continue;
|
|
983
|
+
allFlagged.push({
|
|
984
|
+
source: sourceId,
|
|
985
|
+
target: "EXTERNAL",
|
|
986
|
+
type: "calls_external",
|
|
987
|
+
label: `${methodTag} external fetch: ${call.url}`,
|
|
988
|
+
confidence: "high"
|
|
989
|
+
});
|
|
990
|
+
continue;
|
|
991
|
+
}
|
|
992
|
+
fetchUnresolvedCount++;
|
|
993
|
+
allFlagged.push({
|
|
994
|
+
source: sourceId,
|
|
995
|
+
target: "UNRESOLVED",
|
|
996
|
+
type: "calls_api",
|
|
997
|
+
label: `${methodTag} fetch to unknown path: ${result.normalizedUrl}`,
|
|
998
|
+
confidence: "medium"
|
|
999
|
+
});
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
const externalScanned = new Set(allDiscovered.map((f) => f.replace(/\\/g, "/")));
|
|
1003
|
+
const IGNORE_DIRS = /* @__PURE__ */ new Set([
|
|
1004
|
+
"node_modules",
|
|
1005
|
+
".next",
|
|
1006
|
+
"dist",
|
|
1007
|
+
".launchsecure",
|
|
1008
|
+
".git",
|
|
1009
|
+
"src",
|
|
1010
|
+
"coverage",
|
|
1011
|
+
".turbo",
|
|
1012
|
+
"build",
|
|
1013
|
+
"out",
|
|
1014
|
+
".vercel"
|
|
1015
|
+
]);
|
|
1016
|
+
const externalCandidates = walkWithIgnore(rootDir, [".ts", ".tsx"], IGNORE_DIRS);
|
|
1017
|
+
for (const absPath of externalCandidates) {
|
|
1018
|
+
const normalized = absPath.replace(/\\/g, "/");
|
|
1019
|
+
if (externalScanned.has(normalized)) continue;
|
|
1020
|
+
let parsed;
|
|
1021
|
+
try {
|
|
1022
|
+
parsed = parseFile(absPath);
|
|
1023
|
+
} catch {
|
|
1024
|
+
continue;
|
|
1025
|
+
}
|
|
1026
|
+
const externalId = (0, import_node_path3.relative)(rootDir, absPath).replace(/\\/g, "/");
|
|
1027
|
+
const edgesFromThis = [];
|
|
1028
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1029
|
+
for (const imp of parsed.imports) {
|
|
1030
|
+
const { specifier, isTypeOnly, names } = imp;
|
|
1031
|
+
let resolved = null;
|
|
1032
|
+
if (specifier.startsWith("@/")) {
|
|
1033
|
+
const relToSrc = specifier.slice(2);
|
|
1034
|
+
const barrelMap = barrelMaps.get(relToSrc);
|
|
1035
|
+
if (barrelMap && names.length > 0) {
|
|
1036
|
+
for (const name of names) {
|
|
1037
|
+
const targetAbs = barrelMap.get(name);
|
|
1038
|
+
if (!targetAbs) continue;
|
|
1039
|
+
const targetId2 = toNodeId(srcDir, targetAbs);
|
|
1040
|
+
if (!nodeIdSet.has(targetId2)) continue;
|
|
1041
|
+
const key2 = `${externalId}\u2192${targetId2}`;
|
|
1042
|
+
if (seen.has(key2)) continue;
|
|
1043
|
+
seen.add(key2);
|
|
1044
|
+
edgesFromThis.push({ source: externalId, target: targetId2, type: "imports" });
|
|
1045
|
+
}
|
|
1046
|
+
continue;
|
|
1047
|
+
}
|
|
1048
|
+
resolved = resolveImport(srcDir, specifier);
|
|
1049
|
+
} else if (specifier.startsWith(".")) {
|
|
1050
|
+
resolved = resolveRelativeImport(absPath, specifier);
|
|
1051
|
+
}
|
|
1052
|
+
if (!resolved) continue;
|
|
1053
|
+
const targetId = toNodeId(srcDir, resolved);
|
|
1054
|
+
if (!nodeIdSet.has(targetId)) continue;
|
|
1055
|
+
if (targetId.endsWith("/index.ts") || targetId.endsWith("/index.tsx")) continue;
|
|
1056
|
+
const key = `${externalId}\u2192${targetId}\u2192${isTypeOnly ? "type" : "value"}`;
|
|
1057
|
+
if (seen.has(key)) continue;
|
|
1058
|
+
seen.add(key);
|
|
1059
|
+
edgesFromThis.push({ source: externalId, target: targetId, type: "imports" });
|
|
1060
|
+
}
|
|
1061
|
+
if (edgesFromThis.length === 0) continue;
|
|
1062
|
+
nodes.push({
|
|
1063
|
+
id: externalId,
|
|
1064
|
+
type: "external",
|
|
1065
|
+
name: parsed.name || nameFromFilename(absPath),
|
|
1066
|
+
route: null,
|
|
1067
|
+
module: "external",
|
|
1068
|
+
exports: parsed.exports
|
|
1069
|
+
});
|
|
1070
|
+
nodeIdSet.add(externalId);
|
|
1071
|
+
nodeTypeMap.set(externalId, "external");
|
|
1072
|
+
allEdges.push(...edgesFromThis);
|
|
1073
|
+
}
|
|
618
1074
|
const flaggedSet = /* @__PURE__ */ new Set();
|
|
619
1075
|
const dedupedFlagged = allFlagged.filter((f) => {
|
|
620
1076
|
const key = `${f.source}\u2192${f.target}\u2192${f.label}`;
|
|
@@ -646,6 +1102,7 @@ function generate(rootDir) {
|
|
|
646
1102
|
total_configs: byType("config"),
|
|
647
1103
|
total_utils: byType("util"),
|
|
648
1104
|
total_libs: byType("lib"),
|
|
1105
|
+
total_external: byType("external"),
|
|
649
1106
|
total_edges: allEdges.length,
|
|
650
1107
|
total_flagged: dedupedFlagged.length
|
|
651
1108
|
};
|
|
@@ -657,11 +1114,20 @@ function generate(rootDir) {
|
|
|
657
1114
|
layer: "ui",
|
|
658
1115
|
parser: "react-nextjs-ast",
|
|
659
1116
|
...stats,
|
|
1117
|
+
api_call_detection: {
|
|
1118
|
+
includeExternalFetches,
|
|
1119
|
+
includeConcatFetches: process.env.LAUNCH_CHART_INCLUDE_CONCAT_FETCHES === "1",
|
|
1120
|
+
apiRoutesLoaded: apiRoutes.length,
|
|
1121
|
+
resolved: fetchResolvedCount,
|
|
1122
|
+
dynamic: fetchDynamicCount,
|
|
1123
|
+
unresolved: fetchUnresolvedCount,
|
|
1124
|
+
external: fetchExternalCount
|
|
1125
|
+
},
|
|
660
1126
|
notes: "Auto-generated via TypeScript AST \u2014 edges derived from actual imports, renders from JSX usage, navigations from router/Link calls."
|
|
661
1127
|
},
|
|
662
1128
|
nodes,
|
|
663
1129
|
edges: allEdges,
|
|
664
|
-
cross_refs:
|
|
1130
|
+
cross_refs: crossRefs,
|
|
665
1131
|
contradictions: [],
|
|
666
1132
|
warnings: [],
|
|
667
1133
|
flagged_edges: dedupedFlagged,
|
|
@@ -676,22 +1142,29 @@ function generate(rootDir) {
|
|
|
676
1142
|
}
|
|
677
1143
|
};
|
|
678
1144
|
}
|
|
679
|
-
var
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
1145
|
+
var import_node_fs3, import_node_path3, RENDER_TYPES, reactNextjsParser;
|
|
1146
|
+
var init_react_nextjs = __esm({
|
|
1147
|
+
"src/server/graph/parsers/ui/react-nextjs.ts"() {
|
|
1148
|
+
"use strict";
|
|
1149
|
+
import_node_fs3 = require("node:fs");
|
|
1150
|
+
import_node_path3 = require("node:path");
|
|
1151
|
+
init_ast_helpers();
|
|
1152
|
+
RENDER_TYPES = /* @__PURE__ */ new Set(["component", "ui", "layout", "context"]);
|
|
1153
|
+
reactNextjsParser = {
|
|
1154
|
+
id: "react-nextjs",
|
|
1155
|
+
layer: "ui",
|
|
1156
|
+
detect,
|
|
1157
|
+
generate
|
|
1158
|
+
};
|
|
1159
|
+
}
|
|
1160
|
+
});
|
|
685
1161
|
|
|
686
1162
|
// src/server/graph/parsers/api/nextjs-routes.ts
|
|
687
|
-
var import_node_fs3 = require("node:fs");
|
|
688
|
-
var import_node_path3 = require("node:path");
|
|
689
|
-
var HTTP_METHODS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"]);
|
|
690
1163
|
function walk2(dir) {
|
|
691
1164
|
const results = [];
|
|
692
|
-
if (!(0,
|
|
693
|
-
for (const entry of (0,
|
|
694
|
-
const full = (0,
|
|
1165
|
+
if (!(0, import_node_fs4.existsSync)(dir)) return results;
|
|
1166
|
+
for (const entry of (0, import_node_fs4.readdirSync)(dir, { withFileTypes: true })) {
|
|
1167
|
+
const full = (0, import_node_path4.join)(dir, entry.name);
|
|
695
1168
|
if (entry.isDirectory()) {
|
|
696
1169
|
results.push(...walk2(full));
|
|
697
1170
|
} else if (entry.name === "route.ts" || entry.name === "route.tsx") {
|
|
@@ -701,7 +1174,7 @@ function walk2(dir) {
|
|
|
701
1174
|
return results;
|
|
702
1175
|
}
|
|
703
1176
|
function filePathToRoute(apiDir, absPath) {
|
|
704
|
-
let route = "/" + (0,
|
|
1177
|
+
let route = "/" + (0, import_node_path4.relative)(apiDir, absPath).replace(/\\/g, "/").replace(/\/route\.tsx?$/, "");
|
|
705
1178
|
route = route.replace(/\[([^\]]+)\]/g, ":$1");
|
|
706
1179
|
route = route.replace(/\/+/g, "/");
|
|
707
1180
|
if (route === "/") return "/api";
|
|
@@ -712,10 +1185,10 @@ function camelToPascal(s) {
|
|
|
712
1185
|
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
713
1186
|
}
|
|
714
1187
|
function detect2(rootDir) {
|
|
715
|
-
return (0,
|
|
1188
|
+
return (0, import_node_fs4.existsSync)((0, import_node_path4.join)(rootDir, "src", "app", "api"));
|
|
716
1189
|
}
|
|
717
1190
|
function generate2(rootDir) {
|
|
718
|
-
const apiDir = (0,
|
|
1191
|
+
const apiDir = (0, import_node_path4.join)(rootDir, "src", "app", "api");
|
|
719
1192
|
const routeFiles = walk2(apiDir);
|
|
720
1193
|
const nodes = [];
|
|
721
1194
|
const edges = [];
|
|
@@ -730,10 +1203,10 @@ function generate2(rootDir) {
|
|
|
730
1203
|
const authWrappers = extractAuthWrappers(absPath);
|
|
731
1204
|
const methods = [];
|
|
732
1205
|
for (const exp of parsed.exports) {
|
|
733
|
-
if (
|
|
1206
|
+
if (HTTP_METHODS2.has(exp)) methods.push(exp);
|
|
734
1207
|
}
|
|
735
1208
|
const routePath = filePathToRoute(apiDir, absPath);
|
|
736
|
-
const relPath = (0,
|
|
1209
|
+
const relPath = (0, import_node_path4.relative)(rootDir, absPath).replace(/\\/g, "/");
|
|
737
1210
|
const mutations = dbCalls.filter((c) => c.isMutation);
|
|
738
1211
|
const reads = dbCalls.filter((c) => !c.isMutation);
|
|
739
1212
|
const mutates = mutations.length > 0;
|
|
@@ -800,7 +1273,7 @@ function generate2(rootDir) {
|
|
|
800
1273
|
flagged_edges: [],
|
|
801
1274
|
patterns: {
|
|
802
1275
|
total_endpoints: nodes.length,
|
|
803
|
-
methods_breakdown: [...
|
|
1276
|
+
methods_breakdown: [...HTTP_METHODS2].reduce((acc, m) => {
|
|
804
1277
|
acc[m] = nodes.filter((n) => n.methods.includes(m)).length;
|
|
805
1278
|
return acc;
|
|
806
1279
|
}, {}),
|
|
@@ -810,16 +1283,24 @@ function generate2(rootDir) {
|
|
|
810
1283
|
}
|
|
811
1284
|
};
|
|
812
1285
|
}
|
|
813
|
-
var
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
1286
|
+
var import_node_fs4, import_node_path4, HTTP_METHODS2, nextjsRoutesParser;
|
|
1287
|
+
var init_nextjs_routes = __esm({
|
|
1288
|
+
"src/server/graph/parsers/api/nextjs-routes.ts"() {
|
|
1289
|
+
"use strict";
|
|
1290
|
+
import_node_fs4 = require("node:fs");
|
|
1291
|
+
import_node_path4 = require("node:path");
|
|
1292
|
+
init_ast_helpers();
|
|
1293
|
+
HTTP_METHODS2 = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"]);
|
|
1294
|
+
nextjsRoutesParser = {
|
|
1295
|
+
id: "nextjs-routes",
|
|
1296
|
+
layer: "api",
|
|
1297
|
+
detect: detect2,
|
|
1298
|
+
generate: generate2
|
|
1299
|
+
};
|
|
1300
|
+
}
|
|
1301
|
+
});
|
|
819
1302
|
|
|
820
1303
|
// src/server/graph/parsers/db/prisma-schema.ts
|
|
821
|
-
var import_node_fs4 = require("node:fs");
|
|
822
|
-
var import_node_path4 = require("node:path");
|
|
823
1304
|
function parseModels(content) {
|
|
824
1305
|
const nodes = [];
|
|
825
1306
|
const relations = [];
|
|
@@ -910,11 +1391,11 @@ function parseEnums(content) {
|
|
|
910
1391
|
return nodes;
|
|
911
1392
|
}
|
|
912
1393
|
function detect3(rootDir) {
|
|
913
|
-
return (0,
|
|
1394
|
+
return (0, import_node_fs5.existsSync)((0, import_node_path5.join)(rootDir, "prisma", "schema.prisma"));
|
|
914
1395
|
}
|
|
915
1396
|
function generate3(rootDir) {
|
|
916
|
-
const schemaPath = (0,
|
|
917
|
-
const content = (0,
|
|
1397
|
+
const schemaPath = (0, import_node_path5.join)(rootDir, "prisma", "schema.prisma");
|
|
1398
|
+
const content = (0, import_node_fs5.readFileSync)(schemaPath, "utf-8");
|
|
918
1399
|
const { nodes: modelNodes, relations } = parseModels(content);
|
|
919
1400
|
const enumNodes = parseEnums(content);
|
|
920
1401
|
const allNodes = [...modelNodes, ...enumNodes];
|
|
@@ -963,19 +1444,22 @@ function generate3(rootDir) {
|
|
|
963
1444
|
}
|
|
964
1445
|
};
|
|
965
1446
|
}
|
|
966
|
-
var
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
1447
|
+
var import_node_fs5, import_node_path5, prismaSchemaParser;
|
|
1448
|
+
var init_prisma_schema = __esm({
|
|
1449
|
+
"src/server/graph/parsers/db/prisma-schema.ts"() {
|
|
1450
|
+
"use strict";
|
|
1451
|
+
import_node_fs5 = require("node:fs");
|
|
1452
|
+
import_node_path5 = require("node:path");
|
|
1453
|
+
prismaSchemaParser = {
|
|
1454
|
+
id: "prisma-schema",
|
|
1455
|
+
layer: "db",
|
|
1456
|
+
detect: detect3,
|
|
1457
|
+
generate: generate3
|
|
1458
|
+
};
|
|
1459
|
+
}
|
|
1460
|
+
});
|
|
972
1461
|
|
|
973
1462
|
// src/server/graph/core/graph-builder.ts
|
|
974
|
-
var ALL_PARSERS = [
|
|
975
|
-
reactNextjsParser,
|
|
976
|
-
nextjsRoutesParser,
|
|
977
|
-
prismaSchemaParser
|
|
978
|
-
];
|
|
979
1463
|
function getParser(layer) {
|
|
980
1464
|
return ALL_PARSERS.find((p) => p.layer === layer);
|
|
981
1465
|
}
|
|
@@ -992,37 +1476,49 @@ function generateLayer(rootDir, layer) {
|
|
|
992
1476
|
};
|
|
993
1477
|
}
|
|
994
1478
|
function generateAll(rootDir) {
|
|
995
|
-
const layers = ["
|
|
1479
|
+
const layers = ["api", "db", "ui"];
|
|
996
1480
|
const results = [];
|
|
997
1481
|
for (const layer of layers) {
|
|
998
1482
|
const result = generateLayer(rootDir, layer);
|
|
999
1483
|
if (result) results.push(result);
|
|
1000
1484
|
}
|
|
1001
|
-
|
|
1485
|
+
const byLayer = new Map(results.map((r) => [r.layer, r]));
|
|
1486
|
+
return ["ui", "api", "db"].map((l) => byLayer.get(l)).filter((r) => !!r);
|
|
1002
1487
|
}
|
|
1488
|
+
var ALL_PARSERS;
|
|
1489
|
+
var init_graph_builder = __esm({
|
|
1490
|
+
"src/server/graph/core/graph-builder.ts"() {
|
|
1491
|
+
"use strict";
|
|
1492
|
+
init_react_nextjs();
|
|
1493
|
+
init_nextjs_routes();
|
|
1494
|
+
init_prisma_schema();
|
|
1495
|
+
ALL_PARSERS = [
|
|
1496
|
+
reactNextjsParser,
|
|
1497
|
+
nextjsRoutesParser,
|
|
1498
|
+
prismaSchemaParser
|
|
1499
|
+
];
|
|
1500
|
+
}
|
|
1501
|
+
});
|
|
1003
1502
|
|
|
1004
1503
|
// src/server/graph/index.ts
|
|
1005
|
-
var GRAPHS_DIR = ".launchsecure/graphs";
|
|
1006
|
-
var LAYERS = ["ui", "api", "db"];
|
|
1007
|
-
var graphCache = /* @__PURE__ */ new Map();
|
|
1008
1504
|
function graphsDir(rootDir) {
|
|
1009
|
-
return (0,
|
|
1505
|
+
return (0, import_node_path6.join)(rootDir, GRAPHS_DIR);
|
|
1010
1506
|
}
|
|
1011
1507
|
function graphFilePath(rootDir, layer) {
|
|
1012
|
-
return (0,
|
|
1508
|
+
return (0, import_node_path6.join)(graphsDir(rootDir), `${layer}.json`);
|
|
1013
1509
|
}
|
|
1014
1510
|
function invalidateCache(filePath) {
|
|
1015
1511
|
graphCache.delete(filePath);
|
|
1016
1512
|
}
|
|
1017
1513
|
function readGraph(rootDir, layer) {
|
|
1018
1514
|
const filePath = graphFilePath(rootDir, layer);
|
|
1019
|
-
if (!(0,
|
|
1020
|
-
const stat = (0,
|
|
1515
|
+
if (!(0, import_node_fs6.existsSync)(filePath)) return null;
|
|
1516
|
+
const stat = (0, import_node_fs6.statSync)(filePath);
|
|
1021
1517
|
const cached = graphCache.get(filePath);
|
|
1022
1518
|
if (cached && cached.mtimeMs === stat.mtimeMs) {
|
|
1023
1519
|
return cached.graph;
|
|
1024
1520
|
}
|
|
1025
|
-
const content = (0,
|
|
1521
|
+
const content = (0, import_node_fs6.readFileSync)(filePath, "utf-8");
|
|
1026
1522
|
const graph = JSON.parse(content);
|
|
1027
1523
|
graphCache.set(filePath, { mtimeMs: stat.mtimeMs, graph });
|
|
1028
1524
|
return graph;
|
|
@@ -1037,139 +1533,287 @@ function readAllGraphs(rootDir) {
|
|
|
1037
1533
|
}
|
|
1038
1534
|
function generateGraph(rootDir, layer) {
|
|
1039
1535
|
const dir = graphsDir(rootDir);
|
|
1040
|
-
(0,
|
|
1536
|
+
(0, import_node_fs6.mkdirSync)(dir, { recursive: true });
|
|
1041
1537
|
const results = layer ? [generateLayer(rootDir, layer)].filter((r) => r !== null) : generateAll(rootDir);
|
|
1042
1538
|
for (const result of results) {
|
|
1043
1539
|
const filePath = graphFilePath(rootDir, result.layer);
|
|
1044
|
-
(0,
|
|
1540
|
+
(0, import_node_fs6.writeFileSync)(filePath, JSON.stringify(result.output, null, 2) + "\n", "utf-8");
|
|
1045
1541
|
invalidateCache(filePath);
|
|
1046
1542
|
}
|
|
1047
1543
|
return results;
|
|
1048
1544
|
}
|
|
1545
|
+
var import_node_fs6, import_node_path6, GRAPHS_DIR, LAYERS, graphCache;
|
|
1546
|
+
var init_graph = __esm({
|
|
1547
|
+
"src/server/graph/index.ts"() {
|
|
1548
|
+
"use strict";
|
|
1549
|
+
import_node_fs6 = require("node:fs");
|
|
1550
|
+
import_node_path6 = require("node:path");
|
|
1551
|
+
init_graph_builder();
|
|
1552
|
+
GRAPHS_DIR = ".launchsecure/graphs";
|
|
1553
|
+
LAYERS = ["ui", "api", "db"];
|
|
1554
|
+
graphCache = /* @__PURE__ */ new Map();
|
|
1555
|
+
}
|
|
1556
|
+
});
|
|
1049
1557
|
|
|
1050
|
-
// src/server/
|
|
1051
|
-
var
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1558
|
+
// src/server/chart-serve.ts
|
|
1559
|
+
var chart_serve_exports = {};
|
|
1560
|
+
__export(chart_serve_exports, {
|
|
1561
|
+
runServeCli: () => runServeCli,
|
|
1562
|
+
startChartServer: () => startChartServer
|
|
1563
|
+
});
|
|
1564
|
+
function findProjectRoot(startDir) {
|
|
1565
|
+
let dir = startDir;
|
|
1566
|
+
for (let i = 0; i < 8; i++) {
|
|
1567
|
+
const graphsDir2 = import_node_path7.default.join(dir, ".launchsecure", "graphs");
|
|
1568
|
+
if (import_node_fs7.default.existsSync(import_node_path7.default.join(graphsDir2, "ui.json")) || import_node_fs7.default.existsSync(import_node_path7.default.join(graphsDir2, "api.json")) || import_node_fs7.default.existsSync(import_node_path7.default.join(graphsDir2, "db.json"))) return dir;
|
|
1569
|
+
const parent = import_node_path7.default.dirname(dir);
|
|
1570
|
+
if (parent === dir) break;
|
|
1571
|
+
dir = parent;
|
|
1572
|
+
}
|
|
1573
|
+
dir = startDir;
|
|
1574
|
+
for (let i = 0; i < 8; i++) {
|
|
1575
|
+
if (import_node_fs7.default.existsSync(import_node_path7.default.join(dir, ".git"))) return dir;
|
|
1576
|
+
const parent = import_node_path7.default.dirname(dir);
|
|
1577
|
+
if (parent === dir) break;
|
|
1578
|
+
dir = parent;
|
|
1579
|
+
}
|
|
1580
|
+
return startDir;
|
|
1581
|
+
}
|
|
1582
|
+
function buildMergedGraph(projectRoot) {
|
|
1583
|
+
let graphs = readAllGraphs(projectRoot);
|
|
1584
|
+
if (!graphs.ui && !graphs.api && !graphs.db) {
|
|
1585
|
+
generateGraph(projectRoot);
|
|
1586
|
+
graphs = readAllGraphs(projectRoot);
|
|
1587
|
+
}
|
|
1588
|
+
const nodes = [];
|
|
1589
|
+
const rawLinks = [];
|
|
1590
|
+
const LAYERS2 = ["ui", "api", "db"];
|
|
1591
|
+
for (const layer of LAYERS2) {
|
|
1592
|
+
const g = graphs[layer];
|
|
1593
|
+
if (!g) continue;
|
|
1594
|
+
for (const n of g.nodes) {
|
|
1595
|
+
nodes.push({
|
|
1596
|
+
id: `${layer}:${n.id}`,
|
|
1597
|
+
name: n.name,
|
|
1598
|
+
type: n.type,
|
|
1599
|
+
layer,
|
|
1600
|
+
module: n.module ?? null,
|
|
1601
|
+
path: n.path ?? n.id
|
|
1602
|
+
});
|
|
1603
|
+
}
|
|
1604
|
+
for (const e of g.edges) {
|
|
1605
|
+
rawLinks.push({ source: `${layer}:${e.source}`, target: `${layer}:${e.target}`, type: e.type, layer, cross: false });
|
|
1606
|
+
}
|
|
1607
|
+
for (const c of g.cross_refs ?? []) {
|
|
1608
|
+
rawLinks.push({ source: `${layer}:${c.source}`, target: `${c.layer}:${c.target}`, type: c.type, layer, cross: true });
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
const nodeIds = new Set(nodes.map((n) => n.id));
|
|
1612
|
+
const links = rawLinks.filter((l) => nodeIds.has(l.source) && nodeIds.has(l.target));
|
|
1613
|
+
return {
|
|
1614
|
+
nodes,
|
|
1615
|
+
links,
|
|
1616
|
+
stats: {
|
|
1617
|
+
nodes: nodes.length,
|
|
1618
|
+
links: links.length,
|
|
1619
|
+
byLayer: {
|
|
1620
|
+
ui: graphs.ui ? graphs.ui.nodes.length : 0,
|
|
1621
|
+
api: graphs.api ? graphs.api.nodes.length : 0,
|
|
1622
|
+
db: graphs.db ? graphs.db.nodes.length : 0
|
|
1067
1623
|
}
|
|
1068
1624
|
}
|
|
1069
|
-
}
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1625
|
+
};
|
|
1626
|
+
}
|
|
1627
|
+
function serveStatic(res, filePath) {
|
|
1628
|
+
if (!import_node_fs7.default.existsSync(filePath) || !import_node_fs7.default.statSync(filePath).isFile()) return false;
|
|
1629
|
+
const ext = import_node_path7.default.extname(filePath).toLowerCase();
|
|
1630
|
+
const mime = MIME_TYPES[ext] ?? "application/octet-stream";
|
|
1631
|
+
res.writeHead(200, { "Content-Type": mime, "Cache-Control": "no-cache" });
|
|
1632
|
+
import_node_fs7.default.createReadStream(filePath).pipe(res);
|
|
1633
|
+
return true;
|
|
1634
|
+
}
|
|
1635
|
+
function serveIndex(res, clientDir) {
|
|
1636
|
+
const indexPath = import_node_path7.default.join(clientDir, "index.html");
|
|
1637
|
+
if (!import_node_fs7.default.existsSync(indexPath)) {
|
|
1638
|
+
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
1639
|
+
res.end(`LaunchChart client bundle not found at ${clientDir}. Run 'npm run build:chart-client'.`);
|
|
1640
|
+
return;
|
|
1641
|
+
}
|
|
1642
|
+
serveStatic(res, indexPath);
|
|
1643
|
+
}
|
|
1644
|
+
function tryListen(server, port) {
|
|
1645
|
+
return new Promise((resolve, reject) => {
|
|
1646
|
+
const onError = (err2) => {
|
|
1647
|
+
server.off("listening", onListening);
|
|
1648
|
+
reject(err2);
|
|
1649
|
+
};
|
|
1650
|
+
const onListening = () => {
|
|
1651
|
+
server.off("error", onError);
|
|
1652
|
+
resolve(port);
|
|
1653
|
+
};
|
|
1654
|
+
server.once("error", onError);
|
|
1655
|
+
server.once("listening", onListening);
|
|
1656
|
+
server.listen(port, "127.0.0.1");
|
|
1657
|
+
});
|
|
1658
|
+
}
|
|
1659
|
+
async function bindWithFallback(server, startPort) {
|
|
1660
|
+
let lastErr = null;
|
|
1661
|
+
for (let i = 0; i < MAX_PORT_SCAN; i++) {
|
|
1662
|
+
const port = startPort + i;
|
|
1663
|
+
try {
|
|
1664
|
+
return await tryListen(server, port);
|
|
1665
|
+
} catch (err2) {
|
|
1666
|
+
const code = err2.code;
|
|
1667
|
+
if (code === "EADDRINUSE") {
|
|
1668
|
+
lastErr = err2;
|
|
1669
|
+
continue;
|
|
1670
|
+
}
|
|
1671
|
+
throw err2;
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
throw lastErr ?? new Error("Failed to bind any port");
|
|
1675
|
+
}
|
|
1676
|
+
async function startChartServer(opts = {}) {
|
|
1677
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
1678
|
+
const projectRoot = findProjectRoot(cwd);
|
|
1679
|
+
const existing = getLiveLock();
|
|
1680
|
+
if (existing) {
|
|
1681
|
+
if (!opts.quiet) {
|
|
1682
|
+
process.stderr.write(
|
|
1683
|
+
`[launch-chart] already running (pid ${existing.pid}) at ${existing.url}
|
|
1684
|
+
`
|
|
1685
|
+
);
|
|
1686
|
+
}
|
|
1687
|
+
return { port: existing.port, url: existing.url };
|
|
1688
|
+
}
|
|
1689
|
+
const clientDir = opts.clientDir ?? import_node_path7.default.join(__dirname, "..", "chart-client");
|
|
1690
|
+
const server = import_node_http.default.createServer((req, res) => {
|
|
1691
|
+
try {
|
|
1692
|
+
const url2 = new URL(req.url ?? "/", `http://${req.headers.host}`);
|
|
1693
|
+
if (req.method === "GET" && url2.pathname === "/api/project-graph") {
|
|
1694
|
+
const regenerate = url2.searchParams.get("regenerate") === "1";
|
|
1695
|
+
if (regenerate) generateGraph(projectRoot);
|
|
1696
|
+
const merged = buildMergedGraph(projectRoot);
|
|
1697
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1698
|
+
res.end(JSON.stringify({
|
|
1699
|
+
...merged,
|
|
1700
|
+
debug: { cwd, projectRoot }
|
|
1701
|
+
}));
|
|
1702
|
+
return;
|
|
1703
|
+
}
|
|
1704
|
+
if (req.method === "GET" && url2.pathname === "/api/raw-graphs") {
|
|
1705
|
+
const graphs = readAllGraphs(projectRoot);
|
|
1706
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1707
|
+
res.end(JSON.stringify({ ui: graphs.ui ?? null, api: graphs.api ?? null, db: graphs.db ?? null }));
|
|
1708
|
+
return;
|
|
1709
|
+
}
|
|
1710
|
+
if (req.method === "POST" && url2.pathname === "/api/generate-graph") {
|
|
1711
|
+
try {
|
|
1712
|
+
generateGraph(projectRoot);
|
|
1713
|
+
const graphs = readAllGraphs(projectRoot);
|
|
1714
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1715
|
+
res.end(JSON.stringify({
|
|
1716
|
+
ok: true,
|
|
1717
|
+
ui: graphs.ui ?? null,
|
|
1718
|
+
api: graphs.api ?? null,
|
|
1719
|
+
db: graphs.db ?? null
|
|
1720
|
+
}));
|
|
1721
|
+
} catch (err2) {
|
|
1722
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1723
|
+
res.end(JSON.stringify({ ok: false, error: String(err2) }));
|
|
1120
1724
|
}
|
|
1725
|
+
return;
|
|
1726
|
+
}
|
|
1727
|
+
if (req.method === "GET" && url2.pathname === "/api/health") {
|
|
1728
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1729
|
+
res.end(JSON.stringify({ ok: true, projectRoot }));
|
|
1730
|
+
return;
|
|
1121
1731
|
}
|
|
1732
|
+
if (url2.pathname !== "/") {
|
|
1733
|
+
const staticPath = import_node_path7.default.join(clientDir, url2.pathname);
|
|
1734
|
+
if (serveStatic(res, staticPath)) return;
|
|
1735
|
+
}
|
|
1736
|
+
serveIndex(res, clientDir);
|
|
1737
|
+
} catch (err2) {
|
|
1738
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1739
|
+
res.end(JSON.stringify({ error: String(err2) }));
|
|
1122
1740
|
}
|
|
1123
|
-
}
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
module: { type: "string", description: "UI layer only \u2014 filter by module." },
|
|
1162
|
-
node_id: { type: "string", description: "Center node for neighborhood scope." },
|
|
1163
|
-
hops: { type: "number", description: "Neighborhood radius (default 1)." },
|
|
1164
|
-
case_insensitive: { type: "boolean", description: "Case-insensitive regex. Default false." },
|
|
1165
|
-
context: { type: "number", description: "Context lines around each match. Default 2." },
|
|
1166
|
-
max_matches: { type: "number", description: "Max matches to return total. Default 50." },
|
|
1167
|
-
max_files: { type: "number", description: "Max files to search. Default 50." }
|
|
1168
|
-
},
|
|
1169
|
-
required: ["layer", "pattern"]
|
|
1741
|
+
});
|
|
1742
|
+
const port = await bindWithFallback(server, opts.port ?? DEFAULT_PORT);
|
|
1743
|
+
const url = `http://localhost:${port}`;
|
|
1744
|
+
writeLock({
|
|
1745
|
+
pid: process.pid,
|
|
1746
|
+
port,
|
|
1747
|
+
cwd,
|
|
1748
|
+
url,
|
|
1749
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1750
|
+
});
|
|
1751
|
+
const cleanup = () => {
|
|
1752
|
+
clearLock();
|
|
1753
|
+
server.close();
|
|
1754
|
+
};
|
|
1755
|
+
process.once("SIGINT", () => {
|
|
1756
|
+
cleanup();
|
|
1757
|
+
process.exit(0);
|
|
1758
|
+
});
|
|
1759
|
+
process.once("SIGTERM", () => {
|
|
1760
|
+
cleanup();
|
|
1761
|
+
process.exit(0);
|
|
1762
|
+
});
|
|
1763
|
+
process.once("exit", cleanup);
|
|
1764
|
+
if (!opts.quiet) {
|
|
1765
|
+
process.stderr.write(`[launch-chart] serving ${url}
|
|
1766
|
+
`);
|
|
1767
|
+
process.stderr.write(`[launch-chart] project root: ${projectRoot}
|
|
1768
|
+
`);
|
|
1769
|
+
}
|
|
1770
|
+
return { port, url };
|
|
1771
|
+
}
|
|
1772
|
+
function runServeCli(argv) {
|
|
1773
|
+
let port;
|
|
1774
|
+
for (let i = 0; i < argv.length; i++) {
|
|
1775
|
+
if (argv[i] === "--port" && argv[i + 1]) {
|
|
1776
|
+
port = parseInt(argv[++i], 10);
|
|
1777
|
+
} else if (argv[i].startsWith("--port=")) {
|
|
1778
|
+
port = parseInt(argv[i].slice("--port=".length), 10);
|
|
1170
1779
|
}
|
|
1171
1780
|
}
|
|
1172
|
-
|
|
1781
|
+
startChartServer({ port }).catch((err2) => {
|
|
1782
|
+
process.stderr.write(`[launch-chart] failed to start: ${err2}
|
|
1783
|
+
`);
|
|
1784
|
+
process.exit(1);
|
|
1785
|
+
});
|
|
1786
|
+
}
|
|
1787
|
+
var import_node_http, import_node_fs7, import_node_path7, DEFAULT_PORT, MAX_PORT_SCAN, MIME_TYPES;
|
|
1788
|
+
var init_chart_serve = __esm({
|
|
1789
|
+
"src/server/chart-serve.ts"() {
|
|
1790
|
+
"use strict";
|
|
1791
|
+
import_node_http = __toESM(require("node:http"));
|
|
1792
|
+
import_node_fs7 = __toESM(require("node:fs"));
|
|
1793
|
+
import_node_path7 = __toESM(require("node:path"));
|
|
1794
|
+
init_graph();
|
|
1795
|
+
init_lockfile();
|
|
1796
|
+
DEFAULT_PORT = 52819;
|
|
1797
|
+
MAX_PORT_SCAN = 20;
|
|
1798
|
+
MIME_TYPES = {
|
|
1799
|
+
".html": "text/html; charset=utf-8",
|
|
1800
|
+
".js": "application/javascript; charset=utf-8",
|
|
1801
|
+
".css": "text/css; charset=utf-8",
|
|
1802
|
+
".json": "application/json; charset=utf-8",
|
|
1803
|
+
".png": "image/png",
|
|
1804
|
+
".svg": "image/svg+xml",
|
|
1805
|
+
".ico": "image/x-icon",
|
|
1806
|
+
".woff": "font/woff",
|
|
1807
|
+
".woff2": "font/woff2"
|
|
1808
|
+
};
|
|
1809
|
+
}
|
|
1810
|
+
});
|
|
1811
|
+
|
|
1812
|
+
// src/server/graph-mcp.ts
|
|
1813
|
+
var graph_mcp_exports = {};
|
|
1814
|
+
__export(graph_mcp_exports, {
|
|
1815
|
+
startGraphMcpServer: () => startGraphMcpServer
|
|
1816
|
+
});
|
|
1173
1817
|
function matchesSearch(node, query) {
|
|
1174
1818
|
const q = query.toLowerCase();
|
|
1175
1819
|
if (node.id.toLowerCase().includes(q)) return true;
|
|
@@ -1181,30 +1825,88 @@ function matchesSearch(node, query) {
|
|
|
1181
1825
|
function toMinimal(nodes) {
|
|
1182
1826
|
return nodes.map((n) => {
|
|
1183
1827
|
const out = { id: n.id, type: n.type, name: n.name };
|
|
1184
|
-
if (n.module) out.module = n.module;
|
|
1185
|
-
if (n.route) out.route = n.route;
|
|
1186
|
-
if (n.methods) out.methods = n.methods;
|
|
1828
|
+
if (n.module != null) out.module = n.module;
|
|
1829
|
+
if (n.route != null) out.route = n.route;
|
|
1830
|
+
if (n.methods != null) out.methods = n.methods;
|
|
1187
1831
|
return out;
|
|
1188
1832
|
});
|
|
1189
1833
|
}
|
|
1190
|
-
function
|
|
1834
|
+
function toCompactNode(n) {
|
|
1835
|
+
const out = { i: n.id, t: n.type, n: n.name };
|
|
1836
|
+
if (n.module != null) out.m = n.module;
|
|
1837
|
+
if (n.route != null) out.r = n.route;
|
|
1838
|
+
if (n.methods != null) out.mt = n.methods;
|
|
1839
|
+
if (n.exports != null) out.x = n.exports;
|
|
1840
|
+
if (n.columns != null) out.c = n.columns;
|
|
1841
|
+
for (const k of Object.keys(n)) {
|
|
1842
|
+
if (!COMPACT_NODE_KNOWN_KEYS.has(k) && n[k] != null) out[k] = n[k];
|
|
1843
|
+
}
|
|
1844
|
+
return out;
|
|
1845
|
+
}
|
|
1846
|
+
function toCompactEdges(edges, idx) {
|
|
1847
|
+
return edges.map((e) => {
|
|
1848
|
+
const s = idx.get(e.source);
|
|
1849
|
+
const d = idx.get(e.target);
|
|
1850
|
+
const o = {
|
|
1851
|
+
s: s ?? e.source,
|
|
1852
|
+
d: d ?? e.target,
|
|
1853
|
+
t: e.type
|
|
1854
|
+
};
|
|
1855
|
+
if (e.label != null) o.l = e.label;
|
|
1856
|
+
return o;
|
|
1857
|
+
});
|
|
1858
|
+
}
|
|
1859
|
+
function compactResult(raw) {
|
|
1860
|
+
const nodes = raw.nodes;
|
|
1861
|
+
if (!nodes) return raw;
|
|
1862
|
+
const idx = /* @__PURE__ */ new Map();
|
|
1863
|
+
nodes.forEach((n, i) => idx.set(n.id, i));
|
|
1864
|
+
const compactNodes = nodes.map(toCompactNode);
|
|
1865
|
+
const edges = raw.edges;
|
|
1866
|
+
const compactEdges = edges ? toCompactEdges(edges, idx) : void 0;
|
|
1867
|
+
const out = { _schema: COMPACT_SCHEMA };
|
|
1868
|
+
for (const [k, v] of Object.entries(raw)) {
|
|
1869
|
+
if (k === "nodes" || k === "edges") continue;
|
|
1870
|
+
out[k] = v;
|
|
1871
|
+
}
|
|
1872
|
+
out.nodes = compactNodes;
|
|
1873
|
+
if (compactEdges !== void 0) out.edges = compactEdges;
|
|
1874
|
+
return out;
|
|
1875
|
+
}
|
|
1876
|
+
function neighborhood(graph, centerId, hops, layer, minimal) {
|
|
1191
1877
|
const center = graph.nodes.find((n) => n.id === centerId);
|
|
1192
1878
|
if (!center) return null;
|
|
1193
1879
|
const visited = /* @__PURE__ */ new Set([centerId]);
|
|
1194
1880
|
let frontier = /* @__PURE__ */ new Set([centerId]);
|
|
1881
|
+
let budgetExceeded = false;
|
|
1882
|
+
let stoppedAtHop = 0;
|
|
1195
1883
|
for (let h = 0; h < hops; h++) {
|
|
1196
1884
|
const next = /* @__PURE__ */ new Set();
|
|
1197
1885
|
for (const edge of graph.edges) {
|
|
1198
1886
|
if (frontier.has(edge.source) && !visited.has(edge.target)) next.add(edge.target);
|
|
1199
1887
|
if (frontier.has(edge.target) && !visited.has(edge.source)) next.add(edge.source);
|
|
1200
1888
|
}
|
|
1889
|
+
const projectedVisited = visited.size + next.size;
|
|
1890
|
+
let projectedEdges = 0;
|
|
1891
|
+
for (const e of graph.edges) {
|
|
1892
|
+
const srcIn = visited.has(e.source) || next.has(e.source);
|
|
1893
|
+
const dstIn = visited.has(e.target) || next.has(e.target);
|
|
1894
|
+
if (srcIn && dstIn) projectedEdges++;
|
|
1895
|
+
}
|
|
1896
|
+
const perNode = minimal ? EST_CHARS_PER_NODE_MIN[layer] : EST_CHARS_PER_NODE_FULL[layer];
|
|
1897
|
+
const projectedChars = projectedVisited * perNode + projectedEdges * EST_CHARS_PER_EDGE[layer];
|
|
1898
|
+
if (projectedChars > NEIGHBORHOOD_BUDGET_CHARS) {
|
|
1899
|
+
budgetExceeded = true;
|
|
1900
|
+
break;
|
|
1901
|
+
}
|
|
1201
1902
|
for (const id of next) visited.add(id);
|
|
1202
1903
|
frontier = next;
|
|
1904
|
+
stoppedAtHop = h + 1;
|
|
1203
1905
|
if (frontier.size === 0) break;
|
|
1204
1906
|
}
|
|
1205
1907
|
const nodes = graph.nodes.filter((n) => visited.has(n.id));
|
|
1206
1908
|
const edges = graph.edges.filter((e) => visited.has(e.source) && visited.has(e.target));
|
|
1207
|
-
return { nodes, edges };
|
|
1909
|
+
return { nodes, edges, budgetExceeded, stoppedAtHop };
|
|
1208
1910
|
}
|
|
1209
1911
|
function layerSummary(graph) {
|
|
1210
1912
|
const typeCounts = {};
|
|
@@ -1263,14 +1965,16 @@ Output: .launchsecure/graphs/
|
|
|
1263
1965
|
Use read_graph with filters (search/type/module/node_id) to query.`
|
|
1264
1966
|
);
|
|
1265
1967
|
}
|
|
1266
|
-
function
|
|
1968
|
+
function runReadGraphQueryRaw(rootDir, args) {
|
|
1267
1969
|
const layer = args.layer;
|
|
1268
1970
|
const search = args.search;
|
|
1269
1971
|
const type = args.type;
|
|
1270
1972
|
const module_ = args.module;
|
|
1271
1973
|
const nodeId = args.node_id;
|
|
1272
1974
|
const hops = args.hops ?? 1;
|
|
1273
|
-
const
|
|
1975
|
+
const layerIsDb = args.layer === "db";
|
|
1976
|
+
const minimal = args.minimal ?? layerIsDb;
|
|
1977
|
+
const includeEdges = args.include_edges;
|
|
1274
1978
|
const hasFilter = !!(search || type || module_ || nodeId);
|
|
1275
1979
|
if (layer && !["ui", "api", "db"].includes(layer)) {
|
|
1276
1980
|
return { error: `Invalid layer "${layer}". Must be one of: ui, api, db` };
|
|
@@ -1296,19 +2000,26 @@ function runReadGraphQuery(rootDir, args) {
|
|
|
1296
2000
|
return { error: `No ${layer} graph found at .launchsecure/graphs/${layer}.json. Run generate_graph first.` };
|
|
1297
2001
|
}
|
|
1298
2002
|
if (nodeId) {
|
|
1299
|
-
const nb = neighborhood(graph, nodeId, hops);
|
|
2003
|
+
const nb = neighborhood(graph, nodeId, hops, layer, minimal);
|
|
1300
2004
|
if (!nb) {
|
|
1301
2005
|
return { error: `Node "${nodeId}" not found in ${layer} graph. Try read_graph with search="${nodeId}" to find similar nodes.` };
|
|
1302
2006
|
}
|
|
1303
|
-
|
|
2007
|
+
const wantEdges2 = includeEdges ?? true;
|
|
2008
|
+
const result2 = {
|
|
1304
2009
|
layer,
|
|
1305
2010
|
center: nodeId,
|
|
1306
|
-
hops,
|
|
2011
|
+
hops_requested: hops,
|
|
2012
|
+
hops_traversed: nb.stoppedAtHop,
|
|
1307
2013
|
node_count: nb.nodes.length,
|
|
1308
2014
|
edge_count: nb.edges.length,
|
|
1309
|
-
nodes: minimal ? toMinimal(nb.nodes) : nb.nodes
|
|
1310
|
-
edges: nb.edges
|
|
2015
|
+
nodes: minimal ? toMinimal(nb.nodes) : nb.nodes
|
|
1311
2016
|
};
|
|
2017
|
+
if (wantEdges2) result2.edges = nb.edges;
|
|
2018
|
+
if (nb.budgetExceeded) {
|
|
2019
|
+
result2.budget_exceeded = true;
|
|
2020
|
+
result2.hint = `Neighborhood truncated at hop ${nb.stoppedAtHop} (projected size exceeded budget). To explore further, call read_graph with node_id set to a specific neighbor from the returned nodes, or rerun with hops=${Math.max(1, nb.stoppedAtHop)} to confirm the partial view is what you wanted.`;
|
|
2021
|
+
}
|
|
2022
|
+
return result2;
|
|
1312
2023
|
}
|
|
1313
2024
|
if (!hasFilter) {
|
|
1314
2025
|
return {
|
|
@@ -1333,14 +2044,24 @@ function runReadGraphQuery(rootDir, args) {
|
|
|
1333
2044
|
hint: "No nodes matched. Check spelling, or call read_graph without filter to see the summary and available types/modules."
|
|
1334
2045
|
};
|
|
1335
2046
|
}
|
|
1336
|
-
|
|
2047
|
+
const wantEdges = includeEdges ?? false;
|
|
2048
|
+
const result = {
|
|
1337
2049
|
layer,
|
|
1338
2050
|
filter: { search, type, module: module_ },
|
|
1339
2051
|
matched: matched.length,
|
|
1340
2052
|
edge_count: matchedEdges.length,
|
|
1341
|
-
nodes: minimal ? toMinimal(matched) : matched
|
|
1342
|
-
edges: matchedEdges
|
|
2053
|
+
nodes: minimal ? toMinimal(matched) : matched
|
|
1343
2054
|
};
|
|
2055
|
+
if (wantEdges) {
|
|
2056
|
+
result.edges = matchedEdges;
|
|
2057
|
+
} else {
|
|
2058
|
+
result.edges_hint = `${matchedEdges.length} edges between matched nodes omitted. Pass include_edges:true to retrieve them (only do this when you actually need edge data).`;
|
|
2059
|
+
}
|
|
2060
|
+
return result;
|
|
2061
|
+
}
|
|
2062
|
+
function runReadGraphQuery(rootDir, args) {
|
|
2063
|
+
const raw = runReadGraphQueryRaw(rootDir, args);
|
|
2064
|
+
return compactResult(raw);
|
|
1344
2065
|
}
|
|
1345
2066
|
function handleReadGraph(args) {
|
|
1346
2067
|
const rootDir = process.cwd();
|
|
@@ -1349,16 +2070,57 @@ function handleReadGraph(args) {
|
|
|
1349
2070
|
if (queries.length === 0) {
|
|
1350
2071
|
return err("queries array is empty. Provide at least one query object.");
|
|
1351
2072
|
}
|
|
1352
|
-
const results =
|
|
1353
|
-
|
|
2073
|
+
const results = [];
|
|
2074
|
+
let cumulativeChars = 0;
|
|
2075
|
+
let budgetHit = false;
|
|
2076
|
+
for (let i = 0; i < queries.length; i++) {
|
|
2077
|
+
const q = queries[i];
|
|
2078
|
+
if (budgetHit) {
|
|
2079
|
+
results.push({
|
|
2080
|
+
index: i,
|
|
2081
|
+
query: q,
|
|
2082
|
+
result: {
|
|
2083
|
+
skipped: true,
|
|
2084
|
+
reason: "batch_budget_exhausted",
|
|
2085
|
+
hint: "Previous sub-results filled the batch budget. Re-run this query on its own."
|
|
2086
|
+
}
|
|
2087
|
+
});
|
|
2088
|
+
continue;
|
|
2089
|
+
}
|
|
2090
|
+
const r = runReadGraphQuery(rootDir, q);
|
|
2091
|
+
const entry = { index: i, query: q, result: r };
|
|
2092
|
+
const entrySize = JSON.stringify(entry, null, 2).length;
|
|
2093
|
+
if (cumulativeChars + entrySize > BATCH_BUDGET_CHARS && results.length > 0) {
|
|
2094
|
+
budgetHit = true;
|
|
2095
|
+
results.push({
|
|
2096
|
+
index: i,
|
|
2097
|
+
query: q,
|
|
2098
|
+
result: {
|
|
2099
|
+
skipped: true,
|
|
2100
|
+
reason: "batch_budget_exhausted",
|
|
2101
|
+
hint: "This sub-query would push the batch over its size budget. Re-run it on its own."
|
|
2102
|
+
}
|
|
2103
|
+
});
|
|
2104
|
+
} else {
|
|
2105
|
+
results.push(entry);
|
|
2106
|
+
cumulativeChars += entrySize;
|
|
2107
|
+
}
|
|
2108
|
+
}
|
|
2109
|
+
return okJson({
|
|
2110
|
+
batch: true,
|
|
2111
|
+
count: results.length,
|
|
2112
|
+
cumulative_chars: cumulativeChars,
|
|
2113
|
+
budget_hit: budgetHit,
|
|
2114
|
+
results
|
|
2115
|
+
});
|
|
1354
2116
|
}
|
|
1355
2117
|
const result = runReadGraphQuery(rootDir, args);
|
|
1356
2118
|
return okJson(result);
|
|
1357
2119
|
}
|
|
1358
2120
|
function nodeToFilePath(rootDir, layer, nodeId) {
|
|
1359
|
-
if (layer === "ui") return (0,
|
|
1360
|
-
if (layer === "api") return (0,
|
|
1361
|
-
if (layer === "db") return (0,
|
|
2121
|
+
if (layer === "ui") return (0, import_node_path8.join)(rootDir, "src", nodeId);
|
|
2122
|
+
if (layer === "api") return (0, import_node_path8.join)(rootDir, nodeId);
|
|
2123
|
+
if (layer === "db") return (0, import_node_path8.join)(rootDir, "prisma", "schema.prisma");
|
|
1362
2124
|
return null;
|
|
1363
2125
|
}
|
|
1364
2126
|
function handleGrepNodes(args) {
|
|
@@ -1380,7 +2142,7 @@ function handleGrepNodes(args) {
|
|
|
1380
2142
|
} catch (e) {
|
|
1381
2143
|
return err(`Invalid regex: ${e.message}`);
|
|
1382
2144
|
}
|
|
1383
|
-
const queryResult =
|
|
2145
|
+
const queryResult = runReadGraphQueryRaw(rootDir, {
|
|
1384
2146
|
layer,
|
|
1385
2147
|
search: args.search,
|
|
1386
2148
|
type: args.type,
|
|
@@ -1418,11 +2180,11 @@ function handleGrepNodes(args) {
|
|
|
1418
2180
|
let filesSearched = 0;
|
|
1419
2181
|
let truncated = false;
|
|
1420
2182
|
for (const [filePath, nodeId] of filePaths) {
|
|
1421
|
-
if (!(0,
|
|
2183
|
+
if (!(0, import_node_fs8.existsSync)(filePath)) continue;
|
|
1422
2184
|
filesSearched++;
|
|
1423
2185
|
let content;
|
|
1424
2186
|
try {
|
|
1425
|
-
content = (0,
|
|
2187
|
+
content = (0, import_node_fs8.readFileSync)(filePath, "utf-8");
|
|
1426
2188
|
} catch {
|
|
1427
2189
|
continue;
|
|
1428
2190
|
}
|
|
@@ -1459,6 +2221,23 @@ function handleGrepNodes(args) {
|
|
|
1459
2221
|
truncated
|
|
1460
2222
|
});
|
|
1461
2223
|
}
|
|
2224
|
+
function handleGetGraphUiUrl() {
|
|
2225
|
+
const lock = getLiveLock();
|
|
2226
|
+
if (!lock) {
|
|
2227
|
+
return okJson({
|
|
2228
|
+
running: false,
|
|
2229
|
+
hint: "No launch-chart UI server is currently running. Start one with `launch-chart serve`, or set LAUNCH_CHART_AUTOSERVE=1 in your MCP config to auto-start it alongside the MCP server."
|
|
2230
|
+
});
|
|
2231
|
+
}
|
|
2232
|
+
return okJson({
|
|
2233
|
+
running: true,
|
|
2234
|
+
url: lock.url,
|
|
2235
|
+
port: lock.port,
|
|
2236
|
+
pid: lock.pid,
|
|
2237
|
+
cwd: lock.cwd,
|
|
2238
|
+
startedAt: lock.startedAt
|
|
2239
|
+
});
|
|
2240
|
+
}
|
|
1462
2241
|
function send(msg) {
|
|
1463
2242
|
process.stdout.write(JSON.stringify(msg) + "\n");
|
|
1464
2243
|
}
|
|
@@ -1502,6 +2281,10 @@ function handleMessage(msg) {
|
|
|
1502
2281
|
respond(id ?? null, handleGrepNodes(args));
|
|
1503
2282
|
return;
|
|
1504
2283
|
}
|
|
2284
|
+
if (toolName === "get_graph_ui_url") {
|
|
2285
|
+
respond(id ?? null, handleGetGraphUiUrl());
|
|
2286
|
+
return;
|
|
2287
|
+
}
|
|
1505
2288
|
respondError(id ?? null, -32601, `Unknown tool: ${toolName}`);
|
|
1506
2289
|
return;
|
|
1507
2290
|
}
|
|
@@ -1537,6 +2320,248 @@ function startGraphMcpServer() {
|
|
|
1537
2320
|
process.stderr.write(`[launchsecure-graph] MCP server started (cwd: ${process.cwd()})
|
|
1538
2321
|
`);
|
|
1539
2322
|
}
|
|
2323
|
+
var import_node_fs8, import_node_path8, SERVER_INFO, TOOLS, COMPACT_SCHEMA, COMPACT_NODE_KNOWN_KEYS, EST_CHARS_PER_NODE_FULL, EST_CHARS_PER_NODE_MIN, EST_CHARS_PER_EDGE, NEIGHBORHOOD_BUDGET_CHARS, BATCH_BUDGET_CHARS;
|
|
2324
|
+
var init_graph_mcp = __esm({
|
|
2325
|
+
"src/server/graph-mcp.ts"() {
|
|
2326
|
+
"use strict";
|
|
2327
|
+
import_node_fs8 = require("node:fs");
|
|
2328
|
+
import_node_path8 = require("node:path");
|
|
2329
|
+
init_graph();
|
|
2330
|
+
init_lockfile();
|
|
2331
|
+
SERVER_INFO = {
|
|
2332
|
+
name: "launchsecure-graph",
|
|
2333
|
+
version: "0.0.1"
|
|
2334
|
+
};
|
|
2335
|
+
TOOLS = [
|
|
2336
|
+
{
|
|
2337
|
+
name: "generate_graph",
|
|
2338
|
+
description: "Regenerate the structural project graph by scanning source code in the current working directory. Parses three layers: UI (React/Next.js pages, layouts, components, hooks, imports/renders/navigation edges), API (Next.js App Router endpoints with HTTP methods), DB (Prisma schema models, enums, belongs_to/has_many relations). Writes JSON to .launchsecure/graphs/{layer}.json and returns node/edge counts. Run this when project structure has changed (new files, moved components, schema updates). Fast: typically <1s. After generation, use read_graph with filters to query.",
|
|
2339
|
+
inputSchema: {
|
|
2340
|
+
type: "object",
|
|
2341
|
+
properties: {
|
|
2342
|
+
layer: {
|
|
2343
|
+
type: "string",
|
|
2344
|
+
enum: ["ui", "api", "db"],
|
|
2345
|
+
description: "Specific layer to regenerate. Omit to regenerate all detectable layers."
|
|
2346
|
+
}
|
|
2347
|
+
}
|
|
2348
|
+
}
|
|
2349
|
+
},
|
|
2350
|
+
{
|
|
2351
|
+
name: "read_graph",
|
|
2352
|
+
description: 'Query the structural project graph \u2014 a smart Glob replacement that locates files by type/module/name and returns structural metadata (imports, renders, routes, relations). \n\nUSE THIS FOR: "where is X", "what files are in module Y", "what pages exist under /admin", "what components does Z render", "what tables relate to User", "list all hooks in auth module". \n\nDO NOT USE FOR: finding text/code content (use Grep), reading actual source code (use Read), understanding behavior/logic/patterns (graph has no code semantics \u2014 only names and edges). \n\nQUERY PARAMS (at least one required for node data \u2014 unfiltered calls return summary only to stay in context):\n- search: substring match on node id, name, or route\n- type: filter by node type (ui layer: page, layout, component, ui, hook, context, config, util; api layer: endpoint; db layer: table, enum)\n- module: filter by module (ui layer only: auth, admin, project, org, settings, integrations, shared-ui, layout, root)\n- node_id: return this node + its neighborhood (incoming+outgoing edges within `hops`)\n- hops: neighborhood radius when node_id is set (default 1)\n- minimal: return only id/type/name/module/route per node (skip heavy fields like columns, exports)\n- include_edges: return the actual edge list. Default: TRUE for neighborhood queries (node_id), FALSE for filter queries (search/type/module). Filter responses always include `edge_count`; only pass include_edges:true when you actually need to inspect individual edges (e.g. "which components render X"). This default cuts typical filter responses in half.\n\nBATCH MODE: pass `queries` (array of query objects) to run multiple independent queries in a single call. Each query object uses the same params (layer/search/type/module/node_id/hops/minimal). Returns { batch: true, count, results: [{index, query, result}, ...] }. Use this when you need multiple graph views up-front (e.g. scoping a feature across ui+api+db layers) to save round-trips. When batch mode is used, top-level params are ignored.\n\nReturns: filtered nodes + edges between them. If no filter given, returns per-layer counts and type breakdown only.\n\nWIRE FORMAT (compact): responses that include nodes/edges use short keys and edge-by-index refs to cut payload ~40-60%. Every such response carries a `_schema` legend. Quick reference:\n nodes[]: { i: id, t: type, n: name, m: module, r: route, mt: methods, x: exports, c: columns }\n edges[]: { s: source_node_index, d: target_node_index, t: type, l: label }\nedges.s / edges.d are 0-based indices into THIS response\'s nodes array. If a referenced node is not in the response (boundary case), s/d may instead contain the full node id string \u2014 always check the type.\n\nBUDGET GUARDS:\n- Neighborhood queries stop expanding when the projected response exceeds budget. The response then contains `budget_exceeded: true` plus `hops_traversed < hops_requested`. When this happens, drill into a specific neighbor with another node_id call rather than retrying with larger hops \u2014 it will just truncate again.\n- Batch mode caps total response size. Once the budget is hit, later queries return `{skipped: true, reason: "batch_budget_exhausted"}` and you must re-run them individually.',
|
|
2353
|
+
inputSchema: {
|
|
2354
|
+
type: "object",
|
|
2355
|
+
properties: {
|
|
2356
|
+
layer: {
|
|
2357
|
+
type: "string",
|
|
2358
|
+
enum: ["ui", "api", "db"],
|
|
2359
|
+
description: "Graph layer to query: ui, api, or db. Required if any filter is provided."
|
|
2360
|
+
},
|
|
2361
|
+
search: {
|
|
2362
|
+
type: "string",
|
|
2363
|
+
description: "Case-insensitive substring match against node id, name, or route."
|
|
2364
|
+
},
|
|
2365
|
+
type: {
|
|
2366
|
+
type: "string",
|
|
2367
|
+
description: 'Filter by node type (e.g. "page", "hook", "component", "endpoint", "table").'
|
|
2368
|
+
},
|
|
2369
|
+
module: {
|
|
2370
|
+
type: "string",
|
|
2371
|
+
description: 'UI layer only \u2014 filter by module (e.g. "auth", "admin", "project", "org").'
|
|
2372
|
+
},
|
|
2373
|
+
node_id: {
|
|
2374
|
+
type: "string",
|
|
2375
|
+
description: "Center node for a neighborhood query. Returns the node + all nodes reachable within `hops` edges."
|
|
2376
|
+
},
|
|
2377
|
+
hops: {
|
|
2378
|
+
type: "number",
|
|
2379
|
+
description: "Neighborhood radius for node_id queries. Default 1 (direct neighbors only)."
|
|
2380
|
+
},
|
|
2381
|
+
minimal: {
|
|
2382
|
+
type: "boolean",
|
|
2383
|
+
description: "Return minimal node fields only (id, type, name, module, route). Default false, except db-layer filter queries default to true (db table nodes carry heavy `columns` arrays). Pass minimal:false on a db filter query if you want columns."
|
|
2384
|
+
},
|
|
2385
|
+
include_edges: {
|
|
2386
|
+
type: "boolean",
|
|
2387
|
+
description: "Include the edge list in the response. Default TRUE for neighborhood queries (node_id), FALSE for filter queries. Filter responses always include edge_count. Only set true on filter queries when you actually need edge data."
|
|
2388
|
+
},
|
|
2389
|
+
queries: {
|
|
2390
|
+
type: "array",
|
|
2391
|
+
description: "Batch mode \u2014 array of query objects to run in a single call. Each uses the same param schema. When set, top-level params are ignored. Subject to an aggregate size budget \u2014 later queries may return a skipped stub.",
|
|
2392
|
+
items: {
|
|
2393
|
+
type: "object",
|
|
2394
|
+
properties: {
|
|
2395
|
+
layer: { type: "string", enum: ["ui", "api", "db"] },
|
|
2396
|
+
search: { type: "string" },
|
|
2397
|
+
type: { type: "string" },
|
|
2398
|
+
module: { type: "string" },
|
|
2399
|
+
node_id: { type: "string" },
|
|
2400
|
+
hops: { type: "number" },
|
|
2401
|
+
minimal: { type: "boolean" },
|
|
2402
|
+
include_edges: { type: "boolean" }
|
|
2403
|
+
}
|
|
2404
|
+
}
|
|
2405
|
+
}
|
|
2406
|
+
}
|
|
2407
|
+
}
|
|
2408
|
+
},
|
|
2409
|
+
{
|
|
2410
|
+
name: "grep_nodes",
|
|
2411
|
+
description: `Search for text patterns WITHIN files selected by the project graph. Combines structural filtering (type/module/neighborhood) with regex content search \u2014 narrower than plain Grep because it only scans files matching the graph filter, reducing noise from tests, docs, generated code, unrelated modules.
|
|
2412
|
+
|
|
2413
|
+
USE THIS FOR: "which auth hooks use JWT decoding", "find TODO comments in pages only", "which deployment writers call Sentry", "what validation schemas exist in form components". It's grep scoped to a structurally-selected file set.
|
|
2414
|
+
|
|
2415
|
+
REQUIRED: layer + pattern. At least one filter (search/type/module/node_id) must be set to narrow the file set \u2014 otherwise you are grepping everything and should just use Grep.
|
|
2416
|
+
|
|
2417
|
+
FILTER PARAMS (same semantics as read_graph):
|
|
2418
|
+
- layer: ui, api, or db
|
|
2419
|
+
- search: substring match on node id/name/route
|
|
2420
|
+
- type: node type filter
|
|
2421
|
+
- module: ui-layer module filter
|
|
2422
|
+
- node_id + hops: neighborhood scope
|
|
2423
|
+
|
|
2424
|
+
CONTENT PARAMS:
|
|
2425
|
+
- pattern: regex to search for (required)
|
|
2426
|
+
- case_insensitive: default false
|
|
2427
|
+
- context: lines of context around each match (default 2)
|
|
2428
|
+
- max_matches: cap on total matches returned (default 50)
|
|
2429
|
+
- max_files: cap on files searched (default 50, errors if filter returns more)
|
|
2430
|
+
|
|
2431
|
+
Returns: { pattern, filter, files_searched, total_matches, matches: [{file, line, text, context}], truncated }. Note: for db layer, all nodes map to prisma/schema.prisma so matches are deduped by file.`,
|
|
2432
|
+
inputSchema: {
|
|
2433
|
+
type: "object",
|
|
2434
|
+
properties: {
|
|
2435
|
+
layer: {
|
|
2436
|
+
type: "string",
|
|
2437
|
+
enum: ["ui", "api", "db"],
|
|
2438
|
+
description: "Graph layer to scope files (required)."
|
|
2439
|
+
},
|
|
2440
|
+
pattern: {
|
|
2441
|
+
type: "string",
|
|
2442
|
+
description: "Regex pattern to search for (required)."
|
|
2443
|
+
},
|
|
2444
|
+
search: { type: "string", description: "Substring match on node id/name/route." },
|
|
2445
|
+
type: { type: "string", description: "Filter by node type." },
|
|
2446
|
+
module: { type: "string", description: "UI layer only \u2014 filter by module." },
|
|
2447
|
+
node_id: { type: "string", description: "Center node for neighborhood scope." },
|
|
2448
|
+
hops: { type: "number", description: "Neighborhood radius (default 1)." },
|
|
2449
|
+
case_insensitive: { type: "boolean", description: "Case-insensitive regex. Default false." },
|
|
2450
|
+
context: { type: "number", description: "Context lines around each match. Default 2." },
|
|
2451
|
+
max_matches: { type: "number", description: "Max matches to return total. Default 50." },
|
|
2452
|
+
max_files: { type: "number", description: "Max files to search. Default 50." }
|
|
2453
|
+
},
|
|
2454
|
+
required: ["layer", "pattern"]
|
|
2455
|
+
}
|
|
2456
|
+
},
|
|
2457
|
+
{
|
|
2458
|
+
name: "get_graph_ui_url",
|
|
2459
|
+
description: 'Return the URL of a running launch-chart UI server if one exists. The UI is a visual, interactive view of the merged UI+API+DB project graph served by `launch-chart serve` (or auto-started via LAUNCH_CHART_AUTOSERVE=1). \n\nReturns: { running: boolean, url?: string, port?: number, pid?: number, startedAt?: string, cwd?: string }. If running is false, no server is currently live \u2014 suggest the user run `launch-chart serve` to start one. \n\nUse this when the user asks "open the graph", "show me the project graph UI", "where\'s the chart", etc.',
|
|
2460
|
+
inputSchema: {
|
|
2461
|
+
type: "object",
|
|
2462
|
+
properties: {}
|
|
2463
|
+
}
|
|
2464
|
+
}
|
|
2465
|
+
];
|
|
2466
|
+
COMPACT_SCHEMA = {
|
|
2467
|
+
nodes: {
|
|
2468
|
+
i: "id",
|
|
2469
|
+
t: "type",
|
|
2470
|
+
n: "name",
|
|
2471
|
+
m: "module",
|
|
2472
|
+
r: "route",
|
|
2473
|
+
mt: "methods",
|
|
2474
|
+
x: "exports",
|
|
2475
|
+
c: "columns"
|
|
2476
|
+
},
|
|
2477
|
+
edges: {
|
|
2478
|
+
s: "source_node_index",
|
|
2479
|
+
d: "target_node_index",
|
|
2480
|
+
t: "type",
|
|
2481
|
+
l: "label"
|
|
2482
|
+
},
|
|
2483
|
+
note: "edges.s/d are 0-based indices into this response's nodes array. If a referenced node is outside the response (boundary case), s/d may contain the full node id string instead of an index."
|
|
2484
|
+
};
|
|
2485
|
+
COMPACT_NODE_KNOWN_KEYS = /* @__PURE__ */ new Set([
|
|
2486
|
+
"id",
|
|
2487
|
+
"type",
|
|
2488
|
+
"name",
|
|
2489
|
+
"module",
|
|
2490
|
+
"route",
|
|
2491
|
+
"methods",
|
|
2492
|
+
"exports",
|
|
2493
|
+
"columns"
|
|
2494
|
+
]);
|
|
2495
|
+
EST_CHARS_PER_NODE_FULL = {
|
|
2496
|
+
ui: 300,
|
|
2497
|
+
api: 300,
|
|
2498
|
+
db: 3500
|
|
2499
|
+
};
|
|
2500
|
+
EST_CHARS_PER_NODE_MIN = {
|
|
2501
|
+
ui: 150,
|
|
2502
|
+
api: 200,
|
|
2503
|
+
db: 120
|
|
2504
|
+
};
|
|
2505
|
+
EST_CHARS_PER_EDGE = {
|
|
2506
|
+
ui: 65,
|
|
2507
|
+
api: 65,
|
|
2508
|
+
db: 65
|
|
2509
|
+
};
|
|
2510
|
+
NEIGHBORHOOD_BUDGET_CHARS = 55e3;
|
|
2511
|
+
BATCH_BUDGET_CHARS = 6e4;
|
|
2512
|
+
}
|
|
2513
|
+
});
|
|
1540
2514
|
|
|
1541
2515
|
// src/server/graph-mcp-entry.ts
|
|
1542
|
-
|
|
2516
|
+
var import_node_child_process2 = require("node:child_process");
|
|
2517
|
+
var import_node_fs9 = require("node:fs");
|
|
2518
|
+
var import_node_path9 = __toESM(require("node:path"));
|
|
2519
|
+
var import_node_os2 = require("node:os");
|
|
2520
|
+
var import_node_fs10 = require("node:fs");
|
|
2521
|
+
init_lockfile();
|
|
2522
|
+
function logStderr(msg) {
|
|
2523
|
+
process.stderr.write(`[launch-chart] ${msg}
|
|
2524
|
+
`);
|
|
2525
|
+
}
|
|
2526
|
+
function maybeAutoServe() {
|
|
2527
|
+
if (process.env.LAUNCH_CHART_AUTOSERVE !== "1") return;
|
|
2528
|
+
const existing = getLiveLock();
|
|
2529
|
+
if (existing) {
|
|
2530
|
+
logStderr(`autoserve: reusing existing server at ${existing.url}`);
|
|
2531
|
+
return;
|
|
2532
|
+
}
|
|
2533
|
+
try {
|
|
2534
|
+
const logDir = import_node_path9.default.join((0, import_node_os2.homedir)(), ".launchsecure");
|
|
2535
|
+
(0, import_node_fs10.mkdirSync)(logDir, { recursive: true });
|
|
2536
|
+
const logPath = import_node_path9.default.join(logDir, "launch-chart.log");
|
|
2537
|
+
const out = (0, import_node_fs9.openSync)(logPath, "a");
|
|
2538
|
+
const err2 = (0, import_node_fs9.openSync)(logPath, "a");
|
|
2539
|
+
const entryPath = process.argv[1];
|
|
2540
|
+
const child = (0, import_node_child_process2.spawn)(process.execPath, [entryPath, "serve"], {
|
|
2541
|
+
detached: true,
|
|
2542
|
+
stdio: ["ignore", out, err2],
|
|
2543
|
+
env: { ...process.env, LAUNCH_CHART_AUTOSERVE: "" }
|
|
2544
|
+
});
|
|
2545
|
+
child.unref();
|
|
2546
|
+
logStderr(`autoserve: spawned detached serve process (pid ${child.pid}, log: ${logPath})`);
|
|
2547
|
+
} catch (err2) {
|
|
2548
|
+
logStderr(`autoserve: failed to spawn \u2014 ${err2}`);
|
|
2549
|
+
}
|
|
2550
|
+
}
|
|
2551
|
+
async function main() {
|
|
2552
|
+
const argv = process.argv.slice(2);
|
|
2553
|
+
const subcommand = argv[0];
|
|
2554
|
+
if (subcommand === "serve") {
|
|
2555
|
+
const { runServeCli: runServeCli2 } = await Promise.resolve().then(() => (init_chart_serve(), chart_serve_exports));
|
|
2556
|
+
runServeCli2(argv.slice(1));
|
|
2557
|
+
return;
|
|
2558
|
+
}
|
|
2559
|
+
maybeAutoServe();
|
|
2560
|
+
const { startGraphMcpServer: startGraphMcpServer2 } = await Promise.resolve().then(() => (init_graph_mcp(), graph_mcp_exports));
|
|
2561
|
+
startGraphMcpServer2();
|
|
2562
|
+
}
|
|
2563
|
+
main().catch((err2) => {
|
|
2564
|
+
process.stderr.write(`[launch-chart] fatal: ${err2}
|
|
2565
|
+
`);
|
|
2566
|
+
process.exit(1);
|
|
2567
|
+
});
|