@launchsecure/launch-kit 0.0.3 → 0.0.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/chart-client/assets/index-BUih0oqR.js +358 -0
- package/dist/chart-client/assets/index-DFslt72L.css +1 -0
- package/dist/chart-client/index.html +21 -0
- package/dist/client/assets/index-BCxRNp8I.css +32 -0
- package/dist/client/assets/{index-CcHIoRl6.js → index-DCC--GO-.js} +68 -63
- package/dist/client/index.html +2 -2
- package/dist/server/chart-serve.js +2285 -0
- package/dist/server/cli.js +1222 -120
- package/dist/server/graph-mcp-entry.js +1879 -300
- package/package.json +7 -3
- package/dist/client/assets/index-C8GAsRGO.css +0 -32
|
@@ -1,22 +1,131 @@
|
|
|
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
|
-
|
|
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
|
+
});
|
|
11
107
|
|
|
12
|
-
// src/server/graph/
|
|
13
|
-
|
|
14
|
-
|
|
108
|
+
// src/server/graph/core/config.ts
|
|
109
|
+
function loadConfig(rootDir) {
|
|
110
|
+
const configPath = (0, import_node_path2.join)(rootDir, CONFIG_FILENAME);
|
|
111
|
+
if (!(0, import_node_fs2.existsSync)(configPath)) return {};
|
|
112
|
+
try {
|
|
113
|
+
return JSON.parse((0, import_node_fs2.readFileSync)(configPath, "utf-8"));
|
|
114
|
+
} catch {
|
|
115
|
+
return {};
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
var import_node_fs2, import_node_path2, CONFIG_FILENAME;
|
|
119
|
+
var init_config = __esm({
|
|
120
|
+
"src/server/graph/core/config.ts"() {
|
|
121
|
+
"use strict";
|
|
122
|
+
import_node_fs2 = require("node:fs");
|
|
123
|
+
import_node_path2 = require("node:path");
|
|
124
|
+
CONFIG_FILENAME = ".launchchart.json";
|
|
125
|
+
}
|
|
126
|
+
});
|
|
15
127
|
|
|
16
128
|
// 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
129
|
function getTs() {
|
|
21
130
|
if (!tsModule) {
|
|
22
131
|
tsModule = require("typescript");
|
|
@@ -25,8 +134,8 @@ function getTs() {
|
|
|
25
134
|
}
|
|
26
135
|
function parseFile(absPath) {
|
|
27
136
|
const ts = getTs();
|
|
28
|
-
const content = (0,
|
|
29
|
-
const ext = (0,
|
|
137
|
+
const content = (0, import_node_fs3.readFileSync)(absPath, "utf-8");
|
|
138
|
+
const ext = (0, import_node_path3.extname)(absPath);
|
|
30
139
|
const scriptKind = ext === ".tsx" ? ts.ScriptKind.TSX : ext === ".ts" ? ts.ScriptKind.TS : ext === ".jsx" ? ts.ScriptKind.JSX : ts.ScriptKind.JS;
|
|
31
140
|
const sourceFile = ts.createSourceFile(
|
|
32
141
|
absPath,
|
|
@@ -45,6 +154,8 @@ function parseFile(absPath) {
|
|
|
45
154
|
const reExports = [];
|
|
46
155
|
const jsxElements = /* @__PURE__ */ new Set();
|
|
47
156
|
const navigations = [];
|
|
157
|
+
const fetchCalls = [];
|
|
158
|
+
const includeConcat = process.env.LAUNCH_CHART_INCLUDE_CONCAT_FETCHES === "1";
|
|
48
159
|
function addExport(name2, kind) {
|
|
49
160
|
if (!exportsSet.has(name2)) {
|
|
50
161
|
exportsSet.add(name2);
|
|
@@ -67,6 +178,33 @@ function parseFile(absPath) {
|
|
|
67
178
|
}
|
|
68
179
|
return null;
|
|
69
180
|
}
|
|
181
|
+
function looksLikeUrl(s) {
|
|
182
|
+
return s.startsWith("/") || /^(https?:)?\/\//i.test(s);
|
|
183
|
+
}
|
|
184
|
+
function templateStartsWithSlash(expr) {
|
|
185
|
+
const head = expr.head.text;
|
|
186
|
+
return head.startsWith("/");
|
|
187
|
+
}
|
|
188
|
+
function extractUrlFromFetchArg(arg) {
|
|
189
|
+
if (ts.isStringLiteral(arg) || ts.isNoSubstitutionTemplateLiteral(arg)) {
|
|
190
|
+
if (!looksLikeUrl(arg.text)) return null;
|
|
191
|
+
return { url: arg.text, isTemplate: false };
|
|
192
|
+
}
|
|
193
|
+
if (ts.isTemplateExpression(arg)) {
|
|
194
|
+
if (!templateStartsWithSlash(arg)) return null;
|
|
195
|
+
return { url: arg.getText(sourceFile), isTemplate: true };
|
|
196
|
+
}
|
|
197
|
+
if (includeConcat && ts.isBinaryExpression(arg) && arg.operatorToken.kind === ts.SyntaxKind.PlusToken) {
|
|
198
|
+
let leftmost = arg;
|
|
199
|
+
while (ts.isBinaryExpression(leftmost) && leftmost.operatorToken.kind === ts.SyntaxKind.PlusToken) {
|
|
200
|
+
leftmost = leftmost.left;
|
|
201
|
+
}
|
|
202
|
+
if ((ts.isStringLiteral(leftmost) || ts.isNoSubstitutionTemplateLiteral(leftmost)) && leftmost.text.startsWith("/")) {
|
|
203
|
+
return { url: arg.getText(sourceFile), isTemplate: false, isConcat: true };
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
70
208
|
function visit(node) {
|
|
71
209
|
if (ts.isImportDeclaration(node)) {
|
|
72
210
|
const moduleSpec = node.moduleSpecifier;
|
|
@@ -90,6 +228,8 @@ function parseFile(absPath) {
|
|
|
90
228
|
}
|
|
91
229
|
if (names.length > 0 || isTypeOnly) {
|
|
92
230
|
imports.push({ names, specifier, isTypeOnly, typeNames });
|
|
231
|
+
} else if (!clause) {
|
|
232
|
+
imports.push({ names: [], specifier, isTypeOnly: false, typeNames: /* @__PURE__ */ new Set() });
|
|
93
233
|
}
|
|
94
234
|
}
|
|
95
235
|
}
|
|
@@ -103,6 +243,19 @@ function parseFile(absPath) {
|
|
|
103
243
|
reExports.push({ name: exportedName, from: fromSpec });
|
|
104
244
|
}
|
|
105
245
|
}
|
|
246
|
+
} else if (!node.exportClause && fromSpec) {
|
|
247
|
+
reExports.push({ name: "*", from: fromSpec, isWildcard: true });
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
if (ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.ImportKeyword) {
|
|
251
|
+
const arg = node.arguments[0];
|
|
252
|
+
if (arg && ts.isStringLiteral(arg)) {
|
|
253
|
+
imports.push({
|
|
254
|
+
names: [],
|
|
255
|
+
specifier: arg.text,
|
|
256
|
+
isTypeOnly: false,
|
|
257
|
+
typeNames: /* @__PURE__ */ new Set()
|
|
258
|
+
});
|
|
106
259
|
}
|
|
107
260
|
}
|
|
108
261
|
if (ts.isExportAssignment(node) && !node.isExportEquals) {
|
|
@@ -164,6 +317,36 @@ function parseFile(absPath) {
|
|
|
164
317
|
}
|
|
165
318
|
}
|
|
166
319
|
}
|
|
320
|
+
if (ts.isCallExpression(node) && node.arguments.length > 0) {
|
|
321
|
+
const expr = node.expression;
|
|
322
|
+
const firstArg = node.arguments[0];
|
|
323
|
+
if (ts.isIdentifier(expr) && expr.text === "fetch") {
|
|
324
|
+
const extracted = extractUrlFromFetchArg(firstArg);
|
|
325
|
+
if (extracted) {
|
|
326
|
+
fetchCalls.push({
|
|
327
|
+
url: extracted.url,
|
|
328
|
+
isTemplate: extracted.isTemplate,
|
|
329
|
+
...extracted.isConcat ? { isConcat: true } : {},
|
|
330
|
+
kind: "fetch"
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
if (ts.isPropertyAccessExpression(expr) && ts.isIdentifier(expr.name)) {
|
|
335
|
+
const methodName = expr.name.text;
|
|
336
|
+
if (HTTP_METHODS.has(methodName)) {
|
|
337
|
+
const extracted = extractUrlFromFetchArg(firstArg);
|
|
338
|
+
if (extracted) {
|
|
339
|
+
fetchCalls.push({
|
|
340
|
+
method: methodName.toUpperCase(),
|
|
341
|
+
url: extracted.url,
|
|
342
|
+
isTemplate: extracted.isTemplate,
|
|
343
|
+
...extracted.isConcat ? { isConcat: true } : {},
|
|
344
|
+
kind: "client-method"
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
167
350
|
if (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) {
|
|
168
351
|
const tagName = node.tagName;
|
|
169
352
|
if (ts.isIdentifier(tagName) && tagName.text === "Link") {
|
|
@@ -213,24 +396,14 @@ function parseFile(absPath) {
|
|
|
213
396
|
imports,
|
|
214
397
|
reExports,
|
|
215
398
|
jsxElements,
|
|
216
|
-
navigations
|
|
399
|
+
navigations,
|
|
400
|
+
fetchCalls
|
|
217
401
|
};
|
|
218
402
|
}
|
|
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
403
|
function extractDbCalls(absPath) {
|
|
231
404
|
const ts = getTs();
|
|
232
|
-
const content = (0,
|
|
233
|
-
const ext = (0,
|
|
405
|
+
const content = (0, import_node_fs3.readFileSync)(absPath, "utf-8");
|
|
406
|
+
const ext = (0, import_node_path3.extname)(absPath);
|
|
234
407
|
const scriptKind = ext === ".tsx" ? ts.ScriptKind.TSX : ts.ScriptKind.TS;
|
|
235
408
|
const sourceFile = ts.createSourceFile(absPath, content, ts.ScriptTarget.Latest, true, scriptKind);
|
|
236
409
|
const calls = [];
|
|
@@ -259,8 +432,8 @@ function extractDbCalls(absPath) {
|
|
|
259
432
|
}
|
|
260
433
|
function extractAuthWrappers(absPath) {
|
|
261
434
|
const ts = getTs();
|
|
262
|
-
const content = (0,
|
|
263
|
-
const ext = (0,
|
|
435
|
+
const content = (0, import_node_fs3.readFileSync)(absPath, "utf-8");
|
|
436
|
+
const ext = (0, import_node_path3.extname)(absPath);
|
|
264
437
|
const scriptKind = ext === ".tsx" ? ts.ScriptKind.TSX : ts.ScriptKind.TS;
|
|
265
438
|
const sourceFile = ts.createSourceFile(absPath, content, ts.ScriptTarget.Latest, true, scriptKind);
|
|
266
439
|
const wrappers = /* @__PURE__ */ new Set();
|
|
@@ -276,55 +449,125 @@ function extractAuthWrappers(absPath) {
|
|
|
276
449
|
visit(sourceFile);
|
|
277
450
|
return wrappers;
|
|
278
451
|
}
|
|
452
|
+
var import_node_fs3, import_node_path3, tsModule, HTTP_METHODS, MUTATION_METHODS;
|
|
453
|
+
var init_ast_helpers = __esm({
|
|
454
|
+
"src/server/graph/core/ast-helpers.ts"() {
|
|
455
|
+
"use strict";
|
|
456
|
+
import_node_fs3 = require("node:fs");
|
|
457
|
+
import_node_path3 = require("node:path");
|
|
458
|
+
HTTP_METHODS = /* @__PURE__ */ new Set(["get", "post", "put", "patch", "delete", "head", "options"]);
|
|
459
|
+
MUTATION_METHODS = /* @__PURE__ */ new Set([
|
|
460
|
+
"create",
|
|
461
|
+
"createMany",
|
|
462
|
+
"createManyAndReturn",
|
|
463
|
+
"update",
|
|
464
|
+
"updateMany",
|
|
465
|
+
"updateManyAndReturn",
|
|
466
|
+
"upsert",
|
|
467
|
+
"delete",
|
|
468
|
+
"deleteMany"
|
|
469
|
+
]);
|
|
470
|
+
}
|
|
471
|
+
});
|
|
279
472
|
|
|
280
473
|
// src/server/graph/parsers/ui/react-nextjs.ts
|
|
281
|
-
var RENDER_TYPES = /* @__PURE__ */ new Set(["component", "ui", "layout", "context"]);
|
|
282
474
|
function walk(dir, exts) {
|
|
283
475
|
const results = [];
|
|
284
|
-
if (!(0,
|
|
285
|
-
for (const entry of (0,
|
|
286
|
-
const full = (0,
|
|
476
|
+
if (!(0, import_node_fs4.existsSync)(dir)) return results;
|
|
477
|
+
for (const entry of (0, import_node_fs4.readdirSync)(dir, { withFileTypes: true })) {
|
|
478
|
+
const full = (0, import_node_path4.join)(dir, entry.name);
|
|
287
479
|
if (entry.isDirectory()) {
|
|
288
480
|
results.push(...walk(full, exts));
|
|
289
|
-
} else if (exts.includes((0,
|
|
481
|
+
} else if (exts.includes((0, import_node_path4.extname)(entry.name))) {
|
|
290
482
|
results.push(full);
|
|
291
483
|
}
|
|
292
484
|
}
|
|
293
485
|
return results;
|
|
294
486
|
}
|
|
487
|
+
function walkWithIgnore(dir, exts, ignoreDirs) {
|
|
488
|
+
const results = [];
|
|
489
|
+
if (!(0, import_node_fs4.existsSync)(dir)) return results;
|
|
490
|
+
for (const entry of (0, import_node_fs4.readdirSync)(dir, { withFileTypes: true })) {
|
|
491
|
+
if (entry.isDirectory()) {
|
|
492
|
+
if (ignoreDirs.has(entry.name)) continue;
|
|
493
|
+
results.push(...walkWithIgnore((0, import_node_path4.join)(dir, entry.name), exts, ignoreDirs));
|
|
494
|
+
} else if (exts.includes((0, import_node_path4.extname)(entry.name))) {
|
|
495
|
+
results.push((0, import_node_path4.join)(dir, entry.name));
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
return results;
|
|
499
|
+
}
|
|
295
500
|
function toNodeId(srcDir, absPath) {
|
|
296
|
-
return (0,
|
|
501
|
+
return (0, import_node_path4.relative)(srcDir, absPath).replace(/\\/g, "/");
|
|
297
502
|
}
|
|
298
503
|
function resolveImport(srcDir, specifier) {
|
|
299
504
|
if (!specifier.startsWith("@/")) return null;
|
|
300
505
|
const rel = specifier.slice(2);
|
|
301
|
-
const base = (0,
|
|
302
|
-
for (const c of [base, base + ".ts", base + ".tsx", (0,
|
|
303
|
-
if ((0,
|
|
506
|
+
const base = (0, import_node_path4.join)(srcDir, rel);
|
|
507
|
+
for (const c of [base, base + ".ts", base + ".tsx", (0, import_node_path4.join)(base, "index.ts"), (0, import_node_path4.join)(base, "index.tsx")]) {
|
|
508
|
+
if ((0, import_node_fs4.existsSync)(c) && (0, import_node_fs4.statSync)(c).isFile()) return c;
|
|
304
509
|
}
|
|
305
510
|
return null;
|
|
306
511
|
}
|
|
307
512
|
function resolveRelativeImport(fromFile, specifier) {
|
|
308
|
-
const base = (0,
|
|
309
|
-
for (const c of [base, base + ".ts", base + ".tsx", (0,
|
|
310
|
-
if ((0,
|
|
513
|
+
const base = (0, import_node_path4.join)((0, import_node_path4.dirname)(fromFile), specifier);
|
|
514
|
+
for (const c of [base, base + ".ts", base + ".tsx", (0, import_node_path4.join)(base, "index.ts"), (0, import_node_path4.join)(base, "index.tsx")]) {
|
|
515
|
+
if ((0, import_node_fs4.existsSync)(c) && (0, import_node_fs4.statSync)(c).isFile()) return c;
|
|
311
516
|
}
|
|
312
517
|
return null;
|
|
313
518
|
}
|
|
519
|
+
function resolveBarrelMap(barrelAbsPath, parsedByPath, memo, visiting) {
|
|
520
|
+
const cached = memo.get(barrelAbsPath);
|
|
521
|
+
if (cached) return cached;
|
|
522
|
+
if (visiting.has(barrelAbsPath)) return /* @__PURE__ */ new Map();
|
|
523
|
+
visiting.add(barrelAbsPath);
|
|
524
|
+
const parsed = parsedByPath.get(barrelAbsPath);
|
|
525
|
+
const map = /* @__PURE__ */ new Map();
|
|
526
|
+
if (!parsed) {
|
|
527
|
+
visiting.delete(barrelAbsPath);
|
|
528
|
+
memo.set(barrelAbsPath, map);
|
|
529
|
+
return map;
|
|
530
|
+
}
|
|
531
|
+
for (const re of parsed.reExports) {
|
|
532
|
+
if (!re.from.startsWith(".")) continue;
|
|
533
|
+
const resolved = resolveRelativeImport(barrelAbsPath, re.from);
|
|
534
|
+
if (!resolved) continue;
|
|
535
|
+
if (re.isWildcard) {
|
|
536
|
+
const targetBn = (0, import_node_path4.basename)(resolved);
|
|
537
|
+
const targetIsBarrel = targetBn === "index.ts" || targetBn === "index.tsx";
|
|
538
|
+
if (targetIsBarrel) {
|
|
539
|
+
const nested = resolveBarrelMap(resolved, parsedByPath, memo, visiting);
|
|
540
|
+
for (const [name, target] of nested) {
|
|
541
|
+
if (!map.has(name)) map.set(name, target);
|
|
542
|
+
}
|
|
543
|
+
} else {
|
|
544
|
+
const targetParsed = parsedByPath.get(resolved);
|
|
545
|
+
if (targetParsed) {
|
|
546
|
+
for (const exp of targetParsed.exports) {
|
|
547
|
+
if (!map.has(exp)) map.set(exp, resolved);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
} else {
|
|
552
|
+
if (!map.has(re.name)) map.set(re.name, resolved);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
visiting.delete(barrelAbsPath);
|
|
556
|
+
memo.set(barrelAbsPath, map);
|
|
557
|
+
return map;
|
|
558
|
+
}
|
|
314
559
|
function buildAllBarrelMaps(srcDir, parsedByPath) {
|
|
315
560
|
const barrels = /* @__PURE__ */ new Map();
|
|
561
|
+
const memo = /* @__PURE__ */ new Map();
|
|
316
562
|
for (const [absPath, parsed] of parsedByPath) {
|
|
317
|
-
const bn = (0,
|
|
563
|
+
const bn = (0, import_node_path4.basename)(absPath);
|
|
318
564
|
if (bn !== "index.ts" && bn !== "index.tsx") continue;
|
|
319
565
|
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);
|
|
566
|
+
const map = resolveBarrelMap(absPath, parsedByPath, memo, /* @__PURE__ */ new Set());
|
|
567
|
+
if (map.size > 0) {
|
|
568
|
+
const barrelId = (0, import_node_path4.relative)(srcDir, (0, import_node_path4.dirname)(absPath)).replace(/\\/g, "/");
|
|
569
|
+
barrels.set(barrelId, map);
|
|
326
570
|
}
|
|
327
|
-
if (map.size > 0) barrels.set(barrelId, map);
|
|
328
571
|
}
|
|
329
572
|
return barrels;
|
|
330
573
|
}
|
|
@@ -381,7 +624,7 @@ function extractRoute(id) {
|
|
|
381
624
|
return route || "/";
|
|
382
625
|
}
|
|
383
626
|
function nameFromFilename(absPath) {
|
|
384
|
-
return (0,
|
|
627
|
+
return (0, import_node_path4.basename)(absPath, (0, import_node_path4.extname)(absPath)).replace(/[-_](\w)/g, (_, c) => c.toUpperCase()).replace(/^(\w)/, (_, c) => c.toUpperCase());
|
|
385
628
|
}
|
|
386
629
|
function resolveTemplateLiteralRoute(template, routeToNodeId) {
|
|
387
630
|
const parameterized = template.replace(/\$\{([^}]+)\}/g, (_, expr) => {
|
|
@@ -561,26 +804,26 @@ function extractEdges(srcDir, absPath, sourceId, parsed, nodeIdSet, nodeTypeMap,
|
|
|
561
804
|
return { edges, flagged };
|
|
562
805
|
}
|
|
563
806
|
function detect(rootDir) {
|
|
564
|
-
return (0,
|
|
807
|
+
return (0, import_node_fs4.existsSync)((0, import_node_path4.join)(rootDir, "src", "app")) && (0, import_node_fs4.existsSync)((0, import_node_path4.join)(rootDir, "next.config.ts")) || (0, import_node_fs4.existsSync)((0, import_node_path4.join)(rootDir, "next.config.js")) || (0, import_node_fs4.existsSync)((0, import_node_path4.join)(rootDir, "next.config.mjs"));
|
|
565
808
|
}
|
|
566
809
|
function generate(rootDir) {
|
|
567
|
-
const srcDir = (0,
|
|
568
|
-
const appFiles = walk((0,
|
|
569
|
-
(f) =>
|
|
810
|
+
const srcDir = (0, import_node_path4.join)(rootDir, "src");
|
|
811
|
+
const appFiles = walk((0, import_node_path4.join)(srcDir, "app"), [".tsx", ".ts"]).filter(
|
|
812
|
+
(f) => (0, import_node_path4.basename)(f) !== "route.ts" && (0, import_node_path4.basename)(f) !== "route.tsx"
|
|
570
813
|
);
|
|
571
|
-
const clientFiles = walk((0,
|
|
572
|
-
const serverFiles = walk((0,
|
|
573
|
-
(f) => (0,
|
|
814
|
+
const clientFiles = walk((0, import_node_path4.join)(srcDir, "client"), [".tsx", ".ts"]);
|
|
815
|
+
const serverFiles = walk((0, import_node_path4.join)(srcDir, "server"), [".ts", ".tsx"]).filter(
|
|
816
|
+
(f) => (0, import_node_path4.basename)(f) !== "route.ts" && (0, import_node_path4.basename)(f) !== "route.tsx"
|
|
574
817
|
);
|
|
575
|
-
const libFiles = walk((0,
|
|
576
|
-
const configFiles = walk((0,
|
|
818
|
+
const libFiles = walk((0, import_node_path4.join)(srcDir, "lib"), [".ts", ".tsx"]);
|
|
819
|
+
const configFiles = walk((0, import_node_path4.join)(srcDir, "config"), [".ts", ".tsx"]);
|
|
577
820
|
const allDiscovered = [...appFiles, ...clientFiles, ...serverFiles, ...libFiles, ...configFiles];
|
|
578
821
|
const parsedByPath = /* @__PURE__ */ new Map();
|
|
579
822
|
for (const absPath of allDiscovered) {
|
|
580
823
|
parsedByPath.set(absPath, parseFile(absPath));
|
|
581
824
|
}
|
|
582
825
|
const barrelMaps = buildAllBarrelMaps(srcDir, parsedByPath);
|
|
583
|
-
const fileSet = allDiscovered.filter((f) => !(0,
|
|
826
|
+
const fileSet = allDiscovered.filter((f) => !(0, import_node_path4.basename)(f).startsWith("index."));
|
|
584
827
|
const nodes = [];
|
|
585
828
|
const nodeIdSet = /* @__PURE__ */ new Set();
|
|
586
829
|
const nodeTypeMap = /* @__PURE__ */ new Map();
|
|
@@ -615,6 +858,94 @@ function generate(rootDir) {
|
|
|
615
858
|
allEdges.push(...edges);
|
|
616
859
|
allFlagged.push(...flagged);
|
|
617
860
|
}
|
|
861
|
+
const fetchCallEntries = [];
|
|
862
|
+
for (const absPath of fileSet) {
|
|
863
|
+
const sourceId = toNodeId(srcDir, absPath);
|
|
864
|
+
const parsed = parsedByPath.get(absPath);
|
|
865
|
+
if (parsed.fetchCalls.length === 0) continue;
|
|
866
|
+
fetchCallEntries.push({
|
|
867
|
+
nodeId: sourceId,
|
|
868
|
+
calls: parsed.fetchCalls.map((c) => ({
|
|
869
|
+
url: c.url,
|
|
870
|
+
method: c.method,
|
|
871
|
+
isTemplate: c.isTemplate,
|
|
872
|
+
isConcat: c.isConcat,
|
|
873
|
+
kind: c.kind
|
|
874
|
+
}))
|
|
875
|
+
});
|
|
876
|
+
}
|
|
877
|
+
const externalScanned = new Set(allDiscovered.map((f) => f.replace(/\\/g, "/")));
|
|
878
|
+
const IGNORE_DIRS = /* @__PURE__ */ new Set([
|
|
879
|
+
"node_modules",
|
|
880
|
+
".next",
|
|
881
|
+
"dist",
|
|
882
|
+
".launchsecure",
|
|
883
|
+
".git",
|
|
884
|
+
"src",
|
|
885
|
+
"coverage",
|
|
886
|
+
".turbo",
|
|
887
|
+
"build",
|
|
888
|
+
"out",
|
|
889
|
+
".vercel"
|
|
890
|
+
]);
|
|
891
|
+
const externalCandidates = walkWithIgnore(rootDir, [".ts", ".tsx"], IGNORE_DIRS);
|
|
892
|
+
for (const absPath of externalCandidates) {
|
|
893
|
+
const normalized = absPath.replace(/\\/g, "/");
|
|
894
|
+
if (externalScanned.has(normalized)) continue;
|
|
895
|
+
let parsed;
|
|
896
|
+
try {
|
|
897
|
+
parsed = parseFile(absPath);
|
|
898
|
+
} catch {
|
|
899
|
+
continue;
|
|
900
|
+
}
|
|
901
|
+
const externalId = (0, import_node_path4.relative)(rootDir, absPath).replace(/\\/g, "/");
|
|
902
|
+
const edgesFromThis = [];
|
|
903
|
+
const seen = /* @__PURE__ */ new Set();
|
|
904
|
+
for (const imp of parsed.imports) {
|
|
905
|
+
const { specifier, isTypeOnly, names } = imp;
|
|
906
|
+
let resolved = null;
|
|
907
|
+
if (specifier.startsWith("@/")) {
|
|
908
|
+
const relToSrc = specifier.slice(2);
|
|
909
|
+
const barrelMap = barrelMaps.get(relToSrc);
|
|
910
|
+
if (barrelMap && names.length > 0) {
|
|
911
|
+
for (const name of names) {
|
|
912
|
+
const targetAbs = barrelMap.get(name);
|
|
913
|
+
if (!targetAbs) continue;
|
|
914
|
+
const targetId2 = toNodeId(srcDir, targetAbs);
|
|
915
|
+
if (!nodeIdSet.has(targetId2)) continue;
|
|
916
|
+
const key2 = `${externalId}\u2192${targetId2}`;
|
|
917
|
+
if (seen.has(key2)) continue;
|
|
918
|
+
seen.add(key2);
|
|
919
|
+
edgesFromThis.push({ source: externalId, target: targetId2, type: "imports" });
|
|
920
|
+
}
|
|
921
|
+
continue;
|
|
922
|
+
}
|
|
923
|
+
resolved = resolveImport(srcDir, specifier);
|
|
924
|
+
} else if (specifier.startsWith(".")) {
|
|
925
|
+
resolved = resolveRelativeImport(absPath, specifier);
|
|
926
|
+
}
|
|
927
|
+
if (!resolved) continue;
|
|
928
|
+
const targetId = toNodeId(srcDir, resolved);
|
|
929
|
+
if (!nodeIdSet.has(targetId)) continue;
|
|
930
|
+
if (targetId.endsWith("/index.ts") || targetId.endsWith("/index.tsx")) continue;
|
|
931
|
+
const key = `${externalId}\u2192${targetId}\u2192${isTypeOnly ? "type" : "value"}`;
|
|
932
|
+
if (seen.has(key)) continue;
|
|
933
|
+
seen.add(key);
|
|
934
|
+
edgesFromThis.push({ source: externalId, target: targetId, type: "imports" });
|
|
935
|
+
}
|
|
936
|
+
if (edgesFromThis.length === 0) continue;
|
|
937
|
+
nodes.push({
|
|
938
|
+
id: externalId,
|
|
939
|
+
type: "external",
|
|
940
|
+
name: parsed.name || nameFromFilename(absPath),
|
|
941
|
+
route: null,
|
|
942
|
+
module: "external",
|
|
943
|
+
exports: parsed.exports
|
|
944
|
+
});
|
|
945
|
+
nodeIdSet.add(externalId);
|
|
946
|
+
nodeTypeMap.set(externalId, "external");
|
|
947
|
+
allEdges.push(...edgesFromThis);
|
|
948
|
+
}
|
|
618
949
|
const flaggedSet = /* @__PURE__ */ new Set();
|
|
619
950
|
const dedupedFlagged = allFlagged.filter((f) => {
|
|
620
951
|
const key = `${f.source}\u2192${f.target}\u2192${f.label}`;
|
|
@@ -646,6 +977,7 @@ function generate(rootDir) {
|
|
|
646
977
|
total_configs: byType("config"),
|
|
647
978
|
total_utils: byType("util"),
|
|
648
979
|
total_libs: byType("lib"),
|
|
980
|
+
total_external: byType("external"),
|
|
649
981
|
total_edges: allEdges.length,
|
|
650
982
|
total_flagged: dedupedFlagged.length
|
|
651
983
|
};
|
|
@@ -672,26 +1004,34 @@ function generate(rootDir) {
|
|
|
672
1004
|
renders: allEdges.filter((e) => e.type === "renders").length,
|
|
673
1005
|
imports: allEdges.filter((e) => e.type === "imports").length,
|
|
674
1006
|
navigates: allEdges.filter((e) => e.type === "navigates").length
|
|
675
|
-
}
|
|
1007
|
+
},
|
|
1008
|
+
fetch_calls: fetchCallEntries
|
|
676
1009
|
}
|
|
677
1010
|
};
|
|
678
1011
|
}
|
|
679
|
-
var
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
1012
|
+
var import_node_fs4, import_node_path4, RENDER_TYPES, reactNextjsParser;
|
|
1013
|
+
var init_react_nextjs = __esm({
|
|
1014
|
+
"src/server/graph/parsers/ui/react-nextjs.ts"() {
|
|
1015
|
+
"use strict";
|
|
1016
|
+
import_node_fs4 = require("node:fs");
|
|
1017
|
+
import_node_path4 = require("node:path");
|
|
1018
|
+
init_ast_helpers();
|
|
1019
|
+
RENDER_TYPES = /* @__PURE__ */ new Set(["component", "ui", "layout", "context"]);
|
|
1020
|
+
reactNextjsParser = {
|
|
1021
|
+
id: "react-nextjs",
|
|
1022
|
+
layer: "ui",
|
|
1023
|
+
detect,
|
|
1024
|
+
generate
|
|
1025
|
+
};
|
|
1026
|
+
}
|
|
1027
|
+
});
|
|
685
1028
|
|
|
686
1029
|
// 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
1030
|
function walk2(dir) {
|
|
691
1031
|
const results = [];
|
|
692
|
-
if (!(0,
|
|
693
|
-
for (const entry of (0,
|
|
694
|
-
const full = (0,
|
|
1032
|
+
if (!(0, import_node_fs5.existsSync)(dir)) return results;
|
|
1033
|
+
for (const entry of (0, import_node_fs5.readdirSync)(dir, { withFileTypes: true })) {
|
|
1034
|
+
const full = (0, import_node_path5.join)(dir, entry.name);
|
|
695
1035
|
if (entry.isDirectory()) {
|
|
696
1036
|
results.push(...walk2(full));
|
|
697
1037
|
} else if (entry.name === "route.ts" || entry.name === "route.tsx") {
|
|
@@ -701,7 +1041,7 @@ function walk2(dir) {
|
|
|
701
1041
|
return results;
|
|
702
1042
|
}
|
|
703
1043
|
function filePathToRoute(apiDir, absPath) {
|
|
704
|
-
let route = "/" + (0,
|
|
1044
|
+
let route = "/" + (0, import_node_path5.relative)(apiDir, absPath).replace(/\\/g, "/").replace(/\/route\.tsx?$/, "");
|
|
705
1045
|
route = route.replace(/\[([^\]]+)\]/g, ":$1");
|
|
706
1046
|
route = route.replace(/\/+/g, "/");
|
|
707
1047
|
if (route === "/") return "/api";
|
|
@@ -712,10 +1052,10 @@ function camelToPascal(s) {
|
|
|
712
1052
|
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
713
1053
|
}
|
|
714
1054
|
function detect2(rootDir) {
|
|
715
|
-
return (0,
|
|
1055
|
+
return (0, import_node_fs5.existsSync)((0, import_node_path5.join)(rootDir, "src", "app", "api"));
|
|
716
1056
|
}
|
|
717
1057
|
function generate2(rootDir) {
|
|
718
|
-
const apiDir = (0,
|
|
1058
|
+
const apiDir = (0, import_node_path5.join)(rootDir, "src", "app", "api");
|
|
719
1059
|
const routeFiles = walk2(apiDir);
|
|
720
1060
|
const nodes = [];
|
|
721
1061
|
const edges = [];
|
|
@@ -730,10 +1070,10 @@ function generate2(rootDir) {
|
|
|
730
1070
|
const authWrappers = extractAuthWrappers(absPath);
|
|
731
1071
|
const methods = [];
|
|
732
1072
|
for (const exp of parsed.exports) {
|
|
733
|
-
if (
|
|
1073
|
+
if (HTTP_METHODS2.has(exp)) methods.push(exp);
|
|
734
1074
|
}
|
|
735
1075
|
const routePath = filePathToRoute(apiDir, absPath);
|
|
736
|
-
const relPath = (0,
|
|
1076
|
+
const relPath = (0, import_node_path5.relative)(rootDir, absPath).replace(/\\/g, "/");
|
|
737
1077
|
const mutations = dbCalls.filter((c) => c.isMutation);
|
|
738
1078
|
const reads = dbCalls.filter((c) => !c.isMutation);
|
|
739
1079
|
const mutates = mutations.length > 0;
|
|
@@ -800,7 +1140,7 @@ function generate2(rootDir) {
|
|
|
800
1140
|
flagged_edges: [],
|
|
801
1141
|
patterns: {
|
|
802
1142
|
total_endpoints: nodes.length,
|
|
803
|
-
methods_breakdown: [...
|
|
1143
|
+
methods_breakdown: [...HTTP_METHODS2].reduce((acc, m) => {
|
|
804
1144
|
acc[m] = nodes.filter((n) => n.methods.includes(m)).length;
|
|
805
1145
|
return acc;
|
|
806
1146
|
}, {}),
|
|
@@ -810,16 +1150,24 @@ function generate2(rootDir) {
|
|
|
810
1150
|
}
|
|
811
1151
|
};
|
|
812
1152
|
}
|
|
813
|
-
var
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
1153
|
+
var import_node_fs5, import_node_path5, HTTP_METHODS2, nextjsRoutesParser;
|
|
1154
|
+
var init_nextjs_routes = __esm({
|
|
1155
|
+
"src/server/graph/parsers/api/nextjs-routes.ts"() {
|
|
1156
|
+
"use strict";
|
|
1157
|
+
import_node_fs5 = require("node:fs");
|
|
1158
|
+
import_node_path5 = require("node:path");
|
|
1159
|
+
init_ast_helpers();
|
|
1160
|
+
HTTP_METHODS2 = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"]);
|
|
1161
|
+
nextjsRoutesParser = {
|
|
1162
|
+
id: "nextjs-routes",
|
|
1163
|
+
layer: "api",
|
|
1164
|
+
detect: detect2,
|
|
1165
|
+
generate: generate2
|
|
1166
|
+
};
|
|
1167
|
+
}
|
|
1168
|
+
});
|
|
819
1169
|
|
|
820
1170
|
// 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
1171
|
function parseModels(content) {
|
|
824
1172
|
const nodes = [];
|
|
825
1173
|
const relations = [];
|
|
@@ -910,11 +1258,11 @@ function parseEnums(content) {
|
|
|
910
1258
|
return nodes;
|
|
911
1259
|
}
|
|
912
1260
|
function detect3(rootDir) {
|
|
913
|
-
return (0,
|
|
1261
|
+
return (0, import_node_fs6.existsSync)((0, import_node_path6.join)(rootDir, "prisma", "schema.prisma"));
|
|
914
1262
|
}
|
|
915
1263
|
function generate3(rootDir) {
|
|
916
|
-
const schemaPath = (0,
|
|
917
|
-
const content = (0,
|
|
1264
|
+
const schemaPath = (0, import_node_path6.join)(rootDir, "prisma", "schema.prisma");
|
|
1265
|
+
const content = (0, import_node_fs6.readFileSync)(schemaPath, "utf-8");
|
|
918
1266
|
const { nodes: modelNodes, relations } = parseModels(content);
|
|
919
1267
|
const enumNodes = parseEnums(content);
|
|
920
1268
|
const allNodes = [...modelNodes, ...enumNodes];
|
|
@@ -963,66 +1311,745 @@ function generate3(rootDir) {
|
|
|
963
1311
|
}
|
|
964
1312
|
};
|
|
965
1313
|
}
|
|
966
|
-
var
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
1314
|
+
var import_node_fs6, import_node_path6, prismaSchemaParser;
|
|
1315
|
+
var init_prisma_schema = __esm({
|
|
1316
|
+
"src/server/graph/parsers/db/prisma-schema.ts"() {
|
|
1317
|
+
"use strict";
|
|
1318
|
+
import_node_fs6 = require("node:fs");
|
|
1319
|
+
import_node_path6 = require("node:path");
|
|
1320
|
+
prismaSchemaParser = {
|
|
1321
|
+
id: "prisma-schema",
|
|
1322
|
+
layer: "db",
|
|
1323
|
+
detect: detect3,
|
|
1324
|
+
generate: generate3
|
|
1325
|
+
};
|
|
1326
|
+
}
|
|
1327
|
+
});
|
|
1328
|
+
|
|
1329
|
+
// src/server/graph/core/api-route-matching.ts
|
|
1330
|
+
function loadApiRoutesFromOutput(apiOutput) {
|
|
1331
|
+
const routes = [];
|
|
1332
|
+
for (const n of apiOutput.nodes) {
|
|
1333
|
+
const path3 = n.path;
|
|
1334
|
+
if (!path3 || typeof path3 !== "string") continue;
|
|
1335
|
+
routes.push({
|
|
1336
|
+
path: path3,
|
|
1337
|
+
nodeId: n.id,
|
|
1338
|
+
segments: path3.split("/").filter(Boolean)
|
|
1339
|
+
});
|
|
1340
|
+
}
|
|
1341
|
+
return routes;
|
|
1342
|
+
}
|
|
1343
|
+
function buildApiPathMap(routes) {
|
|
1344
|
+
const map = /* @__PURE__ */ new Map();
|
|
1345
|
+
for (const r of routes) {
|
|
1346
|
+
if (!map.has(r.path)) map.set(r.path, r.nodeId);
|
|
1347
|
+
}
|
|
1348
|
+
return map;
|
|
1349
|
+
}
|
|
1350
|
+
function normalizeFetchUrl(raw) {
|
|
1351
|
+
let s = raw.replace(/^`|`$/g, "");
|
|
1352
|
+
const qIdx = s.indexOf("?");
|
|
1353
|
+
if (qIdx >= 0) s = s.slice(0, qIdx);
|
|
1354
|
+
const hIdx = s.indexOf("#");
|
|
1355
|
+
if (hIdx >= 0) s = s.slice(0, hIdx);
|
|
1356
|
+
let hadInterpolation = false;
|
|
1357
|
+
s = s.replace(/\$\{([^}]+)\}/g, (_, expr) => {
|
|
1358
|
+
hadInterpolation = true;
|
|
1359
|
+
const cleaned = expr.trim();
|
|
1360
|
+
const last = cleaned.split(".").pop() ?? cleaned;
|
|
1361
|
+
const name = last.replace(/[^\w]/g, "") || "param";
|
|
1362
|
+
return ":" + name;
|
|
1363
|
+
});
|
|
1364
|
+
s = s.replace(/\/+/g, "/");
|
|
1365
|
+
if (s.length > 1 && s.endsWith("/")) s = s.slice(0, -1);
|
|
1366
|
+
return { path: s || "/", hadInterpolation };
|
|
1367
|
+
}
|
|
1368
|
+
function scoreApiRouteMatch(candidate, known) {
|
|
1369
|
+
if (candidate.length !== known.length) return -1;
|
|
1370
|
+
let score = 0;
|
|
1371
|
+
for (let i = 0; i < candidate.length; i++) {
|
|
1372
|
+
const a = candidate[i];
|
|
1373
|
+
const b = known[i];
|
|
1374
|
+
if (a === b) {
|
|
1375
|
+
score += 3;
|
|
1376
|
+
continue;
|
|
1377
|
+
}
|
|
1378
|
+
if (a.startsWith(":") && b.startsWith(":")) {
|
|
1379
|
+
score += 2;
|
|
1380
|
+
continue;
|
|
1381
|
+
}
|
|
1382
|
+
if (a.startsWith(":") || b.startsWith(":")) {
|
|
1383
|
+
score += 1;
|
|
1384
|
+
continue;
|
|
1385
|
+
}
|
|
1386
|
+
return -1;
|
|
1387
|
+
}
|
|
1388
|
+
return score;
|
|
1389
|
+
}
|
|
1390
|
+
function resolveFetchCall(call, apiPathMap, apiRoutes) {
|
|
1391
|
+
const raw = call.url;
|
|
1392
|
+
if (/^(https?:)?\/\//i.test(raw)) {
|
|
1393
|
+
return { kind: "external", normalizedUrl: raw };
|
|
1394
|
+
}
|
|
1395
|
+
if (call.isConcat) {
|
|
1396
|
+
return { kind: "dynamic", normalizedUrl: raw };
|
|
1397
|
+
}
|
|
1398
|
+
const { path: path3, hadInterpolation } = normalizeFetchUrl(raw);
|
|
1399
|
+
if (!path3.startsWith("/")) {
|
|
1400
|
+
return { kind: "unresolved", normalizedUrl: path3 };
|
|
1401
|
+
}
|
|
1402
|
+
const segs = path3.split("/").filter(Boolean);
|
|
1403
|
+
if (hadInterpolation && segs.length > 0 && segs[0].startsWith(":")) {
|
|
1404
|
+
return { kind: "dynamic", normalizedUrl: path3 };
|
|
1405
|
+
}
|
|
1406
|
+
const exact = apiPathMap.get(path3);
|
|
1407
|
+
if (exact) return { kind: "resolved", nodeId: exact, normalizedUrl: path3 };
|
|
1408
|
+
let bestScore = -1;
|
|
1409
|
+
let bestId = null;
|
|
1410
|
+
for (const r of apiRoutes) {
|
|
1411
|
+
const score = scoreApiRouteMatch(segs, r.segments);
|
|
1412
|
+
if (score > bestScore) {
|
|
1413
|
+
bestScore = score;
|
|
1414
|
+
bestId = r.nodeId;
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
if (bestId && bestScore > 0) {
|
|
1418
|
+
return { kind: "resolved", nodeId: bestId, normalizedUrl: path3 };
|
|
1419
|
+
}
|
|
1420
|
+
return { kind: "unresolved", normalizedUrl: path3 };
|
|
1421
|
+
}
|
|
1422
|
+
function resolveUrlPath(urlPath, apiPathMap, apiRoutes) {
|
|
1423
|
+
const { path: path3, hadInterpolation } = normalizeFetchUrl(urlPath);
|
|
1424
|
+
if (!path3.startsWith("/")) {
|
|
1425
|
+
return { kind: "unresolved", normalizedUrl: path3 };
|
|
1426
|
+
}
|
|
1427
|
+
const segs = path3.split("/").filter(Boolean);
|
|
1428
|
+
if (hadInterpolation && segs.length > 0 && segs[0].startsWith(":")) {
|
|
1429
|
+
return { kind: "dynamic", normalizedUrl: path3 };
|
|
1430
|
+
}
|
|
1431
|
+
const exact = apiPathMap.get(path3);
|
|
1432
|
+
if (exact) return { kind: "resolved", nodeId: exact, normalizedUrl: path3 };
|
|
1433
|
+
let bestScore = -1;
|
|
1434
|
+
let bestId = null;
|
|
1435
|
+
for (const r of apiRoutes) {
|
|
1436
|
+
const score = scoreApiRouteMatch(segs, r.segments);
|
|
1437
|
+
if (score > bestScore) {
|
|
1438
|
+
bestScore = score;
|
|
1439
|
+
bestId = r.nodeId;
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
if (bestId && bestScore > 0) {
|
|
1443
|
+
return { kind: "resolved", nodeId: bestId, normalizedUrl: path3 };
|
|
1444
|
+
}
|
|
1445
|
+
return { kind: "unresolved", normalizedUrl: path3 };
|
|
1446
|
+
}
|
|
1447
|
+
var init_api_route_matching = __esm({
|
|
1448
|
+
"src/server/graph/core/api-route-matching.ts"() {
|
|
1449
|
+
"use strict";
|
|
1450
|
+
}
|
|
1451
|
+
});
|
|
1452
|
+
|
|
1453
|
+
// src/server/graph/parsers/crosslayer/fetch-resolver.ts
|
|
1454
|
+
var fetchResolverParser;
|
|
1455
|
+
var init_fetch_resolver = __esm({
|
|
1456
|
+
"src/server/graph/parsers/crosslayer/fetch-resolver.ts"() {
|
|
1457
|
+
"use strict";
|
|
1458
|
+
init_api_route_matching();
|
|
1459
|
+
fetchResolverParser = {
|
|
1460
|
+
id: "fetch-resolver",
|
|
1461
|
+
layer: "crosslayer",
|
|
1462
|
+
detect(_rootDir) {
|
|
1463
|
+
return true;
|
|
1464
|
+
},
|
|
1465
|
+
generate(_rootDir, layerOutputs) {
|
|
1466
|
+
const uiOutput = layerOutputs.get("ui");
|
|
1467
|
+
const apiOutput = layerOutputs.get("api");
|
|
1468
|
+
if (!uiOutput || !apiOutput) {
|
|
1469
|
+
return { cross_refs: [], flagged_edges: [], warnings: [] };
|
|
1470
|
+
}
|
|
1471
|
+
const apiRoutes = loadApiRoutesFromOutput(apiOutput);
|
|
1472
|
+
const apiPathMap = buildApiPathMap(apiRoutes);
|
|
1473
|
+
const fetchCallEntries = uiOutput.patterns?.fetch_calls ?? [];
|
|
1474
|
+
if (fetchCallEntries.length === 0) {
|
|
1475
|
+
return { cross_refs: [], flagged_edges: [], warnings: [] };
|
|
1476
|
+
}
|
|
1477
|
+
const includeExternal = process.env.LAUNCH_CHART_INCLUDE_EXTERNAL_FETCHES === "1";
|
|
1478
|
+
const crossRefs = [];
|
|
1479
|
+
const flaggedEdges = [];
|
|
1480
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1481
|
+
let resolvedCount = 0;
|
|
1482
|
+
let dynamicCount = 0;
|
|
1483
|
+
let unresolvedCount = 0;
|
|
1484
|
+
let externalCount = 0;
|
|
1485
|
+
for (const entry of fetchCallEntries) {
|
|
1486
|
+
for (const call of entry.calls) {
|
|
1487
|
+
const result = resolveFetchCall(call, apiPathMap, apiRoutes);
|
|
1488
|
+
const methodTag = call.method ?? (call.kind === "fetch" ? "GET?" : "?");
|
|
1489
|
+
if (result.kind === "resolved" && result.nodeId) {
|
|
1490
|
+
const key = `${entry.nodeId}\u2192${result.nodeId}\u2192calls_api`;
|
|
1491
|
+
if (seen.has(key)) continue;
|
|
1492
|
+
seen.add(key);
|
|
1493
|
+
crossRefs.push({
|
|
1494
|
+
source: entry.nodeId,
|
|
1495
|
+
target: result.nodeId,
|
|
1496
|
+
type: "calls_api",
|
|
1497
|
+
layer: "api"
|
|
1498
|
+
});
|
|
1499
|
+
resolvedCount++;
|
|
1500
|
+
continue;
|
|
1501
|
+
}
|
|
1502
|
+
if (result.kind === "dynamic") {
|
|
1503
|
+
dynamicCount++;
|
|
1504
|
+
flaggedEdges.push({
|
|
1505
|
+
source: entry.nodeId,
|
|
1506
|
+
target: "DYNAMIC",
|
|
1507
|
+
type: "calls_api",
|
|
1508
|
+
label: call.isConcat ? `${methodTag} fetch with concat: ${call.url}` : `${methodTag} fetch with template: ${call.url}`,
|
|
1509
|
+
confidence: call.isConcat ? "low" : "medium"
|
|
1510
|
+
});
|
|
1511
|
+
continue;
|
|
1512
|
+
}
|
|
1513
|
+
if (result.kind === "external") {
|
|
1514
|
+
externalCount++;
|
|
1515
|
+
if (!includeExternal) continue;
|
|
1516
|
+
flaggedEdges.push({
|
|
1517
|
+
source: entry.nodeId,
|
|
1518
|
+
target: "EXTERNAL",
|
|
1519
|
+
type: "calls_external",
|
|
1520
|
+
label: `${methodTag} external fetch: ${call.url}`,
|
|
1521
|
+
confidence: "high"
|
|
1522
|
+
});
|
|
1523
|
+
continue;
|
|
1524
|
+
}
|
|
1525
|
+
unresolvedCount++;
|
|
1526
|
+
flaggedEdges.push({
|
|
1527
|
+
source: entry.nodeId,
|
|
1528
|
+
target: "UNRESOLVED",
|
|
1529
|
+
type: "calls_api",
|
|
1530
|
+
label: `${methodTag} fetch to unknown path: ${result.normalizedUrl}`,
|
|
1531
|
+
confidence: "medium"
|
|
1532
|
+
});
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
return {
|
|
1536
|
+
cross_refs: crossRefs,
|
|
1537
|
+
flagged_edges: flaggedEdges,
|
|
1538
|
+
warnings: [],
|
|
1539
|
+
patterns: {
|
|
1540
|
+
api_call_detection: {
|
|
1541
|
+
resolved: resolvedCount,
|
|
1542
|
+
dynamic: dynamicCount,
|
|
1543
|
+
unresolved: unresolvedCount,
|
|
1544
|
+
external: externalCount
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
};
|
|
1548
|
+
}
|
|
1549
|
+
};
|
|
1550
|
+
}
|
|
1551
|
+
});
|
|
1552
|
+
|
|
1553
|
+
// src/server/graph/parsers/crosslayer/api-annotations.ts
|
|
1554
|
+
function walk3(dir, exts) {
|
|
1555
|
+
if (!(0, import_node_fs7.existsSync)(dir)) return [];
|
|
1556
|
+
const results = [];
|
|
1557
|
+
for (const entry of (0, import_node_fs7.readdirSync)(dir, { withFileTypes: true })) {
|
|
1558
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
1559
|
+
const full = (0, import_node_path7.join)(dir, entry.name);
|
|
1560
|
+
if (entry.isDirectory()) {
|
|
1561
|
+
results.push(...walk3(full, exts));
|
|
1562
|
+
} else if (exts.includes((0, import_node_path7.extname)(entry.name))) {
|
|
1563
|
+
results.push(full);
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
return results;
|
|
1567
|
+
}
|
|
1568
|
+
function toNodeId2(srcDir, absPath) {
|
|
1569
|
+
return (0, import_node_path7.relative)(srcDir, absPath).replace(/\\/g, "/");
|
|
1570
|
+
}
|
|
1571
|
+
var import_node_fs7, import_node_path7, API_ANNOTATION_RE, apiAnnotationsParser;
|
|
1572
|
+
var init_api_annotations = __esm({
|
|
1573
|
+
"src/server/graph/parsers/crosslayer/api-annotations.ts"() {
|
|
1574
|
+
"use strict";
|
|
1575
|
+
import_node_fs7 = require("node:fs");
|
|
1576
|
+
import_node_path7 = require("node:path");
|
|
1577
|
+
init_api_route_matching();
|
|
1578
|
+
API_ANNOTATION_RE = /@api\s+(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+(\/\S+)/g;
|
|
1579
|
+
apiAnnotationsParser = {
|
|
1580
|
+
id: "api-annotations",
|
|
1581
|
+
layer: "crosslayer",
|
|
1582
|
+
detect(rootDir) {
|
|
1583
|
+
return (0, import_node_fs7.existsSync)((0, import_node_path7.join)(rootDir, "src"));
|
|
1584
|
+
},
|
|
1585
|
+
generate(rootDir, layerOutputs) {
|
|
1586
|
+
const apiOutput = layerOutputs.get("api");
|
|
1587
|
+
if (!apiOutput) {
|
|
1588
|
+
return { cross_refs: [], flagged_edges: [], warnings: [] };
|
|
1589
|
+
}
|
|
1590
|
+
const uiOutput = layerOutputs.get("ui");
|
|
1591
|
+
const uiNodeIds = new Set(uiOutput?.nodes.map((n) => n.id) ?? []);
|
|
1592
|
+
const apiRoutes = loadApiRoutesFromOutput(apiOutput);
|
|
1593
|
+
const apiPathMap = buildApiPathMap(apiRoutes);
|
|
1594
|
+
const srcDir = (0, import_node_path7.join)(rootDir, "src");
|
|
1595
|
+
const files = walk3(srcDir, [".ts", ".tsx"]);
|
|
1596
|
+
const crossRefs = [];
|
|
1597
|
+
const flaggedEdges = [];
|
|
1598
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1599
|
+
for (const absPath of files) {
|
|
1600
|
+
const content = (0, import_node_fs7.readFileSync)(absPath, "utf-8");
|
|
1601
|
+
const sourceId = toNodeId2(srcDir, absPath);
|
|
1602
|
+
if (!uiNodeIds.has(sourceId)) continue;
|
|
1603
|
+
let match;
|
|
1604
|
+
API_ANNOTATION_RE.lastIndex = 0;
|
|
1605
|
+
while ((match = API_ANNOTATION_RE.exec(content)) !== null) {
|
|
1606
|
+
const method = match[1];
|
|
1607
|
+
const urlPath = match[2];
|
|
1608
|
+
const result = resolveUrlPath(urlPath, apiPathMap, apiRoutes);
|
|
1609
|
+
if (result.kind === "resolved" && result.nodeId) {
|
|
1610
|
+
const key = `${sourceId}|${result.nodeId}|calls_api`;
|
|
1611
|
+
if (seen.has(key)) continue;
|
|
1612
|
+
seen.add(key);
|
|
1613
|
+
crossRefs.push({
|
|
1614
|
+
source: sourceId,
|
|
1615
|
+
target: result.nodeId,
|
|
1616
|
+
type: "calls_api",
|
|
1617
|
+
layer: "api"
|
|
1618
|
+
});
|
|
1619
|
+
} else {
|
|
1620
|
+
flaggedEdges.push({
|
|
1621
|
+
source: sourceId,
|
|
1622
|
+
target: "UNRESOLVED",
|
|
1623
|
+
type: "annotation_unresolved",
|
|
1624
|
+
label: `@api ${method} ${urlPath} \u2014 no matching API route found`,
|
|
1625
|
+
confidence: "high"
|
|
1626
|
+
});
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
return {
|
|
1631
|
+
cross_refs: crossRefs,
|
|
1632
|
+
flagged_edges: flaggedEdges,
|
|
1633
|
+
warnings: [],
|
|
1634
|
+
patterns: {
|
|
1635
|
+
annotations_found: crossRefs.length + flaggedEdges.length,
|
|
1636
|
+
annotations_resolved: crossRefs.length,
|
|
1637
|
+
annotations_unresolved: flaggedEdges.length
|
|
1638
|
+
}
|
|
1639
|
+
};
|
|
1640
|
+
}
|
|
1641
|
+
};
|
|
1642
|
+
}
|
|
1643
|
+
});
|
|
1644
|
+
|
|
1645
|
+
// src/server/graph/parsers/crosslayer/url-literal-scanner.ts
|
|
1646
|
+
function walk4(dir, exts) {
|
|
1647
|
+
if (!(0, import_node_fs8.existsSync)(dir)) return [];
|
|
1648
|
+
const results = [];
|
|
1649
|
+
for (const entry of (0, import_node_fs8.readdirSync)(dir, { withFileTypes: true })) {
|
|
1650
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
1651
|
+
const full = (0, import_node_path8.join)(dir, entry.name);
|
|
1652
|
+
if (entry.isDirectory()) {
|
|
1653
|
+
results.push(...walk4(full, exts));
|
|
1654
|
+
} else if (exts.includes((0, import_node_path8.extname)(entry.name))) {
|
|
1655
|
+
results.push(full);
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
return results;
|
|
1659
|
+
}
|
|
1660
|
+
function toNodeId3(srcDir, absPath) {
|
|
1661
|
+
return (0, import_node_path8.relative)(srcDir, absPath).replace(/\\/g, "/");
|
|
1662
|
+
}
|
|
1663
|
+
var import_node_fs8, import_node_path8, URL_LITERAL_RE, urlLiteralScannerParser;
|
|
1664
|
+
var init_url_literal_scanner = __esm({
|
|
1665
|
+
"src/server/graph/parsers/crosslayer/url-literal-scanner.ts"() {
|
|
1666
|
+
"use strict";
|
|
1667
|
+
import_node_fs8 = require("node:fs");
|
|
1668
|
+
import_node_path8 = require("node:path");
|
|
1669
|
+
init_api_route_matching();
|
|
1670
|
+
URL_LITERAL_RE = /['"`](\/api\/[^'"`\s]+?)['"`]/g;
|
|
1671
|
+
urlLiteralScannerParser = {
|
|
1672
|
+
id: "url-literal-scanner",
|
|
1673
|
+
layer: "crosslayer",
|
|
1674
|
+
detect(rootDir) {
|
|
1675
|
+
return (0, import_node_fs8.existsSync)((0, import_node_path8.join)(rootDir, "src"));
|
|
1676
|
+
},
|
|
1677
|
+
generate(rootDir, layerOutputs) {
|
|
1678
|
+
const apiOutput = layerOutputs.get("api");
|
|
1679
|
+
if (!apiOutput) {
|
|
1680
|
+
return { cross_refs: [], flagged_edges: [], warnings: [] };
|
|
1681
|
+
}
|
|
1682
|
+
const uiOutput = layerOutputs.get("ui");
|
|
1683
|
+
const uiNodeIds = new Set(uiOutput?.nodes.map((n) => n.id) ?? []);
|
|
1684
|
+
const apiRoutes = loadApiRoutesFromOutput(apiOutput);
|
|
1685
|
+
const apiPathMap = buildApiPathMap(apiRoutes);
|
|
1686
|
+
const srcDir = (0, import_node_path8.join)(rootDir, "src");
|
|
1687
|
+
const clientDir = (0, import_node_path8.join)(srcDir, "client");
|
|
1688
|
+
const appDir = (0, import_node_path8.join)(srcDir, "app");
|
|
1689
|
+
const files = [
|
|
1690
|
+
...walk4(clientDir, [".ts", ".tsx"]),
|
|
1691
|
+
...walk4(appDir, [".ts", ".tsx"])
|
|
1692
|
+
];
|
|
1693
|
+
const crossRefs = [];
|
|
1694
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1695
|
+
for (const absPath of files) {
|
|
1696
|
+
const sourceId = toNodeId3(srcDir, absPath);
|
|
1697
|
+
if (!uiNodeIds.has(sourceId)) continue;
|
|
1698
|
+
const content = (0, import_node_fs8.readFileSync)(absPath, "utf-8");
|
|
1699
|
+
let match;
|
|
1700
|
+
URL_LITERAL_RE.lastIndex = 0;
|
|
1701
|
+
while ((match = URL_LITERAL_RE.exec(content)) !== null) {
|
|
1702
|
+
const urlPath = match[1];
|
|
1703
|
+
const result = resolveUrlPath(urlPath, apiPathMap, apiRoutes);
|
|
1704
|
+
if (result.kind === "resolved" && result.nodeId) {
|
|
1705
|
+
const key = `${sourceId}|${result.nodeId}|references_api`;
|
|
1706
|
+
if (seen.has(key)) continue;
|
|
1707
|
+
seen.add(key);
|
|
1708
|
+
crossRefs.push({
|
|
1709
|
+
source: sourceId,
|
|
1710
|
+
target: result.nodeId,
|
|
1711
|
+
type: "references_api",
|
|
1712
|
+
layer: "api"
|
|
1713
|
+
});
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
return {
|
|
1718
|
+
cross_refs: crossRefs,
|
|
1719
|
+
flagged_edges: [],
|
|
1720
|
+
warnings: [],
|
|
1721
|
+
patterns: {
|
|
1722
|
+
url_literals_resolved: crossRefs.length
|
|
1723
|
+
}
|
|
1724
|
+
};
|
|
1725
|
+
}
|
|
1726
|
+
};
|
|
1727
|
+
}
|
|
1728
|
+
});
|
|
1729
|
+
|
|
1730
|
+
// src/server/graph/core/parser-registry.ts
|
|
1731
|
+
function registerBuiltins(registry, disabled) {
|
|
1732
|
+
const builtins = [
|
|
1733
|
+
reactNextjsParser,
|
|
1734
|
+
nextjsRoutesParser,
|
|
1735
|
+
prismaSchemaParser,
|
|
1736
|
+
fetchResolverParser,
|
|
1737
|
+
apiAnnotationsParser,
|
|
1738
|
+
urlLiteralScannerParser
|
|
1739
|
+
];
|
|
1740
|
+
for (const parser of builtins) {
|
|
1741
|
+
if (disabled.has(parser.id)) continue;
|
|
1742
|
+
registry.register(parser);
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
function loadCustomParsers(registry, config, rootDir, disabled) {
|
|
1746
|
+
for (const entry of config.parsers?.custom ?? []) {
|
|
1747
|
+
try {
|
|
1748
|
+
const absPath = (0, import_node_path9.resolve)(rootDir, entry.path);
|
|
1749
|
+
const mod = require(absPath);
|
|
1750
|
+
const parser = "default" in mod ? mod.default : mod;
|
|
1751
|
+
if (disabled.has(parser.id)) continue;
|
|
1752
|
+
if (parser.layer !== entry.layer) {
|
|
1753
|
+
process.stderr.write(
|
|
1754
|
+
`[launch-chart] custom parser "${parser.id}" declares layer "${parser.layer}" but config says "${entry.layer}" \u2014 using parser's layer
|
|
1755
|
+
`
|
|
1756
|
+
);
|
|
1757
|
+
}
|
|
1758
|
+
registry.register(parser);
|
|
1759
|
+
} catch (err2) {
|
|
1760
|
+
process.stderr.write(`[launch-chart] failed to load custom parser from ${entry.path}: ${err2}
|
|
1761
|
+
`);
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
function createRegistry(config, rootDir) {
|
|
1766
|
+
const registry = new ParserRegistry();
|
|
1767
|
+
const disabled = new Set(config.parsers?.disabled ?? []);
|
|
1768
|
+
registerBuiltins(registry, disabled);
|
|
1769
|
+
loadCustomParsers(registry, config, rootDir, disabled);
|
|
1770
|
+
return registry;
|
|
1771
|
+
}
|
|
1772
|
+
var import_node_path9, ParserRegistry;
|
|
1773
|
+
var init_parser_registry = __esm({
|
|
1774
|
+
"src/server/graph/core/parser-registry.ts"() {
|
|
1775
|
+
"use strict";
|
|
1776
|
+
import_node_path9 = require("node:path");
|
|
1777
|
+
init_react_nextjs();
|
|
1778
|
+
init_nextjs_routes();
|
|
1779
|
+
init_prisma_schema();
|
|
1780
|
+
init_fetch_resolver();
|
|
1781
|
+
init_api_annotations();
|
|
1782
|
+
init_url_literal_scanner();
|
|
1783
|
+
ParserRegistry = class {
|
|
1784
|
+
constructor() {
|
|
1785
|
+
this.parsers = /* @__PURE__ */ new Map();
|
|
1786
|
+
this.ids = /* @__PURE__ */ new Set();
|
|
1787
|
+
}
|
|
1788
|
+
register(parser) {
|
|
1789
|
+
if (this.ids.has(parser.id)) {
|
|
1790
|
+
throw new Error(`Duplicate parser id: ${parser.id}`);
|
|
1791
|
+
}
|
|
1792
|
+
this.ids.add(parser.id);
|
|
1793
|
+
const list = this.parsers.get(parser.layer) ?? [];
|
|
1794
|
+
list.push(parser);
|
|
1795
|
+
this.parsers.set(parser.layer, list);
|
|
1796
|
+
}
|
|
1797
|
+
getParsers(layer) {
|
|
1798
|
+
return this.parsers.get(layer) ?? [];
|
|
1799
|
+
}
|
|
1800
|
+
getCrossLayerParsers() {
|
|
1801
|
+
return this.parsers.get("crosslayer") ?? [];
|
|
1802
|
+
}
|
|
1803
|
+
getAll() {
|
|
1804
|
+
const all = [];
|
|
1805
|
+
for (const list of this.parsers.values()) all.push(...list);
|
|
1806
|
+
return all;
|
|
1807
|
+
}
|
|
1808
|
+
};
|
|
1809
|
+
}
|
|
1810
|
+
});
|
|
1811
|
+
|
|
1812
|
+
// src/server/graph/core/merge.ts
|
|
1813
|
+
function mergeGraphOutputs(outputs, layer) {
|
|
1814
|
+
if (outputs.length === 0) {
|
|
1815
|
+
return {
|
|
1816
|
+
metadata: { generated: (/* @__PURE__ */ new Date()).toISOString(), scope: "", layer },
|
|
1817
|
+
nodes: [],
|
|
1818
|
+
edges: [],
|
|
1819
|
+
cross_refs: [],
|
|
1820
|
+
contradictions: [],
|
|
1821
|
+
warnings: [],
|
|
1822
|
+
flagged_edges: []
|
|
1823
|
+
};
|
|
1824
|
+
}
|
|
1825
|
+
if (outputs.length === 1) return outputs[0];
|
|
1826
|
+
const seenNodes = /* @__PURE__ */ new Set();
|
|
1827
|
+
const seenEdges = /* @__PURE__ */ new Set();
|
|
1828
|
+
const seenCrossRefs = /* @__PURE__ */ new Set();
|
|
1829
|
+
const mergedNodes = [];
|
|
1830
|
+
const mergedEdges = [];
|
|
1831
|
+
const mergedCrossRefs = [];
|
|
1832
|
+
const mergedContradictions = [];
|
|
1833
|
+
const mergedWarnings = [];
|
|
1834
|
+
const mergedFlagged = [];
|
|
1835
|
+
const parserIds = [];
|
|
1836
|
+
for (const output of outputs) {
|
|
1837
|
+
if (output.metadata.parser) {
|
|
1838
|
+
parserIds.push(String(output.metadata.parser));
|
|
1839
|
+
}
|
|
1840
|
+
for (const node of output.nodes) {
|
|
1841
|
+
if (seenNodes.has(node.id)) {
|
|
1842
|
+
mergedWarnings.push({
|
|
1843
|
+
type: "merge_conflict",
|
|
1844
|
+
detail: `Node "${node.id}" produced by multiple parsers; keeping first`
|
|
1845
|
+
});
|
|
1846
|
+
continue;
|
|
1847
|
+
}
|
|
1848
|
+
seenNodes.add(node.id);
|
|
1849
|
+
mergedNodes.push(node);
|
|
1850
|
+
}
|
|
1851
|
+
for (const edge of output.edges) {
|
|
1852
|
+
const key = `${edge.source}|${edge.target}|${edge.type}`;
|
|
1853
|
+
if (seenEdges.has(key)) continue;
|
|
1854
|
+
seenEdges.add(key);
|
|
1855
|
+
mergedEdges.push(edge);
|
|
1856
|
+
}
|
|
1857
|
+
for (const ref of output.cross_refs) {
|
|
1858
|
+
const key = `${ref.source}|${ref.target}|${ref.type}`;
|
|
1859
|
+
if (seenCrossRefs.has(key)) continue;
|
|
1860
|
+
seenCrossRefs.add(key);
|
|
1861
|
+
mergedCrossRefs.push(ref);
|
|
1862
|
+
}
|
|
1863
|
+
mergedContradictions.push(...output.contradictions);
|
|
1864
|
+
mergedWarnings.push(...output.warnings);
|
|
1865
|
+
mergedFlagged.push(...output.flagged_edges);
|
|
1866
|
+
}
|
|
1867
|
+
const metadata = {
|
|
1868
|
+
...outputs[0].metadata,
|
|
1869
|
+
generated: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1870
|
+
parsers: parserIds
|
|
1871
|
+
};
|
|
1872
|
+
return {
|
|
1873
|
+
metadata,
|
|
1874
|
+
nodes: mergedNodes,
|
|
1875
|
+
edges: mergedEdges,
|
|
1876
|
+
cross_refs: mergedCrossRefs,
|
|
1877
|
+
contradictions: mergedContradictions,
|
|
1878
|
+
warnings: mergedWarnings,
|
|
1879
|
+
flagged_edges: mergedFlagged,
|
|
1880
|
+
patterns: outputs[0].patterns
|
|
1881
|
+
};
|
|
1882
|
+
}
|
|
1883
|
+
function dedupCrossRefs(refs) {
|
|
1884
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1885
|
+
const result = [];
|
|
1886
|
+
for (const ref of refs) {
|
|
1887
|
+
const key = `${ref.source}|${ref.target}|${ref.type}`;
|
|
1888
|
+
if (seen.has(key)) continue;
|
|
1889
|
+
seen.add(key);
|
|
1890
|
+
result.push(ref);
|
|
1891
|
+
}
|
|
1892
|
+
return result;
|
|
1893
|
+
}
|
|
1894
|
+
function applyCrossLayerResults(uiOutput, results, primaryId) {
|
|
1895
|
+
const allCrossRefs = [...uiOutput.cross_refs];
|
|
1896
|
+
const allFlagged = [...uiOutput.flagged_edges];
|
|
1897
|
+
const allWarnings = [...uiOutput.warnings];
|
|
1898
|
+
const primaryResult = results.find((r) => r.parserId === primaryId);
|
|
1899
|
+
const secondaryResults = results.filter((r) => r.parserId !== primaryId);
|
|
1900
|
+
if (primaryResult) {
|
|
1901
|
+
allCrossRefs.push(...primaryResult.output.cross_refs);
|
|
1902
|
+
allFlagged.push(...primaryResult.output.flagged_edges);
|
|
1903
|
+
allWarnings.push(...primaryResult.output.warnings);
|
|
1904
|
+
}
|
|
1905
|
+
const primarySet = new Set(
|
|
1906
|
+
(primaryResult?.output.cross_refs ?? []).map((r) => `${r.source}|${r.target}|${r.type}`)
|
|
1907
|
+
);
|
|
1908
|
+
for (const sec of secondaryResults) {
|
|
1909
|
+
for (const ref of sec.output.cross_refs) {
|
|
1910
|
+
const key = `${ref.source}|${ref.target}|${ref.type}`;
|
|
1911
|
+
if (primarySet.has(key)) {
|
|
1912
|
+
allCrossRefs.push(ref);
|
|
1913
|
+
} else {
|
|
1914
|
+
allFlagged.push({
|
|
1915
|
+
source: ref.source,
|
|
1916
|
+
target: ref.target,
|
|
1917
|
+
type: "out_of_pattern",
|
|
1918
|
+
label: `API call detected by ${sec.parserId} but not by primary (${primaryId})`,
|
|
1919
|
+
confidence: "medium"
|
|
1920
|
+
});
|
|
1921
|
+
allCrossRefs.push(ref);
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1924
|
+
allFlagged.push(...sec.output.flagged_edges);
|
|
1925
|
+
allWarnings.push(...sec.output.warnings);
|
|
1926
|
+
}
|
|
1927
|
+
return {
|
|
1928
|
+
...uiOutput,
|
|
1929
|
+
cross_refs: dedupCrossRefs(allCrossRefs),
|
|
1930
|
+
flagged_edges: allFlagged,
|
|
1931
|
+
warnings: allWarnings
|
|
1932
|
+
};
|
|
1933
|
+
}
|
|
1934
|
+
var init_merge = __esm({
|
|
1935
|
+
"src/server/graph/core/merge.ts"() {
|
|
1936
|
+
"use strict";
|
|
1937
|
+
}
|
|
1938
|
+
});
|
|
972
1939
|
|
|
973
1940
|
// src/server/graph/core/graph-builder.ts
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
1941
|
+
function readGraphFromDisk(rootDir, layer) {
|
|
1942
|
+
const filePath = (0, import_node_path10.join)(rootDir, ".launchsecure", "graphs", `${layer}.json`);
|
|
1943
|
+
if (!(0, import_node_fs9.existsSync)(filePath)) return null;
|
|
1944
|
+
try {
|
|
1945
|
+
return JSON.parse((0, import_node_fs9.readFileSync)(filePath, "utf-8"));
|
|
1946
|
+
} catch {
|
|
1947
|
+
return null;
|
|
1948
|
+
}
|
|
981
1949
|
}
|
|
982
1950
|
function generateLayer(rootDir, layer) {
|
|
983
|
-
const
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
const
|
|
1951
|
+
const config = loadConfig(rootDir);
|
|
1952
|
+
const registry = createRegistry(config, rootDir);
|
|
1953
|
+
const parsers = registry.getParsers(layer);
|
|
1954
|
+
const outputs = [];
|
|
1955
|
+
for (const parser of parsers) {
|
|
1956
|
+
if (!parser.detect(rootDir)) continue;
|
|
1957
|
+
outputs.push(parser.generate(rootDir));
|
|
1958
|
+
}
|
|
1959
|
+
if (outputs.length === 0) return null;
|
|
1960
|
+
let merged = outputs.length === 1 ? outputs[0] : mergeGraphOutputs(outputs, layer);
|
|
1961
|
+
if (layer === "ui") {
|
|
1962
|
+
const layerOutputs = /* @__PURE__ */ new Map();
|
|
1963
|
+
layerOutputs.set("ui", merged);
|
|
1964
|
+
for (const otherLayer of ["api", "db"]) {
|
|
1965
|
+
const existing = readGraphFromDisk(rootDir, otherLayer);
|
|
1966
|
+
if (existing) layerOutputs.set(otherLayer, existing);
|
|
1967
|
+
}
|
|
1968
|
+
const crossParsers = registry.getCrossLayerParsers();
|
|
1969
|
+
const primaryId = config.parsers?.primary?.crosslayer ?? crossParsers[0]?.id ?? null;
|
|
1970
|
+
const crossResults = crossParsers.filter((p) => p.detect(rootDir)).map((p) => ({ parserId: p.id, output: p.generate(rootDir, layerOutputs) }));
|
|
1971
|
+
if (crossResults.length > 0) {
|
|
1972
|
+
merged = applyCrossLayerResults(merged, crossResults, primaryId);
|
|
1973
|
+
}
|
|
1974
|
+
}
|
|
987
1975
|
return {
|
|
988
1976
|
layer,
|
|
989
|
-
output,
|
|
990
|
-
nodeCount:
|
|
991
|
-
edgeCount:
|
|
1977
|
+
output: merged,
|
|
1978
|
+
nodeCount: merged.nodes.length,
|
|
1979
|
+
edgeCount: merged.edges.length
|
|
992
1980
|
};
|
|
993
1981
|
}
|
|
994
1982
|
function generateAll(rootDir) {
|
|
995
|
-
const
|
|
1983
|
+
const config = loadConfig(rootDir);
|
|
1984
|
+
const registry = createRegistry(config, rootDir);
|
|
1985
|
+
const layerOrder = ["api", "db", "ui"];
|
|
1986
|
+
const layerOutputs = /* @__PURE__ */ new Map();
|
|
996
1987
|
const results = [];
|
|
997
|
-
for (const layer of
|
|
998
|
-
const
|
|
999
|
-
|
|
1988
|
+
for (const layer of layerOrder) {
|
|
1989
|
+
const parsers = registry.getParsers(layer);
|
|
1990
|
+
const outputs = [];
|
|
1991
|
+
for (const parser of parsers) {
|
|
1992
|
+
if (!parser.detect(rootDir)) continue;
|
|
1993
|
+
outputs.push(parser.generate(rootDir));
|
|
1994
|
+
}
|
|
1995
|
+
if (outputs.length === 0) continue;
|
|
1996
|
+
const merged = outputs.length === 1 ? outputs[0] : mergeGraphOutputs(outputs, layer);
|
|
1997
|
+
layerOutputs.set(layer, merged);
|
|
1998
|
+
results.push({
|
|
1999
|
+
layer,
|
|
2000
|
+
output: merged,
|
|
2001
|
+
nodeCount: merged.nodes.length,
|
|
2002
|
+
edgeCount: merged.edges.length
|
|
2003
|
+
});
|
|
1000
2004
|
}
|
|
1001
|
-
|
|
2005
|
+
const crossParsers = registry.getCrossLayerParsers();
|
|
2006
|
+
const primaryId = config.parsers?.primary?.crosslayer ?? crossParsers[0]?.id ?? null;
|
|
2007
|
+
const crossResults = crossParsers.filter((p) => p.detect(rootDir)).map((p) => ({ parserId: p.id, output: p.generate(rootDir, layerOutputs) }));
|
|
2008
|
+
if (crossResults.length > 0 && layerOutputs.has("ui")) {
|
|
2009
|
+
const uiOutput = layerOutputs.get("ui");
|
|
2010
|
+
const merged = applyCrossLayerResults(uiOutput, crossResults, primaryId);
|
|
2011
|
+
layerOutputs.set("ui", merged);
|
|
2012
|
+
const uiResult = results.find((r) => r.layer === "ui");
|
|
2013
|
+
if (uiResult) {
|
|
2014
|
+
uiResult.output = merged;
|
|
2015
|
+
uiResult.nodeCount = merged.nodes.length;
|
|
2016
|
+
uiResult.edgeCount = merged.edges.length;
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
const byLayer = new Map(results.map((r) => [r.layer, r]));
|
|
2020
|
+
return ["ui", "api", "db"].map((l) => byLayer.get(l)).filter((r) => !!r);
|
|
1002
2021
|
}
|
|
2022
|
+
var import_node_fs9, import_node_path10;
|
|
2023
|
+
var init_graph_builder = __esm({
|
|
2024
|
+
"src/server/graph/core/graph-builder.ts"() {
|
|
2025
|
+
"use strict";
|
|
2026
|
+
import_node_fs9 = require("node:fs");
|
|
2027
|
+
import_node_path10 = require("node:path");
|
|
2028
|
+
init_config();
|
|
2029
|
+
init_parser_registry();
|
|
2030
|
+
init_merge();
|
|
2031
|
+
}
|
|
2032
|
+
});
|
|
1003
2033
|
|
|
1004
2034
|
// src/server/graph/index.ts
|
|
1005
|
-
var GRAPHS_DIR = ".launchsecure/graphs";
|
|
1006
|
-
var LAYERS = ["ui", "api", "db"];
|
|
1007
|
-
var graphCache = /* @__PURE__ */ new Map();
|
|
1008
2035
|
function graphsDir(rootDir) {
|
|
1009
|
-
return (0,
|
|
2036
|
+
return (0, import_node_path11.join)(rootDir, GRAPHS_DIR);
|
|
1010
2037
|
}
|
|
1011
2038
|
function graphFilePath(rootDir, layer) {
|
|
1012
|
-
return (0,
|
|
2039
|
+
return (0, import_node_path11.join)(graphsDir(rootDir), `${layer}.json`);
|
|
1013
2040
|
}
|
|
1014
2041
|
function invalidateCache(filePath) {
|
|
1015
2042
|
graphCache.delete(filePath);
|
|
1016
2043
|
}
|
|
1017
2044
|
function readGraph(rootDir, layer) {
|
|
1018
2045
|
const filePath = graphFilePath(rootDir, layer);
|
|
1019
|
-
if (!(0,
|
|
1020
|
-
const stat = (0,
|
|
2046
|
+
if (!(0, import_node_fs10.existsSync)(filePath)) return null;
|
|
2047
|
+
const stat = (0, import_node_fs10.statSync)(filePath);
|
|
1021
2048
|
const cached = graphCache.get(filePath);
|
|
1022
2049
|
if (cached && cached.mtimeMs === stat.mtimeMs) {
|
|
1023
2050
|
return cached.graph;
|
|
1024
2051
|
}
|
|
1025
|
-
const content = (0,
|
|
2052
|
+
const content = (0, import_node_fs10.readFileSync)(filePath, "utf-8");
|
|
1026
2053
|
const graph = JSON.parse(content);
|
|
1027
2054
|
graphCache.set(filePath, { mtimeMs: stat.mtimeMs, graph });
|
|
1028
2055
|
return graph;
|
|
@@ -1037,144 +2064,326 @@ function readAllGraphs(rootDir) {
|
|
|
1037
2064
|
}
|
|
1038
2065
|
function generateGraph(rootDir, layer) {
|
|
1039
2066
|
const dir = graphsDir(rootDir);
|
|
1040
|
-
(0,
|
|
2067
|
+
(0, import_node_fs10.mkdirSync)(dir, { recursive: true });
|
|
1041
2068
|
const results = layer ? [generateLayer(rootDir, layer)].filter((r) => r !== null) : generateAll(rootDir);
|
|
1042
2069
|
for (const result of results) {
|
|
1043
2070
|
const filePath = graphFilePath(rootDir, result.layer);
|
|
1044
|
-
(0,
|
|
2071
|
+
(0, import_node_fs10.writeFileSync)(filePath, JSON.stringify(result.output, null, 2) + "\n", "utf-8");
|
|
1045
2072
|
invalidateCache(filePath);
|
|
1046
2073
|
}
|
|
1047
2074
|
return results;
|
|
1048
2075
|
}
|
|
2076
|
+
var import_node_fs10, import_node_path11, GRAPHS_DIR, LAYERS, graphCache;
|
|
2077
|
+
var init_graph = __esm({
|
|
2078
|
+
"src/server/graph/index.ts"() {
|
|
2079
|
+
"use strict";
|
|
2080
|
+
import_node_fs10 = require("node:fs");
|
|
2081
|
+
import_node_path11 = require("node:path");
|
|
2082
|
+
init_graph_builder();
|
|
2083
|
+
GRAPHS_DIR = ".launchsecure/graphs";
|
|
2084
|
+
LAYERS = ["ui", "api", "db"];
|
|
2085
|
+
graphCache = /* @__PURE__ */ new Map();
|
|
2086
|
+
}
|
|
2087
|
+
});
|
|
1049
2088
|
|
|
1050
|
-
// src/server/
|
|
1051
|
-
var
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
2089
|
+
// src/server/chart-serve.ts
|
|
2090
|
+
var chart_serve_exports = {};
|
|
2091
|
+
__export(chart_serve_exports, {
|
|
2092
|
+
runServeCli: () => runServeCli,
|
|
2093
|
+
startChartServer: () => startChartServer
|
|
2094
|
+
});
|
|
2095
|
+
function findProjectRoot2(startDir) {
|
|
2096
|
+
let dir = startDir;
|
|
2097
|
+
for (let i = 0; i < 8; i++) {
|
|
2098
|
+
const graphsDir2 = import_node_path12.default.join(dir, ".launchsecure", "graphs");
|
|
2099
|
+
if (import_node_fs11.default.existsSync(import_node_path12.default.join(graphsDir2, "ui.json")) || import_node_fs11.default.existsSync(import_node_path12.default.join(graphsDir2, "api.json")) || import_node_fs11.default.existsSync(import_node_path12.default.join(graphsDir2, "db.json"))) return dir;
|
|
2100
|
+
const parent = import_node_path12.default.dirname(dir);
|
|
2101
|
+
if (parent === dir) break;
|
|
2102
|
+
dir = parent;
|
|
2103
|
+
}
|
|
2104
|
+
dir = startDir;
|
|
2105
|
+
for (let i = 0; i < 8; i++) {
|
|
2106
|
+
if (import_node_fs11.default.existsSync(import_node_path12.default.join(dir, ".git"))) return dir;
|
|
2107
|
+
const parent = import_node_path12.default.dirname(dir);
|
|
2108
|
+
if (parent === dir) break;
|
|
2109
|
+
dir = parent;
|
|
2110
|
+
}
|
|
2111
|
+
return startDir;
|
|
2112
|
+
}
|
|
2113
|
+
function buildMergedGraph(projectRoot) {
|
|
2114
|
+
let graphs = readAllGraphs(projectRoot);
|
|
2115
|
+
if (!graphs.ui && !graphs.api && !graphs.db) {
|
|
2116
|
+
generateGraph(projectRoot);
|
|
2117
|
+
graphs = readAllGraphs(projectRoot);
|
|
2118
|
+
}
|
|
2119
|
+
const nodes = [];
|
|
2120
|
+
const rawLinks = [];
|
|
2121
|
+
const LAYERS2 = ["ui", "api", "db"];
|
|
2122
|
+
for (const layer of LAYERS2) {
|
|
2123
|
+
const g = graphs[layer];
|
|
2124
|
+
if (!g) continue;
|
|
2125
|
+
for (const n of g.nodes) {
|
|
2126
|
+
nodes.push({
|
|
2127
|
+
id: `${layer}:${n.id}`,
|
|
2128
|
+
name: n.name,
|
|
2129
|
+
type: n.type,
|
|
2130
|
+
layer,
|
|
2131
|
+
module: n.module ?? null,
|
|
2132
|
+
path: n.path ?? n.id
|
|
2133
|
+
});
|
|
2134
|
+
}
|
|
2135
|
+
for (const e of g.edges) {
|
|
2136
|
+
rawLinks.push({ source: `${layer}:${e.source}`, target: `${layer}:${e.target}`, type: e.type, layer, cross: false });
|
|
2137
|
+
}
|
|
2138
|
+
for (const c of g.cross_refs ?? []) {
|
|
2139
|
+
rawLinks.push({ source: `${layer}:${c.source}`, target: `${c.layer}:${c.target}`, type: c.type, layer, cross: true });
|
|
2140
|
+
}
|
|
2141
|
+
}
|
|
2142
|
+
const nodeIds = new Set(nodes.map((n) => n.id));
|
|
2143
|
+
const links = rawLinks.filter((l) => nodeIds.has(l.source) && nodeIds.has(l.target));
|
|
2144
|
+
return {
|
|
2145
|
+
nodes,
|
|
2146
|
+
links,
|
|
2147
|
+
stats: {
|
|
2148
|
+
nodes: nodes.length,
|
|
2149
|
+
links: links.length,
|
|
2150
|
+
byLayer: {
|
|
2151
|
+
ui: graphs.ui ? graphs.ui.nodes.length : 0,
|
|
2152
|
+
api: graphs.api ? graphs.api.nodes.length : 0,
|
|
2153
|
+
db: graphs.db ? graphs.db.nodes.length : 0
|
|
1067
2154
|
}
|
|
1068
2155
|
}
|
|
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
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
2156
|
+
};
|
|
2157
|
+
}
|
|
2158
|
+
function serveStatic(res, filePath) {
|
|
2159
|
+
if (!import_node_fs11.default.existsSync(filePath) || !import_node_fs11.default.statSync(filePath).isFile()) return false;
|
|
2160
|
+
const ext = import_node_path12.default.extname(filePath).toLowerCase();
|
|
2161
|
+
const mime = MIME_TYPES[ext] ?? "application/octet-stream";
|
|
2162
|
+
res.writeHead(200, { "Content-Type": mime, "Cache-Control": "no-cache" });
|
|
2163
|
+
import_node_fs11.default.createReadStream(filePath).pipe(res);
|
|
2164
|
+
return true;
|
|
2165
|
+
}
|
|
2166
|
+
function serveIndex(res, clientDir) {
|
|
2167
|
+
const indexPath = import_node_path12.default.join(clientDir, "index.html");
|
|
2168
|
+
if (!import_node_fs11.default.existsSync(indexPath)) {
|
|
2169
|
+
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
2170
|
+
res.end(`LaunchChart client bundle not found at ${clientDir}. Run 'npm run build:chart-client'.`);
|
|
2171
|
+
return;
|
|
2172
|
+
}
|
|
2173
|
+
serveStatic(res, indexPath);
|
|
2174
|
+
}
|
|
2175
|
+
function tryListen(server, port) {
|
|
2176
|
+
return new Promise((resolve2, reject) => {
|
|
2177
|
+
const onError = (err2) => {
|
|
2178
|
+
server.off("listening", onListening);
|
|
2179
|
+
reject(err2);
|
|
2180
|
+
};
|
|
2181
|
+
const onListening = () => {
|
|
2182
|
+
server.off("error", onError);
|
|
2183
|
+
resolve2(port);
|
|
2184
|
+
};
|
|
2185
|
+
server.once("error", onError);
|
|
2186
|
+
server.once("listening", onListening);
|
|
2187
|
+
server.listen(port, "127.0.0.1");
|
|
2188
|
+
});
|
|
2189
|
+
}
|
|
2190
|
+
async function bindWithFallback(server, startPort) {
|
|
2191
|
+
let lastErr = null;
|
|
2192
|
+
for (let i = 0; i < MAX_PORT_SCAN; i++) {
|
|
2193
|
+
const port = startPort + i;
|
|
2194
|
+
try {
|
|
2195
|
+
return await tryListen(server, port);
|
|
2196
|
+
} catch (err2) {
|
|
2197
|
+
const code = err2.code;
|
|
2198
|
+
if (code === "EADDRINUSE") {
|
|
2199
|
+
lastErr = err2;
|
|
2200
|
+
continue;
|
|
2201
|
+
}
|
|
2202
|
+
throw err2;
|
|
2203
|
+
}
|
|
2204
|
+
}
|
|
2205
|
+
throw lastErr ?? new Error("Failed to bind any port");
|
|
2206
|
+
}
|
|
2207
|
+
async function startChartServer(opts = {}) {
|
|
2208
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
2209
|
+
const projectRoot = findProjectRoot2(cwd);
|
|
2210
|
+
const existing = getLiveLock();
|
|
2211
|
+
if (existing) {
|
|
2212
|
+
if (!opts.quiet) {
|
|
2213
|
+
process.stderr.write(
|
|
2214
|
+
`[launch-chart] already running (pid ${existing.pid}) at ${existing.url}
|
|
2215
|
+
`
|
|
2216
|
+
);
|
|
2217
|
+
}
|
|
2218
|
+
return { port: existing.port, url: existing.url };
|
|
2219
|
+
}
|
|
2220
|
+
const clientDir = opts.clientDir ?? import_node_path12.default.join(__dirname, "..", "chart-client");
|
|
2221
|
+
const server = import_node_http.default.createServer((req, res) => {
|
|
2222
|
+
try {
|
|
2223
|
+
const url2 = new URL(req.url ?? "/", `http://${req.headers.host}`);
|
|
2224
|
+
if (req.method === "GET" && url2.pathname === "/api/project-graph") {
|
|
2225
|
+
const regenerate = url2.searchParams.get("regenerate") === "1";
|
|
2226
|
+
if (regenerate) generateGraph(projectRoot);
|
|
2227
|
+
const merged = buildMergedGraph(projectRoot);
|
|
2228
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2229
|
+
res.end(JSON.stringify({
|
|
2230
|
+
...merged,
|
|
2231
|
+
debug: { cwd, projectRoot }
|
|
2232
|
+
}));
|
|
2233
|
+
return;
|
|
2234
|
+
}
|
|
2235
|
+
if (req.method === "GET" && url2.pathname === "/api/raw-graphs") {
|
|
2236
|
+
const graphs = readAllGraphs(projectRoot);
|
|
2237
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2238
|
+
res.end(JSON.stringify({ ui: graphs.ui ?? null, api: graphs.api ?? null, db: graphs.db ?? null }));
|
|
2239
|
+
return;
|
|
2240
|
+
}
|
|
2241
|
+
if (req.method === "POST" && url2.pathname === "/api/generate-graph") {
|
|
2242
|
+
try {
|
|
2243
|
+
generateGraph(projectRoot);
|
|
2244
|
+
const graphs = readAllGraphs(projectRoot);
|
|
2245
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2246
|
+
res.end(JSON.stringify({
|
|
2247
|
+
ok: true,
|
|
2248
|
+
ui: graphs.ui ?? null,
|
|
2249
|
+
api: graphs.api ?? null,
|
|
2250
|
+
db: graphs.db ?? null
|
|
2251
|
+
}));
|
|
2252
|
+
} catch (err2) {
|
|
2253
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
2254
|
+
res.end(JSON.stringify({ ok: false, error: String(err2) }));
|
|
1125
2255
|
}
|
|
2256
|
+
return;
|
|
2257
|
+
}
|
|
2258
|
+
if (req.method === "GET" && url2.pathname === "/api/health") {
|
|
2259
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2260
|
+
res.end(JSON.stringify({ ok: true, projectRoot }));
|
|
2261
|
+
return;
|
|
2262
|
+
}
|
|
2263
|
+
if (req.method === "GET" && url2.pathname === "/api/parser-config") {
|
|
2264
|
+
const config = loadConfig(projectRoot);
|
|
2265
|
+
const detection = [
|
|
2266
|
+
{ id: "react-nextjs", layer: "ui", label: "React + Next.js", detected: reactNextjsParser.detect(projectRoot) },
|
|
2267
|
+
{ id: "nextjs-routes", layer: "api", label: "Next.js API Routes", detected: nextjsRoutesParser.detect(projectRoot) },
|
|
2268
|
+
{ id: "prisma-schema", layer: "db", label: "Prisma Schema", detected: prismaSchemaParser.detect(projectRoot) }
|
|
2269
|
+
];
|
|
2270
|
+
const crosslayerParsers = [
|
|
2271
|
+
{ id: "fetch-resolver", label: "Fetch / api.method() calls" },
|
|
2272
|
+
{ id: "api-annotations", label: "@api annotations" },
|
|
2273
|
+
{ id: "url-literal-scanner", label: "/api/... URL literals" }
|
|
2274
|
+
];
|
|
2275
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2276
|
+
res.end(JSON.stringify({ config, detection, crosslayerParsers }));
|
|
2277
|
+
return;
|
|
2278
|
+
}
|
|
2279
|
+
if (req.method === "POST" && url2.pathname === "/api/parser-config") {
|
|
2280
|
+
let body = "";
|
|
2281
|
+
req.on("data", (chunk) => {
|
|
2282
|
+
body += chunk.toString();
|
|
2283
|
+
});
|
|
2284
|
+
req.on("end", () => {
|
|
2285
|
+
try {
|
|
2286
|
+
const newConfig = JSON.parse(body);
|
|
2287
|
+
const configPath = import_node_path12.default.join(projectRoot, ".launchchart.json");
|
|
2288
|
+
import_node_fs11.default.writeFileSync(configPath, JSON.stringify(newConfig, null, 2) + "\n", "utf-8");
|
|
2289
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2290
|
+
res.end(JSON.stringify({ ok: true }));
|
|
2291
|
+
} catch (err2) {
|
|
2292
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
2293
|
+
res.end(JSON.stringify({ ok: false, error: String(err2) }));
|
|
2294
|
+
}
|
|
2295
|
+
});
|
|
2296
|
+
return;
|
|
1126
2297
|
}
|
|
2298
|
+
if (url2.pathname !== "/") {
|
|
2299
|
+
const staticPath = import_node_path12.default.join(clientDir, url2.pathname);
|
|
2300
|
+
if (serveStatic(res, staticPath)) return;
|
|
2301
|
+
}
|
|
2302
|
+
serveIndex(res, clientDir);
|
|
2303
|
+
} catch (err2) {
|
|
2304
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
2305
|
+
res.end(JSON.stringify({ error: String(err2) }));
|
|
1127
2306
|
}
|
|
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
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
module: { type: "string", description: "UI layer only \u2014 filter by module." },
|
|
1167
|
-
node_id: { type: "string", description: "Center node for neighborhood scope." },
|
|
1168
|
-
hops: { type: "number", description: "Neighborhood radius (default 1)." },
|
|
1169
|
-
case_insensitive: { type: "boolean", description: "Case-insensitive regex. Default false." },
|
|
1170
|
-
context: { type: "number", description: "Context lines around each match. Default 2." },
|
|
1171
|
-
max_matches: { type: "number", description: "Max matches to return total. Default 50." },
|
|
1172
|
-
max_files: { type: "number", description: "Max files to search. Default 50." }
|
|
1173
|
-
},
|
|
1174
|
-
required: ["layer", "pattern"]
|
|
2307
|
+
});
|
|
2308
|
+
const port = await bindWithFallback(server, opts.port ?? DEFAULT_PORT);
|
|
2309
|
+
const url = `http://localhost:${port}`;
|
|
2310
|
+
writeLock({
|
|
2311
|
+
pid: process.pid,
|
|
2312
|
+
port,
|
|
2313
|
+
cwd,
|
|
2314
|
+
url,
|
|
2315
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2316
|
+
});
|
|
2317
|
+
const cleanup = () => {
|
|
2318
|
+
clearLock();
|
|
2319
|
+
server.close();
|
|
2320
|
+
};
|
|
2321
|
+
process.once("SIGINT", () => {
|
|
2322
|
+
cleanup();
|
|
2323
|
+
process.exit(0);
|
|
2324
|
+
});
|
|
2325
|
+
process.once("SIGTERM", () => {
|
|
2326
|
+
cleanup();
|
|
2327
|
+
process.exit(0);
|
|
2328
|
+
});
|
|
2329
|
+
process.once("exit", cleanup);
|
|
2330
|
+
if (!opts.quiet) {
|
|
2331
|
+
process.stderr.write(`[launch-chart] serving ${url}
|
|
2332
|
+
`);
|
|
2333
|
+
process.stderr.write(`[launch-chart] project root: ${projectRoot}
|
|
2334
|
+
`);
|
|
2335
|
+
}
|
|
2336
|
+
return { port, url };
|
|
2337
|
+
}
|
|
2338
|
+
function runServeCli(argv) {
|
|
2339
|
+
let port;
|
|
2340
|
+
for (let i = 0; i < argv.length; i++) {
|
|
2341
|
+
if (argv[i] === "--port" && argv[i + 1]) {
|
|
2342
|
+
port = parseInt(argv[++i], 10);
|
|
2343
|
+
} else if (argv[i].startsWith("--port=")) {
|
|
2344
|
+
port = parseInt(argv[i].slice("--port=".length), 10);
|
|
1175
2345
|
}
|
|
1176
2346
|
}
|
|
1177
|
-
|
|
2347
|
+
startChartServer({ port }).catch((err2) => {
|
|
2348
|
+
process.stderr.write(`[launch-chart] failed to start: ${err2}
|
|
2349
|
+
`);
|
|
2350
|
+
process.exit(1);
|
|
2351
|
+
});
|
|
2352
|
+
}
|
|
2353
|
+
var import_node_http, import_node_fs11, import_node_path12, DEFAULT_PORT, MAX_PORT_SCAN, MIME_TYPES;
|
|
2354
|
+
var init_chart_serve = __esm({
|
|
2355
|
+
"src/server/chart-serve.ts"() {
|
|
2356
|
+
"use strict";
|
|
2357
|
+
import_node_http = __toESM(require("node:http"));
|
|
2358
|
+
import_node_fs11 = __toESM(require("node:fs"));
|
|
2359
|
+
import_node_path12 = __toESM(require("node:path"));
|
|
2360
|
+
init_graph();
|
|
2361
|
+
init_lockfile();
|
|
2362
|
+
init_config();
|
|
2363
|
+
init_react_nextjs();
|
|
2364
|
+
init_nextjs_routes();
|
|
2365
|
+
init_prisma_schema();
|
|
2366
|
+
DEFAULT_PORT = 52819;
|
|
2367
|
+
MAX_PORT_SCAN = 20;
|
|
2368
|
+
MIME_TYPES = {
|
|
2369
|
+
".html": "text/html; charset=utf-8",
|
|
2370
|
+
".js": "application/javascript; charset=utf-8",
|
|
2371
|
+
".css": "text/css; charset=utf-8",
|
|
2372
|
+
".json": "application/json; charset=utf-8",
|
|
2373
|
+
".png": "image/png",
|
|
2374
|
+
".svg": "image/svg+xml",
|
|
2375
|
+
".ico": "image/x-icon",
|
|
2376
|
+
".woff": "font/woff",
|
|
2377
|
+
".woff2": "font/woff2"
|
|
2378
|
+
};
|
|
2379
|
+
}
|
|
2380
|
+
});
|
|
2381
|
+
|
|
2382
|
+
// src/server/graph-mcp.ts
|
|
2383
|
+
var graph_mcp_exports = {};
|
|
2384
|
+
__export(graph_mcp_exports, {
|
|
2385
|
+
startGraphMcpServer: () => startGraphMcpServer
|
|
2386
|
+
});
|
|
1178
2387
|
function matchesSearch(node, query) {
|
|
1179
2388
|
const q = query.toLowerCase();
|
|
1180
2389
|
if (node.id.toLowerCase().includes(q)) return true;
|
|
@@ -1192,52 +2401,6 @@ function toMinimal(nodes) {
|
|
|
1192
2401
|
return out;
|
|
1193
2402
|
});
|
|
1194
2403
|
}
|
|
1195
|
-
var COMPACT_SCHEMA = {
|
|
1196
|
-
nodes: {
|
|
1197
|
-
i: "id",
|
|
1198
|
-
t: "type",
|
|
1199
|
-
n: "name",
|
|
1200
|
-
m: "module",
|
|
1201
|
-
r: "route",
|
|
1202
|
-
mt: "methods",
|
|
1203
|
-
x: "exports",
|
|
1204
|
-
c: "columns"
|
|
1205
|
-
},
|
|
1206
|
-
edges: {
|
|
1207
|
-
s: "source_node_index",
|
|
1208
|
-
d: "target_node_index",
|
|
1209
|
-
t: "type",
|
|
1210
|
-
l: "label"
|
|
1211
|
-
},
|
|
1212
|
-
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."
|
|
1213
|
-
};
|
|
1214
|
-
var COMPACT_NODE_KNOWN_KEYS = /* @__PURE__ */ new Set([
|
|
1215
|
-
"id",
|
|
1216
|
-
"type",
|
|
1217
|
-
"name",
|
|
1218
|
-
"module",
|
|
1219
|
-
"route",
|
|
1220
|
-
"methods",
|
|
1221
|
-
"exports",
|
|
1222
|
-
"columns"
|
|
1223
|
-
]);
|
|
1224
|
-
var EST_CHARS_PER_NODE_FULL = {
|
|
1225
|
-
ui: 300,
|
|
1226
|
-
api: 300,
|
|
1227
|
-
db: 3500
|
|
1228
|
-
};
|
|
1229
|
-
var EST_CHARS_PER_NODE_MIN = {
|
|
1230
|
-
ui: 150,
|
|
1231
|
-
api: 200,
|
|
1232
|
-
db: 120
|
|
1233
|
-
};
|
|
1234
|
-
var EST_CHARS_PER_EDGE = {
|
|
1235
|
-
ui: 65,
|
|
1236
|
-
api: 65,
|
|
1237
|
-
db: 65
|
|
1238
|
-
};
|
|
1239
|
-
var NEIGHBORHOOD_BUDGET_CHARS = 55e3;
|
|
1240
|
-
var BATCH_BUDGET_CHARS = 6e4;
|
|
1241
2404
|
function toCompactNode(n) {
|
|
1242
2405
|
const out = { i: n.id, t: n.type, n: n.name };
|
|
1243
2406
|
if (n.module != null) out.m = n.module;
|
|
@@ -1525,9 +2688,9 @@ function handleReadGraph(args) {
|
|
|
1525
2688
|
return okJson(result);
|
|
1526
2689
|
}
|
|
1527
2690
|
function nodeToFilePath(rootDir, layer, nodeId) {
|
|
1528
|
-
if (layer === "ui") return (0,
|
|
1529
|
-
if (layer === "api") return (0,
|
|
1530
|
-
if (layer === "db") return (0,
|
|
2691
|
+
if (layer === "ui") return (0, import_node_path13.join)(rootDir, "src", nodeId);
|
|
2692
|
+
if (layer === "api") return (0, import_node_path13.join)(rootDir, nodeId);
|
|
2693
|
+
if (layer === "db") return (0, import_node_path13.join)(rootDir, "prisma", "schema.prisma");
|
|
1531
2694
|
return null;
|
|
1532
2695
|
}
|
|
1533
2696
|
function handleGrepNodes(args) {
|
|
@@ -1587,11 +2750,11 @@ function handleGrepNodes(args) {
|
|
|
1587
2750
|
let filesSearched = 0;
|
|
1588
2751
|
let truncated = false;
|
|
1589
2752
|
for (const [filePath, nodeId] of filePaths) {
|
|
1590
|
-
if (!(0,
|
|
2753
|
+
if (!(0, import_node_fs12.existsSync)(filePath)) continue;
|
|
1591
2754
|
filesSearched++;
|
|
1592
2755
|
let content;
|
|
1593
2756
|
try {
|
|
1594
|
-
content = (0,
|
|
2757
|
+
content = (0, import_node_fs12.readFileSync)(filePath, "utf-8");
|
|
1595
2758
|
} catch {
|
|
1596
2759
|
continue;
|
|
1597
2760
|
}
|
|
@@ -1628,6 +2791,127 @@ function handleGrepNodes(args) {
|
|
|
1628
2791
|
truncated
|
|
1629
2792
|
});
|
|
1630
2793
|
}
|
|
2794
|
+
function handleChartServerStatus() {
|
|
2795
|
+
const lock = getLiveLock();
|
|
2796
|
+
if (!lock) {
|
|
2797
|
+
return okJson({ running: false });
|
|
2798
|
+
}
|
|
2799
|
+
return okJson({
|
|
2800
|
+
running: true,
|
|
2801
|
+
url: lock.url,
|
|
2802
|
+
port: lock.port,
|
|
2803
|
+
pid: lock.pid,
|
|
2804
|
+
cwd: lock.cwd,
|
|
2805
|
+
startedAt: lock.startedAt
|
|
2806
|
+
});
|
|
2807
|
+
}
|
|
2808
|
+
function handleStartChartServer(args) {
|
|
2809
|
+
const lock = getLiveLock();
|
|
2810
|
+
if (lock) {
|
|
2811
|
+
return okJson({
|
|
2812
|
+
started: false,
|
|
2813
|
+
reason: "already_running",
|
|
2814
|
+
url: lock.url,
|
|
2815
|
+
port: lock.port,
|
|
2816
|
+
pid: lock.pid
|
|
2817
|
+
});
|
|
2818
|
+
}
|
|
2819
|
+
const entryPath = process.argv[1];
|
|
2820
|
+
const logDir = (0, import_node_path13.join)((0, import_node_os2.homedir)(), ".launchsecure");
|
|
2821
|
+
(0, import_node_fs12.mkdirSync)(logDir, { recursive: true });
|
|
2822
|
+
const logPath = (0, import_node_path13.join)(logDir, "launch-chart.log");
|
|
2823
|
+
const out = (0, import_node_fs12.openSync)(logPath, "a");
|
|
2824
|
+
const err2 = (0, import_node_fs12.openSync)(logPath, "a");
|
|
2825
|
+
const portArgs = args.port ? ["--port", String(args.port)] : [];
|
|
2826
|
+
const child = (0, import_node_child_process2.spawn)(process.execPath, [entryPath, "serve", ...portArgs], {
|
|
2827
|
+
detached: true,
|
|
2828
|
+
stdio: ["ignore", out, err2],
|
|
2829
|
+
env: { ...process.env, LAUNCH_CHART_AUTOSERVE: "" }
|
|
2830
|
+
});
|
|
2831
|
+
child.unref();
|
|
2832
|
+
return okJson({
|
|
2833
|
+
started: true,
|
|
2834
|
+
pid: child.pid,
|
|
2835
|
+
logPath
|
|
2836
|
+
});
|
|
2837
|
+
}
|
|
2838
|
+
function handleStopChartServer() {
|
|
2839
|
+
const lock = getLiveLock();
|
|
2840
|
+
if (!lock) {
|
|
2841
|
+
return okJson({ stopped: false, reason: "not_running" });
|
|
2842
|
+
}
|
|
2843
|
+
try {
|
|
2844
|
+
process.kill(lock.pid, "SIGTERM");
|
|
2845
|
+
return okJson({ stopped: true, pid: lock.pid });
|
|
2846
|
+
} catch (e) {
|
|
2847
|
+
const code = e.code;
|
|
2848
|
+
if (code === "ESRCH") {
|
|
2849
|
+
clearLock();
|
|
2850
|
+
return okJson({ stopped: true, pid: lock.pid, note: "process was already gone, lock cleaned up" });
|
|
2851
|
+
}
|
|
2852
|
+
return okJson({ stopped: false, reason: `kill failed: ${code ?? e}` });
|
|
2853
|
+
}
|
|
2854
|
+
}
|
|
2855
|
+
function handleDetectProjectStack() {
|
|
2856
|
+
const rootDir = findProjectRoot(process.cwd());
|
|
2857
|
+
const parsers = [
|
|
2858
|
+
{ id: "react-nextjs", layer: "ui", detected: reactNextjsParser.detect(rootDir) },
|
|
2859
|
+
{ id: "nextjs-routes", layer: "api", detected: nextjsRoutesParser.detect(rootDir) },
|
|
2860
|
+
{ id: "prisma-schema", layer: "db", detected: prismaSchemaParser.detect(rootDir) }
|
|
2861
|
+
];
|
|
2862
|
+
const config = loadConfig(rootDir);
|
|
2863
|
+
let stats = { calls_api: 0, references_api: 0, out_of_pattern: 0, annotations: 0 };
|
|
2864
|
+
const uiGraph = readGraph(rootDir, "ui");
|
|
2865
|
+
if (uiGraph) {
|
|
2866
|
+
for (const ref of uiGraph.cross_refs ?? []) {
|
|
2867
|
+
if (ref.type === "calls_api") stats.calls_api++;
|
|
2868
|
+
if (ref.type === "references_api") stats.references_api++;
|
|
2869
|
+
}
|
|
2870
|
+
for (const f of uiGraph.flagged_edges ?? []) {
|
|
2871
|
+
if (f.type === "out_of_pattern") stats.out_of_pattern++;
|
|
2872
|
+
}
|
|
2873
|
+
}
|
|
2874
|
+
const srcDir = (0, import_node_path13.join)(rootDir, "src");
|
|
2875
|
+
if ((0, import_node_fs12.existsSync)(srcDir)) {
|
|
2876
|
+
const scanDir = (dir) => {
|
|
2877
|
+
if (!(0, import_node_fs12.existsSync)(dir)) return;
|
|
2878
|
+
for (const entry of (0, import_node_fs12.readdirSync)(dir, { withFileTypes: true })) {
|
|
2879
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
2880
|
+
const full = (0, import_node_path13.join)(dir, entry.name);
|
|
2881
|
+
if (entry.isDirectory()) {
|
|
2882
|
+
scanDir(full);
|
|
2883
|
+
continue;
|
|
2884
|
+
}
|
|
2885
|
+
if (![".ts", ".tsx"].includes((0, import_node_path13.extname)(entry.name))) continue;
|
|
2886
|
+
try {
|
|
2887
|
+
const content = (0, import_node_fs12.readFileSync)(full, "utf-8");
|
|
2888
|
+
const matches = content.match(/@api\s+(GET|POST|PUT|DELETE|PATCH)\s+\/\S+/g);
|
|
2889
|
+
if (matches) stats.annotations += matches.length;
|
|
2890
|
+
} catch {
|
|
2891
|
+
}
|
|
2892
|
+
}
|
|
2893
|
+
};
|
|
2894
|
+
scanDir(srcDir);
|
|
2895
|
+
}
|
|
2896
|
+
let recommendedPrimary = "fetch-resolver";
|
|
2897
|
+
if (stats.annotations > 0 && stats.annotations >= stats.calls_api) {
|
|
2898
|
+
recommendedPrimary = "api-annotations";
|
|
2899
|
+
} else if (stats.calls_api === 0 && stats.references_api > 0) {
|
|
2900
|
+
recommendedPrimary = "url-literal-scanner";
|
|
2901
|
+
}
|
|
2902
|
+
return okJson({
|
|
2903
|
+
parsers,
|
|
2904
|
+
crosslayer_parsers: [
|
|
2905
|
+
{ id: "fetch-resolver", description: "Detects direct fetch()/api.get() calls with inline URLs" },
|
|
2906
|
+
{ id: "api-annotations", description: "Scans for @api METHOD /path annotations in JSDoc/comments" },
|
|
2907
|
+
{ id: "url-literal-scanner", description: "Finds /api/... string literals as fallback detection" }
|
|
2908
|
+
],
|
|
2909
|
+
stats,
|
|
2910
|
+
recommended_primary: recommendedPrimary,
|
|
2911
|
+
current_config: Object.keys(config).length > 0 ? config : null,
|
|
2912
|
+
config_path: ".launchchart.json"
|
|
2913
|
+
});
|
|
2914
|
+
}
|
|
1631
2915
|
function send(msg) {
|
|
1632
2916
|
process.stdout.write(JSON.stringify(msg) + "\n");
|
|
1633
2917
|
}
|
|
@@ -1671,6 +2955,22 @@ function handleMessage(msg) {
|
|
|
1671
2955
|
respond(id ?? null, handleGrepNodes(args));
|
|
1672
2956
|
return;
|
|
1673
2957
|
}
|
|
2958
|
+
if (toolName === "chart_server_status") {
|
|
2959
|
+
respond(id ?? null, handleChartServerStatus());
|
|
2960
|
+
return;
|
|
2961
|
+
}
|
|
2962
|
+
if (toolName === "start_chart_server") {
|
|
2963
|
+
respond(id ?? null, handleStartChartServer(args));
|
|
2964
|
+
return;
|
|
2965
|
+
}
|
|
2966
|
+
if (toolName === "stop_chart_server") {
|
|
2967
|
+
respond(id ?? null, handleStopChartServer());
|
|
2968
|
+
return;
|
|
2969
|
+
}
|
|
2970
|
+
if (toolName === "detect_project_stack") {
|
|
2971
|
+
respond(id ?? null, handleDetectProjectStack());
|
|
2972
|
+
return;
|
|
2973
|
+
}
|
|
1674
2974
|
respondError(id ?? null, -32601, `Unknown tool: ${toolName}`);
|
|
1675
2975
|
return;
|
|
1676
2976
|
}
|
|
@@ -1706,6 +3006,285 @@ function startGraphMcpServer() {
|
|
|
1706
3006
|
process.stderr.write(`[launchsecure-graph] MCP server started (cwd: ${process.cwd()})
|
|
1707
3007
|
`);
|
|
1708
3008
|
}
|
|
3009
|
+
var import_node_fs12, import_node_path13, import_node_child_process2, import_node_os2, 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;
|
|
3010
|
+
var init_graph_mcp = __esm({
|
|
3011
|
+
"src/server/graph-mcp.ts"() {
|
|
3012
|
+
"use strict";
|
|
3013
|
+
import_node_fs12 = require("node:fs");
|
|
3014
|
+
import_node_path13 = require("node:path");
|
|
3015
|
+
import_node_child_process2 = require("node:child_process");
|
|
3016
|
+
import_node_os2 = require("node:os");
|
|
3017
|
+
init_graph();
|
|
3018
|
+
init_lockfile();
|
|
3019
|
+
init_config();
|
|
3020
|
+
init_react_nextjs();
|
|
3021
|
+
init_nextjs_routes();
|
|
3022
|
+
init_prisma_schema();
|
|
3023
|
+
SERVER_INFO = {
|
|
3024
|
+
name: "launchsecure-graph",
|
|
3025
|
+
version: "0.0.1"
|
|
3026
|
+
};
|
|
3027
|
+
TOOLS = [
|
|
3028
|
+
{
|
|
3029
|
+
name: "generate_graph",
|
|
3030
|
+
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.",
|
|
3031
|
+
inputSchema: {
|
|
3032
|
+
type: "object",
|
|
3033
|
+
properties: {
|
|
3034
|
+
layer: {
|
|
3035
|
+
type: "string",
|
|
3036
|
+
enum: ["ui", "api", "db"],
|
|
3037
|
+
description: "Specific layer to regenerate. Omit to regenerate all detectable layers."
|
|
3038
|
+
}
|
|
3039
|
+
}
|
|
3040
|
+
}
|
|
3041
|
+
},
|
|
3042
|
+
{
|
|
3043
|
+
name: "read_graph",
|
|
3044
|
+
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.',
|
|
3045
|
+
inputSchema: {
|
|
3046
|
+
type: "object",
|
|
3047
|
+
properties: {
|
|
3048
|
+
layer: {
|
|
3049
|
+
type: "string",
|
|
3050
|
+
enum: ["ui", "api", "db"],
|
|
3051
|
+
description: "Graph layer to query: ui, api, or db. Required if any filter is provided."
|
|
3052
|
+
},
|
|
3053
|
+
search: {
|
|
3054
|
+
type: "string",
|
|
3055
|
+
description: "Case-insensitive substring match against node id, name, or route."
|
|
3056
|
+
},
|
|
3057
|
+
type: {
|
|
3058
|
+
type: "string",
|
|
3059
|
+
description: 'Filter by node type (e.g. "page", "hook", "component", "endpoint", "table").'
|
|
3060
|
+
},
|
|
3061
|
+
module: {
|
|
3062
|
+
type: "string",
|
|
3063
|
+
description: 'UI layer only \u2014 filter by module (e.g. "auth", "admin", "project", "org").'
|
|
3064
|
+
},
|
|
3065
|
+
node_id: {
|
|
3066
|
+
type: "string",
|
|
3067
|
+
description: "Center node for a neighborhood query. Returns the node + all nodes reachable within `hops` edges."
|
|
3068
|
+
},
|
|
3069
|
+
hops: {
|
|
3070
|
+
type: "number",
|
|
3071
|
+
description: "Neighborhood radius for node_id queries. Default 1 (direct neighbors only)."
|
|
3072
|
+
},
|
|
3073
|
+
minimal: {
|
|
3074
|
+
type: "boolean",
|
|
3075
|
+
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."
|
|
3076
|
+
},
|
|
3077
|
+
include_edges: {
|
|
3078
|
+
type: "boolean",
|
|
3079
|
+
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."
|
|
3080
|
+
},
|
|
3081
|
+
queries: {
|
|
3082
|
+
type: "array",
|
|
3083
|
+
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.",
|
|
3084
|
+
items: {
|
|
3085
|
+
type: "object",
|
|
3086
|
+
properties: {
|
|
3087
|
+
layer: { type: "string", enum: ["ui", "api", "db"] },
|
|
3088
|
+
search: { type: "string" },
|
|
3089
|
+
type: { type: "string" },
|
|
3090
|
+
module: { type: "string" },
|
|
3091
|
+
node_id: { type: "string" },
|
|
3092
|
+
hops: { type: "number" },
|
|
3093
|
+
minimal: { type: "boolean" },
|
|
3094
|
+
include_edges: { type: "boolean" }
|
|
3095
|
+
}
|
|
3096
|
+
}
|
|
3097
|
+
}
|
|
3098
|
+
}
|
|
3099
|
+
}
|
|
3100
|
+
},
|
|
3101
|
+
{
|
|
3102
|
+
name: "grep_nodes",
|
|
3103
|
+
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.
|
|
3104
|
+
|
|
3105
|
+
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.
|
|
3106
|
+
|
|
3107
|
+
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.
|
|
3108
|
+
|
|
3109
|
+
FILTER PARAMS (same semantics as read_graph):
|
|
3110
|
+
- layer: ui, api, or db
|
|
3111
|
+
- search: substring match on node id/name/route
|
|
3112
|
+
- type: node type filter
|
|
3113
|
+
- module: ui-layer module filter
|
|
3114
|
+
- node_id + hops: neighborhood scope
|
|
3115
|
+
|
|
3116
|
+
CONTENT PARAMS:
|
|
3117
|
+
- pattern: regex to search for (required)
|
|
3118
|
+
- case_insensitive: default false
|
|
3119
|
+
- context: lines of context around each match (default 2)
|
|
3120
|
+
- max_matches: cap on total matches returned (default 50)
|
|
3121
|
+
- max_files: cap on files searched (default 50, errors if filter returns more)
|
|
3122
|
+
|
|
3123
|
+
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.`,
|
|
3124
|
+
inputSchema: {
|
|
3125
|
+
type: "object",
|
|
3126
|
+
properties: {
|
|
3127
|
+
layer: {
|
|
3128
|
+
type: "string",
|
|
3129
|
+
enum: ["ui", "api", "db"],
|
|
3130
|
+
description: "Graph layer to scope files (required)."
|
|
3131
|
+
},
|
|
3132
|
+
pattern: {
|
|
3133
|
+
type: "string",
|
|
3134
|
+
description: "Regex pattern to search for (required)."
|
|
3135
|
+
},
|
|
3136
|
+
search: { type: "string", description: "Substring match on node id/name/route." },
|
|
3137
|
+
type: { type: "string", description: "Filter by node type." },
|
|
3138
|
+
module: { type: "string", description: "UI layer only \u2014 filter by module." },
|
|
3139
|
+
node_id: { type: "string", description: "Center node for neighborhood scope." },
|
|
3140
|
+
hops: { type: "number", description: "Neighborhood radius (default 1)." },
|
|
3141
|
+
case_insensitive: { type: "boolean", description: "Case-insensitive regex. Default false." },
|
|
3142
|
+
context: { type: "number", description: "Context lines around each match. Default 2." },
|
|
3143
|
+
max_matches: { type: "number", description: "Max matches to return total. Default 50." },
|
|
3144
|
+
max_files: { type: "number", description: "Max files to search. Default 50." }
|
|
3145
|
+
},
|
|
3146
|
+
required: ["layer", "pattern"]
|
|
3147
|
+
}
|
|
3148
|
+
},
|
|
3149
|
+
{
|
|
3150
|
+
name: "chart_server_status",
|
|
3151
|
+
description: `Check whether the launch-chart UI server is running. Returns: { running: boolean, url?: string, port?: number, pid?: number, startedAt?: string, cwd?: string }.
|
|
3152
|
+
|
|
3153
|
+
Use this when the user asks "is the chart running", "show me the project graph UI", "where's the chart", etc.`,
|
|
3154
|
+
inputSchema: {
|
|
3155
|
+
type: "object",
|
|
3156
|
+
properties: {}
|
|
3157
|
+
}
|
|
3158
|
+
},
|
|
3159
|
+
{
|
|
3160
|
+
name: "start_chart_server",
|
|
3161
|
+
description: 'Start the launch-chart UI server as a detached background process. The server serves the interactive project graph visualization at http://localhost:<port>. If the server is already running, returns the existing URL without spawning a duplicate. \n\nUse this when the user asks to "start the chart", "fire up charts", "open the graph UI", etc.',
|
|
3162
|
+
inputSchema: {
|
|
3163
|
+
type: "object",
|
|
3164
|
+
properties: {
|
|
3165
|
+
port: {
|
|
3166
|
+
type: "number",
|
|
3167
|
+
description: "Port to bind the server to. Defaults to 52819 with automatic fallback if in use."
|
|
3168
|
+
}
|
|
3169
|
+
}
|
|
3170
|
+
}
|
|
3171
|
+
},
|
|
3172
|
+
{
|
|
3173
|
+
name: "stop_chart_server",
|
|
3174
|
+
description: 'Stop the running launch-chart UI server. Sends SIGTERM to the server process and cleans up the lock file. If no server is running, returns a no-op response. \n\nUse this when the user asks to "stop the chart", "cool down charts", "kill the graph server", etc.',
|
|
3175
|
+
inputSchema: {
|
|
3176
|
+
type: "object",
|
|
3177
|
+
properties: {}
|
|
3178
|
+
}
|
|
3179
|
+
},
|
|
3180
|
+
{
|
|
3181
|
+
name: "detect_project_stack",
|
|
3182
|
+
description: "Detect project frameworks, available parsers, and recommend parser configuration. Scans the project to identify the tech stack (Next.js, Prisma, React, etc.), reports which built-in parsers are applicable, and provides cross-layer detection stats (fetch calls, @api annotations, URL literals). Returns a recommended primary parser and current .launchchart.json config if present. \n\nUse this when setting up launch-chart for a new project or reviewing parser configuration.",
|
|
3183
|
+
inputSchema: {
|
|
3184
|
+
type: "object",
|
|
3185
|
+
properties: {}
|
|
3186
|
+
}
|
|
3187
|
+
}
|
|
3188
|
+
];
|
|
3189
|
+
COMPACT_SCHEMA = {
|
|
3190
|
+
nodes: {
|
|
3191
|
+
i: "id",
|
|
3192
|
+
t: "type",
|
|
3193
|
+
n: "name",
|
|
3194
|
+
m: "module",
|
|
3195
|
+
r: "route",
|
|
3196
|
+
mt: "methods",
|
|
3197
|
+
x: "exports",
|
|
3198
|
+
c: "columns"
|
|
3199
|
+
},
|
|
3200
|
+
edges: {
|
|
3201
|
+
s: "source_node_index",
|
|
3202
|
+
d: "target_node_index",
|
|
3203
|
+
t: "type",
|
|
3204
|
+
l: "label"
|
|
3205
|
+
},
|
|
3206
|
+
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."
|
|
3207
|
+
};
|
|
3208
|
+
COMPACT_NODE_KNOWN_KEYS = /* @__PURE__ */ new Set([
|
|
3209
|
+
"id",
|
|
3210
|
+
"type",
|
|
3211
|
+
"name",
|
|
3212
|
+
"module",
|
|
3213
|
+
"route",
|
|
3214
|
+
"methods",
|
|
3215
|
+
"exports",
|
|
3216
|
+
"columns"
|
|
3217
|
+
]);
|
|
3218
|
+
EST_CHARS_PER_NODE_FULL = {
|
|
3219
|
+
ui: 300,
|
|
3220
|
+
api: 300,
|
|
3221
|
+
db: 3500
|
|
3222
|
+
};
|
|
3223
|
+
EST_CHARS_PER_NODE_MIN = {
|
|
3224
|
+
ui: 150,
|
|
3225
|
+
api: 200,
|
|
3226
|
+
db: 120
|
|
3227
|
+
};
|
|
3228
|
+
EST_CHARS_PER_EDGE = {
|
|
3229
|
+
ui: 65,
|
|
3230
|
+
api: 65,
|
|
3231
|
+
db: 65
|
|
3232
|
+
};
|
|
3233
|
+
NEIGHBORHOOD_BUDGET_CHARS = 55e3;
|
|
3234
|
+
BATCH_BUDGET_CHARS = 6e4;
|
|
3235
|
+
}
|
|
3236
|
+
});
|
|
1709
3237
|
|
|
1710
3238
|
// src/server/graph-mcp-entry.ts
|
|
1711
|
-
|
|
3239
|
+
var import_node_child_process3 = require("node:child_process");
|
|
3240
|
+
var import_node_fs13 = require("node:fs");
|
|
3241
|
+
var import_node_path14 = __toESM(require("node:path"));
|
|
3242
|
+
var import_node_os3 = require("node:os");
|
|
3243
|
+
var import_node_fs14 = require("node:fs");
|
|
3244
|
+
init_lockfile();
|
|
3245
|
+
function logStderr(msg) {
|
|
3246
|
+
process.stderr.write(`[launch-chart] ${msg}
|
|
3247
|
+
`);
|
|
3248
|
+
}
|
|
3249
|
+
function maybeAutoServe() {
|
|
3250
|
+
if (process.env.LAUNCH_CHART_AUTOSERVE !== "1") return;
|
|
3251
|
+
const existing = getLiveLock();
|
|
3252
|
+
if (existing) {
|
|
3253
|
+
logStderr(`autoserve: reusing existing server at ${existing.url}`);
|
|
3254
|
+
return;
|
|
3255
|
+
}
|
|
3256
|
+
try {
|
|
3257
|
+
const logDir = import_node_path14.default.join((0, import_node_os3.homedir)(), ".launchsecure");
|
|
3258
|
+
(0, import_node_fs14.mkdirSync)(logDir, { recursive: true });
|
|
3259
|
+
const logPath = import_node_path14.default.join(logDir, "launch-chart.log");
|
|
3260
|
+
const out = (0, import_node_fs13.openSync)(logPath, "a");
|
|
3261
|
+
const err2 = (0, import_node_fs13.openSync)(logPath, "a");
|
|
3262
|
+
const entryPath = process.argv[1];
|
|
3263
|
+
const child = (0, import_node_child_process3.spawn)(process.execPath, [entryPath, "serve"], {
|
|
3264
|
+
detached: true,
|
|
3265
|
+
stdio: ["ignore", out, err2],
|
|
3266
|
+
env: { ...process.env, LAUNCH_CHART_AUTOSERVE: "" }
|
|
3267
|
+
});
|
|
3268
|
+
child.unref();
|
|
3269
|
+
logStderr(`autoserve: spawned detached serve process (pid ${child.pid}, log: ${logPath})`);
|
|
3270
|
+
} catch (err2) {
|
|
3271
|
+
logStderr(`autoserve: failed to spawn \u2014 ${err2}`);
|
|
3272
|
+
}
|
|
3273
|
+
}
|
|
3274
|
+
async function main() {
|
|
3275
|
+
const argv = process.argv.slice(2);
|
|
3276
|
+
const subcommand = argv[0];
|
|
3277
|
+
if (subcommand === "serve") {
|
|
3278
|
+
const { runServeCli: runServeCli2 } = await Promise.resolve().then(() => (init_chart_serve(), chart_serve_exports));
|
|
3279
|
+
runServeCli2(argv.slice(1));
|
|
3280
|
+
return;
|
|
3281
|
+
}
|
|
3282
|
+
maybeAutoServe();
|
|
3283
|
+
const { startGraphMcpServer: startGraphMcpServer2 } = await Promise.resolve().then(() => (init_graph_mcp(), graph_mcp_exports));
|
|
3284
|
+
startGraphMcpServer2();
|
|
3285
|
+
}
|
|
3286
|
+
main().catch((err2) => {
|
|
3287
|
+
process.stderr.write(`[launch-chart] fatal: ${err2}
|
|
3288
|
+
`);
|
|
3289
|
+
process.exit(1);
|
|
3290
|
+
});
|