@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.
@@ -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/graph-mcp.ts
5
- var import_node_fs6 = require("node:fs");
6
- var import_node_path6 = require("node:path");
7
-
8
- // src/server/graph/index.ts
9
- var import_node_fs5 = require("node:fs");
10
- var import_node_path5 = require("node:path");
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/parsers/ui/react-nextjs.ts
13
- var import_node_fs2 = require("node:fs");
14
- var import_node_path2 = require("node:path");
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, import_node_fs.readFileSync)(absPath, "utf-8");
29
- const ext = (0, import_node_path.extname)(absPath);
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, import_node_fs.readFileSync)(absPath, "utf-8");
233
- const ext = (0, import_node_path.extname)(absPath);
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, import_node_fs.readFileSync)(absPath, "utf-8");
263
- const ext = (0, import_node_path.extname)(absPath);
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, import_node_fs2.existsSync)(dir)) return results;
285
- for (const entry of (0, import_node_fs2.readdirSync)(dir, { withFileTypes: true })) {
286
- const full = (0, import_node_path2.join)(dir, entry.name);
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, import_node_path2.extname)(entry.name))) {
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, import_node_path2.relative)(srcDir, absPath).replace(/\\/g, "/");
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, import_node_path2.join)(srcDir, rel);
302
- for (const c of [base, base + ".ts", base + ".tsx", (0, import_node_path2.join)(base, "index.ts"), (0, import_node_path2.join)(base, "index.tsx")]) {
303
- if ((0, import_node_fs2.existsSync)(c) && (0, import_node_fs2.statSync)(c).isFile()) return c;
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, import_node_path2.join)((0, import_node_path2.dirname)(fromFile), specifier);
309
- for (const c of [base, base + ".ts", base + ".tsx", (0, import_node_path2.join)(base, "index.ts"), (0, import_node_path2.join)(base, "index.tsx")]) {
310
- if ((0, import_node_fs2.existsSync)(c) && (0, import_node_fs2.statSync)(c).isFile()) return c;
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, import_node_path2.basename)(absPath);
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 barrelId = (0, import_node_path2.relative)(srcDir, (0, import_node_path2.dirname)(absPath)).replace(/\\/g, "/");
321
- const map = /* @__PURE__ */ new Map();
322
- for (const re of parsed.reExports) {
323
- if (!re.from.startsWith(".")) continue;
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, import_node_path2.basename)(absPath, (0, import_node_path2.extname)(absPath)).replace(/[-_](\w)/g, (_, c) => c.toUpperCase()).replace(/^(\w)/, (_, c) => c.toUpperCase());
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, import_node_fs2.existsSync)((0, import_node_path2.join)(rootDir, "src", "app")) && (0, import_node_fs2.existsSync)((0, import_node_path2.join)(rootDir, "next.config.ts")) || (0, import_node_fs2.existsSync)((0, import_node_path2.join)(rootDir, "next.config.js")) || (0, import_node_fs2.existsSync)((0, import_node_path2.join)(rootDir, "next.config.mjs"));
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, import_node_path2.join)(rootDir, "src");
568
- const appFiles = walk((0, import_node_path2.join)(srcDir, "app"), [".tsx", ".ts"]).filter(
569
- (f) => f.endsWith("/page.tsx") || f.endsWith("/layout.tsx")
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, import_node_path2.join)(srcDir, "client"), [".tsx", ".ts"]);
572
- const serverFiles = walk((0, import_node_path2.join)(srcDir, "server"), [".ts", ".tsx"]).filter(
573
- (f) => (0, import_node_path2.basename)(f) !== "route.ts" && (0, import_node_path2.basename)(f) !== "route.tsx"
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, import_node_path2.join)(srcDir, "lib"), [".ts", ".tsx"]);
576
- const configFiles = walk((0, import_node_path2.join)(srcDir, "config"), [".ts", ".tsx"]);
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, import_node_path2.basename)(f).startsWith("index."));
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 reactNextjsParser = {
680
- id: "react-nextjs",
681
- layer: "ui",
682
- detect,
683
- generate
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, import_node_fs3.existsSync)(dir)) return results;
693
- for (const entry of (0, import_node_fs3.readdirSync)(dir, { withFileTypes: true })) {
694
- const full = (0, import_node_path3.join)(dir, entry.name);
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, import_node_path3.relative)(apiDir, absPath).replace(/\\/g, "/").replace(/\/route\.tsx?$/, "");
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, import_node_fs3.existsSync)((0, import_node_path3.join)(rootDir, "src", "app", "api"));
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, import_node_path3.join)(rootDir, "src", "app", "api");
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 (HTTP_METHODS.has(exp)) methods.push(exp);
1073
+ if (HTTP_METHODS2.has(exp)) methods.push(exp);
734
1074
  }
735
1075
  const routePath = filePathToRoute(apiDir, absPath);
