@launchsecure/launch-kit 0.0.4 → 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) {
@@ -6344,8 +6365,8 @@ function getTs() {
6344
6365
  var HTTP_METHODS = /* @__PURE__ */ new Set(["get", "post", "put", "patch", "delete", "head", "options"]);
6345
6366
  function parseFile(absPath) {
6346
6367
  const ts = getTs();
6347
- const content = (0, import_node_fs.readFileSync)(absPath, "utf-8");
6348
- 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);
6349
6370
  const scriptKind = ext === ".tsx" ? ts.ScriptKind.TSX : ext === ".ts" ? ts.ScriptKind.TS : ext === ".jsx" ? ts.ScriptKind.JSX : ts.ScriptKind.JS;
6350
6371
  const sourceFile = ts.createSourceFile(
6351
6372
  absPath,
@@ -6623,8 +6644,8 @@ var MUTATION_METHODS = /* @__PURE__ */ new Set([
6623
6644
  ]);
6624
6645
  function extractDbCalls(absPath) {
6625
6646
  const ts = getTs();
6626
- const content = (0, import_node_fs.readFileSync)(absPath, "utf-8");
6627
- 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);
6628
6649
  const scriptKind = ext === ".tsx" ? ts.ScriptKind.TSX : ts.ScriptKind.TS;
6629
6650
  const sourceFile = ts.createSourceFile(absPath, content, ts.ScriptTarget.Latest, true, scriptKind);
6630
6651
  const calls = [];
@@ -6653,8 +6674,8 @@ function extractDbCalls(absPath) {
6653
6674
  }
6654
6675
  function extractAuthWrappers(absPath) {
6655
6676
  const ts = getTs();
6656
- const content = (0, import_node_fs.readFileSync)(absPath, "utf-8");
6657
- 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);
6658
6679
  const scriptKind = ext === ".tsx" ? ts.ScriptKind.TSX : ts.ScriptKind.TS;
6659
6680
  const sourceFile = ts.createSourceFile(absPath, content, ts.ScriptTarget.Latest, true, scriptKind);
6660
6681
  const wrappers = /* @__PURE__ */ new Set();
@@ -6675,12 +6696,12 @@ function extractAuthWrappers(absPath) {
6675
6696
  var RENDER_TYPES = /* @__PURE__ */ new Set(["component", "ui", "layout", "context"]);
6676
6697
  function walk(dir, exts) {
6677
6698
  const results = [];
6678
- if (!(0, import_node_fs2.existsSync)(dir)) return results;
6679
- for (const entry of (0, import_node_fs2.readdirSync)(dir, { withFileTypes: true })) {
6680
- 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);
6681
6702
  if (entry.isDirectory()) {
6682
6703
  results.push(...walk(full, exts));
6683
- } else if (exts.includes((0, import_node_path2.extname)(entry.name))) {
6704
+ } else if (exts.includes((0, import_node_path3.extname)(entry.name))) {
6684
6705
  results.push(full);
6685
6706
  }
6686
6707
  }
@@ -6688,33 +6709,33 @@ function walk(dir, exts) {
6688
6709
  }
6689
6710
  function walkWithIgnore(dir, exts, ignoreDirs) {
6690
6711
  const results = [];
6691
- if (!(0, import_node_fs2.existsSync)(dir)) return results;
6692
- for (const entry of (0, import_node_fs2.readdirSync)(dir, { withFileTypes: true })) {
6712
+ if (!(0, import_node_fs3.existsSync)(dir)) return results;
6713
+ for (const entry of (0, import_node_fs3.readdirSync)(dir, { withFileTypes: true })) {
6693
6714
  if (entry.isDirectory()) {
6694
6715
  if (ignoreDirs.has(entry.name)) continue;
6695
- results.push(...walkWithIgnore((0, import_node_path2.join)(dir, entry.name), exts, ignoreDirs));
6696
- } else if (exts.includes((0, import_node_path2.extname)(entry.name))) {
6697
- results.push((0, import_node_path2.join)(dir, entry.name));
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));
6698
6719
  }
6699
6720
  }
6700
6721
  return results;
6701
6722
  }
6702
6723
  function toNodeId(srcDir, absPath) {
6703
- return (0, import_node_path2.relative)(srcDir, absPath).replace(/\\/g, "/");
6724
+ return (0, import_node_path3.relative)(srcDir, absPath).replace(/\\/g, "/");
6704
6725
  }
6705
6726
  function resolveImport(srcDir, specifier) {
6706
6727
  if (!specifier.startsWith("@/")) return null;
6707
6728
  const rel = specifier.slice(2);
6708
- const base = (0, import_node_path2.join)(srcDir, rel);
6709
- 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")]) {
6710
- 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;
6711
6732
  }
6712
6733
  return null;
6713
6734
  }
6714
6735
  function resolveRelativeImport(fromFile, specifier) {
6715
- const base = (0, import_node_path2.join)((0, import_node_path2.dirname)(fromFile), specifier);
6716
- 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")]) {
6717
- 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;
6718
6739
  }
6719
6740
  return null;
6720
6741
  }
@@ -6735,7 +6756,7 @@ function resolveBarrelMap(barrelAbsPath, parsedByPath, memo, visiting) {
6735
6756
  const resolved = resolveRelativeImport(barrelAbsPath, re.from);
6736
6757
  if (!resolved) continue;
6737
6758
  if (re.isWildcard) {
6738
- const targetBn = (0, import_node_path2.basename)(resolved);
6759
+ const targetBn = (0, import_node_path3.basename)(resolved);
6739
6760
  const targetIsBarrel = targetBn === "index.ts" || targetBn === "index.tsx";
6740
6761
  if (targetIsBarrel) {
6741
6762
  const nested = resolveBarrelMap(resolved, parsedByPath, memo, visiting);
@@ -6762,12 +6783,12 @@ function buildAllBarrelMaps(srcDir, parsedByPath) {
6762
6783
  const barrels = /* @__PURE__ */ new Map();
6763
6784
  const memo = /* @__PURE__ */ new Map();
6764
6785
  for (const [absPath, parsed] of parsedByPath) {
6765
- const bn = (0, import_node_path2.basename)(absPath);
6786
+ const bn = (0, import_node_path3.basename)(absPath);
6766
6787
  if (bn !== "index.ts" && bn !== "index.tsx") continue;
6767
6788
  if (parsed.reExports.length === 0) continue;
6768
6789
  const map = resolveBarrelMap(absPath, parsedByPath, memo, /* @__PURE__ */ new Set());
6769
6790
  if (map.size > 0) {
6770
- const barrelId = (0, import_node_path2.relative)(srcDir, (0, import_node_path2.dirname)(absPath)).replace(/\\/g, "/");
6791
+ const barrelId = (0, import_node_path3.relative)(srcDir, (0, import_node_path3.dirname)(absPath)).replace(/\\/g, "/");
6771
6792
  barrels.set(barrelId, map);
6772
6793
  }
6773
6794
  }
@@ -6826,7 +6847,7 @@ function extractRoute(id) {
6826
6847
  return route || "/";
6827
6848
  }
6828
6849
  function nameFromFilename(absPath) {
6829
- 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());
6830
6851
  }
6831
6852
  function resolveTemplateLiteralRoute(template, routeToNodeId) {
6832
6853
  const parameterized = template.replace(/\$\{([^}]+)\}/g, (_, expr) => {
@@ -6907,105 +6928,6 @@ function matchRouteToPage(route, routeToNodeId) {
6907
6928
  if (routeToNodeId.has(normalized)) return routeToNodeId.get(normalized);
6908
6929
  return null;
6909
6930
  }
6910
- function loadApiRoutes(rootDir) {
6911
- const apiJsonPath = (0, import_node_path2.join)(rootDir, ".launchsecure", "graphs", "api.json");
6912
- if (!(0, import_node_fs2.existsSync)(apiJsonPath)) return [];
6913
- try {
6914
- const parsed = JSON.parse((0, import_node_fs2.readFileSync)(apiJsonPath, "utf-8"));
6915
- const routes = [];
6916
- for (const n of parsed.nodes ?? []) {
6917
- const path9 = n.path;
6918
- if (!path9 || typeof path9 !== "string") continue;
6919
- routes.push({
6920
- path: path9,
6921
- nodeId: n.id,
6922
- segments: path9.split("/").filter(Boolean)
6923
- });
6924
- }
6925
- return routes;
6926
- } catch {
6927
- return [];
6928
- }
6929
- }
6930
- function buildApiPathMap(routes) {
6931
- const map = /* @__PURE__ */ new Map();
6932
- for (const r of routes) {
6933
- if (!map.has(r.path)) map.set(r.path, r.nodeId);
6934
- }
6935
- return map;
6936
- }
6937
- function normalizeFetchUrl(raw) {
6938
- let s = raw.replace(/^`|`$/g, "");
6939
- const qIdx = s.indexOf("?");
6940
- if (qIdx >= 0) s = s.slice(0, qIdx);
6941
- const hIdx = s.indexOf("#");
6942
- if (hIdx >= 0) s = s.slice(0, hIdx);
6943
- let hadInterpolation = false;
6944
- s = s.replace(/\$\{([^}]+)\}/g, (_, expr) => {
6945
- hadInterpolation = true;
6946
- const cleaned = expr.trim();
6947
- const last = cleaned.split(".").pop() ?? cleaned;
6948
- const name = last.replace(/[^\w]/g, "") || "param";
6949
- return ":" + name;
6950
- });
6951
- s = s.replace(/\/+/g, "/");
6952
- if (s.length > 1 && s.endsWith("/")) s = s.slice(0, -1);
6953
- return { path: s || "/", hadInterpolation };
6954
- }
6955
- function scoreApiRouteMatch(candidate, known) {
6956
- if (candidate.length !== known.length) return -1;
6957
- let score = 0;
6958
- for (let i = 0; i < candidate.length; i++) {
6959
- const a = candidate[i];
6960
- const b = known[i];
6961
- if (a === b) {
6962
- score += 3;
6963
- continue;
6964
- }
6965
- if (a.startsWith(":") && b.startsWith(":")) {
6966
- score += 2;
6967
- continue;
6968
- }
6969
- if (a.startsWith(":") || b.startsWith(":")) {
6970
- score += 1;
6971
- continue;
6972
- }
6973
- return -1;
6974
- }
6975
- return score;
6976
- }
6977
- function resolveFetchCall(call, apiPathMap, apiRoutes) {
6978
- const raw = call.url;
6979
- if (/^(https?:)?\/\//i.test(raw)) {
6980
- return { kind: "external", normalizedUrl: raw };
6981
- }
6982
- if (call.isConcat) {
6983
- return { kind: "dynamic", normalizedUrl: raw };
6984
- }
6985
- const { path: path9, hadInterpolation } = normalizeFetchUrl(raw);
6986
- if (!path9.startsWith("/")) {
6987
- return { kind: "unresolved", normalizedUrl: path9 };
6988
- }
6989
- const segs = path9.split("/").filter(Boolean);
6990
- if (hadInterpolation && segs.length > 0 && segs[0].startsWith(":")) {
6991
- return { kind: "dynamic", normalizedUrl: path9 };
6992
- }
6993
- const exact = apiPathMap.get(path9);
6994
- if (exact) return { kind: "resolved", nodeId: exact, normalizedUrl: path9 };
6995
- let bestScore = -1;
6996
- let bestId = null;
6997
- for (const r of apiRoutes) {
6998
- const score = scoreApiRouteMatch(segs, r.segments);
6999
- if (score > bestScore) {
7000
- bestScore = score;
7001
- bestId = r.nodeId;
7002
- }
7003
- }
7004
- if (bestId && bestScore > 0) {
7005
- return { kind: "resolved", nodeId: bestId, normalizedUrl: path9 };
7006
- }
7007
- return { kind: "unresolved", normalizedUrl: path9 };
7008
- }
7009
6931
  function extractEdges(srcDir, absPath, sourceId, parsed, nodeIdSet, nodeTypeMap, barrelMaps, routeToNodeId) {
7010
6932
  const edges = [];
7011
6933
  const flagged = [];
@@ -7105,26 +7027,26 @@ function extractEdges(srcDir, absPath, sourceId, parsed, nodeIdSet, nodeTypeMap,
7105
7027
  return { edges, flagged };
7106
7028
  }
7107
7029
  function detect(rootDir) {
7108
- 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"));
7109
7031
  }
7110
7032
  function generate(rootDir) {
7111
- const srcDir = (0, import_node_path2.join)(rootDir, "src");
7112
- const appFiles = walk((0, import_node_path2.join)(srcDir, "app"), [".tsx", ".ts"]).filter(
7113
- (f) => (0, import_node_path2.basename)(f) !== "route.ts" && (0, import_node_path2.basename)(f) !== "route.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"
7114
7036
  );
7115
- const clientFiles = walk((0, import_node_path2.join)(srcDir, "client"), [".tsx", ".ts"]);
7116
- const serverFiles = walk((0, import_node_path2.join)(srcDir, "server"), [".ts", ".tsx"]).filter(
7117
- (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"
7118
7040
  );
7119
- const libFiles = walk((0, import_node_path2.join)(srcDir, "lib"), [".ts", ".tsx"]);
7120
- 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"]);
7121
7043
  const allDiscovered = [...appFiles, ...clientFiles, ...serverFiles, ...libFiles, ...configFiles];
7122
7044
  const parsedByPath = /* @__PURE__ */ new Map();
7123
7045
  for (const absPath of allDiscovered) {
7124
7046
  parsedByPath.set(absPath, parseFile(absPath));
7125
7047
  }
7126
7048
  const barrelMaps = buildAllBarrelMaps(srcDir, parsedByPath);
7127
- 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."));
7128
7050
  const nodes = [];
7129
7051
  const nodeIdSet = /* @__PURE__ */ new Set();
7130
7052
  const nodeTypeMap = /* @__PURE__ */ new Map();
@@ -7143,7 +7065,6 @@ function generate(rootDir) {
7143
7065
  }
7144
7066
  const allEdges = [];
7145
7067
  const allFlagged = [];
7146
- const crossRefs = [];
7147
7068
  for (const absPath of fileSet) {
7148
7069
  const sourceId = toNodeId(srcDir, absPath);
7149
7070
  const parsed = parsedByPath.get(absPath);
@@ -7160,66 +7081,21 @@ function generate(rootDir) {
7160
7081
  allEdges.push(...edges);
7161
7082
  allFlagged.push(...flagged);
7162
7083
  }
7163
- const apiRoutes = loadApiRoutes(rootDir);
7164
- const apiPathMap = buildApiPathMap(apiRoutes);
7165
- const includeExternalFetches = process.env.LAUNCH_CHART_INCLUDE_EXTERNAL_FETCHES === "1";
7166
- const fetchSeen = /* @__PURE__ */ new Set();
7167
- let fetchResolvedCount = 0;
7168
- let fetchDynamicCount = 0;
7169
- let fetchUnresolvedCount = 0;
7170
- let fetchExternalCount = 0;
7084
+ const fetchCallEntries = [];
7171
7085
  for (const absPath of fileSet) {
7172
7086
  const sourceId = toNodeId(srcDir, absPath);
7173
7087
  const parsed = parsedByPath.get(absPath);
7174
7088
  if (parsed.fetchCalls.length === 0) continue;
7175
- for (const call of parsed.fetchCalls) {
7176
- const result = resolveFetchCall(call, apiPathMap, apiRoutes);
7177
- const methodTag = call.method ?? (call.kind === "fetch" ? "GET?" : "?");
7178
- if (result.kind === "resolved" && result.nodeId) {
7179
- const key = `${sourceId}\u2192${result.nodeId}\u2192calls_api`;
7180
- if (fetchSeen.has(key)) continue;
7181
- fetchSeen.add(key);
7182
- crossRefs.push({
7183
- source: sourceId,
7184
- target: result.nodeId,
7185
- type: "calls_api",
7186
- layer: "api"
7187
- });
7188
- fetchResolvedCount++;
7189
- continue;
7190
- }
7191
- if (result.kind === "dynamic") {
7192
- fetchDynamicCount++;
7193
- allFlagged.push({
7194
- source: sourceId,
7195
- target: "DYNAMIC",
7196
- type: "calls_api",
7197
- label: call.isConcat ? `${methodTag} fetch with concat: ${call.url}` : `${methodTag} fetch with template: ${call.url}`,
7198
- confidence: call.isConcat ? "low" : "medium"
7199
- });
7200
- continue;
7201
- }
7202
- if (result.kind === "external") {
7203
- fetchExternalCount++;
7204
- if (!includeExternalFetches) continue;
7205
- allFlagged.push({
7206
- source: sourceId,
7207
- target: "EXTERNAL",
7208
- type: "calls_external",
7209
- label: `${methodTag} external fetch: ${call.url}`,
7210
- confidence: "high"
7211
- });
7212
- continue;
7213
- }
7214
- fetchUnresolvedCount++;
7215
- allFlagged.push({
7216
- source: sourceId,
7217
- target: "UNRESOLVED",
7218
- type: "calls_api",
7219
- label: `${methodTag} fetch to unknown path: ${result.normalizedUrl}`,
7220
- confidence: "medium"
7221
- });
7222
- }
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
+ });
7223
7099
  }
7224
7100
  const externalScanned = new Set(allDiscovered.map((f) => f.replace(/\\/g, "/")));
7225
7101
  const IGNORE_DIRS = /* @__PURE__ */ new Set([
@@ -7245,7 +7121,7 @@ function generate(rootDir) {
7245
7121
  } catch {
7246
7122
  continue;
7247
7123
  }
7248
- const externalId = (0, import_node_path2.relative)(rootDir, absPath).replace(/\\/g, "/");
7124
+ const externalId = (0, import_node_path3.relative)(rootDir, absPath).replace(/\\/g, "/");
7249
7125
  const edgesFromThis = [];
7250
7126
  const seen = /* @__PURE__ */ new Set();
7251
7127
  for (const imp of parsed.imports) {
@@ -7336,20 +7212,11 @@ function generate(rootDir) {
7336
7212
  layer: "ui",
7337
7213
  parser: "react-nextjs-ast",
7338
7214
  ...stats,
7339
- api_call_detection: {
7340
- includeExternalFetches,
7341
- includeConcatFetches: process.env.LAUNCH_CHART_INCLUDE_CONCAT_FETCHES === "1",
7342
- apiRoutesLoaded: apiRoutes.length,
7343
- resolved: fetchResolvedCount,
7344
- dynamic: fetchDynamicCount,
7345
- unresolved: fetchUnresolvedCount,
7346
- external: fetchExternalCount
7347
- },
7348
7215
  notes: "Auto-generated via TypeScript AST \u2014 edges derived from actual imports, renders from JSX usage, navigations from router/Link calls."
7349
7216
  },
7350
7217
  nodes,
7351
7218
  edges: allEdges,
7352
- cross_refs: crossRefs,
7219
+ cross_refs: [],
7353
7220
  contradictions: [],
7354
7221
  warnings: [],
7355
7222
  flagged_edges: dedupedFlagged,
@@ -7360,7 +7227,8 @@ function generate(rootDir) {
7360
7227
  renders: allEdges.filter((e) => e.type === "renders").length,
7361
7228
  imports: allEdges.filter((e) => e.type === "imports").length,
7362
7229
  navigates: allEdges.filter((e) => e.type === "navigates").length
7363
- }
7230
+ },
7231
+ fetch_calls: fetchCallEntries
7364
7232
  }
7365
7233
  };
7366
7234
  }
@@ -7372,14 +7240,14 @@ var reactNextjsParser = {
7372
7240
  };
7373
7241
 
7374
7242
  // src/server/graph/parsers/api/nextjs-routes.ts
7375
- var import_node_fs3 = require("node:fs");
7376
- var import_node_path3 = require("node:path");
7243
+ var import_node_fs4 = require("node:fs");
7244
+ var import_node_path4 = require("node:path");
7377
7245
  var HTTP_METHODS2 = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"]);
7378
7246
  function walk2(dir) {
7379
7247
  const results = [];
7380
- if (!(0, import_node_fs3.existsSync)(dir)) return results;
7381
- for (const entry of (0, import_node_fs3.readdirSync)(dir, { withFileTypes: true })) {
7382
- 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);
7383
7251
  if (entry.isDirectory()) {
7384
7252
  results.push(...walk2(full));
7385
7253
  } else if (entry.name === "route.ts" || entry.name === "route.tsx") {
@@ -7389,7 +7257,7 @@ function walk2(dir) {
7389
7257
  return results;
7390
7258
  }
7391
7259
  function filePathToRoute(apiDir, absPath) {
7392
- 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?$/, "");
7393
7261
  route = route.replace(/\[([^\]]+)\]/g, ":$1");
7394
7262
  route = route.replace(/\/+/g, "/");
7395
7263
  if (route === "/") return "/api";
@@ -7400,10 +7268,10 @@ function camelToPascal(s) {
7400
7268
  return s.charAt(0).toUpperCase() + s.slice(1);
7401
7269
  }
7402
7270
  function detect2(rootDir) {
7403
- 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"));
7404
7272
  }
7405
7273
  function generate2(rootDir) {
7406
- const apiDir = (0, import_node_path3.join)(rootDir, "src", "app", "api");
7274
+ const apiDir = (0, import_node_path4.join)(rootDir, "src", "app", "api");
7407
7275
  const routeFiles = walk2(apiDir);
7408
7276
  const nodes = [];
7409
7277
  const edges = [];
@@ -7421,7 +7289,7 @@ function generate2(rootDir) {
7421
7289
  if (HTTP_METHODS2.has(exp)) methods.push(exp);
7422
7290
  }
7423
7291
  const routePath = filePathToRoute(apiDir, absPath);
7424
- const relPath = (0, import_node_path3.relative)(rootDir, absPath).replace(/\\/g, "/");
7292
+ const relPath = (0, import_node_path4.relative)(rootDir, absPath).replace(/\\/g, "/");
7425
7293
  const mutations = dbCalls.filter((c) => c.isMutation);
7426
7294
  const reads = dbCalls.filter((c) => !c.isMutation);
7427
7295
  const mutates = mutations.length > 0;
@@ -7506,8 +7374,8 @@ var nextjsRoutesParser = {
7506
7374
  };
7507
7375
 
7508
7376
  // src/server/graph/parsers/db/prisma-schema.ts
7509
- var import_node_fs4 = require("node:fs");
7510
- var import_node_path4 = require("node:path");
7377
+ var import_node_fs5 = require("node:fs");
7378
+ var import_node_path5 = require("node:path");
7511
7379
  function parseModels(content) {
7512
7380
  const nodes = [];
7513
7381
  const relations = [];
@@ -7598,11 +7466,11 @@ function parseEnums(content) {
7598
7466
  return nodes;
7599
7467
  }
7600
7468
  function detect3(rootDir) {
7601
- 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"));
7602
7470
  }
7603
7471
  function generate3(rootDir) {
7604
- const schemaPath = (0, import_node_path4.join)(rootDir, "prisma", "schema.prisma");
7605
- 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");
7606
7474
  const { nodes: modelNodes, relations } = parseModels(content);
7607
7475
  const enumNodes = parseEnums(content);
7608
7476
  const allNodes = [...modelNodes, ...enumNodes];
@@ -7658,33 +7526,651 @@ var prismaSchemaParser = {
7658
7526
  generate: generate3
7659
7527
  };
7660
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
+
7661
8096
  // src/server/graph/core/graph-builder.ts
7662
- var ALL_PARSERS = [
7663
- reactNextjsParser,
7664
- nextjsRoutesParser,
7665
- prismaSchemaParser
7666
- ];
7667
- function getParser(layer) {
7668
- 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
+ }
7669
8105
  }
7670
8106
  function generateLayer(rootDir, layer) {
7671
- const parser = getParser(layer);
7672
- if (!parser) return null;
7673
- if (!parser.detect(rootDir)) return null;
7674
- 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
+ }
7675
8131
  return {
7676
8132
  layer,
7677
- output,
7678
- nodeCount: output.nodes.length,
7679
- edgeCount: output.edges.length
8133
+ output: merged,
8134
+ nodeCount: merged.nodes.length,
8135
+ edgeCount: merged.edges.length
7680
8136
  };
7681
8137
  }
7682
8138
  function generateAll(rootDir) {
7683
- const layers = ["api", "db", "ui"];
8139
+ const config = loadConfig(rootDir);
8140
+ const registry = createRegistry(config, rootDir);
8141
+ const layerOrder = ["api", "db", "ui"];
8142
+ const layerOutputs = /* @__PURE__ */ new Map();
7684
8143
  const results = [];
7685
- for (const layer of layers) {
7686
- const result = generateLayer(rootDir, layer);
7687
- 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
+ });
8160
+ }
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
+ }
7688
8174
  }
7689
8175
  const byLayer = new Map(results.map((r) => [r.layer, r]));
7690
8176
  return ["ui", "api", "db"].map((l) => byLayer.get(l)).filter((r) => !!r);
@@ -7695,23 +8181,23 @@ var GRAPHS_DIR = ".launchsecure/graphs";
7695
8181
  var LAYERS = ["ui", "api", "db"];
7696
8182
  var graphCache = /* @__PURE__ */ new Map();
7697
8183
  function graphsDir(rootDir) {
7698
- return (0, import_node_path5.join)(rootDir, GRAPHS_DIR);
8184
+ return (0, import_node_path10.join)(rootDir, GRAPHS_DIR);
7699
8185
  }
7700
8186
  function graphFilePath(rootDir, layer) {
7701
- return (0, import_node_path5.join)(graphsDir(rootDir), `${layer}.json`);
8187
+ return (0, import_node_path10.join)(graphsDir(rootDir), `${layer}.json`);
7702
8188
  }
7703
8189
  function invalidateCache(filePath) {
7704
8190
  graphCache.delete(filePath);
7705
8191
  }
7706
8192
  function readGraph(rootDir, layer) {
7707
8193
  const filePath = graphFilePath(rootDir, layer);
7708
- if (!(0, import_node_fs5.existsSync)(filePath)) return null;
7709
- 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);
7710
8196
  const cached = graphCache.get(filePath);
7711
8197
  if (cached && cached.mtimeMs === stat.mtimeMs) {
7712
8198
  return cached.graph;
7713
8199
  }
7714
- const content = (0, import_node_fs5.readFileSync)(filePath, "utf-8");
8200
+ const content = (0, import_node_fs9.readFileSync)(filePath, "utf-8");
7715
8201
  const graph = JSON.parse(content);
7716
8202
  graphCache.set(filePath, { mtimeMs: stat.mtimeMs, graph });
7717
8203
  return graph;
@@ -7726,11 +8212,11 @@ function readAllGraphs(rootDir) {
7726
8212
  }
7727
8213
  function generateGraph(rootDir, layer) {
7728
8214
  const dir = graphsDir(rootDir);
7729
- (0, import_node_fs5.mkdirSync)(dir, { recursive: true });
8215
+ (0, import_node_fs9.mkdirSync)(dir, { recursive: true });
7730
8216
  const results = layer ? [generateLayer(rootDir, layer)].filter((r) => r !== null) : generateAll(rootDir);
7731
8217
  for (const result of results) {
7732
8218
  const filePath = graphFilePath(rootDir, result.layer);
7733
- (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");
7734
8220
  invalidateCache(filePath);
7735
8221
  }
7736
8222
  return results;
@@ -7792,25 +8278,27 @@ function handleGraphCommand(subcommand, args) {
7792
8278
  }
7793
8279
 
7794
8280
  // src/server/graph-mcp.ts
7795
- var import_node_fs7 = require("node:fs");
7796
- var import_node_path7 = 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");
7797
8285
 
7798
8286
  // src/server/lockfile.ts
7799
8287
  var import_node_child_process = require("node:child_process");
7800
- var import_node_fs6 = require("node:fs");
8288
+ var import_node_fs10 = require("node:fs");
7801
8289
  var import_node_os = require("node:os");
7802
- var import_node_path6 = require("node:path");
8290
+ var import_node_path11 = require("node:path");
7803
8291
  function lockDir() {
7804
- return (0, import_node_path6.join)((0, import_node_os.homedir)(), ".launchsecure");
8292
+ return (0, import_node_path11.join)((0, import_node_os.homedir)(), ".launchsecure");
7805
8293
  }
7806
8294
  function lockPath() {
7807
- return (0, import_node_path6.join)(lockDir(), "launch-chart.lock");
8295
+ return (0, import_node_path11.join)(lockDir(), "launch-chart.lock");
7808
8296
  }
7809
8297
  function readLock() {
7810
8298
  const p = lockPath();
7811
- if (!(0, import_node_fs6.existsSync)(p)) return null;
8299
+ if (!(0, import_node_fs10.existsSync)(p)) return null;
7812
8300
  try {
7813
- const data = JSON.parse((0, import_node_fs6.readFileSync)(p, "utf-8"));
8301
+ const data = JSON.parse((0, import_node_fs10.readFileSync)(p, "utf-8"));
7814
8302
  if (typeof data.pid !== "number" || typeof data.port !== "number") return null;
7815
8303
  return data;
7816
8304
  } catch {
@@ -7846,13 +8334,19 @@ function getLiveLock() {
7846
8334
  const live = listenerPid !== null ? listenerPid === lock.pid : isPidAlive(lock.pid);
7847
8335
  if (!live) {
7848
8336
  try {
7849
- (0, import_node_fs6.unlinkSync)(lockPath());
8337
+ (0, import_node_fs10.unlinkSync)(lockPath());
7850
8338
  } catch {
7851
8339
  }
7852
8340
  return null;
7853
8341
  }
7854
8342
  return lock;
7855
8343
  }
8344
+ function clearLock() {
8345
+ try {
8346
+ (0, import_node_fs10.unlinkSync)(lockPath());
8347
+ } catch {
8348
+ }
8349
+ }
7856
8350
 
7857
8351
  // src/server/graph-mcp.ts
7858
8352
  var SERVER_INFO = {
@@ -7982,8 +8476,39 @@ Returns: { pattern, filter, files_searched, total_matches, matches: [{file, line
7982
8476
  }
7983
8477
  },
7984
8478
  {
7985
- name: "get_graph_ui_url",
7986
- 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.',
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.",
7987
8512
  inputSchema: {
7988
8513
  type: "object",
7989
8514
  properties: {}
@@ -8340,9 +8865,9 @@ function handleReadGraph(args) {
8340
8865
  return okJson(result);
8341
8866
  }
8342
8867
  function nodeToFilePath(rootDir, layer, nodeId) {
8343
- if (layer === "ui") return (0, import_node_path7.join)(rootDir, "src", nodeId);
8344
- if (layer === "api") return (0, import_node_path7.join)(rootDir, nodeId);
8345
- if (layer === "db") return (0, import_node_path7.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");
8346
8871
  return null;
8347
8872
  }
8348
8873
  function handleGrepNodes(args) {
@@ -8402,11 +8927,11 @@ function handleGrepNodes(args) {
8402
8927
  let filesSearched = 0;
8403
8928
  let truncated = false;
8404
8929
  for (const [filePath, nodeId] of filePaths) {
8405
- if (!(0, import_node_fs7.existsSync)(filePath)) continue;
8930
+ if (!(0, import_node_fs11.existsSync)(filePath)) continue;
8406
8931
  filesSearched++;
8407
8932
  let content;
8408
8933
  try {
8409
- content = (0, import_node_fs7.readFileSync)(filePath, "utf-8");
8934
+ content = (0, import_node_fs11.readFileSync)(filePath, "utf-8");
8410
8935
  } catch {
8411
8936
  continue;
8412
8937
  }
@@ -8443,13 +8968,10 @@ function handleGrepNodes(args) {
8443
8968
  truncated
8444
8969
  });
8445
8970
  }
8446
- function handleGetGraphUiUrl() {
8971
+ function handleChartServerStatus() {
8447
8972
  const lock = getLiveLock();
8448
8973
  if (!lock) {
8449
- return okJson({
8450
- running: false,
8451
- 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."
8452
- });
8974
+ return okJson({ running: false });
8453
8975
  }
8454
8976
  return okJson({
8455
8977
  running: true,
@@ -8460,6 +8982,113 @@ function handleGetGraphUiUrl() {
8460
8982
  startedAt: lock.startedAt
8461
8983
  });
8462
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
+ }
8463
9092
  function send(msg) {
8464
9093
  process.stdout.write(JSON.stringify(msg) + "\n");
8465
9094
  }
@@ -8503,8 +9132,20 @@ function handleMessage(msg) {
8503
9132
  respond(id ?? null, handleGrepNodes(args));
8504
9133
  return;
8505
9134
  }
8506
- if (toolName === "get_graph_ui_url") {
8507
- respond(id ?? null, handleGetGraphUiUrl());
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());
8508
9149
  return;
8509
9150
  }
8510
9151
  respondError(id ?? null, -32601, `Unknown tool: ${toolName}`);
@@ -8604,7 +9245,7 @@ function parseArgs() {
8604
9245
  return { port, token, serverUrl: LAUNCHSECURE_URL, subcommand };
8605
9246
  }
8606
9247
  function tryListen(server, port, maxRetries = 10) {
8607
- return new Promise((resolve, reject) => {
9248
+ return new Promise((resolve2, reject) => {
8608
9249
  let attempts = 0;
8609
9250
  function attempt(p) {
8610
9251
  server.once("error", (err2) => {
@@ -8615,7 +9256,7 @@ function tryListen(server, port, maxRetries = 10) {
8615
9256
  reject(err2);
8616
9257
  }
8617
9258
  });
8618
- server.listen(p, () => resolve(p));
9259
+ server.listen(p, () => resolve2(p));
8619
9260
  }
8620
9261
  attempt(port);
8621
9262
  });
@@ -8636,7 +9277,7 @@ function saveCredentials(creds) {
8636
9277
  });
8637
9278
  }
8638
9279
  function verifyToken(serverUrl, token) {
8639
- return new Promise((resolve) => {
9280
+ return new Promise((resolve2) => {
8640
9281
  const url = new URL("/api/mcp/verify", serverUrl);
8641
9282
  const body = JSON.stringify({ token });
8642
9283
  const mod = url.protocol === "https:" ? import_https.default : import_http.default;
@@ -8651,30 +9292,30 @@ function verifyToken(serverUrl, token) {
8651
9292
  res.on("data", (chunk) => data += chunk);
8652
9293
  res.on("end", () => {
8653
9294
  try {
8654
- resolve(JSON.parse(data));
9295
+ resolve2(JSON.parse(data));
8655
9296
  } catch {
8656
- resolve({ valid: false, error: "Invalid response from server" });
9297
+ resolve2({ valid: false, error: "Invalid response from server" });
8657
9298
  }
8658
9299
  });
8659
9300
  });
8660
9301
  req.on("error", (err2) => {
8661
- resolve({ valid: false, error: `Cannot reach server: ${err2.message}` });
9302
+ resolve2({ valid: false, error: `Cannot reach server: ${err2.message}` });
8662
9303
  });
8663
9304
  req.setTimeout(1e4, () => {
8664
9305
  req.destroy();
8665
- resolve({ valid: false, error: "Connection timed out" });
9306
+ resolve2({ valid: false, error: "Connection timed out" });
8666
9307
  });
8667
9308
  req.write(body);
8668
9309
  req.end();
8669
9310
  });
8670
9311
  }
8671
9312
  function httpRequest(reqUrl, options, body, timeout = 3e4) {
8672
- return new Promise((resolve, reject) => {
9313
+ return new Promise((resolve2, reject) => {
8673
9314
  const mod = reqUrl.protocol === "https:" ? import_https.default : import_http.default;
8674
9315
  const r = mod.request(reqUrl, options, (resp) => {
8675
9316
  let data = "";
8676
9317
  resp.on("data", (chunk) => data += chunk);
8677
- 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 }));
8678
9319
  });
8679
9320
  r.on("error", reject);
8680
9321
  r.setTimeout(timeout, () => {