@launchsecure/launch-kit 0.0.2 → 0.0.4

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