736
- const relPath = (0, import_node_path3.relative)(rootDir, absPath).replace(/\\/g, "/");
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: [...HTTP_METHODS].reduce((acc, m) => {
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 nextjsRoutesParser = {
814
- id: "nextjs-routes",
815
- layer: "api",
816
- detect: detect2,
817
- generate: generate2
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, import_node_fs4.existsSync)((0, import_node_path4.join)(rootDir, "prisma", "schema.prisma"));
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, import_node_path4.join)(rootDir, "prisma", "schema.prisma");
917
- const content = (0, import_node_fs4.readFileSync)(schemaPath, "utf-8");
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 prismaSchemaParser = {
967
- id: "prisma-schema",
968
- layer: "db",
969
- detect: detect3,
970
- generate: generate3
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
- var ALL_PARSERS = [
975
- reactNextjsParser,
976
- nextjsRoutesParser,
977
- prismaSchemaParser
978
- ];
979
- function getParser(layer) {
980
- return ALL_PARSERS.find((p) => p.layer === layer);
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 parser = getParser(layer);
984
- if (!parser) return null;
985
- if (!parser.detect(rootDir)) return null;
986
- const output = parser.generate(rootDir);
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: output.nodes.length,
991
- edgeCount: output.edges.length
1977
+ output: merged,
1978
+ nodeCount: merged.nodes.length,
1979
+ edgeCount: merged.edges.length
992
1980
  };
993
1981
  }
994
1982
  function generateAll(rootDir) {
995
- const layers = ["ui", "api", "db"];
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 layers) {
998
- const result = generateLayer(rootDir, layer);
999
- if (result) results.push(result);
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
- return results;
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, import_node_path5.join)(rootDir, GRAPHS_DIR);
2036
+ return (0, import_node_path11.join)(rootDir, GRAPHS_DIR);
1010
2037
  }
1011
2038
  function graphFilePath(rootDir, layer) {
1012
- return (0, import_node_path5.join)(graphsDir(rootDir), `${layer}.json`);
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, import_node_fs5.existsSync)(filePath)) return null;
1020
- const stat = (0, import_node_fs5.statSync)(filePath);
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, import_node_fs5.readFileSync)(filePath, "utf-8");
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, import_node_fs5.mkdirSync)(dir, { recursive: true });
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, import_node_fs5.writeFileSync)(filePath, JSON.stringify(result.output, null, 2) + "\n", "utf-8");
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/graph-mcp.ts
1051
- var SERVER_INFO = {
1052
- name: "launchsecure-graph",
1053
- version: "0.0.1"
1054
- };
1055
- var TOOLS = [
1056
- {
1057
- name: "generate_graph",
1058
- 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.",
1059
- inputSchema: {
1060
- type: "object",
1061
- properties: {
1062
- layer: {
1063
- type: "string",
1064
- enum: ["ui", "api", "db"],
1065
- description: "Specific layer to regenerate. Omit to regenerate all detectable layers."
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
- name: "read_graph",
1072
- 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.',
1073
- inputSchema: {
1074
- type: "object",
1075
- properties: {
1076
- layer: {
1077
- type: "string",
1078
- enum: ["ui", "api", "db"],
1079
- description: "Graph layer to query: ui, api, or db. Required if any filter is provided."
1080
- },
1081
- search: {
1082
- type: "string",
1083
- description: "Case-insensitive substring match against node id, name, or route."
1084
- },
1085
- type: {
1086
- type: "string",
1087
- description: 'Filter by node type (e.g. "page", "hook", "component", "endpoint", "table").'
1088
- },
1089
- module: {
1090
- type: "string",
1091
- description: 'UI layer only \u2014 filter by module (e.g. "auth", "admin", "project", "org").'
1092
- },
1093
- node_id: {
1094
- type: "string",
1095
- description: "Center node for a neighborhood query. Returns the node + all nodes reachable within `hops` edges."
1096
- },
1097
- hops: {
1098
- type: "number",
1099
- description: "Neighborhood radius for node_id queries. Default 1 (direct neighbors only)."
1100
- },
1101
- minimal: {
1102
- type: "boolean",
1103
- 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."
1104
- },
1105
- include_edges: {
1106
- type: "boolean",
1107
- 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."
1108
- },
1109
- queries: {
1110
- type: "array",
1111
- 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.",
1112
- items: {
1113
- type: "object",
1114
- properties: {
1115
- layer: { type: "string", enum: ["ui", "api", "db"] },
1116
- search: { type: "string" },
1117
- type: { type: "string" },
1118
- module: { type: "string" },
1119
- node_id: { type: "string" },
1120
- hops: { type: "number" },
1121
- minimal: { type: "boolean" },
1122
- include_edges: { type: "boolean" }
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
- name: "grep_nodes",
1131
- 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.
1132
-
1133
- 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.
1134
-
1135
- 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.
1136
-
1137
- FILTER PARAMS (same semantics as read_graph):
1138
- - layer: ui, api, or db
1139
- - search: substring match on node id/name/route
1140
- - type: node type filter
1141
- - module: ui-layer module filter
1142
- - node_id + hops: neighborhood scope
1143
-
1144
- CONTENT PARAMS:
1145
- - pattern: regex to search for (required)
1146
- - case_insensitive: default false
1147
- - context: lines of context around each match (default 2)
1148
- - max_matches: cap on total matches returned (default 50)
1149
- - max_files: cap on files searched (default 50, errors if filter returns more)
1150
-
1151
- 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.`,
1152
- inputSchema: {
1153
- type: "object",
1154
- properties: {
1155
- layer: {
1156
- type: "string",
1157
- enum: ["ui", "api", "db"],
1158
- description: "Graph layer to scope files (required)."
1159
- },
1160
- pattern: {
1161
- type: "string",
1162
- description: "Regex pattern to search for (required)."
1163
- },
1164
- search: { type: "string", description: "Substring match on node id/name/route." },
1165
- type: { type: "string", description: "Filter by node type." },
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, import_node_path6.join)(rootDir, "src", nodeId);
1529
- if (layer === "api") return (0, import_node_path6.join)(rootDir, nodeId);
1530
- if (layer === "db") return (0, import_node_path6.join)(rootDir, "prisma", "schema.prisma");
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, import_node_fs6.existsSync)(filePath)) continue;
2753
+ if (!(0, import_node_fs12.existsSync)(filePath)) continue;
1591
2754
  filesSearched++;
1592
2755
  let content;
1593
2756
  try {
1594
- content = (0, import_node_fs6.readFileSync)(filePath, "utf-8");
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
- startGraphMcpServer();
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
+ });