@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.
@@ -552,7 +552,7 @@ var init_esm_node = __esm({
552
552
  var require_claude_bridge = __commonJS({
553
553
  "../claude-code-web/src/claude-bridge.js"(exports2, module2) {
554
554
  "use strict";
555
- var { spawn: spawn2 } = require("node-pty");
555
+ var { spawn: spawn3 } = require("node-pty");
556
556
  var path9 = require("path");
557
557
  var fs9 = require("fs");
558
558
  var ClaudeBridge = class {
@@ -624,7 +624,7 @@ var require_claude_bridge = __commonJS({
624
624
  if (dangerouslySkipPermissions) args.push("--dangerously-skip-permissions");
625
625
  if (appendSystemPrompt) args.push("--append-system-prompt", appendSystemPrompt);
626
626
  if (initialPrompt) args.push(initialPrompt);
627
- const claudeProcess = spawn2(this.claudeCommand, args, {
627
+ const claudeProcess = spawn3(this.claudeCommand, args, {
628
628
  cwd: workingDir,
629
629
  env: {
630
630
  ...process.env,
@@ -771,7 +771,7 @@ var require_claude_bridge = __commonJS({
771
771
  var require_codex_bridge = __commonJS({
772
772
  "../claude-code-web/src/codex-bridge.js"(exports2, module2) {
773
773
  "use strict";
774
- var { spawn: spawn2 } = require("node-pty");
774
+ var { spawn: spawn3 } = require("node-pty");
775
775
  var path9 = require("path");
776
776
  var fs9 = require("fs");
777
777
  var CodexBridge = class {
@@ -834,7 +834,7 @@ var require_codex_bridge = __commonJS({
834
834
  console.log(`\u26A0\uFE0F WARNING: Bypassing approvals and sandbox with --dangerously-bypass-approvals-and-sandbox flag`);
835
835
  }
836
836
  const args = dangerouslySkipPermissions ? ["--dangerously-bypass-approvals-and-sandbox"] : [];
837
- const codexProcess = spawn2(this.codexCommand, args, {
837
+ const codexProcess = spawn3(this.codexCommand, args, {
838
838
  cwd: workingDir,
839
839
  env: {
840
840
  ...process.env,
@@ -964,7 +964,7 @@ var require_codex_bridge = __commonJS({
964
964
  var require_agent_bridge = __commonJS({
965
965
  "../claude-code-web/src/agent-bridge.js"(exports2, module2) {
966
966
  "use strict";
967
- var { spawn: spawn2 } = require("node-pty");
967
+ var { spawn: spawn3 } = require("node-pty");
968
968
  var path9 = require("path");
969
969
  var fs9 = require("fs");
970
970
  var AgentBridge = class {
@@ -1021,7 +1021,7 @@ var require_agent_bridge = __commonJS({
1021
1021
  console.log(`Command: ${this.agentCommand}`);
1022
1022
  console.log(`Working directory: ${workingDir}`);
1023
1023
  console.log(`Terminal size: ${cols}x${rows}`);
1024
- const agentProcess = spawn2(this.agentCommand, [], {
1024
+ const agentProcess = spawn3(this.agentCommand, [], {
1025
1025
  cwd: workingDir,
1026
1026
  env: {
1027
1027
  ...process.env,
@@ -1151,7 +1151,7 @@ var require_agent_bridge = __commonJS({
1151
1151
  var require_script_bridge = __commonJS({
1152
1152
  "../claude-code-web/src/script-bridge.js"(exports2, module2) {
1153
1153
  "use strict";
1154
- var { spawn: spawn2 } = require("node-pty");
1154
+ var { spawn: spawn3 } = require("node-pty");
1155
1155
  var ScriptBridge = class {
1156
1156
  constructor() {
1157
1157
  this.sessions = /* @__PURE__ */ new Map();
@@ -1180,7 +1180,7 @@ var require_script_bridge = __commonJS({
1180
1180
  try {
1181
1181
  console.log(`Starting script session ${sessionId}: ${command} ${args.join(" ")}`);
1182
1182
  console.log(`Working directory: ${workingDir}`);
1183
- const proc = spawn2(command, args, {
1183
+ const proc = spawn3(command, args, {
1184
1184
  cwd: workingDir,
1185
1185
  env: {
1186
1186
  ...process.env,
@@ -1727,7 +1727,7 @@ var require_usage_reader = __commonJS({
1727
1727
  async readJsonlFile(filePath, cutoffTime) {
1728
1728
  const entries = [];
1729
1729
  const fileProcessedEntries = /* @__PURE__ */ new Set();
1730
- return new Promise((resolve) => {
1730
+ return new Promise((resolve2) => {
1731
1731
  const rl = readline.createInterface({
1732
1732
  input: createReadStream(filePath),
1733
1733
  crlfDelay: Infinity
@@ -1785,11 +1785,11 @@ var require_usage_reader = __commonJS({
1785
1785
  }
1786
1786
  });
1787
1787
  rl.on("close", () => {
1788
- resolve(entries);
1788
+ resolve2(entries);
1789
1789
  });
1790
1790
  rl.on("error", (error) => {
1791
1791
  console.error("Error reading file:", filePath, error);
1792
- resolve(entries);
1792
+ resolve2(entries);
1793
1793
  });
1794
1794
  });
1795
1795
  }
@@ -3587,7 +3587,7 @@ var require_src = __commonJS({
3587
3587
  if (session.active) throw new Error(`Agent already running in session ${sessionId}`);
3588
3588
  const { command, args = [], env = {} } = options;
3589
3589
  if (!command) throw new Error("startScriptInSession requires a command");
3590
- return new Promise((resolve, reject) => {
3590
+ return new Promise((resolve2, reject) => {
3591
3591
  this.scriptBridge.startSession(sessionId, {
3592
3592
  command,
3593
3593
  args,
@@ -3609,7 +3609,7 @@ var require_src = __commonJS({
3609
3609
  session.lastActivity = /* @__PURE__ */ new Date();
3610
3610
  this.broadcastToSession(sessionId, { type: "script_stopped", sessionId });
3611
3611
  if (exitCode === 0) {
3612
- resolve({ code: exitCode, signal });
3612
+ resolve2({ code: exitCode, signal });
3613
3613
  } else {
3614
3614
  reject(new Error(`Script exited with code ${exitCode}`));
3615
3615
  }
@@ -5270,7 +5270,7 @@ var PostImplLaunchExecutor = class {
5270
5270
  return 3001;
5271
5271
  }
5272
5272
  startDevServer(port, databaseUrl) {
5273
- return new Promise((resolve) => {
5273
+ return new Promise((resolve2) => {
5274
5274
  const env = { ...process.env, PORT: String(port), ...databaseUrl ? { DATABASE_URL: databaseUrl } : {} };
5275
5275
  this.devProcess = (0, import_child_process3.spawn)("npm", ["run", "dev"], {
5276
5276
  cwd: this.workingDir,
@@ -5282,7 +5282,7 @@ var PostImplLaunchExecutor = class {
5282
5282
  const timeout = setTimeout(() => {
5283
5283
  if (!resolved) {
5284
5284
  resolved = true;
5285
- this.healthCheck(port).then(resolve);
5285
+ this.healthCheck(port).then(resolve2);
5286
5286
  }
5287
5287
  }, 15e3);
5288
5288
  const onData = (data) => {
@@ -5291,7 +5291,7 @@ var PostImplLaunchExecutor = class {
5291
5291
  if (!resolved) {
5292
5292
  resolved = true;
5293
5293
  clearTimeout(timeout);
5294
- resolve(true);
5294
+ resolve2(true);
5295
5295
  }
5296
5296
  }
5297
5297
  };
@@ -5302,7 +5302,7 @@ var PostImplLaunchExecutor = class {
5302
5302
  if (!resolved) {
5303
5303
  resolved = true;
5304
5304
  clearTimeout(timeout);
5305
- resolve(false);
5305
+ resolve2(false);
5306
5306
  }
5307
5307
  });
5308
5308
  this.devProcess.unref();
@@ -6324,16 +6324,37 @@ ${links}
6324
6324
  }
6325
6325
 
6326
6326
  // src/server/graph/index.ts
6327
- var import_node_fs5 = require("node:fs");
6328
- var import_node_path5 = require("node:path");
6327
+ var import_node_fs9 = require("node:fs");
6328
+ var import_node_path10 = require("node:path");
6329
6329
 
6330
- // src/server/graph/parsers/ui/react-nextjs.ts
6331
- var import_node_fs2 = require("node:fs");
6332
- var import_node_path2 = require("node:path");
6330
+ // src/server/graph/core/graph-builder.ts
6331
+ var import_node_fs8 = require("node:fs");
6332
+ var import_node_path9 = require("node:path");
6333
6333
 
6334
- // src/server/graph/core/ast-helpers.ts
6334
+ // src/server/graph/core/config.ts
6335
6335
  var import_node_fs = require("node:fs");
6336
6336
  var import_node_path = require("node:path");
6337
+ var CONFIG_FILENAME = ".launchchart.json";
6338
+ function loadConfig(rootDir) {
6339
+ const configPath = (0, import_node_path.join)(rootDir, CONFIG_FILENAME);
6340
+ if (!(0, import_node_fs.existsSync)(configPath)) return {};
6341
+ try {
6342
+ return JSON.parse((0, import_node_fs.readFileSync)(configPath, "utf-8"));
6343
+ } catch {
6344
+ return {};
6345
+ }
6346
+ }
6347
+
6348
+ // src/server/graph/core/parser-registry.ts
6349
+ var import_node_path8 = require("node:path");
6350
+
6351
+ // src/server/graph/parsers/ui/react-nextjs.ts
6352
+ var import_node_fs3 = require("node:fs");
6353
+ var import_node_path3 = require("node:path");
6354
+
6355
+ // src/server/graph/core/ast-helpers.ts
6356
+ var import_node_fs2 = require("node:fs");
6357
+ var import_node_path2 = require("node:path");
6337
6358
  var tsModule;
6338
6359
  function getTs() {
6339
6360
  if (!tsModule) {
@@ -6341,10 +6362,11 @@ function getTs() {
6341
6362
  }
6342
6363
  return tsModule;
6343
6364
  }
6365
+ var HTTP_METHODS = /* @__PURE__ */ new Set(["get", "post", "put", "patch", "delete", "head", "options"]);
6344
6366
  function parseFile(absPath) {
6345
6367
  const ts = getTs();
6346
- const content = (0, import_node_fs.readFileSync)(absPath, "utf-8");
6347
- const ext = (0, import_node_path.extname)(absPath);
6368
+ const content = (0, import_node_fs2.readFileSync)(absPath, "utf-8");
6369
+ const ext = (0, import_node_path2.extname)(absPath);
6348
6370
  const scriptKind = ext === ".tsx" ? ts.ScriptKind.TSX : ext === ".ts" ? ts.ScriptKind.TS : ext === ".jsx" ? ts.ScriptKind.JSX : ts.ScriptKind.JS;
6349
6371
  const sourceFile = ts.createSourceFile(
6350
6372
  absPath,
@@ -6363,6 +6385,8 @@ function parseFile(absPath) {
6363
6385
  const reExports = [];
6364
6386
  const jsxElements = /* @__PURE__ */ new Set();
6365
6387
  const navigations = [];
6388
+ const fetchCalls = [];
6389
+ const includeConcat = process.env.LAUNCH_CHART_INCLUDE_CONCAT_FETCHES === "1";
6366
6390
  function addExport(name2, kind) {
6367
6391
  if (!exportsSet.has(name2)) {
6368
6392
  exportsSet.add(name2);
@@ -6385,6 +6409,33 @@ function parseFile(absPath) {
6385
6409
  }
6386
6410
  return null;
6387
6411
  }
6412
+ function looksLikeUrl(s) {
6413
+ return s.startsWith("/") || /^(https?:)?\/\//i.test(s);
6414
+ }
6415
+ function templateStartsWithSlash(expr) {
6416
+ const head = expr.head.text;
6417
+ return head.startsWith("/");
6418
+ }
6419
+ function extractUrlFromFetchArg(arg) {
6420
+ if (ts.isStringLiteral(arg) || ts.isNoSubstitutionTemplateLiteral(arg)) {
6421
+ if (!looksLikeUrl(arg.text)) return null;
6422
+ return { url: arg.text, isTemplate: false };
6423
+ }
6424
+ if (ts.isTemplateExpression(arg)) {
6425
+ if (!templateStartsWithSlash(arg)) return null;
6426
+ return { url: arg.getText(sourceFile), isTemplate: true };
6427
+ }
6428
+ if (includeConcat && ts.isBinaryExpression(arg) && arg.operatorToken.kind === ts.SyntaxKind.PlusToken) {
6429
+ let leftmost = arg;
6430
+ while (ts.isBinaryExpression(leftmost) && leftmost.operatorToken.kind === ts.SyntaxKind.PlusToken) {
6431
+ leftmost = leftmost.left;
6432
+ }
6433
+ if ((ts.isStringLiteral(leftmost) || ts.isNoSubstitutionTemplateLiteral(leftmost)) && leftmost.text.startsWith("/")) {
6434
+ return { url: arg.getText(sourceFile), isTemplate: false, isConcat: true };
6435
+ }
6436
+ }
6437
+ return null;
6438
+ }
6388
6439
  function visit(node) {
6389
6440
  if (ts.isImportDeclaration(node)) {
6390
6441
  const moduleSpec = node.moduleSpecifier;
@@ -6408,6 +6459,8 @@ function parseFile(absPath) {
6408
6459
  }
6409
6460
  if (names.length > 0 || isTypeOnly) {
6410
6461
  imports.push({ names, specifier, isTypeOnly, typeNames });
6462
+ } else if (!clause) {
6463
+ imports.push({ names: [], specifier, isTypeOnly: false, typeNames: /* @__PURE__ */ new Set() });
6411
6464
  }
6412
6465
  }
6413
6466
  }
@@ -6421,6 +6474,19 @@ function parseFile(absPath) {
6421
6474
  reExports.push({ name: exportedName, from: fromSpec });
6422
6475
  }
6423
6476
  }
6477
+ } else if (!node.exportClause && fromSpec) {
6478
+ reExports.push({ name: "*", from: fromSpec, isWildcard: true });
6479
+ }
6480
+ }
6481
+ if (ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.ImportKeyword) {
6482
+ const arg = node.arguments[0];
6483
+ if (arg && ts.isStringLiteral(arg)) {
6484
+ imports.push({
6485
+ names: [],
6486
+ specifier: arg.text,
6487
+ isTypeOnly: false,
6488
+ typeNames: /* @__PURE__ */ new Set()
6489
+ });
6424
6490
  }
6425
6491
  }
6426
6492
  if (ts.isExportAssignment(node) && !node.isExportEquals) {
@@ -6482,6 +6548,36 @@ function parseFile(absPath) {
6482
6548
  }
6483
6549
  }
6484
6550
  }
6551
+ if (ts.isCallExpression(node) && node.arguments.length > 0) {
6552
+ const expr = node.expression;
6553
+ const firstArg = node.arguments[0];
6554
+ if (ts.isIdentifier(expr) && expr.text === "fetch") {
6555
+ const extracted = extractUrlFromFetchArg(firstArg);
6556
+ if (extracted) {
6557
+ fetchCalls.push({
6558
+ url: extracted.url,
6559
+ isTemplate: extracted.isTemplate,
6560
+ ...extracted.isConcat ? { isConcat: true } : {},
6561
+ kind: "fetch"
6562
+ });
6563
+ }
6564
+ }
6565
+ if (ts.isPropertyAccessExpression(expr) && ts.isIdentifier(expr.name)) {
6566
+ const methodName = expr.name.text;
6567
+ if (HTTP_METHODS.has(methodName)) {
6568
+ const extracted = extractUrlFromFetchArg(firstArg);
6569
+ if (extracted) {
6570
+ fetchCalls.push({
6571
+ method: methodName.toUpperCase(),
6572
+ url: extracted.url,
6573
+ isTemplate: extracted.isTemplate,
6574
+ ...extracted.isConcat ? { isConcat: true } : {},
6575
+ kind: "client-method"
6576
+ });
6577
+ }
6578
+ }
6579
+ }
6580
+ }
6485
6581
  if (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) {
6486
6582
  const tagName = node.tagName;
6487
6583
  if (ts.isIdentifier(tagName) && tagName.text === "Link") {
@@ -6531,7 +6627,8 @@ function parseFile(absPath) {
6531
6627
  imports,
6532
6628
  reExports,
6533
6629
  jsxElements,
6534
- navigations
6630
+ navigations,
6631
+ fetchCalls
6535
6632
  };
6536
6633
  }
6537
6634
  var MUTATION_METHODS = /* @__PURE__ */ new Set([
@@ -6547,8 +6644,8 @@ var MUTATION_METHODS = /* @__PURE__ */ new Set([
6547
6644
  ]);
6548
6645
  function extractDbCalls(absPath) {
6549
6646
  const ts = getTs();
6550
- const content = (0, import_node_fs.readFileSync)(absPath, "utf-8");
6551
- const ext = (0, import_node_path.extname)(absPath);
6647
+ const content = (0, import_node_fs2.readFileSync)(absPath, "utf-8");
6648
+ const ext = (0, import_node_path2.extname)(absPath);
6552
6649
  const scriptKind = ext === ".tsx" ? ts.ScriptKind.TSX : ts.ScriptKind.TS;
6553
6650
  const sourceFile = ts.createSourceFile(absPath, content, ts.ScriptTarget.Latest, true, scriptKind);
6554
6651
  const calls = [];
@@ -6577,8 +6674,8 @@ function extractDbCalls(absPath) {
6577
6674
  }
6578
6675
  function extractAuthWrappers(absPath) {
6579
6676
  const ts = getTs();
6580
- const content = (0, import_node_fs.readFileSync)(absPath, "utf-8");
6581
- const ext = (0, import_node_path.extname)(absPath);
6677
+ const content = (0, import_node_fs2.readFileSync)(absPath, "utf-8");
6678
+ const ext = (0, import_node_path2.extname)(absPath);
6582
6679
  const scriptKind = ext === ".tsx" ? ts.ScriptKind.TSX : ts.ScriptKind.TS;
6583
6680
  const sourceFile = ts.createSourceFile(absPath, content, ts.ScriptTarget.Latest, true, scriptKind);
6584
6681
  const wrappers = /* @__PURE__ */ new Set();
@@ -6599,50 +6696,101 @@ function extractAuthWrappers(absPath) {
6599
6696
  var RENDER_TYPES = /* @__PURE__ */ new Set(["component", "ui", "layout", "context"]);
6600
6697
  function walk(dir, exts) {
6601
6698
  const results = [];
6602
- if (!(0, import_node_fs2.existsSync)(dir)) return results;
6603
- for (const entry of (0, import_node_fs2.readdirSync)(dir, { withFileTypes: true })) {
6604
- const full = (0, import_node_path2.join)(dir, entry.name);
6699
+ if (!(0, import_node_fs3.existsSync)(dir)) return results;
6700
+ for (const entry of (0, import_node_fs3.readdirSync)(dir, { withFileTypes: true })) {
6701
+ const full = (0, import_node_path3.join)(dir, entry.name);
6605
6702
  if (entry.isDirectory()) {
6606
6703
  results.push(...walk(full, exts));
6607
- } else if (exts.includes((0, import_node_path2.extname)(entry.name))) {
6704
+ } else if (exts.includes((0, import_node_path3.extname)(entry.name))) {
6608
6705
  results.push(full);
6609
6706
  }
6610
6707
  }
6611
6708
  return results;
6612
6709
  }
6710
+ function walkWithIgnore(dir, exts, ignoreDirs) {
6711
+ const results = [];
6712
+ if (!(0, import_node_fs3.existsSync)(dir)) return results;
6713
+ for (const entry of (0, import_node_fs3.readdirSync)(dir, { withFileTypes: true })) {
6714
+ if (entry.isDirectory()) {
6715
+ if (ignoreDirs.has(entry.name)) continue;
6716
+ results.push(...walkWithIgnore((0, import_node_path3.join)(dir, entry.name), exts, ignoreDirs));
6717
+ } else if (exts.includes((0, import_node_path3.extname)(entry.name))) {
6718
+ results.push((0, import_node_path3.join)(dir, entry.name));
6719
+ }
6720
+ }
6721
+ return results;
6722
+ }
6613
6723
  function toNodeId(srcDir, absPath) {
6614
- return (0, import_node_path2.relative)(srcDir, absPath).replace(/\\/g, "/");
6724
+ return (0, import_node_path3.relative)(srcDir, absPath).replace(/\\/g, "/");
6615
6725
  }
6616
6726
  function resolveImport(srcDir, specifier) {
6617
6727
  if (!specifier.startsWith("@/")) return null;
6618
6728
  const rel = specifier.slice(2);
6619
- const base = (0, import_node_path2.join)(srcDir, rel);
6620
- 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")]) {
6621
- if ((0, import_node_fs2.existsSync)(c) && (0, import_node_fs2.statSync)(c).isFile()) return c;
6729
+ const base = (0, import_node_path3.join)(srcDir, rel);
6730
+ 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")]) {
6731
+ if ((0, import_node_fs3.existsSync)(c) && (0, import_node_fs3.statSync)(c).isFile()) return c;
6622
6732
  }
6623
6733
  return null;
6624
6734
  }
6625
6735
  function resolveRelativeImport(fromFile, specifier) {
6626
- const base = (0, import_node_path2.join)((0, import_node_path2.dirname)(fromFile), specifier);
6627
- 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")]) {
6628
- if ((0, import_node_fs2.existsSync)(c) && (0, import_node_fs2.statSync)(c).isFile()) return c;
6736
+ const base = (0, import_node_path3.join)((0, import_node_path3.dirname)(fromFile), specifier);
6737
+ 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")]) {
6738
+ if ((0, import_node_fs3.existsSync)(c) && (0, import_node_fs3.statSync)(c).isFile()) return c;
6629
6739
  }
6630
6740
  return null;
6631
6741
  }
6742
+ function resolveBarrelMap(barrelAbsPath, parsedByPath, memo, visiting) {
6743
+ const cached = memo.get(barrelAbsPath);
6744
+ if (cached) return cached;
6745
+ if (visiting.has(barrelAbsPath)) return /* @__PURE__ */ new Map();
6746
+ visiting.add(barrelAbsPath);
6747
+ const parsed = parsedByPath.get(barrelAbsPath);
6748
+ const map = /* @__PURE__ */ new Map();
6749
+ if (!parsed) {
6750
+ visiting.delete(barrelAbsPath);
6751
+ memo.set(barrelAbsPath, map);
6752
+ return map;
6753
+ }
6754
+ for (const re of parsed.reExports) {
6755
+ if (!re.from.startsWith(".")) continue;
6756
+ const resolved = resolveRelativeImport(barrelAbsPath, re.from);
6757
+ if (!resolved) continue;
6758
+ if (re.isWildcard) {
6759
+ const targetBn = (0, import_node_path3.basename)(resolved);
6760
+ const targetIsBarrel = targetBn === "index.ts" || targetBn === "index.tsx";
6761
+ if (targetIsBarrel) {
6762
+ const nested = resolveBarrelMap(resolved, parsedByPath, memo, visiting);
6763
+ for (const [name, target] of nested) {
6764
+ if (!map.has(name)) map.set(name, target);
6765
+ }
6766
+ } else {
6767
+ const targetParsed = parsedByPath.get(resolved);
6768
+ if (targetParsed) {
6769
+ for (const exp of targetParsed.exports) {
6770
+ if (!map.has(exp)) map.set(exp, resolved);
6771
+ }
6772
+ }
6773
+ }
6774
+ } else {
6775
+ if (!map.has(re.name)) map.set(re.name, resolved);
6776
+ }
6777
+ }
6778
+ visiting.delete(barrelAbsPath);
6779
+ memo.set(barrelAbsPath, map);
6780
+ return map;
6781
+ }
6632
6782
  function buildAllBarrelMaps(srcDir, parsedByPath) {
6633
6783
  const barrels = /* @__PURE__ */ new Map();
6784
+ const memo = /* @__PURE__ */ new Map();
6634
6785
  for (const [absPath, parsed] of parsedByPath) {
6635
- const bn = (0, import_node_path2.basename)(absPath);
6786
+ const bn = (0, import_node_path3.basename)(absPath);
6636
6787
  if (bn !== "index.ts" && bn !== "index.tsx") continue;
6637
6788
  if (parsed.reExports.length === 0) continue;
6638
- const barrelId = (0, import_node_path2.relative)(srcDir, (0, import_node_path2.dirname)(absPath)).replace(/\\/g, "/");
6639
- const map = /* @__PURE__ */ new Map();
6640
- for (const re of parsed.reExports) {
6641
- if (!re.from.startsWith(".")) continue;
6642
- const resolved = resolveRelativeImport(absPath, re.from);
6643
- if (resolved) map.set(re.name, resolved);
6789
+ const map = resolveBarrelMap(absPath, parsedByPath, memo, /* @__PURE__ */ new Set());
6790
+ if (map.size > 0) {
6791
+ const barrelId = (0, import_node_path3.relative)(srcDir, (0, import_node_path3.dirname)(absPath)).replace(/\\/g, "/");
6792
+ barrels.set(barrelId, map);
6644
6793
  }
6645
- if (map.size > 0) barrels.set(barrelId, map);
6646
6794
  }
6647
6795
  return barrels;
6648
6796
  }
@@ -6699,7 +6847,7 @@ function extractRoute(id) {
6699
6847
  return route || "/";
6700
6848
  }
6701
6849
  function nameFromFilename(absPath) {
6702
- return (0, import_node_path2.basename)(absPath, (0, import_node_path2.extname)(absPath)).replace(/[-_](\w)/g, (_, c) => c.toUpperCase()).replace(/^(\w)/, (_, c) => c.toUpperCase());
6850
+ return (0, import_node_path3.basename)(absPath, (0, import_node_path3.extname)(absPath)).replace(/[-_](\w)/g, (_, c) => c.toUpperCase()).replace(/^(\w)/, (_, c) => c.toUpperCase());
6703
6851
  }
6704
6852
  function resolveTemplateLiteralRoute(template, routeToNodeId) {
6705
6853
  const parameterized = template.replace(/\$\{([^}]+)\}/g, (_, expr) => {
@@ -6879,26 +7027,26 @@ function extractEdges(srcDir, absPath, sourceId, parsed, nodeIdSet, nodeTypeMap,
6879
7027
  return { edges, flagged };
6880
7028
  }
6881
7029
  function detect(rootDir) {
6882
- 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"));
7030
+ 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"));
6883
7031
  }
6884
7032
  function generate(rootDir) {
6885
- const srcDir = (0, import_node_path2.join)(rootDir, "src");
6886
- const appFiles = walk((0, import_node_path2.join)(srcDir, "app"), [".tsx", ".ts"]).filter(
6887
- (f) => f.endsWith("/page.tsx") || f.endsWith("/layout.tsx")
7033
+ const srcDir = (0, import_node_path3.join)(rootDir, "src");
7034
+ const appFiles = walk((0, import_node_path3.join)(srcDir, "app"), [".tsx", ".ts"]).filter(
7035
+ (f) => (0, import_node_path3.basename)(f) !== "route.ts" && (0, import_node_path3.basename)(f) !== "route.tsx"
6888
7036
  );
6889
- const clientFiles = walk((0, import_node_path2.join)(srcDir, "client"), [".tsx", ".ts"]);
6890
- const serverFiles = walk((0, import_node_path2.join)(srcDir, "server"), [".ts", ".tsx"]).filter(
6891
- (f) => (0, import_node_path2.basename)(f) !== "route.ts" && (0, import_node_path2.basename)(f) !== "route.tsx"
7037
+ const clientFiles = walk((0, import_node_path3.join)(srcDir, "client"), [".tsx", ".ts"]);
7038
+ const serverFiles = walk((0, import_node_path3.join)(srcDir, "server"), [".ts", ".tsx"]).filter(
7039
+ (f) => (0, import_node_path3.basename)(f) !== "route.ts" && (0, import_node_path3.basename)(f) !== "route.tsx"
6892
7040
  );
6893
- const libFiles = walk((0, import_node_path2.join)(srcDir, "lib"), [".ts", ".tsx"]);
6894
- const configFiles = walk((0, import_node_path2.join)(srcDir, "config"), [".ts", ".tsx"]);
7041
+ const libFiles = walk((0, import_node_path3.join)(srcDir, "lib"), [".ts", ".tsx"]);
7042
+ const configFiles = walk((0, import_node_path3.join)(srcDir, "config"), [".ts", ".tsx"]);
6895
7043
  const allDiscovered = [...appFiles, ...clientFiles, ...serverFiles, ...libFiles, ...configFiles];
6896
7044
  const parsedByPath = /* @__PURE__ */ new Map();
6897
7045
  for (const absPath of allDiscovered) {
6898
7046
  parsedByPath.set(absPath, parseFile(absPath));
6899
7047
  }
6900
7048
  const barrelMaps = buildAllBarrelMaps(srcDir, parsedByPath);
6901
- const fileSet = allDiscovered.filter((f) => !(0, import_node_path2.basename)(f).startsWith("index."));
7049
+ const fileSet = allDiscovered.filter((f) => !(0, import_node_path3.basename)(f).startsWith("index."));
6902
7050
  const nodes = [];
6903
7051
  const nodeIdSet = /* @__PURE__ */ new Set();
6904
7052
  const nodeTypeMap = /* @__PURE__ */ new Map();
@@ -6933,6 +7081,94 @@ function generate(rootDir) {
6933
7081
  allEdges.push(...edges);
6934
7082
  allFlagged.push(...flagged);
6935
7083
  }
7084
+ const fetchCallEntries = [];
7085
+ for (const absPath of fileSet) {
7086
+ const sourceId = toNodeId(srcDir, absPath);
7087
+ const parsed = parsedByPath.get(absPath);
7088
+ if (parsed.fetchCalls.length === 0) continue;
7089
+ fetchCallEntries.push({
7090
+ nodeId: sourceId,
7091
+ calls: parsed.fetchCalls.map((c) => ({
7092
+ url: c.url,
7093
+ method: c.method,
7094
+ isTemplate: c.isTemplate,
7095
+ isConcat: c.isConcat,
7096
+ kind: c.kind
7097
+ }))
7098
+ });
7099
+ }
7100
+ const externalScanned = new Set(allDiscovered.map((f) => f.replace(/\\/g, "/")));
7101
+ const IGNORE_DIRS = /* @__PURE__ */ new Set([
7102
+ "node_modules",
7103
+ ".next",
7104
+ "dist",
7105
+ ".launchsecure",
7106
+ ".git",
7107
+ "src",
7108
+ "coverage",
7109
+ ".turbo",
7110
+ "build",
7111
+ "out",
7112
+ ".vercel"
7113
+ ]);
7114
+ const externalCandidates = walkWithIgnore(rootDir, [".ts", ".tsx"], IGNORE_DIRS);
7115
+ for (const absPath of externalCandidates) {
7116
+ const normalized = absPath.replace(/\\/g, "/");
7117
+ if (externalScanned.has(normalized)) continue;
7118
+ let parsed;
7119
+ try {
7120
+ parsed = parseFile(absPath);
7121
+ } catch {
7122
+ continue;
7123
+ }
7124
+ const externalId = (0, import_node_path3.relative)(rootDir, absPath).replace(/\\/g, "/");
7125
+ const edgesFromThis = [];
7126
+ const seen = /* @__PURE__ */ new Set();
7127
+ for (const imp of parsed.imports) {
7128
+ const { specifier, isTypeOnly, names } = imp;
7129
+ let resolved = null;
7130
+ if (specifier.startsWith("@/")) {
7131
+ const relToSrc = specifier.slice(2);
7132
+ const barrelMap = barrelMaps.get(relToSrc);
7133
+ if (barrelMap && names.length > 0) {
7134
+ for (const name of names) {
7135
+ const targetAbs = barrelMap.get(name);
7136
+ if (!targetAbs) continue;
7137
+ const targetId2 = toNodeId(srcDir, targetAbs);
7138
+ if (!nodeIdSet.has(targetId2)) continue;
7139
+ const key2 = `${externalId}\u2192${targetId2}`;
7140
+ if (seen.has(key2)) continue;
7141
+ seen.add(key2);
7142
+ edgesFromThis.push({ source: externalId, target: targetId2, type: "imports" });
7143
+ }
7144
+ continue;
7145
+ }
7146
+ resolved = resolveImport(srcDir, specifier);
7147
+ } else if (specifier.startsWith(".")) {
7148
+ resolved = resolveRelativeImport(absPath, specifier);
7149
+ }
7150
+ if (!resolved) continue;
7151
+ const targetId = toNodeId(srcDir, resolved);
7152
+ if (!nodeIdSet.has(targetId)) continue;
7153
+ if (targetId.endsWith("/index.ts") || targetId.endsWith("/index.tsx")) continue;
7154
+ const key = `${externalId}\u2192${targetId}\u2192${isTypeOnly ? "type" : "value"}`;
7155
+ if (seen.has(key)) continue;
7156
+ seen.add(key);
7157
+ edgesFromThis.push({ source: externalId, target: targetId, type: "imports" });
7158
+ }
7159
+ if (edgesFromThis.length === 0) continue;
7160
+ nodes.push({
7161
+ id: externalId,
7162
+ type: "external",
7163
+ name: parsed.name || nameFromFilename(absPath),
7164
+ route: null,
7165
+ module: "external",
7166
+ exports: parsed.exports
7167
+ });
7168
+ nodeIdSet.add(externalId);
7169
+ nodeTypeMap.set(externalId, "external");
7170
+ allEdges.push(...edgesFromThis);
7171
+ }
6936
7172
  const flaggedSet = /* @__PURE__ */ new Set();
6937
7173
  const dedupedFlagged = allFlagged.filter((f) => {
6938
7174
  const key = `${f.source}\u2192${f.target}\u2192${f.label}`;
@@ -6964,6 +7200,7 @@ function generate(rootDir) {
6964
7200
  total_configs: byType("config"),
6965
7201
  total_utils: byType("util"),
6966
7202
  total_libs: byType("lib"),
7203
+ total_external: byType("external"),
6967
7204
  total_edges: allEdges.length,
6968
7205
  total_flagged: dedupedFlagged.length
6969
7206
  };
@@ -6990,7 +7227,8 @@ function generate(rootDir) {
6990
7227
  renders: allEdges.filter((e) => e.type === "renders").length,
6991
7228
  imports: allEdges.filter((e) => e.type === "imports").length,
6992
7229
  navigates: allEdges.filter((e) => e.type === "navigates").length
6993
- }
7230
+ },
7231
+ fetch_calls: fetchCallEntries
6994
7232
  }
6995
7233
  };
6996
7234
  }
@@ -7002,14 +7240,14 @@ var reactNextjsParser = {
7002
7240
  };
7003
7241
 
7004
7242
  // src/server/graph/parsers/api/nextjs-routes.ts
7005
- var import_node_fs3 = require("node:fs");
7006
- var import_node_path3 = require("node:path");
7007
- var HTTP_METHODS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"]);
7243
+ var import_node_fs4 = require("node:fs");
7244
+ var import_node_path4 = require("node:path");
7245
+ var HTTP_METHODS2 = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"]);
7008
7246
  function walk2(dir) {
7009
7247
  const results = [];
7010
- if (!(0, import_node_fs3.existsSync)(dir)) return results;
7011
- for (const entry of (0, import_node_fs3.readdirSync)(dir, { withFileTypes: true })) {
7012
- const full = (0, import_node_path3.join)(dir, entry.name);
7248
+ if (!(0, import_node_fs4.existsSync)(dir)) return results;
7249
+ for (const entry of (0, import_node_fs4.readdirSync)(dir, { withFileTypes: true })) {
7250
+ const full = (0, import_node_path4.join)(dir, entry.name);
7013
7251
  if (entry.isDirectory()) {
7014
7252
  results.push(...walk2(full));
7015
7253
  } else if (entry.name === "route.ts" || entry.name === "route.tsx") {
@@ -7019,7 +7257,7 @@ function walk2(dir) {
7019
7257
  return results;
7020
7258
  }
7021
7259
  function filePathToRoute(apiDir, absPath) {
7022
- let route = "/" + (0, import_node_path3.relative)(apiDir, absPath).replace(/\\/g, "/").replace(/\/route\.tsx?$/, "");
7260
+ let route = "/" + (0, import_node_path4.relative)(apiDir, absPath).replace(/\\/g, "/").replace(/\/route\.tsx?$/, "");
7023
7261
  route = route.replace(/\[([^\]]+)\]/g, ":$1");
7024
7262
  route = route.replace(/\/+/g, "/");
7025
7263
  if (route === "/") return "/api";
@@ -7030,10 +7268,10 @@ function camelToPascal(s) {
7030
7268
  return s.charAt(0).toUpperCase() + s.slice(1);
7031
7269
  }
7032
7270
  function detect2(rootDir) {
7033
- return (0, import_node_fs3.existsSync)((0, import_node_path3.join)(rootDir, "src", "app", "api"));
7271
+ return (0, import_node_fs4.existsSync)((0, import_node_path4.join)(rootDir, "src", "app", "api"));
7034
7272
  }
7035
7273
  function generate2(rootDir) {
7036
- const apiDir = (0, import_node_path3.join)(rootDir, "src", "app", "api");
7274
+ const apiDir = (0, import_node_path4.join)(rootDir, "src", "app", "api");
7037
7275
  const routeFiles = walk2(apiDir);
7038
7276
  const nodes = [];
7039
7277
  const edges = [];
@@ -7048,10 +7286,10 @@ function generate2(rootDir) {
7048
7286
  const authWrappers = extractAuthWrappers(absPath);
7049
7287
  const methods = [];
7050
7288
  for (const exp of parsed.exports) {
7051
- if (HTTP_METHODS.has(exp)) methods.push(exp);
7289
+ if (HTTP_METHODS2.has(exp)) methods.push(exp);
7052
7290
  }
7053
7291
  const routePath = filePathToRoute(apiDir, absPath);
7054
- const relPath = (0, import_node_path3.relative)(rootDir, absPath).replace(/\\/g, "/");
7292
+ const relPath = (0, import_node_path4.relative)(rootDir, absPath).replace(/\\/g, "/");
7055
7293
  const mutations = dbCalls.filter((c) => c.isMutation);
7056
7294
  const reads = dbCalls.filter((c) => !c.isMutation);
7057
7295
  const mutates = mutations.length > 0;
@@ -7118,7 +7356,7 @@ function generate2(rootDir) {
7118
7356
  flagged_edges: [],
7119
7357
  patterns: {
7120
7358
  total_endpoints: nodes.length,
7121
- methods_breakdown: [...HTTP_METHODS].reduce((acc, m) => {
7359
+ methods_breakdown: [...HTTP_METHODS2].reduce((acc, m) => {
7122
7360
  acc[m] = nodes.filter((n) => n.methods.includes(m)).length;
7123
7361
  return acc;
7124
7362
  }, {}),
@@ -7136,8 +7374,8 @@ var nextjsRoutesParser = {
7136
7374
  };
7137
7375
 
7138
7376
  // src/server/graph/parsers/db/prisma-schema.ts
7139
- var import_node_fs4 = require("node:fs");
7140
- var import_node_path4 = require("node:path");
7377
+ var import_node_fs5 = require("node:fs");
7378
+ var import_node_path5 = require("node:path");
7141
7379
  function parseModels(content) {
7142
7380
  const nodes = [];
7143
7381
  const relations = [];
@@ -7228,11 +7466,11 @@ function parseEnums(content) {
7228
7466
  return nodes;
7229
7467
  }
7230
7468
  function detect3(rootDir) {
7231
- return (0, import_node_fs4.existsSync)((0, import_node_path4.join)(rootDir, "prisma", "schema.prisma"));
7469
+ return (0, import_node_fs5.existsSync)((0, import_node_path5.join)(rootDir, "prisma", "schema.prisma"));
7232
7470
  }
7233
7471
  function generate3(rootDir) {
7234
- const schemaPath = (0, import_node_path4.join)(rootDir, "prisma", "schema.prisma");
7235
- const content = (0, import_node_fs4.readFileSync)(schemaPath, "utf-8");
7472
+ const schemaPath = (0, import_node_path5.join)(rootDir, "prisma", "schema.prisma");
7473
+ const content = (0, import_node_fs5.readFileSync)(schemaPath, "utf-8");
7236
7474
  const { nodes: modelNodes, relations } = parseModels(content);
7237
7475
  const enumNodes = parseEnums(content);
7238
7476
  const allNodes = [...modelNodes, ...enumNodes];
@@ -7288,35 +7526,654 @@ var prismaSchemaParser = {
7288
7526
  generate: generate3
7289
7527
  };
7290
7528
 
7529
+ // src/server/graph/core/api-route-matching.ts
7530
+ function loadApiRoutesFromOutput(apiOutput) {
7531
+ const routes = [];
7532
+ for (const n of apiOutput.nodes) {
7533
+ const path9 = n.path;
7534
+ if (!path9 || typeof path9 !== "string") continue;
7535
+ routes.push({
7536
+ path: path9,
7537
+ nodeId: n.id,
7538
+ segments: path9.split("/").filter(Boolean)
7539
+ });
7540
+ }
7541
+ return routes;
7542
+ }
7543
+ function buildApiPathMap(routes) {
7544
+ const map = /* @__PURE__ */ new Map();
7545
+ for (const r of routes) {
7546
+ if (!map.has(r.path)) map.set(r.path, r.nodeId);
7547
+ }
7548
+ return map;
7549
+ }
7550
+ function normalizeFetchUrl(raw) {
7551
+ let s = raw.replace(/^`|`$/g, "");
7552
+ const qIdx = s.indexOf("?");
7553
+ if (qIdx >= 0) s = s.slice(0, qIdx);
7554
+ const hIdx = s.indexOf("#");
7555
+ if (hIdx >= 0) s = s.slice(0, hIdx);
7556
+ let hadInterpolation = false;
7557
+ s = s.replace(/\$\{([^}]+)\}/g, (_, expr) => {
7558
+ hadInterpolation = true;
7559
+ const cleaned = expr.trim();
7560
+ const last = cleaned.split(".").pop() ?? cleaned;
7561
+ const name = last.replace(/[^\w]/g, "") || "param";
7562
+ return ":" + name;
7563
+ });
7564
+ s = s.replace(/\/+/g, "/");
7565
+ if (s.length > 1 && s.endsWith("/")) s = s.slice(0, -1);
7566
+ return { path: s || "/", hadInterpolation };
7567
+ }
7568
+ function scoreApiRouteMatch(candidate, known) {
7569
+ if (candidate.length !== known.length) return -1;
7570
+ let score = 0;
7571
+ for (let i = 0; i < candidate.length; i++) {
7572
+ const a = candidate[i];
7573
+ const b = known[i];
7574
+ if (a === b) {
7575
+ score += 3;
7576
+ continue;
7577
+ }
7578
+ if (a.startsWith(":") && b.startsWith(":")) {
7579
+ score += 2;
7580
+ continue;
7581
+ }
7582
+ if (a.startsWith(":") || b.startsWith(":")) {
7583
+ score += 1;
7584
+ continue;
7585
+ }
7586
+ return -1;
7587
+ }
7588
+ return score;
7589
+ }
7590
+ function resolveFetchCall(call, apiPathMap, apiRoutes) {
7591
+ const raw = call.url;
7592
+ if (/^(https?:)?\/\//i.test(raw)) {
7593
+ return { kind: "external", normalizedUrl: raw };
7594
+ }
7595
+ if (call.isConcat) {
7596
+ return { kind: "dynamic", normalizedUrl: raw };
7597
+ }
7598
+ const { path: path9, hadInterpolation } = normalizeFetchUrl(raw);
7599
+ if (!path9.startsWith("/")) {
7600
+ return { kind: "unresolved", normalizedUrl: path9 };
7601
+ }
7602
+ const segs = path9.split("/").filter(Boolean);
7603
+ if (hadInterpolation && segs.length > 0 && segs[0].startsWith(":")) {
7604
+ return { kind: "dynamic", normalizedUrl: path9 };
7605
+ }
7606
+ const exact = apiPathMap.get(path9);
7607
+ if (exact) return { kind: "resolved", nodeId: exact, normalizedUrl: path9 };
7608
+ let bestScore = -1;
7609
+ let bestId = null;
7610
+ for (const r of apiRoutes) {
7611
+ const score = scoreApiRouteMatch(segs, r.segments);
7612
+ if (score > bestScore) {
7613
+ bestScore = score;
7614
+ bestId = r.nodeId;
7615
+ }
7616
+ }
7617
+ if (bestId && bestScore > 0) {
7618
+ return { kind: "resolved", nodeId: bestId, normalizedUrl: path9 };
7619
+ }
7620
+ return { kind: "unresolved", normalizedUrl: path9 };
7621
+ }
7622
+ function resolveUrlPath(urlPath, apiPathMap, apiRoutes) {
7623
+ const { path: path9, hadInterpolation } = normalizeFetchUrl(urlPath);
7624
+ if (!path9.startsWith("/")) {
7625
+ return { kind: "unresolved", normalizedUrl: path9 };
7626
+ }
7627
+ const segs = path9.split("/").filter(Boolean);
7628
+ if (hadInterpolation && segs.length > 0 && segs[0].startsWith(":")) {
7629
+ return { kind: "dynamic", normalizedUrl: path9 };
7630
+ }
7631
+ const exact = apiPathMap.get(path9);
7632
+ if (exact) return { kind: "resolved", nodeId: exact, normalizedUrl: path9 };
7633
+ let bestScore = -1;
7634
+ let bestId = null;
7635
+ for (const r of apiRoutes) {
7636
+ const score = scoreApiRouteMatch(segs, r.segments);
7637
+ if (score > bestScore) {
7638
+ bestScore = score;
7639
+ bestId = r.nodeId;
7640
+ }
7641
+ }
7642
+ if (bestId && bestScore > 0) {
7643
+ return { kind: "resolved", nodeId: bestId, normalizedUrl: path9 };
7644
+ }
7645
+ return { kind: "unresolved", normalizedUrl: path9 };
7646
+ }
7647
+
7648
+ // src/server/graph/parsers/crosslayer/fetch-resolver.ts
7649
+ var fetchResolverParser = {
7650
+ id: "fetch-resolver",
7651
+ layer: "crosslayer",
7652
+ detect(_rootDir) {
7653
+ return true;
7654
+ },
7655
+ generate(_rootDir, layerOutputs) {
7656
+ const uiOutput = layerOutputs.get("ui");
7657
+ const apiOutput = layerOutputs.get("api");
7658
+ if (!uiOutput || !apiOutput) {
7659
+ return { cross_refs: [], flagged_edges: [], warnings: [] };
7660
+ }
7661
+ const apiRoutes = loadApiRoutesFromOutput(apiOutput);
7662
+ const apiPathMap = buildApiPathMap(apiRoutes);
7663
+ const fetchCallEntries = uiOutput.patterns?.fetch_calls ?? [];
7664
+ if (fetchCallEntries.length === 0) {
7665
+ return { cross_refs: [], flagged_edges: [], warnings: [] };
7666
+ }
7667
+ const includeExternal = process.env.LAUNCH_CHART_INCLUDE_EXTERNAL_FETCHES === "1";
7668
+ const crossRefs = [];
7669
+ const flaggedEdges = [];
7670
+ const seen = /* @__PURE__ */ new Set();
7671
+ let resolvedCount = 0;
7672
+ let dynamicCount = 0;
7673
+ let unresolvedCount = 0;
7674
+ let externalCount = 0;
7675
+ for (const entry of fetchCallEntries) {
7676
+ for (const call of entry.calls) {
7677
+ const result = resolveFetchCall(call, apiPathMap, apiRoutes);
7678
+ const methodTag = call.method ?? (call.kind === "fetch" ? "GET?" : "?");
7679
+ if (result.kind === "resolved" && result.nodeId) {
7680
+ const key = `${entry.nodeId}\u2192${result.nodeId}\u2192calls_api`;
7681
+ if (seen.has(key)) continue;
7682
+ seen.add(key);
7683
+ crossRefs.push({
7684
+ source: entry.nodeId,
7685
+ target: result.nodeId,
7686
+ type: "calls_api",
7687
+ layer: "api"
7688
+ });
7689
+ resolvedCount++;
7690
+ continue;
7691
+ }
7692
+ if (result.kind === "dynamic") {
7693
+ dynamicCount++;
7694
+ flaggedEdges.push({
7695
+ source: entry.nodeId,
7696
+ target: "DYNAMIC",
7697
+ type: "calls_api",
7698
+ label: call.isConcat ? `${methodTag} fetch with concat: ${call.url}` : `${methodTag} fetch with template: ${call.url}`,
7699
+ confidence: call.isConcat ? "low" : "medium"
7700
+ });
7701
+ continue;
7702
+ }
7703
+ if (result.kind === "external") {
7704
+ externalCount++;
7705
+ if (!includeExternal) continue;
7706
+ flaggedEdges.push({
7707
+ source: entry.nodeId,
7708
+ target: "EXTERNAL",
7709
+ type: "calls_external",
7710
+ label: `${methodTag} external fetch: ${call.url}`,
7711
+ confidence: "high"
7712
+ });
7713
+ continue;
7714
+ }
7715
+ unresolvedCount++;
7716
+ flaggedEdges.push({
7717
+ source: entry.nodeId,
7718
+ target: "UNRESOLVED",
7719
+ type: "calls_api",
7720
+ label: `${methodTag} fetch to unknown path: ${result.normalizedUrl}`,
7721
+ confidence: "medium"
7722
+ });
7723
+ }
7724
+ }
7725
+ return {
7726
+ cross_refs: crossRefs,
7727
+ flagged_edges: flaggedEdges,
7728
+ warnings: [],
7729
+ patterns: {
7730
+ api_call_detection: {
7731
+ resolved: resolvedCount,
7732
+ dynamic: dynamicCount,
7733
+ unresolved: unresolvedCount,
7734
+ external: externalCount
7735
+ }
7736
+ }
7737
+ };
7738
+ }
7739
+ };
7740
+
7741
+ // src/server/graph/parsers/crosslayer/api-annotations.ts
7742
+ var import_node_fs6 = require("node:fs");
7743
+ var import_node_path6 = require("node:path");
7744
+ var API_ANNOTATION_RE = /@api\s+(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+(\/\S+)/g;
7745
+ function walk3(dir, exts) {
7746
+ if (!(0, import_node_fs6.existsSync)(dir)) return [];
7747
+ const results = [];
7748
+ for (const entry of (0, import_node_fs6.readdirSync)(dir, { withFileTypes: true })) {
7749
+ if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
7750
+ const full = (0, import_node_path6.join)(dir, entry.name);
7751
+ if (entry.isDirectory()) {
7752
+ results.push(...walk3(full, exts));
7753
+ } else if (exts.includes((0, import_node_path6.extname)(entry.name))) {
7754
+ results.push(full);
7755
+ }
7756
+ }
7757
+ return results;
7758
+ }
7759
+ function toNodeId2(srcDir, absPath) {
7760
+ return (0, import_node_path6.relative)(srcDir, absPath).replace(/\\/g, "/");
7761
+ }
7762
+ var apiAnnotationsParser = {
7763
+ id: "api-annotations",
7764
+ layer: "crosslayer",
7765
+ detect(rootDir) {
7766
+ return (0, import_node_fs6.existsSync)((0, import_node_path6.join)(rootDir, "src"));
7767
+ },
7768
+ generate(rootDir, layerOutputs) {
7769
+ const apiOutput = layerOutputs.get("api");
7770
+ if (!apiOutput) {
7771
+ return { cross_refs: [], flagged_edges: [], warnings: [] };
7772
+ }
7773
+ const uiOutput = layerOutputs.get("ui");
7774
+ const uiNodeIds = new Set(uiOutput?.nodes.map((n) => n.id) ?? []);
7775
+ const apiRoutes = loadApiRoutesFromOutput(apiOutput);
7776
+ const apiPathMap = buildApiPathMap(apiRoutes);
7777
+ const srcDir = (0, import_node_path6.join)(rootDir, "src");
7778
+ const files = walk3(srcDir, [".ts", ".tsx"]);
7779
+ const crossRefs = [];
7780
+ const flaggedEdges = [];
7781
+ const seen = /* @__PURE__ */ new Set();
7782
+ for (const absPath of files) {
7783
+ const content = (0, import_node_fs6.readFileSync)(absPath, "utf-8");
7784
+ const sourceId = toNodeId2(srcDir, absPath);
7785
+ if (!uiNodeIds.has(sourceId)) continue;
7786
+ let match;
7787
+ API_ANNOTATION_RE.lastIndex = 0;
7788
+ while ((match = API_ANNOTATION_RE.exec(content)) !== null) {
7789
+ const method = match[1];
7790
+ const urlPath = match[2];
7791
+ const result = resolveUrlPath(urlPath, apiPathMap, apiRoutes);
7792
+ if (result.kind === "resolved" && result.nodeId) {
7793
+ const key = `${sourceId}|${result.nodeId}|calls_api`;
7794
+ if (seen.has(key)) continue;
7795
+ seen.add(key);
7796
+ crossRefs.push({
7797
+ source: sourceId,
7798
+ target: result.nodeId,
7799
+ type: "calls_api",
7800
+ layer: "api"
7801
+ });
7802
+ } else {
7803
+ flaggedEdges.push({
7804
+ source: sourceId,
7805
+ target: "UNRESOLVED",
7806
+ type: "annotation_unresolved",
7807
+ label: `@api ${method} ${urlPath} \u2014 no matching API route found`,
7808
+ confidence: "high"
7809
+ });
7810
+ }
7811
+ }
7812
+ }
7813
+ return {
7814
+ cross_refs: crossRefs,
7815
+ flagged_edges: flaggedEdges,
7816
+ warnings: [],
7817
+ patterns: {
7818
+ annotations_found: crossRefs.length + flaggedEdges.length,
7819
+ annotations_resolved: crossRefs.length,
7820
+ annotations_unresolved: flaggedEdges.length
7821
+ }
7822
+ };
7823
+ }
7824
+ };
7825
+
7826
+ // src/server/graph/parsers/crosslayer/url-literal-scanner.ts
7827
+ var import_node_fs7 = require("node:fs");
7828
+ var import_node_path7 = require("node:path");
7829
+ var URL_LITERAL_RE = /['"`](\/api\/[^'"`\s]+?)['"`]/g;
7830
+ function walk4(dir, exts) {
7831
+ if (!(0, import_node_fs7.existsSync)(dir)) return [];
7832
+ const results = [];
7833
+ for (const entry of (0, import_node_fs7.readdirSync)(dir, { withFileTypes: true })) {
7834
+ if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
7835
+ const full = (0, import_node_path7.join)(dir, entry.name);
7836
+ if (entry.isDirectory()) {
7837
+ results.push(...walk4(full, exts));
7838
+ } else if (exts.includes((0, import_node_path7.extname)(entry.name))) {
7839
+ results.push(full);
7840
+ }
7841
+ }
7842
+ return results;
7843
+ }
7844
+ function toNodeId3(srcDir, absPath) {
7845
+ return (0, import_node_path7.relative)(srcDir, absPath).replace(/\\/g, "/");
7846
+ }
7847
+ var urlLiteralScannerParser = {
7848
+ id: "url-literal-scanner",
7849
+ layer: "crosslayer",
7850
+ detect(rootDir) {
7851
+ return (0, import_node_fs7.existsSync)((0, import_node_path7.join)(rootDir, "src"));
7852
+ },
7853
+ generate(rootDir, layerOutputs) {
7854
+ const apiOutput = layerOutputs.get("api");
7855
+ if (!apiOutput) {
7856
+ return { cross_refs: [], flagged_edges: [], warnings: [] };
7857
+ }
7858
+ const uiOutput = layerOutputs.get("ui");
7859
+ const uiNodeIds = new Set(uiOutput?.nodes.map((n) => n.id) ?? []);
7860
+ const apiRoutes = loadApiRoutesFromOutput(apiOutput);
7861
+ const apiPathMap = buildApiPathMap(apiRoutes);
7862
+ const srcDir = (0, import_node_path7.join)(rootDir, "src");
7863
+ const clientDir = (0, import_node_path7.join)(srcDir, "client");
7864
+ const appDir = (0, import_node_path7.join)(srcDir, "app");
7865
+ const files = [
7866
+ ...walk4(clientDir, [".ts", ".tsx"]),
7867
+ ...walk4(appDir, [".ts", ".tsx"])
7868
+ ];
7869
+ const crossRefs = [];
7870
+ const seen = /* @__PURE__ */ new Set();
7871
+ for (const absPath of files) {
7872
+ const sourceId = toNodeId3(srcDir, absPath);
7873
+ if (!uiNodeIds.has(sourceId)) continue;
7874
+ const content = (0, import_node_fs7.readFileSync)(absPath, "utf-8");
7875
+ let match;
7876
+ URL_LITERAL_RE.lastIndex = 0;
7877
+ while ((match = URL_LITERAL_RE.exec(content)) !== null) {
7878
+ const urlPath = match[1];
7879
+ const result = resolveUrlPath(urlPath, apiPathMap, apiRoutes);
7880
+ if (result.kind === "resolved" && result.nodeId) {
7881
+ const key = `${sourceId}|${result.nodeId}|references_api`;
7882
+ if (seen.has(key)) continue;
7883
+ seen.add(key);
7884
+ crossRefs.push({
7885
+ source: sourceId,
7886
+ target: result.nodeId,
7887
+ type: "references_api",
7888
+ layer: "api"
7889
+ });
7890
+ }
7891
+ }
7892
+ }
7893
+ return {
7894
+ cross_refs: crossRefs,
7895
+ flagged_edges: [],
7896
+ warnings: [],
7897
+ patterns: {
7898
+ url_literals_resolved: crossRefs.length
7899
+ }
7900
+ };
7901
+ }
7902
+ };
7903
+
7904
+ // src/server/graph/core/parser-registry.ts
7905
+ var ParserRegistry = class {
7906
+ constructor() {
7907
+ this.parsers = /* @__PURE__ */ new Map();
7908
+ this.ids = /* @__PURE__ */ new Set();
7909
+ }
7910
+ register(parser) {
7911
+ if (this.ids.has(parser.id)) {
7912
+ throw new Error(`Duplicate parser id: ${parser.id}`);
7913
+ }
7914
+ this.ids.add(parser.id);
7915
+ const list = this.parsers.get(parser.layer) ?? [];
7916
+ list.push(parser);
7917
+ this.parsers.set(parser.layer, list);
7918
+ }
7919
+ getParsers(layer) {
7920
+ return this.parsers.get(layer) ?? [];
7921
+ }
7922
+ getCrossLayerParsers() {
7923
+ return this.parsers.get("crosslayer") ?? [];
7924
+ }
7925
+ getAll() {
7926
+ const all = [];
7927
+ for (const list of this.parsers.values()) all.push(...list);
7928
+ return all;
7929
+ }
7930
+ };
7931
+ function registerBuiltins(registry, disabled) {
7932
+ const builtins = [
7933
+ reactNextjsParser,
7934
+ nextjsRoutesParser,
7935
+ prismaSchemaParser,
7936
+ fetchResolverParser,
7937
+ apiAnnotationsParser,
7938
+ urlLiteralScannerParser
7939
+ ];
7940
+ for (const parser of builtins) {
7941
+ if (disabled.has(parser.id)) continue;
7942
+ registry.register(parser);
7943
+ }
7944
+ }
7945
+ function loadCustomParsers(registry, config, rootDir, disabled) {
7946
+ for (const entry of config.parsers?.custom ?? []) {
7947
+ try {
7948
+ const absPath = (0, import_node_path8.resolve)(rootDir, entry.path);
7949
+ const mod = require(absPath);
7950
+ const parser = "default" in mod ? mod.default : mod;
7951
+ if (disabled.has(parser.id)) continue;
7952
+ if (parser.layer !== entry.layer) {
7953
+ process.stderr.write(
7954
+ `[launch-chart] custom parser "${parser.id}" declares layer "${parser.layer}" but config says "${entry.layer}" \u2014 using parser's layer
7955
+ `
7956
+ );
7957
+ }
7958
+ registry.register(parser);
7959
+ } catch (err2) {
7960
+ process.stderr.write(`[launch-chart] failed to load custom parser from ${entry.path}: ${err2}
7961
+ `);
7962
+ }
7963
+ }
7964
+ }
7965
+ function createRegistry(config, rootDir) {
7966
+ const registry = new ParserRegistry();
7967
+ const disabled = new Set(config.parsers?.disabled ?? []);
7968
+ registerBuiltins(registry, disabled);
7969
+ loadCustomParsers(registry, config, rootDir, disabled);
7970
+ return registry;
7971
+ }
7972
+
7973
+ // src/server/graph/core/merge.ts
7974
+ function mergeGraphOutputs(outputs, layer) {
7975
+ if (outputs.length === 0) {
7976
+ return {
7977
+ metadata: { generated: (/* @__PURE__ */ new Date()).toISOString(), scope: "", layer },
7978
+ nodes: [],
7979
+ edges: [],
7980
+ cross_refs: [],
7981
+ contradictions: [],
7982
+ warnings: [],
7983
+ flagged_edges: []
7984
+ };
7985
+ }
7986
+ if (outputs.length === 1) return outputs[0];
7987
+ const seenNodes = /* @__PURE__ */ new Set();
7988
+ const seenEdges = /* @__PURE__ */ new Set();
7989
+ const seenCrossRefs = /* @__PURE__ */ new Set();
7990
+ const mergedNodes = [];
7991
+ const mergedEdges = [];
7992
+ const mergedCrossRefs = [];
7993
+ const mergedContradictions = [];
7994
+ const mergedWarnings = [];
7995
+ const mergedFlagged = [];
7996
+ const parserIds = [];
7997
+ for (const output of outputs) {
7998
+ if (output.metadata.parser) {
7999
+ parserIds.push(String(output.metadata.parser));
8000
+ }
8001
+ for (const node of output.nodes) {
8002
+ if (seenNodes.has(node.id)) {
8003
+ mergedWarnings.push({
8004
+ type: "merge_conflict",
8005
+ detail: `Node "${node.id}" produced by multiple parsers; keeping first`
8006
+ });
8007
+ continue;
8008
+ }
8009
+ seenNodes.add(node.id);
8010
+ mergedNodes.push(node);
8011
+ }
8012
+ for (const edge of output.edges) {
8013
+ const key = `${edge.source}|${edge.target}|${edge.type}`;
8014
+ if (seenEdges.has(key)) continue;
8015
+ seenEdges.add(key);
8016
+ mergedEdges.push(edge);
8017
+ }
8018
+ for (const ref of output.cross_refs) {
8019
+ const key = `${ref.source}|${ref.target}|${ref.type}`;
8020
+ if (seenCrossRefs.has(key)) continue;
8021
+ seenCrossRefs.add(key);
8022
+ mergedCrossRefs.push(ref);
8023
+ }
8024
+ mergedContradictions.push(...output.contradictions);
8025
+ mergedWarnings.push(...output.warnings);
8026
+ mergedFlagged.push(...output.flagged_edges);
8027
+ }
8028
+ const metadata = {
8029
+ ...outputs[0].metadata,
8030
+ generated: (/* @__PURE__ */ new Date()).toISOString(),
8031
+ parsers: parserIds
8032
+ };
8033
+ return {
8034
+ metadata,
8035
+ nodes: mergedNodes,
8036
+ edges: mergedEdges,
8037
+ cross_refs: mergedCrossRefs,
8038
+ contradictions: mergedContradictions,
8039
+ warnings: mergedWarnings,
8040
+ flagged_edges: mergedFlagged,
8041
+ patterns: outputs[0].patterns
8042
+ };
8043
+ }
8044
+ function dedupCrossRefs(refs) {
8045
+ const seen = /* @__PURE__ */ new Set();
8046
+ const result = [];
8047
+ for (const ref of refs) {
8048
+ const key = `${ref.source}|${ref.target}|${ref.type}`;
8049
+ if (seen.has(key)) continue;
8050
+ seen.add(key);
8051
+ result.push(ref);
8052
+ }
8053
+ return result;
8054
+ }
8055
+ function applyCrossLayerResults(uiOutput, results, primaryId) {
8056
+ const allCrossRefs = [...uiOutput.cross_refs];
8057
+ const allFlagged = [...uiOutput.flagged_edges];
8058
+ const allWarnings = [...uiOutput.warnings];
8059
+ const primaryResult = results.find((r) => r.parserId === primaryId);
8060
+ const secondaryResults = results.filter((r) => r.parserId !== primaryId);
8061
+ if (primaryResult) {
8062
+ allCrossRefs.push(...primaryResult.output.cross_refs);
8063
+ allFlagged.push(...primaryResult.output.flagged_edges);
8064
+ allWarnings.push(...primaryResult.output.warnings);
8065
+ }
8066
+ const primarySet = new Set(
8067
+ (primaryResult?.output.cross_refs ?? []).map((r) => `${r.source}|${r.target}|${r.type}`)
8068
+ );
8069
+ for (const sec of secondaryResults) {
8070
+ for (const ref of sec.output.cross_refs) {
8071
+ const key = `${ref.source}|${ref.target}|${ref.type}`;
8072
+ if (primarySet.has(key)) {
8073
+ allCrossRefs.push(ref);
8074
+ } else {
8075
+ allFlagged.push({
8076
+ source: ref.source,
8077
+ target: ref.target,
8078
+ type: "out_of_pattern",
8079
+ label: `API call detected by ${sec.parserId} but not by primary (${primaryId})`,
8080
+ confidence: "medium"
8081
+ });
8082
+ allCrossRefs.push(ref);
8083
+ }
8084
+ }
8085
+ allFlagged.push(...sec.output.flagged_edges);
8086
+ allWarnings.push(...sec.output.warnings);
8087
+ }
8088
+ return {
8089
+ ...uiOutput,
8090
+ cross_refs: dedupCrossRefs(allCrossRefs),
8091
+ flagged_edges: allFlagged,
8092
+ warnings: allWarnings
8093
+ };
8094
+ }
8095
+
7291
8096
  // src/server/graph/core/graph-builder.ts
7292
- var ALL_PARSERS = [
7293
- reactNextjsParser,
7294
- nextjsRoutesParser,
7295
- prismaSchemaParser
7296
- ];
7297
- function getParser(layer) {
7298
- return ALL_PARSERS.find((p) => p.layer === layer);
8097
+ function readGraphFromDisk(rootDir, layer) {
8098
+ const filePath = (0, import_node_path9.join)(rootDir, ".launchsecure", "graphs", `${layer}.json`);
8099
+ if (!(0, import_node_fs8.existsSync)(filePath)) return null;
8100
+ try {
8101
+ return JSON.parse((0, import_node_fs8.readFileSync)(filePath, "utf-8"));
8102
+ } catch {
8103
+ return null;
8104
+ }
7299
8105
  }
7300
8106
  function generateLayer(rootDir, layer) {
7301
- const parser = getParser(layer);
7302
- if (!parser) return null;
7303
- if (!parser.detect(rootDir)) return null;
7304
- const output = parser.generate(rootDir);
8107
+ const config = loadConfig(rootDir);
8108
+ const registry = createRegistry(config, rootDir);
8109
+ const parsers = registry.getParsers(layer);
8110
+ const outputs = [];
8111
+ for (const parser of parsers) {
8112
+ if (!parser.detect(rootDir)) continue;
8113
+ outputs.push(parser.generate(rootDir));
8114
+ }
8115
+ if (outputs.length === 0) return null;
8116
+ let merged = outputs.length === 1 ? outputs[0] : mergeGraphOutputs(outputs, layer);
8117
+ if (layer === "ui") {
8118
+ const layerOutputs = /* @__PURE__ */ new Map();
8119
+ layerOutputs.set("ui", merged);
8120
+ for (const otherLayer of ["api", "db"]) {
8121
+ const existing = readGraphFromDisk(rootDir, otherLayer);
8122
+ if (existing) layerOutputs.set(otherLayer, existing);
8123
+ }
8124
+ const crossParsers = registry.getCrossLayerParsers();
8125
+ const primaryId = config.parsers?.primary?.crosslayer ?? crossParsers[0]?.id ?? null;
8126
+ const crossResults = crossParsers.filter((p) => p.detect(rootDir)).map((p) => ({ parserId: p.id, output: p.generate(rootDir, layerOutputs) }));
8127
+ if (crossResults.length > 0) {
8128
+ merged = applyCrossLayerResults(merged, crossResults, primaryId);
8129
+ }
8130
+ }
7305
8131
  return {
7306
8132
  layer,
7307
- output,
7308
- nodeCount: output.nodes.length,
7309
- edgeCount: output.edges.length
8133
+ output: merged,
8134
+ nodeCount: merged.nodes.length,
8135
+ edgeCount: merged.edges.length
7310
8136
  };
7311
8137
  }
7312
8138
  function generateAll(rootDir) {
7313
- const layers = ["ui", "api", "db"];
8139
+ const config = loadConfig(rootDir);
8140
+ const registry = createRegistry(config, rootDir);
8141
+ const layerOrder = ["api", "db", "ui"];
8142
+ const layerOutputs = /* @__PURE__ */ new Map();
7314
8143
  const results = [];
7315
- for (const layer of layers) {
7316
- const result = generateLayer(rootDir, layer);
7317
- if (result) results.push(result);
8144
+ for (const layer of layerOrder) {
8145
+ const parsers = registry.getParsers(layer);
8146
+ const outputs = [];
8147
+ for (const parser of parsers) {
8148
+ if (!parser.detect(rootDir)) continue;
8149
+ outputs.push(parser.generate(rootDir));
8150
+ }
8151
+ if (outputs.length === 0) continue;
8152
+ const merged = outputs.length === 1 ? outputs[0] : mergeGraphOutputs(outputs, layer);
8153
+ layerOutputs.set(layer, merged);
8154
+ results.push({
8155
+ layer,
8156
+ output: merged,
8157
+ nodeCount: merged.nodes.length,
8158
+ edgeCount: merged.edges.length
8159
+ });
7318
8160
  }
7319
- return results;
8161
+ const crossParsers = registry.getCrossLayerParsers();
8162
+ const primaryId = config.parsers?.primary?.crosslayer ?? crossParsers[0]?.id ?? null;
8163
+ const crossResults = crossParsers.filter((p) => p.detect(rootDir)).map((p) => ({ parserId: p.id, output: p.generate(rootDir, layerOutputs) }));
8164
+ if (crossResults.length > 0 && layerOutputs.has("ui")) {
8165
+ const uiOutput = layerOutputs.get("ui");
8166
+ const merged = applyCrossLayerResults(uiOutput, crossResults, primaryId);
8167
+ layerOutputs.set("ui", merged);
8168
+ const uiResult = results.find((r) => r.layer === "ui");
8169
+ if (uiResult) {
8170
+ uiResult.output = merged;
8171
+ uiResult.nodeCount = merged.nodes.length;
8172
+ uiResult.edgeCount = merged.edges.length;
8173
+ }
8174
+ }
8175
+ const byLayer = new Map(results.map((r) => [r.layer, r]));
8176
+ return ["ui", "api", "db"].map((l) => byLayer.get(l)).filter((r) => !!r);
7320
8177
  }
7321
8178
 
7322
8179
  // src/server/graph/index.ts
@@ -7324,23 +8181,23 @@ var GRAPHS_DIR = ".launchsecure/graphs";
7324
8181
  var LAYERS = ["ui", "api", "db"];
7325
8182
  var graphCache = /* @__PURE__ */ new Map();
7326
8183
  function graphsDir(rootDir) {
7327
- return (0, import_node_path5.join)(rootDir, GRAPHS_DIR);
8184
+ return (0, import_node_path10.join)(rootDir, GRAPHS_DIR);
7328
8185
  }
7329
8186
  function graphFilePath(rootDir, layer) {
7330
- return (0, import_node_path5.join)(graphsDir(rootDir), `${layer}.json`);
8187
+ return (0, import_node_path10.join)(graphsDir(rootDir), `${layer}.json`);
7331
8188
  }
7332
8189
  function invalidateCache(filePath) {
7333
8190
  graphCache.delete(filePath);
7334
8191
  }
7335
8192
  function readGraph(rootDir, layer) {
7336
8193
  const filePath = graphFilePath(rootDir, layer);
7337
- if (!(0, import_node_fs5.existsSync)(filePath)) return null;
7338
- const stat = (0, import_node_fs5.statSync)(filePath);
8194
+ if (!(0, import_node_fs9.existsSync)(filePath)) return null;
8195
+ const stat = (0, import_node_fs9.statSync)(filePath);
7339
8196
  const cached = graphCache.get(filePath);
7340
8197
  if (cached && cached.mtimeMs === stat.mtimeMs) {
7341
8198
  return cached.graph;
7342
8199
  }
7343
- const content = (0, import_node_fs5.readFileSync)(filePath, "utf-8");
8200
+ const content = (0, import_node_fs9.readFileSync)(filePath, "utf-8");
7344
8201
  const graph = JSON.parse(content);
7345
8202
  graphCache.set(filePath, { mtimeMs: stat.mtimeMs, graph });
7346
8203
  return graph;
@@ -7355,11 +8212,11 @@ function readAllGraphs(rootDir) {
7355
8212
  }
7356
8213
  function generateGraph(rootDir, layer) {
7357
8214
  const dir = graphsDir(rootDir);
7358
- (0, import_node_fs5.mkdirSync)(dir, { recursive: true });
8215
+ (0, import_node_fs9.mkdirSync)(dir, { recursive: true });
7359
8216
  const results = layer ? [generateLayer(rootDir, layer)].filter((r) => r !== null) : generateAll(rootDir);
7360
8217
  for (const result of results) {
7361
8218
  const filePath = graphFilePath(rootDir, result.layer);
7362
- (0, import_node_fs5.writeFileSync)(filePath, JSON.stringify(result.output, null, 2) + "\n", "utf-8");
8219
+ (0, import_node_fs9.writeFileSync)(filePath, JSON.stringify(result.output, null, 2) + "\n", "utf-8");
7363
8220
  invalidateCache(filePath);
7364
8221
  }
7365
8222
  return results;
@@ -7421,8 +8278,77 @@ function handleGraphCommand(subcommand, args) {
7421
8278
  }
7422
8279
 
7423
8280
  // src/server/graph-mcp.ts
7424
- var import_node_fs6 = require("node:fs");
7425
- var import_node_path6 = require("node:path");
8281
+ var import_node_fs11 = require("node:fs");
8282
+ var import_node_path12 = require("node:path");
8283
+ var import_node_child_process2 = require("node:child_process");
8284
+ var import_node_os2 = require("node:os");
8285
+
8286
+ // src/server/lockfile.ts
8287
+ var import_node_child_process = require("node:child_process");
8288
+ var import_node_fs10 = require("node:fs");
8289
+ var import_node_os = require("node:os");
8290
+ var import_node_path11 = require("node:path");
8291
+ function lockDir() {
8292
+ return (0, import_node_path11.join)((0, import_node_os.homedir)(), ".launchsecure");
8293
+ }
8294
+ function lockPath() {
8295
+ return (0, import_node_path11.join)(lockDir(), "launch-chart.lock");
8296
+ }
8297
+ function readLock() {
8298
+ const p = lockPath();
8299
+ if (!(0, import_node_fs10.existsSync)(p)) return null;
8300
+ try {
8301
+ const data = JSON.parse((0, import_node_fs10.readFileSync)(p, "utf-8"));
8302
+ if (typeof data.pid !== "number" || typeof data.port !== "number") return null;
8303
+ return data;
8304
+ } catch {
8305
+ return null;
8306
+ }
8307
+ }
8308
+ function isPidAlive(pid) {
8309
+ try {
8310
+ process.kill(pid, 0);
8311
+ return true;
8312
+ } catch {
8313
+ return false;
8314
+ }
8315
+ }
8316
+ function getListenerPid(port) {
8317
+ try {
8318
+ const out = (0, import_node_child_process.execFileSync)("lsof", ["-nP", "-iTCP:" + port, "-sTCP:LISTEN", "-t"], {
8319
+ encoding: "utf-8",
8320
+ stdio: ["ignore", "pipe", "ignore"],
8321
+ timeout: 500
8322
+ }).trim();
8323
+ if (!out) return null;
8324
+ const pid = parseInt(out.split("\n")[0], 10);
8325
+ return Number.isFinite(pid) ? pid : null;
8326
+ } catch {
8327
+ return null;
8328
+ }
8329
+ }
8330
+ function getLiveLock() {
8331
+ const lock = readLock();
8332
+ if (!lock) return null;
8333
+ const listenerPid = getListenerPid(lock.port);
8334
+ const live = listenerPid !== null ? listenerPid === lock.pid : isPidAlive(lock.pid);
8335
+ if (!live) {
8336
+ try {
8337
+ (0, import_node_fs10.unlinkSync)(lockPath());
8338
+ } catch {
8339
+ }
8340
+ return null;
8341
+ }
8342
+ return lock;
8343
+ }
8344
+ function clearLock() {
8345
+ try {
8346
+ (0, import_node_fs10.unlinkSync)(lockPath());
8347
+ } catch {
8348
+ }
8349
+ }
8350
+
8351
+ // src/server/graph-mcp.ts
7426
8352
  var SERVER_INFO = {
7427
8353
  name: "launchsecure-graph",
7428
8354
  version: "0.0.1"
@@ -7548,6 +8474,45 @@ Returns: { pattern, filter, files_searched, total_matches, matches: [{file, line
7548
8474
  },
7549
8475
  required: ["layer", "pattern"]
7550
8476
  }
8477
+ },
8478
+ {
8479
+ name: "chart_server_status",
8480
+ description: `Check whether the launch-chart UI server is running. Returns: { running: boolean, url?: string, port?: number, pid?: number, startedAt?: string, cwd?: string }.
8481
+
8482
+ Use this when the user asks "is the chart running", "show me the project graph UI", "where's the chart", etc.`,
8483
+ inputSchema: {
8484
+ type: "object",
8485
+ properties: {}
8486
+ }
8487
+ },
8488
+ {
8489
+ name: "start_chart_server",
8490
+ 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.',
8491
+ inputSchema: {
8492
+ type: "object",
8493
+ properties: {
8494
+ port: {
8495
+ type: "number",
8496
+ description: "Port to bind the server to. Defaults to 52819 with automatic fallback if in use."
8497
+ }
8498
+ }
8499
+ }
8500
+ },
8501
+ {
8502
+ name: "stop_chart_server",
8503
+ 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.',
8504
+ inputSchema: {
8505
+ type: "object",
8506
+ properties: {}
8507
+ }
8508
+ },
8509
+ {
8510
+ name: "detect_project_stack",
8511
+ 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.",
8512
+ inputSchema: {
8513
+ type: "object",
8514
+ properties: {}
8515
+ }
7551
8516
  }
7552
8517
  ];
7553
8518
  function matchesSearch(node, query) {
@@ -7900,9 +8865,9 @@ function handleReadGraph(args) {
7900
8865
  return okJson(result);
7901
8866
  }
7902
8867
  function nodeToFilePath(rootDir, layer, nodeId) {
7903
- if (layer === "ui") return (0, import_node_path6.join)(rootDir, "src", nodeId);
7904
- if (layer === "api") return (0, import_node_path6.join)(rootDir, nodeId);
7905
- if (layer === "db") return (0, import_node_path6.join)(rootDir, "prisma", "schema.prisma");
8868
+ if (layer === "ui") return (0, import_node_path12.join)(rootDir, "src", nodeId);
8869
+ if (layer === "api") return (0, import_node_path12.join)(rootDir, nodeId);
8870
+ if (layer === "db") return (0, import_node_path12.join)(rootDir, "prisma", "schema.prisma");
7906
8871
  return null;
7907
8872
  }
7908
8873
  function handleGrepNodes(args) {
@@ -7962,11 +8927,11 @@ function handleGrepNodes(args) {
7962
8927
  let filesSearched = 0;
7963
8928
  let truncated = false;
7964
8929
  for (const [filePath, nodeId] of filePaths) {
7965
- if (!(0, import_node_fs6.existsSync)(filePath)) continue;
8930
+ if (!(0, import_node_fs11.existsSync)(filePath)) continue;
7966
8931
  filesSearched++;
7967
8932
  let content;
7968
8933
  try {
7969
- content = (0, import_node_fs6.readFileSync)(filePath, "utf-8");
8934
+ content = (0, import_node_fs11.readFileSync)(filePath, "utf-8");
7970
8935
  } catch {
7971
8936
  continue;
7972
8937
  }
@@ -8003,6 +8968,127 @@ function handleGrepNodes(args) {
8003
8968
  truncated
8004
8969
  });
8005
8970
  }
8971
+ function handleChartServerStatus() {
8972
+ const lock = getLiveLock();
8973
+ if (!lock) {
8974
+ return okJson({ running: false });
8975
+ }
8976
+ return okJson({
8977
+ running: true,
8978
+ url: lock.url,
8979
+ port: lock.port,
8980
+ pid: lock.pid,
8981
+ cwd: lock.cwd,
8982
+ startedAt: lock.startedAt
8983
+ });
8984
+ }
8985
+ function handleStartChartServer(args) {
8986
+ const lock = getLiveLock();
8987
+ if (lock) {
8988
+ return okJson({
8989
+ started: false,
8990
+ reason: "already_running",
8991
+ url: lock.url,
8992
+ port: lock.port,
8993
+ pid: lock.pid
8994
+ });
8995
+ }
8996
+ const entryPath = process.argv[1];
8997
+ const logDir = (0, import_node_path12.join)((0, import_node_os2.homedir)(), ".launchsecure");
8998
+ (0, import_node_fs11.mkdirSync)(logDir, { recursive: true });
8999
+ const logPath = (0, import_node_path12.join)(logDir, "launch-chart.log");
9000
+ const out = (0, import_node_fs11.openSync)(logPath, "a");
9001
+ const err2 = (0, import_node_fs11.openSync)(logPath, "a");
9002
+ const portArgs = args.port ? ["--port", String(args.port)] : [];
9003
+ const child = (0, import_node_child_process2.spawn)(process.execPath, [entryPath, "serve", ...portArgs], {
9004
+ detached: true,
9005
+ stdio: ["ignore", out, err2],
9006
+ env: { ...process.env, LAUNCH_CHART_AUTOSERVE: "" }
9007
+ });
9008
+ child.unref();
9009
+ return okJson({
9010
+ started: true,
9011
+ pid: child.pid,
9012
+ logPath
9013
+ });
9014
+ }
9015
+ function handleStopChartServer() {
9016
+ const lock = getLiveLock();
9017
+ if (!lock) {
9018
+ return okJson({ stopped: false, reason: "not_running" });
9019
+ }
9020
+ try {
9021
+ process.kill(lock.pid, "SIGTERM");
9022
+ return okJson({ stopped: true, pid: lock.pid });
9023
+ } catch (e) {
9024
+ const code = e.code;
9025
+ if (code === "ESRCH") {
9026
+ clearLock();
9027
+ return okJson({ stopped: true, pid: lock.pid, note: "process was already gone, lock cleaned up" });
9028
+ }
9029
+ return okJson({ stopped: false, reason: `kill failed: ${code ?? e}` });
9030
+ }
9031
+ }
9032
+ function handleDetectProjectStack() {
9033
+ const rootDir = findProjectRoot(process.cwd());
9034
+ const parsers = [
9035
+ { id: "react-nextjs", layer: "ui", detected: reactNextjsParser.detect(rootDir) },
9036
+ { id: "nextjs-routes", layer: "api", detected: nextjsRoutesParser.detect(rootDir) },
9037
+ { id: "prisma-schema", layer: "db", detected: prismaSchemaParser.detect(rootDir) }
9038
+ ];
9039
+ const config = loadConfig(rootDir);
9040
+ let stats = { calls_api: 0, references_api: 0, out_of_pattern: 0, annotations: 0 };
9041
+ const uiGraph = readGraph(rootDir, "ui");
9042
+ if (uiGraph) {
9043
+ for (const ref of uiGraph.cross_refs ?? []) {
9044
+ if (ref.type === "calls_api") stats.calls_api++;
9045
+ if (ref.type === "references_api") stats.references_api++;
9046
+ }
9047
+ for (const f of uiGraph.flagged_edges ?? []) {
9048
+ if (f.type === "out_of_pattern") stats.out_of_pattern++;
9049
+ }
9050
+ }
9051
+ const srcDir = (0, import_node_path12.join)(rootDir, "src");
9052
+ if ((0, import_node_fs11.existsSync)(srcDir)) {
9053
+ const scanDir = (dir) => {
9054
+ if (!(0, import_node_fs11.existsSync)(dir)) return;
9055
+ for (const entry of (0, import_node_fs11.readdirSync)(dir, { withFileTypes: true })) {
9056
+ if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
9057
+ const full = (0, import_node_path12.join)(dir, entry.name);
9058
+ if (entry.isDirectory()) {
9059
+ scanDir(full);
9060
+ continue;
9061
+ }
9062
+ if (![".ts", ".tsx"].includes((0, import_node_path12.extname)(entry.name))) continue;
9063
+ try {
9064
+ const content = (0, import_node_fs11.readFileSync)(full, "utf-8");
9065
+ const matches = content.match(/@api\s+(GET|POST|PUT|DELETE|PATCH)\s+\/\S+/g);
9066
+ if (matches) stats.annotations += matches.length;
9067
+ } catch {
9068
+ }
9069
+ }
9070
+ };
9071
+ scanDir(srcDir);
9072
+ }
9073
+ let recommendedPrimary = "fetch-resolver";
9074
+ if (stats.annotations > 0 && stats.annotations >= stats.calls_api) {
9075
+ recommendedPrimary = "api-annotations";
9076
+ } else if (stats.calls_api === 0 && stats.references_api > 0) {
9077
+ recommendedPrimary = "url-literal-scanner";
9078
+ }
9079
+ return okJson({
9080
+ parsers,
9081
+ crosslayer_parsers: [
9082
+ { id: "fetch-resolver", description: "Detects direct fetch()/api.get() calls with inline URLs" },
9083
+ { id: "api-annotations", description: "Scans for @api METHOD /path annotations in JSDoc/comments" },
9084
+ { id: "url-literal-scanner", description: "Finds /api/... string literals as fallback detection" }
9085
+ ],
9086
+ stats,
9087
+ recommended_primary: recommendedPrimary,
9088
+ current_config: Object.keys(config).length > 0 ? config : null,
9089
+ config_path: ".launchchart.json"
9090
+ });
9091
+ }
8006
9092
  function send(msg) {
8007
9093
  process.stdout.write(JSON.stringify(msg) + "\n");
8008
9094
  }
@@ -8046,6 +9132,22 @@ function handleMessage(msg) {
8046
9132
  respond(id ?? null, handleGrepNodes(args));
8047
9133
  return;
8048
9134
  }
9135
+ if (toolName === "chart_server_status") {
9136
+ respond(id ?? null, handleChartServerStatus());
9137
+ return;
9138
+ }
9139
+ if (toolName === "start_chart_server") {
9140
+ respond(id ?? null, handleStartChartServer(args));
9141
+ return;
9142
+ }
9143
+ if (toolName === "stop_chart_server") {
9144
+ respond(id ?? null, handleStopChartServer());
9145
+ return;
9146
+ }
9147
+ if (toolName === "detect_project_stack") {
9148
+ respond(id ?? null, handleDetectProjectStack());
9149
+ return;
9150
+ }
8049
9151
  respondError(id ?? null, -32601, `Unknown tool: ${toolName}`);
8050
9152
  return;
8051
9153
  }
@@ -8143,7 +9245,7 @@ function parseArgs() {
8143
9245
  return { port, token, serverUrl: LAUNCHSECURE_URL, subcommand };
8144
9246
  }
8145
9247
  function tryListen(server, port, maxRetries = 10) {
8146
- return new Promise((resolve, reject) => {
9248
+ return new Promise((resolve2, reject) => {
8147
9249
  let attempts = 0;
8148
9250
  function attempt(p) {
8149
9251
  server.once("error", (err2) => {
@@ -8154,7 +9256,7 @@ function tryListen(server, port, maxRetries = 10) {
8154
9256
  reject(err2);
8155
9257
  }
8156
9258
  });
8157
- server.listen(p, () => resolve(p));
9259
+ server.listen(p, () => resolve2(p));
8158
9260
  }
8159
9261
  attempt(port);
8160
9262
  });
@@ -8175,7 +9277,7 @@ function saveCredentials(creds) {
8175
9277
  });
8176
9278
  }
8177
9279
  function verifyToken(serverUrl, token) {
8178
- return new Promise((resolve) => {
9280
+ return new Promise((resolve2) => {
8179
9281
  const url = new URL("/api/mcp/verify", serverUrl);
8180
9282
  const body = JSON.stringify({ token });
8181
9283
  const mod = url.protocol === "https:" ? import_https.default : import_http.default;
@@ -8190,30 +9292,30 @@ function verifyToken(serverUrl, token) {
8190
9292
  res.on("data", (chunk) => data += chunk);
8191
9293
  res.on("end", () => {
8192
9294
  try {
8193
- resolve(JSON.parse(data));
9295
+ resolve2(JSON.parse(data));
8194
9296
  } catch {
8195
- resolve({ valid: false, error: "Invalid response from server" });
9297
+ resolve2({ valid: false, error: "Invalid response from server" });
8196
9298
  }
8197
9299
  });
8198
9300
  });
8199
9301
  req.on("error", (err2) => {
8200
- resolve({ valid: false, error: `Cannot reach server: ${err2.message}` });
9302
+ resolve2({ valid: false, error: `Cannot reach server: ${err2.message}` });
8201
9303
  });
8202
9304
  req.setTimeout(1e4, () => {
8203
9305
  req.destroy();
8204
- resolve({ valid: false, error: "Connection timed out" });
9306
+ resolve2({ valid: false, error: "Connection timed out" });
8205
9307
  });
8206
9308
  req.write(body);
8207
9309
  req.end();
8208
9310
  });
8209
9311
  }
8210
9312
  function httpRequest(reqUrl, options, body, timeout = 3e4) {
8211
- return new Promise((resolve, reject) => {
9313
+ return new Promise((resolve2, reject) => {
8212
9314
  const mod = reqUrl.protocol === "https:" ? import_https.default : import_http.default;
8213
9315
  const r = mod.request(reqUrl, options, (resp) => {
8214
9316
  let data = "";
8215
9317
  resp.on("data", (chunk) => data += chunk);
8216
- resp.on("end", () => resolve({ status: resp.statusCode || 0, headers: resp.headers, body: data }));
9318
+ resp.on("end", () => resolve2({ status: resp.statusCode || 0, headers: resp.headers, body: data }));
8217
9319
  });
8218
9320
  r.on("error", reject);
8219
9321
  r.setTimeout(timeout, () => {