@launchsecure/launch-kit 0.0.4 → 0.0.6

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((resolve3) => {
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
+ resolve3(entries);
1789
1789
  });
1790
1790
  rl.on("error", (error) => {
1791
1791
  console.error("Error reading file:", filePath, error);
1792
- resolve(entries);
1792
+ resolve3(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((resolve3, 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
+ resolve3({ code: exitCode, signal });
3613
3613
  } else {
3614
3614
  reject(new Error(`Script exited with code ${exitCode}`));
3615
3615
  }
@@ -3701,6 +3701,30 @@ var require_src = __commonJS({
3701
3701
  }
3702
3702
  });
3703
3703
 
3704
+ // src/server/graph/core/config.ts
3705
+ var config_exports = {};
3706
+ __export(config_exports, {
3707
+ loadConfig: () => loadConfig
3708
+ });
3709
+ function loadConfig(rootDir) {
3710
+ const configPath = (0, import_node_path.join)(rootDir, CONFIG_FILENAME);
3711
+ if (!(0, import_node_fs.existsSync)(configPath)) return {};
3712
+ try {
3713
+ return JSON.parse((0, import_node_fs.readFileSync)(configPath, "utf-8"));
3714
+ } catch {
3715
+ return {};
3716
+ }
3717
+ }
3718
+ var import_node_fs, import_node_path, CONFIG_FILENAME;
3719
+ var init_config = __esm({
3720
+ "src/server/graph/core/config.ts"() {
3721
+ "use strict";
3722
+ import_node_fs = require("node:fs");
3723
+ import_node_path = require("node:path");
3724
+ CONFIG_FILENAME = ".launchchart.json";
3725
+ }
3726
+ });
3727
+
3704
3728
  // src/server/cli.ts
3705
3729
  var import_http = __toESM(require("http"));
3706
3730
  var import_https = __toESM(require("https"));
@@ -5270,7 +5294,7 @@ var PostImplLaunchExecutor = class {
5270
5294
  return 3001;
5271
5295
  }
5272
5296
  startDevServer(port, databaseUrl) {
5273
- return new Promise((resolve) => {
5297
+ return new Promise((resolve3) => {
5274
5298
  const env = { ...process.env, PORT: String(port), ...databaseUrl ? { DATABASE_URL: databaseUrl } : {} };
5275
5299
  this.devProcess = (0, import_child_process3.spawn)("npm", ["run", "dev"], {
5276
5300
  cwd: this.workingDir,
@@ -5282,7 +5306,7 @@ var PostImplLaunchExecutor = class {
5282
5306
  const timeout = setTimeout(() => {
5283
5307
  if (!resolved) {
5284
5308
  resolved = true;
5285
- this.healthCheck(port).then(resolve);
5309
+ this.healthCheck(port).then(resolve3);
5286
5310
  }
5287
5311
  }, 15e3);
5288
5312
  const onData = (data) => {
@@ -5291,7 +5315,7 @@ var PostImplLaunchExecutor = class {
5291
5315
  if (!resolved) {
5292
5316
  resolved = true;
5293
5317
  clearTimeout(timeout);
5294
- resolve(true);
5318
+ resolve3(true);
5295
5319
  }
5296
5320
  }
5297
5321
  };
@@ -5302,7 +5326,7 @@ var PostImplLaunchExecutor = class {
5302
5326
  if (!resolved) {
5303
5327
  resolved = true;
5304
5328
  clearTimeout(timeout);
5305
- resolve(false);
5329
+ resolve3(false);
5306
5330
  }
5307
5331
  });
5308
5332
  this.devProcess.unref();
@@ -6324,16 +6348,24 @@ ${links}
6324
6348
  }
6325
6349
 
6326
6350
  // src/server/graph/index.ts
6327
- var import_node_fs5 = require("node:fs");
6328
- var import_node_path5 = require("node:path");
6351
+ var import_node_fs11 = require("node:fs");
6352
+ var import_node_path13 = require("node:path");
6353
+
6354
+ // src/server/graph/core/graph-builder.ts
6355
+ var import_node_fs8 = require("node:fs");
6356
+ var import_node_path9 = require("node:path");
6357
+ init_config();
6358
+
6359
+ // src/server/graph/core/parser-registry.ts
6360
+ var import_node_path8 = require("node:path");
6329
6361
 
6330
6362
  // src/server/graph/parsers/ui/react-nextjs.ts
6331
- var import_node_fs2 = require("node:fs");
6332
- var import_node_path2 = require("node:path");
6363
+ var import_node_fs3 = require("node:fs");
6364
+ var import_node_path3 = require("node:path");
6333
6365
 
6334
6366
  // src/server/graph/core/ast-helpers.ts
6335
- var import_node_fs = require("node:fs");
6336
- var import_node_path = require("node:path");
6367
+ var import_node_fs2 = require("node:fs");
6368
+ var import_node_path2 = require("node:path");
6337
6369
  var tsModule;
6338
6370
  function getTs() {
6339
6371
  if (!tsModule) {
@@ -6344,8 +6376,8 @@ function getTs() {
6344
6376
  var HTTP_METHODS = /* @__PURE__ */ new Set(["get", "post", "put", "patch", "delete", "head", "options"]);
6345
6377
  function parseFile(absPath) {
6346
6378
  const ts = getTs();
6347
- const content = (0, import_node_fs.readFileSync)(absPath, "utf-8");
6348
- const ext = (0, import_node_path.extname)(absPath);
6379
+ const content = (0, import_node_fs2.readFileSync)(absPath, "utf-8");
6380
+ const ext = (0, import_node_path2.extname)(absPath);
6349
6381
  const scriptKind = ext === ".tsx" ? ts.ScriptKind.TSX : ext === ".ts" ? ts.ScriptKind.TS : ext === ".jsx" ? ts.ScriptKind.JSX : ts.ScriptKind.JS;
6350
6382
  const sourceFile = ts.createSourceFile(
6351
6383
  absPath,
@@ -6623,8 +6655,8 @@ var MUTATION_METHODS = /* @__PURE__ */ new Set([
6623
6655
  ]);
6624
6656
  function extractDbCalls(absPath) {
6625
6657
  const ts = getTs();
6626
- const content = (0, import_node_fs.readFileSync)(absPath, "utf-8");
6627
- const ext = (0, import_node_path.extname)(absPath);
6658
+ const content = (0, import_node_fs2.readFileSync)(absPath, "utf-8");
6659
+ const ext = (0, import_node_path2.extname)(absPath);
6628
6660
  const scriptKind = ext === ".tsx" ? ts.ScriptKind.TSX : ts.ScriptKind.TS;
6629
6661
  const sourceFile = ts.createSourceFile(absPath, content, ts.ScriptTarget.Latest, true, scriptKind);
6630
6662
  const calls = [];
@@ -6653,8 +6685,8 @@ function extractDbCalls(absPath) {
6653
6685
  }
6654
6686
  function extractAuthWrappers(absPath) {
6655
6687
  const ts = getTs();
6656
- const content = (0, import_node_fs.readFileSync)(absPath, "utf-8");
6657
- const ext = (0, import_node_path.extname)(absPath);
6688
+ const content = (0, import_node_fs2.readFileSync)(absPath, "utf-8");
6689
+ const ext = (0, import_node_path2.extname)(absPath);
6658
6690
  const scriptKind = ext === ".tsx" ? ts.ScriptKind.TSX : ts.ScriptKind.TS;
6659
6691
  const sourceFile = ts.createSourceFile(absPath, content, ts.ScriptTarget.Latest, true, scriptKind);
6660
6692
  const wrappers = /* @__PURE__ */ new Set();
@@ -6675,12 +6707,12 @@ function extractAuthWrappers(absPath) {
6675
6707
  var RENDER_TYPES = /* @__PURE__ */ new Set(["component", "ui", "layout", "context"]);
6676
6708
  function walk(dir, exts) {
6677
6709
  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);
6710
+ if (!(0, import_node_fs3.existsSync)(dir)) return results;
6711
+ for (const entry of (0, import_node_fs3.readdirSync)(dir, { withFileTypes: true })) {
6712
+ const full = (0, import_node_path3.join)(dir, entry.name);
6681
6713
  if (entry.isDirectory()) {
6682
6714
  results.push(...walk(full, exts));
6683
- } else if (exts.includes((0, import_node_path2.extname)(entry.name))) {
6715
+ } else if (exts.includes((0, import_node_path3.extname)(entry.name))) {
6684
6716
  results.push(full);
6685
6717
  }
6686
6718
  }
@@ -6688,33 +6720,33 @@ function walk(dir, exts) {
6688
6720
  }
6689
6721
  function walkWithIgnore(dir, exts, ignoreDirs) {
6690
6722
  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 })) {
6723
+ if (!(0, import_node_fs3.existsSync)(dir)) return results;
6724
+ for (const entry of (0, import_node_fs3.readdirSync)(dir, { withFileTypes: true })) {
6693
6725
  if (entry.isDirectory()) {
6694
6726
  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));
6727
+ results.push(...walkWithIgnore((0, import_node_path3.join)(dir, entry.name), exts, ignoreDirs));
6728
+ } else if (exts.includes((0, import_node_path3.extname)(entry.name))) {
6729
+ results.push((0, import_node_path3.join)(dir, entry.name));
6698
6730
  }
6699
6731
  }
6700
6732
  return results;
6701
6733
  }
6702
6734
  function toNodeId(srcDir, absPath) {
6703
- return (0, import_node_path2.relative)(srcDir, absPath).replace(/\\/g, "/");
6735
+ return (0, import_node_path3.relative)(srcDir, absPath).replace(/\\/g, "/");
6704
6736
  }
6705
6737
  function resolveImport(srcDir, specifier) {
6706
6738
  if (!specifier.startsWith("@/")) return null;
6707
6739
  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;
6740
+ const base = (0, import_node_path3.join)(srcDir, rel);
6741
+ 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")]) {
6742
+ if ((0, import_node_fs3.existsSync)(c) && (0, import_node_fs3.statSync)(c).isFile()) return c;
6711
6743
  }
6712
6744
  return null;
6713
6745
  }
6714
6746
  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;
6747
+ const base = (0, import_node_path3.join)((0, import_node_path3.dirname)(fromFile), specifier);
6748
+ 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")]) {
6749
+ if ((0, import_node_fs3.existsSync)(c) && (0, import_node_fs3.statSync)(c).isFile()) return c;
6718
6750
  }
6719
6751
  return null;
6720
6752
  }
@@ -6735,7 +6767,7 @@ function resolveBarrelMap(barrelAbsPath, parsedByPath, memo, visiting) {
6735
6767
  const resolved = resolveRelativeImport(barrelAbsPath, re.from);
6736
6768
  if (!resolved) continue;
6737
6769
  if (re.isWildcard) {
6738
- const targetBn = (0, import_node_path2.basename)(resolved);
6770
+ const targetBn = (0, import_node_path3.basename)(resolved);
6739
6771
  const targetIsBarrel = targetBn === "index.ts" || targetBn === "index.tsx";
6740
6772
  if (targetIsBarrel) {
6741
6773
  const nested = resolveBarrelMap(resolved, parsedByPath, memo, visiting);
@@ -6762,12 +6794,12 @@ function buildAllBarrelMaps(srcDir, parsedByPath) {
6762
6794
  const barrels = /* @__PURE__ */ new Map();
6763
6795
  const memo = /* @__PURE__ */ new Map();
6764
6796
  for (const [absPath, parsed] of parsedByPath) {
6765
- const bn = (0, import_node_path2.basename)(absPath);
6797
+ const bn = (0, import_node_path3.basename)(absPath);
6766
6798
  if (bn !== "index.ts" && bn !== "index.tsx") continue;
6767
6799
  if (parsed.reExports.length === 0) continue;
6768
6800
  const map = resolveBarrelMap(absPath, parsedByPath, memo, /* @__PURE__ */ new Set());
6769
6801
  if (map.size > 0) {
6770
- const barrelId = (0, import_node_path2.relative)(srcDir, (0, import_node_path2.dirname)(absPath)).replace(/\\/g, "/");
6802
+ const barrelId = (0, import_node_path3.relative)(srcDir, (0, import_node_path3.dirname)(absPath)).replace(/\\/g, "/");
6771
6803
  barrels.set(barrelId, map);
6772
6804
  }
6773
6805
  }
@@ -6788,34 +6820,6 @@ function classifyType(id) {
6788
6820
  if (id.startsWith("lib/") || id.startsWith("config/")) return "lib";
6789
6821
  return "component";
6790
6822
  }
6791
- function classifyModule(id) {
6792
- if (/app\/\(auth\)\//.test(id)) return "auth";
6793
- if (/app\/\(admin\)\//.test(id)) return "admin";
6794
- if (/app\/\(settings\)\//.test(id)) return "settings";
6795
- if (/app\/\(app\)\/\[orgSlug\]\/\(project-pages\)\//.test(id)) return "project";
6796
- if (/app\/\(app\)\/\[orgSlug\]\/\(org-pages\)\//.test(id)) return "org";
6797
- if (/app\/\(app\)\/\[orgSlug\]\//.test(id)) return "org";
6798
- if (id.startsWith("app/integrations/")) return "integrations";
6799
- if (id.startsWith("app/docs/")) return "admin";
6800
- if (id.startsWith("client/components/ui/")) return "shared-ui";
6801
- if (id.startsWith("client/components/layout/") || /client\/lib\/navigation/.test(id)) return "layout";
6802
- if (/client\/components\/auth\//.test(id) || /client\/lib\/auth-/.test(id) || /client\/lib\/github-oauth/.test(id) || /client\/lib\/permission-service/.test(id) || /client\/hooks\/use-permissions/.test(id)) return "auth";
6803
- if (/client\/components\/prd-/.test(id) || /client\/hooks\/use-admin/.test(id)) return "admin";
6804
- if (/client\/components\/org-/.test(id) || /client\/hooks\/use-org-/.test(id) || /client\/hooks\/use-provider-def/.test(id)) return "org";
6805
- if (/client\/components\/project/.test(id) || /client\/hooks\/use-project-/.test(id) || /client\/hooks\/use-pipeline/.test(id) || /client\/hooks\/use-databases/.test(id) || /client\/hooks\/use-provider-env/.test(id) || /client\/hooks\/use-role-assign/.test(id) || /client\/components\/pipeline/.test(id) || /client\/components\/deployments/.test(id)) return "project";
6806
- if (/client\/hooks\/use-(profile|sessions|organizations|notification)/.test(id)) return "settings";
6807
- if (id.startsWith("server/auth/")) return "auth";
6808
- if (id.startsWith("server/mcp/")) return "mcp";
6809
- if (id.startsWith("server/lib/")) return "server-lib";
6810
- if (id.startsWith("server/middleware")) return "middleware";
6811
- if (id.startsWith("server/services/")) return "services";
6812
- if (id.startsWith("server/db")) return "db";
6813
- if (id.startsWith("server/errors")) return "errors";
6814
- if (id.startsWith("server/")) return "server-lib";
6815
- if (id.startsWith("config/")) return "config";
6816
- if (id.startsWith("lib/")) return "lib";
6817
- return "root";
6818
- }
6819
6823
  function extractRoute(id) {
6820
6824
  if (!id.endsWith("/page.tsx")) return null;
6821
6825
  let route = id.replace(/^app\//, "/").replace(/\/page\.tsx$/, "");
@@ -6826,7 +6830,7 @@ function extractRoute(id) {
6826
6830
  return route || "/";
6827
6831
  }
6828
6832
  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());
6833
+ 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
6834
  }
6831
6835
  function resolveTemplateLiteralRoute(template, routeToNodeId) {
6832
6836
  const parameterized = template.replace(/\$\{([^}]+)\}/g, (_, expr) => {
@@ -6907,105 +6911,6 @@ function matchRouteToPage(route, routeToNodeId) {
6907
6911
  if (routeToNodeId.has(normalized)) return routeToNodeId.get(normalized);
6908
6912
  return null;
6909
6913
  }
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
6914
  function extractEdges(srcDir, absPath, sourceId, parsed, nodeIdSet, nodeTypeMap, barrelMaps, routeToNodeId) {
7010
6915
  const edges = [];
7011
6916
  const flagged = [];
@@ -7105,26 +7010,26 @@ function extractEdges(srcDir, absPath, sourceId, parsed, nodeIdSet, nodeTypeMap,
7105
7010
  return { edges, flagged };
7106
7011
  }
7107
7012
  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"));
7013
+ 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
7014
  }
7110
7015
  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"
7016
+ const srcDir = (0, import_node_path3.join)(rootDir, "src");
7017
+ const appFiles = walk((0, import_node_path3.join)(srcDir, "app"), [".tsx", ".ts"]).filter(
7018
+ (f) => (0, import_node_path3.basename)(f) !== "route.ts" && (0, import_node_path3.basename)(f) !== "route.tsx"
7114
7019
  );
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"
7020
+ const clientFiles = walk((0, import_node_path3.join)(srcDir, "client"), [".tsx", ".ts"]);
7021
+ const serverFiles = walk((0, import_node_path3.join)(srcDir, "server"), [".ts", ".tsx"]).filter(
7022
+ (f) => (0, import_node_path3.basename)(f) !== "route.ts" && (0, import_node_path3.basename)(f) !== "route.tsx"
7118
7023
  );
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"]);
7024
+ const libFiles = walk((0, import_node_path3.join)(srcDir, "lib"), [".ts", ".tsx"]);
7025
+ const configFiles = walk((0, import_node_path3.join)(srcDir, "config"), [".ts", ".tsx"]);
7121
7026
  const allDiscovered = [...appFiles, ...clientFiles, ...serverFiles, ...libFiles, ...configFiles];
7122
7027
  const parsedByPath = /* @__PURE__ */ new Map();
7123
7028
  for (const absPath of allDiscovered) {
7124
7029
  parsedByPath.set(absPath, parseFile(absPath));
7125
7030
  }
7126
7031
  const barrelMaps = buildAllBarrelMaps(srcDir, parsedByPath);
7127
- const fileSet = allDiscovered.filter((f) => !(0, import_node_path2.basename)(f).startsWith("index."));
7032
+ const fileSet = allDiscovered.filter((f) => !(0, import_node_path3.basename)(f).startsWith("index."));
7128
7033
  const nodes = [];
7129
7034
  const nodeIdSet = /* @__PURE__ */ new Set();
7130
7035
  const nodeTypeMap = /* @__PURE__ */ new Map();
@@ -7135,15 +7040,13 @@ function generate(rootDir) {
7135
7040
  const parsed = parsedByPath.get(absPath);
7136
7041
  const name = parsed.name || nameFromFilename(absPath);
7137
7042
  const route = extractRoute(id);
7138
- const module_ = classifyModule(id);
7139
- nodes.push({ id, type, name, route, module: module_, exports: parsed.exports });
7043
+ nodes.push({ id, type, name, route, exports: parsed.exports });
7140
7044
  nodeIdSet.add(id);
7141
7045
  nodeTypeMap.set(id, type);
7142
7046
  if (route) routeToNodeId.set(route, id);
7143
7047
  }
7144
7048
  const allEdges = [];
7145
7049
  const allFlagged = [];
7146
- const crossRefs = [];
7147
7050
  for (const absPath of fileSet) {
7148
7051
  const sourceId = toNodeId(srcDir, absPath);
7149
7052
  const parsed = parsedByPath.get(absPath);
@@ -7160,66 +7063,21 @@ function generate(rootDir) {
7160
7063
  allEdges.push(...edges);
7161
7064
  allFlagged.push(...flagged);
7162
7065
  }
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;
7066
+ const fetchCallEntries = [];
7171
7067
  for (const absPath of fileSet) {
7172
7068
  const sourceId = toNodeId(srcDir, absPath);
7173
7069
  const parsed = parsedByPath.get(absPath);
7174
7070
  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
- }
7071
+ fetchCallEntries.push({
7072
+ nodeId: sourceId,
7073
+ calls: parsed.fetchCalls.map((c) => ({
7074
+ url: c.url,
7075
+ method: c.method,
7076
+ isTemplate: c.isTemplate,
7077
+ isConcat: c.isConcat,
7078
+ kind: c.kind
7079
+ }))
7080
+ });
7223
7081
  }
7224
7082
  const externalScanned = new Set(allDiscovered.map((f) => f.replace(/\\/g, "/")));
7225
7083
  const IGNORE_DIRS = /* @__PURE__ */ new Set([
@@ -7245,7 +7103,7 @@ function generate(rootDir) {
7245
7103
  } catch {
7246
7104
  continue;
7247
7105
  }
7248
- const externalId = (0, import_node_path2.relative)(rootDir, absPath).replace(/\\/g, "/");
7106
+ const externalId = (0, import_node_path3.relative)(rootDir, absPath).replace(/\\/g, "/");
7249
7107
  const edgesFromThis = [];
7250
7108
  const seen = /* @__PURE__ */ new Set();
7251
7109
  for (const imp of parsed.imports) {
@@ -7286,7 +7144,6 @@ function generate(rootDir) {
7286
7144
  type: "external",
7287
7145
  name: parsed.name || nameFromFilename(absPath),
7288
7146
  route: null,
7289
- module: "external",
7290
7147
  exports: parsed.exports
7291
7148
  });
7292
7149
  nodeIdSet.add(externalId);
@@ -7336,20 +7193,11 @@ function generate(rootDir) {
7336
7193
  layer: "ui",
7337
7194
  parser: "react-nextjs-ast",
7338
7195
  ...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
7196
  notes: "Auto-generated via TypeScript AST \u2014 edges derived from actual imports, renders from JSX usage, navigations from router/Link calls."
7349
7197
  },
7350
7198
  nodes,
7351
7199
  edges: allEdges,
7352
- cross_refs: crossRefs,
7200
+ cross_refs: [],
7353
7201
  contradictions: [],
7354
7202
  warnings: [],
7355
7203
  flagged_edges: dedupedFlagged,
@@ -7360,7 +7208,8 @@ function generate(rootDir) {
7360
7208
  renders: allEdges.filter((e) => e.type === "renders").length,
7361
7209
  imports: allEdges.filter((e) => e.type === "imports").length,
7362
7210
  navigates: allEdges.filter((e) => e.type === "navigates").length
7363
- }
7211
+ },
7212
+ fetch_calls: fetchCallEntries
7364
7213
  }
7365
7214
  };
7366
7215
  }
@@ -7372,14 +7221,14 @@ var reactNextjsParser = {
7372
7221
  };
7373
7222
 
7374
7223
  // src/server/graph/parsers/api/nextjs-routes.ts
7375
- var import_node_fs3 = require("node:fs");
7376
- var import_node_path3 = require("node:path");
7224
+ var import_node_fs4 = require("node:fs");
7225
+ var import_node_path4 = require("node:path");
7377
7226
  var HTTP_METHODS2 = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"]);
7378
7227
  function walk2(dir) {
7379
7228
  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);
7229
+ if (!(0, import_node_fs4.existsSync)(dir)) return results;
7230
+ for (const entry of (0, import_node_fs4.readdirSync)(dir, { withFileTypes: true })) {
7231
+ const full = (0, import_node_path4.join)(dir, entry.name);
7383
7232
  if (entry.isDirectory()) {
7384
7233
  results.push(...walk2(full));
7385
7234
  } else if (entry.name === "route.ts" || entry.name === "route.tsx") {
@@ -7389,7 +7238,7 @@ function walk2(dir) {
7389
7238
  return results;
7390
7239
  }
7391
7240
  function filePathToRoute(apiDir, absPath) {
7392
- let route = "/" + (0, import_node_path3.relative)(apiDir, absPath).replace(/\\/g, "/").replace(/\/route\.tsx?$/, "");
7241
+ let route = "/" + (0, import_node_path4.relative)(apiDir, absPath).replace(/\\/g, "/").replace(/\/route\.tsx?$/, "");
7393
7242
  route = route.replace(/\[([^\]]+)\]/g, ":$1");
7394
7243
  route = route.replace(/\/+/g, "/");
7395
7244
  if (route === "/") return "/api";
@@ -7400,10 +7249,10 @@ function camelToPascal(s) {
7400
7249
  return s.charAt(0).toUpperCase() + s.slice(1);
7401
7250
  }
7402
7251
  function detect2(rootDir) {
7403
- return (0, import_node_fs3.existsSync)((0, import_node_path3.join)(rootDir, "src", "app", "api"));
7252
+ return (0, import_node_fs4.existsSync)((0, import_node_path4.join)(rootDir, "src", "app", "api"));
7404
7253
  }
7405
7254
  function generate2(rootDir) {
7406
- const apiDir = (0, import_node_path3.join)(rootDir, "src", "app", "api");
7255
+ const apiDir = (0, import_node_path4.join)(rootDir, "src", "app", "api");
7407
7256
  const routeFiles = walk2(apiDir);
7408
7257
  const nodes = [];
7409
7258
  const edges = [];
@@ -7421,7 +7270,7 @@ function generate2(rootDir) {
7421
7270
  if (HTTP_METHODS2.has(exp)) methods.push(exp);
7422
7271
  }
7423
7272
  const routePath = filePathToRoute(apiDir, absPath);
7424
- const relPath = (0, import_node_path3.relative)(rootDir, absPath).replace(/\\/g, "/");
7273
+ const relPath = (0, import_node_path4.relative)(rootDir, absPath).replace(/\\/g, "/");
7425
7274
  const mutations = dbCalls.filter((c) => c.isMutation);
7426
7275
  const reads = dbCalls.filter((c) => !c.isMutation);
7427
7276
  const mutates = mutations.length > 0;
@@ -7506,8 +7355,8 @@ var nextjsRoutesParser = {
7506
7355
  };
7507
7356
 
7508
7357
  // src/server/graph/parsers/db/prisma-schema.ts
7509
- var import_node_fs4 = require("node:fs");
7510
- var import_node_path4 = require("node:path");
7358
+ var import_node_fs5 = require("node:fs");
7359
+ var import_node_path5 = require("node:path");
7511
7360
  function parseModels(content) {
7512
7361
  const nodes = [];
7513
7362
  const relations = [];
@@ -7598,11 +7447,11 @@ function parseEnums(content) {
7598
7447
  return nodes;
7599
7448
  }
7600
7449
  function detect3(rootDir) {
7601
- return (0, import_node_fs4.existsSync)((0, import_node_path4.join)(rootDir, "prisma", "schema.prisma"));
7450
+ return (0, import_node_fs5.existsSync)((0, import_node_path5.join)(rootDir, "prisma", "schema.prisma"));
7602
7451
  }
7603
7452
  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");
7453
+ const schemaPath = (0, import_node_path5.join)(rootDir, "prisma", "schema.prisma");
7454
+ const content = (0, import_node_fs5.readFileSync)(schemaPath, "utf-8");
7606
7455
  const { nodes: modelNodes, relations } = parseModels(content);
7607
7456
  const enumNodes = parseEnums(content);
7608
7457
  const allNodes = [...modelNodes, ...enumNodes];
@@ -7658,97 +7507,1088 @@ var prismaSchemaParser = {
7658
7507
  generate: generate3
7659
7508
  };
7660
7509
 
7661
- // 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);
7669
- }
7670
- 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);
7675
- return {
7676
- layer,
7677
- output,
7678
- nodeCount: output.nodes.length,
7679
- edgeCount: output.edges.length
7680
- };
7681
- }
7682
- function generateAll(rootDir) {
7683
- const layers = ["api", "db", "ui"];
7684
- const results = [];
7685
- for (const layer of layers) {
7686
- const result = generateLayer(rootDir, layer);
7687
- if (result) results.push(result);
7510
+ // src/server/graph/core/api-route-matching.ts
7511
+ function loadApiRoutesFromOutput(apiOutput) {
7512
+ const routes = [];
7513
+ for (const n of apiOutput.nodes) {
7514
+ const path9 = n.path;
7515
+ if (!path9 || typeof path9 !== "string") continue;
7516
+ routes.push({
7517
+ path: path9,
7518
+ nodeId: n.id,
7519
+ segments: path9.split("/").filter(Boolean)
7520
+ });
7688
7521
  }
7689
- const byLayer = new Map(results.map((r) => [r.layer, r]));
7690
- return ["ui", "api", "db"].map((l) => byLayer.get(l)).filter((r) => !!r);
7691
- }
7692
-
7693
- // src/server/graph/index.ts
7694
- var GRAPHS_DIR = ".launchsecure/graphs";
7695
- var LAYERS = ["ui", "api", "db"];
7696
- var graphCache = /* @__PURE__ */ new Map();
7697
- function graphsDir(rootDir) {
7698
- return (0, import_node_path5.join)(rootDir, GRAPHS_DIR);
7699
- }
7700
- function graphFilePath(rootDir, layer) {
7701
- return (0, import_node_path5.join)(graphsDir(rootDir), `${layer}.json`);
7522
+ return routes;
7702
7523
  }
7703
- function invalidateCache(filePath) {
7704
- graphCache.delete(filePath);
7705
- }
7706
- function readGraph(rootDir, layer) {
7707
- const filePath = graphFilePath(rootDir, layer);
7708
- if (!(0, import_node_fs5.existsSync)(filePath)) return null;
7709
- const stat = (0, import_node_fs5.statSync)(filePath);
7710
- const cached = graphCache.get(filePath);
7711
- if (cached && cached.mtimeMs === stat.mtimeMs) {
7712
- return cached.graph;
7524
+ function buildApiPathMap(routes) {
7525
+ const map = /* @__PURE__ */ new Map();
7526
+ for (const r of routes) {
7527
+ if (!map.has(r.path)) map.set(r.path, r.nodeId);
7713
7528
  }
7714
- const content = (0, import_node_fs5.readFileSync)(filePath, "utf-8");
7715
- const graph = JSON.parse(content);
7716
- graphCache.set(filePath, { mtimeMs: stat.mtimeMs, graph });
7717
- return graph;
7529
+ return map;
7718
7530
  }
7719
- function readAllGraphs(rootDir) {
7720
- const result = {};
7721
- for (const layer of LAYERS) {
7722
- const graph = readGraph(rootDir, layer);
7723
- if (graph) result[layer] = graph;
7724
- }
7725
- return result;
7531
+ function normalizeFetchUrl(raw) {
7532
+ let s = raw.replace(/^`|`$/g, "");
7533
+ const qIdx = s.indexOf("?");
7534
+ if (qIdx >= 0) s = s.slice(0, qIdx);
7535
+ const hIdx = s.indexOf("#");
7536
+ if (hIdx >= 0) s = s.slice(0, hIdx);
7537
+ let hadInterpolation = false;
7538
+ s = s.replace(/\$\{([^}]+)\}/g, (_, expr) => {
7539
+ hadInterpolation = true;
7540
+ const cleaned = expr.trim();
7541
+ const last = cleaned.split(".").pop() ?? cleaned;
7542
+ const name = last.replace(/[^\w]/g, "") || "param";
7543
+ return ":" + name;
7544
+ });
7545
+ s = s.replace(/\/+/g, "/");
7546
+ if (s.length > 1 && s.endsWith("/")) s = s.slice(0, -1);
7547
+ return { path: s || "/", hadInterpolation };
7726
7548
  }
7727
- function generateGraph(rootDir, layer) {
7728
- const dir = graphsDir(rootDir);
7729
- (0, import_node_fs5.mkdirSync)(dir, { recursive: true });
7730
- const results = layer ? [generateLayer(rootDir, layer)].filter((r) => r !== null) : generateAll(rootDir);
7731
- for (const result of results) {
7732
- const filePath = graphFilePath(rootDir, result.layer);
7733
- (0, import_node_fs5.writeFileSync)(filePath, JSON.stringify(result.output, null, 2) + "\n", "utf-8");
7734
- invalidateCache(filePath);
7549
+ function scoreApiRouteMatch(candidate, known) {
7550
+ if (candidate.length !== known.length) return -1;
7551
+ let score = 0;
7552
+ for (let i = 0; i < candidate.length; i++) {
7553
+ const a = candidate[i];
7554
+ const b = known[i];
7555
+ if (a === b) {
7556
+ score += 3;
7557
+ continue;
7558
+ }
7559
+ if (a.startsWith(":") && b.startsWith(":")) {
7560
+ score += 2;
7561
+ continue;
7562
+ }
7563
+ if (a.startsWith(":") || b.startsWith(":")) {
7564
+ score += 1;
7565
+ continue;
7566
+ }
7567
+ return -1;
7735
7568
  }
7736
- return results;
7569
+ return score;
7737
7570
  }
7738
-
7739
- // src/server/graph-cli.ts
7740
- var VALID_LAYERS = ["ui", "api", "db"];
7741
- function parseLayerFlag(args) {
7742
- const idx = args.indexOf("--layer");
7743
- if (idx < 0 || idx + 1 >= args.length) return void 0;
7744
- const value = args[idx + 1];
7745
- if (!VALID_LAYERS.includes(value)) {
7746
- console.error(`Invalid layer "${value}". Must be one of: ${VALID_LAYERS.join(", ")}`);
7747
- process.exit(1);
7571
+ function resolveFetchCall(call, apiPathMap, apiRoutes) {
7572
+ const raw = call.url;
7573
+ if (/^(https?:)?\/\//i.test(raw)) {
7574
+ return { kind: "external", normalizedUrl: raw };
7748
7575
  }
7749
- return value;
7750
- }
7751
- function handleGraphCommand(subcommand, args) {
7576
+ if (call.isConcat) {
7577
+ return { kind: "dynamic", normalizedUrl: raw };
7578
+ }
7579
+ const { path: path9, hadInterpolation } = normalizeFetchUrl(raw);
7580
+ if (!path9.startsWith("/")) {
7581
+ return { kind: "unresolved", normalizedUrl: path9 };
7582
+ }
7583
+ const segs = path9.split("/").filter(Boolean);
7584
+ if (hadInterpolation && segs.length > 0 && segs[0].startsWith(":")) {
7585
+ return { kind: "dynamic", normalizedUrl: path9 };
7586
+ }
7587
+ const exact = apiPathMap.get(path9);
7588
+ if (exact) return { kind: "resolved", nodeId: exact, normalizedUrl: path9 };
7589
+ let bestScore = -1;
7590
+ let bestId = null;
7591
+ for (const r of apiRoutes) {
7592
+ const score = scoreApiRouteMatch(segs, r.segments);
7593
+ if (score > bestScore) {
7594
+ bestScore = score;
7595
+ bestId = r.nodeId;
7596
+ }
7597
+ }
7598
+ if (bestId && bestScore > 0) {
7599
+ return { kind: "resolved", nodeId: bestId, normalizedUrl: path9 };
7600
+ }
7601
+ return { kind: "unresolved", normalizedUrl: path9 };
7602
+ }
7603
+ function resolveUrlPath(urlPath, apiPathMap, apiRoutes) {
7604
+ const { path: path9, hadInterpolation } = normalizeFetchUrl(urlPath);
7605
+ if (!path9.startsWith("/")) {
7606
+ return { kind: "unresolved", normalizedUrl: path9 };
7607
+ }
7608
+ const segs = path9.split("/").filter(Boolean);
7609
+ if (hadInterpolation && segs.length > 0 && segs[0].startsWith(":")) {
7610
+ return { kind: "dynamic", normalizedUrl: path9 };
7611
+ }
7612
+ const exact = apiPathMap.get(path9);
7613
+ if (exact) return { kind: "resolved", nodeId: exact, normalizedUrl: path9 };
7614
+ let bestScore = -1;
7615
+ let bestId = null;
7616
+ for (const r of apiRoutes) {
7617
+ const score = scoreApiRouteMatch(segs, r.segments);
7618
+ if (score > bestScore) {
7619
+ bestScore = score;
7620
+ bestId = r.nodeId;
7621
+ }
7622
+ }
7623
+ if (bestId && bestScore > 0) {
7624
+ return { kind: "resolved", nodeId: bestId, normalizedUrl: path9 };
7625
+ }
7626
+ return { kind: "unresolved", normalizedUrl: path9 };
7627
+ }
7628
+
7629
+ // src/server/graph/parsers/crosslayer/fetch-resolver.ts
7630
+ var fetchResolverParser = {
7631
+ id: "fetch-resolver",
7632
+ layer: "crosslayer",
7633
+ detect(_rootDir) {
7634
+ return true;
7635
+ },
7636
+ generate(_rootDir, layerOutputs) {
7637
+ const uiOutput = layerOutputs.get("ui");
7638
+ const apiOutput = layerOutputs.get("api");
7639
+ if (!uiOutput || !apiOutput) {
7640
+ return { cross_refs: [], flagged_edges: [], warnings: [] };
7641
+ }
7642
+ const apiRoutes = loadApiRoutesFromOutput(apiOutput);
7643
+ const apiPathMap = buildApiPathMap(apiRoutes);
7644
+ const fetchCallEntries = uiOutput.patterns?.fetch_calls ?? [];
7645
+ if (fetchCallEntries.length === 0) {
7646
+ return { cross_refs: [], flagged_edges: [], warnings: [] };
7647
+ }
7648
+ const includeExternal = process.env.LAUNCH_CHART_INCLUDE_EXTERNAL_FETCHES === "1";
7649
+ const crossRefs = [];
7650
+ const flaggedEdges = [];
7651
+ const seen = /* @__PURE__ */ new Set();
7652
+ let resolvedCount = 0;
7653
+ let dynamicCount = 0;
7654
+ let unresolvedCount = 0;
7655
+ let externalCount = 0;
7656
+ for (const entry of fetchCallEntries) {
7657
+ for (const call of entry.calls) {
7658
+ const result = resolveFetchCall(call, apiPathMap, apiRoutes);
7659
+ const methodTag = call.method ?? (call.kind === "fetch" ? "GET?" : "?");
7660
+ if (result.kind === "resolved" && result.nodeId) {
7661
+ const key = `${entry.nodeId}\u2192${result.nodeId}\u2192calls_api`;
7662
+ if (seen.has(key)) continue;
7663
+ seen.add(key);
7664
+ crossRefs.push({
7665
+ source: entry.nodeId,
7666
+ target: result.nodeId,
7667
+ type: "calls_api",
7668
+ layer: "api"
7669
+ });
7670
+ resolvedCount++;
7671
+ continue;
7672
+ }
7673
+ if (result.kind === "dynamic") {
7674
+ dynamicCount++;
7675
+ flaggedEdges.push({
7676
+ source: entry.nodeId,
7677
+ target: "DYNAMIC",
7678
+ type: "calls_api",
7679
+ label: call.isConcat ? `${methodTag} fetch with concat: ${call.url}` : `${methodTag} fetch with template: ${call.url}`,
7680
+ confidence: call.isConcat ? "low" : "medium"
7681
+ });
7682
+ continue;
7683
+ }
7684
+ if (result.kind === "external") {
7685
+ externalCount++;
7686
+ if (!includeExternal) continue;
7687
+ flaggedEdges.push({
7688
+ source: entry.nodeId,
7689
+ target: "EXTERNAL",
7690
+ type: "calls_external",
7691
+ label: `${methodTag} external fetch: ${call.url}`,
7692
+ confidence: "high"
7693
+ });
7694
+ continue;
7695
+ }
7696
+ unresolvedCount++;
7697
+ flaggedEdges.push({
7698
+ source: entry.nodeId,
7699
+ target: "UNRESOLVED",
7700
+ type: "calls_api",
7701
+ label: `${methodTag} fetch to unknown path: ${result.normalizedUrl}`,
7702
+ confidence: "medium"
7703
+ });
7704
+ }
7705
+ }
7706
+ return {
7707
+ cross_refs: crossRefs,
7708
+ flagged_edges: flaggedEdges,
7709
+ warnings: [],
7710
+ patterns: {
7711
+ api_call_detection: {
7712
+ resolved: resolvedCount,
7713
+ dynamic: dynamicCount,
7714
+ unresolved: unresolvedCount,
7715
+ external: externalCount
7716
+ }
7717
+ }
7718
+ };
7719
+ }
7720
+ };
7721
+
7722
+ // src/server/graph/parsers/crosslayer/api-annotations.ts
7723
+ var import_node_fs6 = require("node:fs");
7724
+ var import_node_path6 = require("node:path");
7725
+ var API_ANNOTATION_RE = /@api\s+(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+(\/\S+)/g;
7726
+ function walk3(dir, exts) {
7727
+ if (!(0, import_node_fs6.existsSync)(dir)) return [];
7728
+ const results = [];
7729
+ for (const entry of (0, import_node_fs6.readdirSync)(dir, { withFileTypes: true })) {
7730
+ if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
7731
+ const full = (0, import_node_path6.join)(dir, entry.name);
7732
+ if (entry.isDirectory()) {
7733
+ results.push(...walk3(full, exts));
7734
+ } else if (exts.includes((0, import_node_path6.extname)(entry.name))) {
7735
+ results.push(full);
7736
+ }
7737
+ }
7738
+ return results;
7739
+ }
7740
+ function toNodeId2(srcDir, absPath) {
7741
+ return (0, import_node_path6.relative)(srcDir, absPath).replace(/\\/g, "/");
7742
+ }
7743
+ var apiAnnotationsParser = {
7744
+ id: "api-annotations",
7745
+ layer: "crosslayer",
7746
+ detect(rootDir) {
7747
+ return (0, import_node_fs6.existsSync)((0, import_node_path6.join)(rootDir, "src"));
7748
+ },
7749
+ generate(rootDir, layerOutputs) {
7750
+ const apiOutput = layerOutputs.get("api");
7751
+ if (!apiOutput) {
7752
+ return { cross_refs: [], flagged_edges: [], warnings: [] };
7753
+ }
7754
+ const uiOutput = layerOutputs.get("ui");
7755
+ const uiNodeIds = new Set(uiOutput?.nodes.map((n) => n.id) ?? []);
7756
+ const apiRoutes = loadApiRoutesFromOutput(apiOutput);
7757
+ const apiPathMap = buildApiPathMap(apiRoutes);
7758
+ const srcDir = (0, import_node_path6.join)(rootDir, "src");
7759
+ const files = walk3(srcDir, [".ts", ".tsx"]);
7760
+ const crossRefs = [];
7761
+ const flaggedEdges = [];
7762
+ const seen = /* @__PURE__ */ new Set();
7763
+ for (const absPath of files) {
7764
+ const content = (0, import_node_fs6.readFileSync)(absPath, "utf-8");
7765
+ const sourceId = toNodeId2(srcDir, absPath);
7766
+ if (!uiNodeIds.has(sourceId)) continue;
7767
+ let match;
7768
+ API_ANNOTATION_RE.lastIndex = 0;
7769
+ while ((match = API_ANNOTATION_RE.exec(content)) !== null) {
7770
+ const method = match[1];
7771
+ const urlPath = match[2];
7772
+ const result = resolveUrlPath(urlPath, apiPathMap, apiRoutes);
7773
+ if (result.kind === "resolved" && result.nodeId) {
7774
+ const key = `${sourceId}|${result.nodeId}|calls_api`;
7775
+ if (seen.has(key)) continue;
7776
+ seen.add(key);
7777
+ crossRefs.push({
7778
+ source: sourceId,
7779
+ target: result.nodeId,
7780
+ type: "calls_api",
7781
+ layer: "api"
7782
+ });
7783
+ } else {
7784
+ flaggedEdges.push({
7785
+ source: sourceId,
7786
+ target: "UNRESOLVED",
7787
+ type: "annotation_unresolved",
7788
+ label: `@api ${method} ${urlPath} \u2014 no matching API route found`,
7789
+ confidence: "high"
7790
+ });
7791
+ }
7792
+ }
7793
+ }
7794
+ return {
7795
+ cross_refs: crossRefs,
7796
+ flagged_edges: flaggedEdges,
7797
+ warnings: [],
7798
+ patterns: {
7799
+ annotations_found: crossRefs.length + flaggedEdges.length,
7800
+ annotations_resolved: crossRefs.length,
7801
+ annotations_unresolved: flaggedEdges.length
7802
+ }
7803
+ };
7804
+ }
7805
+ };
7806
+
7807
+ // src/server/graph/parsers/crosslayer/url-literal-scanner.ts
7808
+ var import_node_fs7 = require("node:fs");
7809
+ var import_node_path7 = require("node:path");
7810
+ var URL_LITERAL_RE = /['"`](\/api\/[^'"`\s]+?)['"`]/g;
7811
+ function walk4(dir, exts) {
7812
+ if (!(0, import_node_fs7.existsSync)(dir)) return [];
7813
+ const results = [];
7814
+ for (const entry of (0, import_node_fs7.readdirSync)(dir, { withFileTypes: true })) {
7815
+ if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
7816
+ const full = (0, import_node_path7.join)(dir, entry.name);
7817
+ if (entry.isDirectory()) {
7818
+ results.push(...walk4(full, exts));
7819
+ } else if (exts.includes((0, import_node_path7.extname)(entry.name))) {
7820
+ results.push(full);
7821
+ }
7822
+ }
7823
+ return results;
7824
+ }
7825
+ function toNodeId3(srcDir, absPath) {
7826
+ return (0, import_node_path7.relative)(srcDir, absPath).replace(/\\/g, "/");
7827
+ }
7828
+ var urlLiteralScannerParser = {
7829
+ id: "url-literal-scanner",
7830
+ layer: "crosslayer",
7831
+ detect(rootDir) {
7832
+ return (0, import_node_fs7.existsSync)((0, import_node_path7.join)(rootDir, "src"));
7833
+ },
7834
+ generate(rootDir, layerOutputs) {
7835
+ const apiOutput = layerOutputs.get("api");
7836
+ if (!apiOutput) {
7837
+ return { cross_refs: [], flagged_edges: [], warnings: [] };
7838
+ }
7839
+ const uiOutput = layerOutputs.get("ui");
7840
+ const uiNodeIds = new Set(uiOutput?.nodes.map((n) => n.id) ?? []);
7841
+ const apiRoutes = loadApiRoutesFromOutput(apiOutput);
7842
+ const apiPathMap = buildApiPathMap(apiRoutes);
7843
+ const srcDir = (0, import_node_path7.join)(rootDir, "src");
7844
+ const clientDir = (0, import_node_path7.join)(srcDir, "client");
7845
+ const appDir = (0, import_node_path7.join)(srcDir, "app");
7846
+ const files = [
7847
+ ...walk4(clientDir, [".ts", ".tsx"]),
7848
+ ...walk4(appDir, [".ts", ".tsx"])
7849
+ ];
7850
+ const crossRefs = [];
7851
+ const seen = /* @__PURE__ */ new Set();
7852
+ for (const absPath of files) {
7853
+ const sourceId = toNodeId3(srcDir, absPath);
7854
+ if (!uiNodeIds.has(sourceId)) continue;
7855
+ const content = (0, import_node_fs7.readFileSync)(absPath, "utf-8");
7856
+ let match;
7857
+ URL_LITERAL_RE.lastIndex = 0;
7858
+ while ((match = URL_LITERAL_RE.exec(content)) !== null) {
7859
+ const urlPath = match[1];
7860
+ const result = resolveUrlPath(urlPath, apiPathMap, apiRoutes);
7861
+ if (result.kind === "resolved" && result.nodeId) {
7862
+ const key = `${sourceId}|${result.nodeId}|references_api`;
7863
+ if (seen.has(key)) continue;
7864
+ seen.add(key);
7865
+ crossRefs.push({
7866
+ source: sourceId,
7867
+ target: result.nodeId,
7868
+ type: "references_api",
7869
+ layer: "api"
7870
+ });
7871
+ }
7872
+ }
7873
+ }
7874
+ return {
7875
+ cross_refs: crossRefs,
7876
+ flagged_edges: [],
7877
+ warnings: [],
7878
+ patterns: {
7879
+ url_literals_resolved: crossRefs.length
7880
+ }
7881
+ };
7882
+ }
7883
+ };
7884
+
7885
+ // src/server/graph/core/parser-registry.ts
7886
+ var ParserRegistry = class {
7887
+ constructor() {
7888
+ this.parsers = /* @__PURE__ */ new Map();
7889
+ this.ids = /* @__PURE__ */ new Set();
7890
+ }
7891
+ register(parser) {
7892
+ if (this.ids.has(parser.id)) {
7893
+ throw new Error(`Duplicate parser id: ${parser.id}`);
7894
+ }
7895
+ this.ids.add(parser.id);
7896
+ const list = this.parsers.get(parser.layer) ?? [];
7897
+ list.push(parser);
7898
+ this.parsers.set(parser.layer, list);
7899
+ }
7900
+ getParsers(layer) {
7901
+ return this.parsers.get(layer) ?? [];
7902
+ }
7903
+ getCrossLayerParsers() {
7904
+ return this.parsers.get("crosslayer") ?? [];
7905
+ }
7906
+ getAll() {
7907
+ const all = [];
7908
+ for (const list of this.parsers.values()) all.push(...list);
7909
+ return all;
7910
+ }
7911
+ };
7912
+ function registerBuiltins(registry, disabled) {
7913
+ const builtins = [
7914
+ reactNextjsParser,
7915
+ nextjsRoutesParser,
7916
+ prismaSchemaParser,
7917
+ fetchResolverParser,
7918
+ apiAnnotationsParser,
7919
+ urlLiteralScannerParser
7920
+ ];
7921
+ for (const parser of builtins) {
7922
+ if (disabled.has(parser.id)) continue;
7923
+ registry.register(parser);
7924
+ }
7925
+ }
7926
+ function loadCustomParsers(registry, config, rootDir, disabled) {
7927
+ for (const entry of config.parsers?.custom ?? []) {
7928
+ try {
7929
+ const absPath = (0, import_node_path8.resolve)(rootDir, entry.path);
7930
+ const mod = require(absPath);
7931
+ const parser = "default" in mod ? mod.default : mod;
7932
+ if (disabled.has(parser.id)) continue;
7933
+ if (parser.layer !== entry.layer) {
7934
+ process.stderr.write(
7935
+ `[launch-chart] custom parser "${parser.id}" declares layer "${parser.layer}" but config says "${entry.layer}" \u2014 using parser's layer
7936
+ `
7937
+ );
7938
+ }
7939
+ registry.register(parser);
7940
+ } catch (err2) {
7941
+ process.stderr.write(`[launch-chart] failed to load custom parser from ${entry.path}: ${err2}
7942
+ `);
7943
+ }
7944
+ }
7945
+ }
7946
+ function createRegistry(config, rootDir) {
7947
+ const registry = new ParserRegistry();
7948
+ const disabled = new Set(config.parsers?.disabled ?? []);
7949
+ registerBuiltins(registry, disabled);
7950
+ loadCustomParsers(registry, config, rootDir, disabled);
7951
+ return registry;
7952
+ }
7953
+
7954
+ // src/server/graph/core/merge.ts
7955
+ function mergeGraphOutputs(outputs, layer) {
7956
+ if (outputs.length === 0) {
7957
+ return {
7958
+ metadata: { generated: (/* @__PURE__ */ new Date()).toISOString(), scope: "", layer },
7959
+ nodes: [],
7960
+ edges: [],
7961
+ cross_refs: [],
7962
+ contradictions: [],
7963
+ warnings: [],
7964
+ flagged_edges: []
7965
+ };
7966
+ }
7967
+ if (outputs.length === 1) return outputs[0];
7968
+ const seenNodes = /* @__PURE__ */ new Set();
7969
+ const seenEdges = /* @__PURE__ */ new Set();
7970
+ const seenCrossRefs = /* @__PURE__ */ new Set();
7971
+ const mergedNodes = [];
7972
+ const mergedEdges = [];
7973
+ const mergedCrossRefs = [];
7974
+ const mergedContradictions = [];
7975
+ const mergedWarnings = [];
7976
+ const mergedFlagged = [];
7977
+ const parserIds = [];
7978
+ for (const output of outputs) {
7979
+ if (output.metadata.parser) {
7980
+ parserIds.push(String(output.metadata.parser));
7981
+ }
7982
+ for (const node of output.nodes) {
7983
+ if (seenNodes.has(node.id)) {
7984
+ mergedWarnings.push({
7985
+ type: "merge_conflict",
7986
+ detail: `Node "${node.id}" produced by multiple parsers; keeping first`
7987
+ });
7988
+ continue;
7989
+ }
7990
+ seenNodes.add(node.id);
7991
+ mergedNodes.push(node);
7992
+ }
7993
+ for (const edge of output.edges) {
7994
+ const key = `${edge.source}|${edge.target}|${edge.type}`;
7995
+ if (seenEdges.has(key)) continue;
7996
+ seenEdges.add(key);
7997
+ mergedEdges.push(edge);
7998
+ }
7999
+ for (const ref of output.cross_refs) {
8000
+ const key = `${ref.source}|${ref.target}|${ref.type}`;
8001
+ if (seenCrossRefs.has(key)) continue;
8002
+ seenCrossRefs.add(key);
8003
+ mergedCrossRefs.push(ref);
8004
+ }
8005
+ mergedContradictions.push(...output.contradictions);
8006
+ mergedWarnings.push(...output.warnings);
8007
+ mergedFlagged.push(...output.flagged_edges);
8008
+ }
8009
+ const metadata = {
8010
+ ...outputs[0].metadata,
8011
+ generated: (/* @__PURE__ */ new Date()).toISOString(),
8012
+ parsers: parserIds
8013
+ };
8014
+ return {
8015
+ metadata,
8016
+ nodes: mergedNodes,
8017
+ edges: mergedEdges,
8018
+ cross_refs: mergedCrossRefs,
8019
+ contradictions: mergedContradictions,
8020
+ warnings: mergedWarnings,
8021
+ flagged_edges: mergedFlagged,
8022
+ patterns: outputs[0].patterns
8023
+ };
8024
+ }
8025
+ function dedupCrossRefs(refs) {
8026
+ const seen = /* @__PURE__ */ new Set();
8027
+ const result = [];
8028
+ for (const ref of refs) {
8029
+ const key = `${ref.source}|${ref.target}|${ref.type}`;
8030
+ if (seen.has(key)) continue;
8031
+ seen.add(key);
8032
+ result.push(ref);
8033
+ }
8034
+ return result;
8035
+ }
8036
+ function applyCrossLayerResults(uiOutput, results, primaryId) {
8037
+ const allCrossRefs = [...uiOutput.cross_refs];
8038
+ const allFlagged = [...uiOutput.flagged_edges];
8039
+ const allWarnings = [...uiOutput.warnings];
8040
+ const primaryResult = results.find((r) => r.parserId === primaryId);
8041
+ const secondaryResults = results.filter((r) => r.parserId !== primaryId);
8042
+ if (primaryResult) {
8043
+ allCrossRefs.push(...primaryResult.output.cross_refs);
8044
+ allFlagged.push(...primaryResult.output.flagged_edges);
8045
+ allWarnings.push(...primaryResult.output.warnings);
8046
+ }
8047
+ const primarySet = new Set(
8048
+ (primaryResult?.output.cross_refs ?? []).map((r) => `${r.source}|${r.target}|${r.type}`)
8049
+ );
8050
+ for (const sec of secondaryResults) {
8051
+ for (const ref of sec.output.cross_refs) {
8052
+ const key = `${ref.source}|${ref.target}|${ref.type}`;
8053
+ if (primarySet.has(key)) {
8054
+ allCrossRefs.push(ref);
8055
+ } else {
8056
+ allFlagged.push({
8057
+ source: ref.source,
8058
+ target: ref.target,
8059
+ type: "out_of_pattern",
8060
+ label: `API call detected by ${sec.parserId} but not by primary (${primaryId})`,
8061
+ confidence: "medium"
8062
+ });
8063
+ allCrossRefs.push(ref);
8064
+ }
8065
+ }
8066
+ allFlagged.push(...sec.output.flagged_edges);
8067
+ allWarnings.push(...sec.output.warnings);
8068
+ }
8069
+ return {
8070
+ ...uiOutput,
8071
+ cross_refs: dedupCrossRefs(allCrossRefs),
8072
+ flagged_edges: allFlagged,
8073
+ warnings: allWarnings
8074
+ };
8075
+ }
8076
+
8077
+ // src/server/graph/core/graph-builder.ts
8078
+ function readGraphFromDisk(rootDir, layer) {
8079
+ const filePath = (0, import_node_path9.join)(rootDir, ".launchsecure", "graphs", `${layer}.json`);
8080
+ if (!(0, import_node_fs8.existsSync)(filePath)) return null;
8081
+ try {
8082
+ return JSON.parse((0, import_node_fs8.readFileSync)(filePath, "utf-8"));
8083
+ } catch {
8084
+ return null;
8085
+ }
8086
+ }
8087
+ function generateLayer(rootDir, layer) {
8088
+ const config = loadConfig(rootDir);
8089
+ const registry = createRegistry(config, rootDir);
8090
+ const parsers = registry.getParsers(layer);
8091
+ const outputs = [];
8092
+ for (const parser of parsers) {
8093
+ if (!parser.detect(rootDir)) continue;
8094
+ outputs.push(parser.generate(rootDir));
8095
+ }
8096
+ if (outputs.length === 0) return null;
8097
+ let merged = outputs.length === 1 ? outputs[0] : mergeGraphOutputs(outputs, layer);
8098
+ if (layer === "ui") {
8099
+ const layerOutputs = /* @__PURE__ */ new Map();
8100
+ layerOutputs.set("ui", merged);
8101
+ for (const otherLayer of ["api", "db"]) {
8102
+ const existing = readGraphFromDisk(rootDir, otherLayer);
8103
+ if (existing) layerOutputs.set(otherLayer, existing);
8104
+ }
8105
+ const crossParsers = registry.getCrossLayerParsers();
8106
+ const primaryId = config.parsers?.primary?.crosslayer ?? crossParsers[0]?.id ?? null;
8107
+ const crossResults = crossParsers.filter((p) => p.detect(rootDir)).map((p) => ({ parserId: p.id, output: p.generate(rootDir, layerOutputs) }));
8108
+ if (crossResults.length > 0) {
8109
+ merged = applyCrossLayerResults(merged, crossResults, primaryId);
8110
+ }
8111
+ }
8112
+ return {
8113
+ layer,
8114
+ output: merged,
8115
+ nodeCount: merged.nodes.length,
8116
+ edgeCount: merged.edges.length
8117
+ };
8118
+ }
8119
+ function generateAll(rootDir) {
8120
+ const config = loadConfig(rootDir);
8121
+ const registry = createRegistry(config, rootDir);
8122
+ const layerOrder = ["api", "db", "ui"];
8123
+ const layerOutputs = /* @__PURE__ */ new Map();
8124
+ const results = [];
8125
+ for (const layer of layerOrder) {
8126
+ const parsers = registry.getParsers(layer);
8127
+ const outputs = [];
8128
+ for (const parser of parsers) {
8129
+ if (!parser.detect(rootDir)) continue;
8130
+ outputs.push(parser.generate(rootDir));
8131
+ }
8132
+ if (outputs.length === 0) continue;
8133
+ const merged = outputs.length === 1 ? outputs[0] : mergeGraphOutputs(outputs, layer);
8134
+ layerOutputs.set(layer, merged);
8135
+ results.push({
8136
+ layer,
8137
+ output: merged,
8138
+ nodeCount: merged.nodes.length,
8139
+ edgeCount: merged.edges.length
8140
+ });
8141
+ }
8142
+ const crossParsers = registry.getCrossLayerParsers();
8143
+ const primaryId = config.parsers?.primary?.crosslayer ?? crossParsers[0]?.id ?? null;
8144
+ const crossResults = crossParsers.filter((p) => p.detect(rootDir)).map((p) => ({ parserId: p.id, output: p.generate(rootDir, layerOutputs) }));
8145
+ if (crossResults.length > 0 && layerOutputs.has("ui")) {
8146
+ const uiOutput = layerOutputs.get("ui");
8147
+ const merged = applyCrossLayerResults(uiOutput, crossResults, primaryId);
8148
+ layerOutputs.set("ui", merged);
8149
+ const uiResult = results.find((r) => r.layer === "ui");
8150
+ if (uiResult) {
8151
+ uiResult.output = merged;
8152
+ uiResult.nodeCount = merged.nodes.length;
8153
+ uiResult.edgeCount = merged.edges.length;
8154
+ }
8155
+ }
8156
+ const byLayer = new Map(results.map((r) => [r.layer, r]));
8157
+ return ["ui", "api", "db"].map((l) => byLayer.get(l)).filter((r) => !!r);
8158
+ }
8159
+
8160
+ // src/server/graph/index.ts
8161
+ init_config();
8162
+
8163
+ // src/server/graph/core/tagger-registry.ts
8164
+ var import_node_path11 = require("node:path");
8165
+
8166
+ // src/server/graph/taggers/module-tagger.ts
8167
+ var import_node_fs9 = require("node:fs");
8168
+ var import_node_path10 = require("node:path");
8169
+ function matchGlob(pattern, id) {
8170
+ const patParts = pattern.split("/");
8171
+ const idParts = id.split("/");
8172
+ return matchParts(patParts, 0, idParts, 0);
8173
+ }
8174
+ function matchParts(pat, pi, id, ii) {
8175
+ while (pi < pat.length && ii < id.length) {
8176
+ const p = pat[pi];
8177
+ if (p === "**") {
8178
+ for (let skip = ii; skip <= id.length; skip++) {
8179
+ if (matchParts(pat, pi + 1, id, skip)) return true;
8180
+ }
8181
+ return false;
8182
+ }
8183
+ if (p === "*") {
8184
+ pi++;
8185
+ ii++;
8186
+ continue;
8187
+ }
8188
+ if (p !== id[ii]) return false;
8189
+ pi++;
8190
+ ii++;
8191
+ }
8192
+ while (pi < pat.length && pat[pi] === "**") pi++;
8193
+ return pi === pat.length && ii === id.length;
8194
+ }
8195
+ var CONVENTION_DIRS = ["features", "modules", "domains", "areas"];
8196
+ function detectConventionDirs(rootDir) {
8197
+ const result = /* @__PURE__ */ new Map();
8198
+ const searchDirs = [
8199
+ rootDir,
8200
+ (0, import_node_path10.join)(rootDir, "src"),
8201
+ (0, import_node_path10.join)(rootDir, "app"),
8202
+ (0, import_node_path10.join)(rootDir, "lib")
8203
+ ];
8204
+ for (const base of searchDirs) {
8205
+ for (const convention of CONVENTION_DIRS) {
8206
+ const dir = (0, import_node_path10.join)(base, convention);
8207
+ if (!(0, import_node_fs9.existsSync)(dir)) continue;
8208
+ try {
8209
+ const stat = (0, import_node_fs9.statSync)(dir);
8210
+ if (!stat.isDirectory()) continue;
8211
+ const entries = (0, import_node_fs9.readdirSync)(dir, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => e.name);
8212
+ if (entries.length > 0) {
8213
+ const relPath = dir.replace(rootDir + "/", "").replace(rootDir + "\\", "");
8214
+ result.set(relPath, entries);
8215
+ }
8216
+ } catch {
8217
+ }
8218
+ }
8219
+ }
8220
+ return result;
8221
+ }
8222
+ function extractRouteGroups(id) {
8223
+ const groups = [];
8224
+ const re = /\(([^)]+)\)/g;
8225
+ let m;
8226
+ while ((m = re.exec(id)) !== null) {
8227
+ groups.push(m[1]);
8228
+ }
8229
+ return groups;
8230
+ }
8231
+ var SKIP_SEGMENTS = /* @__PURE__ */ new Set([
8232
+ "src",
8233
+ "app",
8234
+ "client",
8235
+ "server",
8236
+ "lib",
8237
+ "config"
8238
+ ]);
8239
+ function isRouteGroup(segment) {
8240
+ return segment.startsWith("(") && segment.endsWith(")");
8241
+ }
8242
+ function isDynamicSegment(segment) {
8243
+ return segment.startsWith("[") || segment.startsWith(":");
8244
+ }
8245
+ function isDomainDir(segment) {
8246
+ return segment.includes(".") && !segment.endsWith(".tsx") && !segment.endsWith(".ts") && !segment.endsWith(".js") && !segment.endsWith(".jsx") && !segment.endsWith(".vue");
8247
+ }
8248
+ var TRIVIAL_GROUPS = /* @__PURE__ */ new Set([
8249
+ "app",
8250
+ "all",
8251
+ "ee",
8252
+ "home",
8253
+ "root"
8254
+ ]);
8255
+ function isTrivialGroup(name) {
8256
+ if (TRIVIAL_GROUPS.has(name)) return true;
8257
+ const lower = name.toLowerCase();
8258
+ const wrapperPatterns = [
8259
+ /^.*-?wrapper$/,
8260
+ // "page-wrapper", "use-page-wrapper"
8261
+ /^.*-?layout$/,
8262
+ // "admin-layout", "settings-layout"
8263
+ /^use-/,
8264
+ // "use-page-wrapper"
8265
+ /^default$/
8266
+ ];
8267
+ return wrapperPatterns.some((p) => p.test(lower));
8268
+ }
8269
+ function normalizeGroupName(name) {
8270
+ return name.replace(/-pages?$/, "").replace(/-layout$/, "").replace(/-wrapper$/, "");
8271
+ }
8272
+ function extractModuleFromPath(id) {
8273
+ const segments = id.split("/");
8274
+ const routeGroups = extractRouteGroups(id);
8275
+ const moduleGroups = routeGroups.filter((g) => !isTrivialGroup(g)).map(normalizeGroupName);
8276
+ if (moduleGroups.length > 0) {
8277
+ return moduleGroups[moduleGroups.length - 1];
8278
+ }
8279
+ const meaningful = [];
8280
+ for (const seg of segments) {
8281
+ if (seg.includes(".")) continue;
8282
+ if (isRouteGroup(seg)) continue;
8283
+ if (isDynamicSegment(seg)) continue;
8284
+ if (isDomainDir(seg)) continue;
8285
+ if (SKIP_SEGMENTS.has(seg)) continue;
8286
+ meaningful.push(seg);
8287
+ }
8288
+ if (meaningful.length > 0) {
8289
+ return meaningful[0];
8290
+ }
8291
+ return "root";
8292
+ }
8293
+ var cachedRootDir = null;
8294
+ var cachedConventionDirs = /* @__PURE__ */ new Map();
8295
+ var moduleTagger = {
8296
+ id: "module",
8297
+ tagKey: "module",
8298
+ trackUntagged: true,
8299
+ layers: null,
8300
+ // applies to all layers
8301
+ tag(nodes, layer, rootDir) {
8302
+ if (cachedRootDir !== rootDir) {
8303
+ cachedConventionDirs = detectConventionDirs(rootDir);
8304
+ cachedRootDir = rootDir;
8305
+ }
8306
+ let configRules = [];
8307
+ try {
8308
+ const { loadConfig: loadConfig2 } = (init_config(), __toCommonJS(config_exports));
8309
+ const config = loadConfig2(rootDir);
8310
+ configRules = config.taggers?.module?.rules ?? [];
8311
+ } catch {
8312
+ }
8313
+ const result = /* @__PURE__ */ new Map();
8314
+ for (const node of nodes) {
8315
+ const id = node.id;
8316
+ let matched = false;
8317
+ for (const rule of configRules) {
8318
+ if (matchGlob(rule.match, id)) {
8319
+ result.set(id, rule.module);
8320
+ matched = true;
8321
+ break;
8322
+ }
8323
+ }
8324
+ if (matched) continue;
8325
+ matched = false;
8326
+ for (const [convDir, moduleNames] of cachedConventionDirs) {
8327
+ if (id.startsWith(convDir + "/")) {
8328
+ const rest = id.slice(convDir.length + 1);
8329
+ const firstSeg = rest.split("/")[0];
8330
+ if (moduleNames.includes(firstSeg)) {
8331
+ result.set(id, firstSeg);
8332
+ matched = true;
8333
+ break;
8334
+ }
8335
+ }
8336
+ }
8337
+ if (matched) continue;
8338
+ const module2 = extractModuleFromPath(id);
8339
+ result.set(id, module2);
8340
+ }
8341
+ return result;
8342
+ }
8343
+ };
8344
+
8345
+ // src/server/graph/taggers/screen-tagger.ts
8346
+ var SCREEN_TYPES = /* @__PURE__ */ new Set(["page", "layout"]);
8347
+ var screenTagger = {
8348
+ id: "screen",
8349
+ tagKey: "screen",
8350
+ trackUntagged: true,
8351
+ layers: ["ui"],
8352
+ tag(nodes, layer) {
8353
+ if (layer !== "ui") return /* @__PURE__ */ new Map();
8354
+ const result = /* @__PURE__ */ new Map();
8355
+ for (const node of nodes) {
8356
+ if (SCREEN_TYPES.has(node.type)) {
8357
+ result.set(node.id, "true");
8358
+ }
8359
+ }
8360
+ return result;
8361
+ }
8362
+ };
8363
+
8364
+ // src/server/graph/core/tagger-registry.ts
8365
+ var TaggerRegistry = class {
8366
+ constructor() {
8367
+ this.taggers = [];
8368
+ this.ids = /* @__PURE__ */ new Set();
8369
+ }
8370
+ register(tagger) {
8371
+ if (this.ids.has(tagger.id)) {
8372
+ throw new Error(`Duplicate tagger id: ${tagger.id}`);
8373
+ }
8374
+ this.ids.add(tagger.id);
8375
+ this.taggers.push(tagger);
8376
+ }
8377
+ getAll() {
8378
+ return this.taggers;
8379
+ }
8380
+ getForLayer(layer) {
8381
+ return this.taggers.filter((t) => t.layers === null || t.layers.includes(layer));
8382
+ }
8383
+ };
8384
+ var BUILTIN_TAGGERS = [moduleTagger, screenTagger];
8385
+ function registerBuiltins2(registry, disabled, config) {
8386
+ for (const tagger of BUILTIN_TAGGERS) {
8387
+ if (disabled.has(tagger.id)) continue;
8388
+ const override = config.taggers?.trackUntagged?.[tagger.id];
8389
+ if (override !== void 0) {
8390
+ tagger.trackUntagged = override;
8391
+ }
8392
+ registry.register(tagger);
8393
+ }
8394
+ }
8395
+ function loadCustomTaggers(registry, config, rootDir, disabled) {
8396
+ for (const entry of config.taggers?.custom ?? []) {
8397
+ if (disabled.has(entry.id)) continue;
8398
+ try {
8399
+ const absPath = (0, import_node_path11.resolve)(rootDir, entry.path);
8400
+ const mod = require(absPath);
8401
+ const tagger = "default" in mod ? mod.default : mod;
8402
+ const override = config.taggers?.trackUntagged?.[tagger.id];
8403
+ if (override !== void 0) {
8404
+ tagger.trackUntagged = override;
8405
+ }
8406
+ registry.register(tagger);
8407
+ } catch (err2) {
8408
+ process.stderr.write(`[launch-chart] failed to load custom tagger from ${entry.path}: ${err2}
8409
+ `);
8410
+ }
8411
+ }
8412
+ }
8413
+ function createTaggerRegistry(config, rootDir) {
8414
+ const registry = new TaggerRegistry();
8415
+ const disabled = new Set(config.taggers?.disabled ?? []);
8416
+ registerBuiltins2(registry, disabled, config);
8417
+ loadCustomTaggers(registry, config, rootDir, disabled);
8418
+ return registry;
8419
+ }
8420
+
8421
+ // src/server/graph/core/tag-store.ts
8422
+ var import_node_fs10 = require("node:fs");
8423
+ var import_node_path12 = require("node:path");
8424
+ var TAGS_FILENAME = "tags.json";
8425
+ var GRAPHS_DIR = ".launchsecure/graphs";
8426
+ var tagCache = /* @__PURE__ */ new Map();
8427
+ function tagsFilePath(rootDir) {
8428
+ return (0, import_node_path12.join)(rootDir, GRAPHS_DIR, TAGS_FILENAME);
8429
+ }
8430
+ function readTagStore(rootDir) {
8431
+ const filePath = tagsFilePath(rootDir);
8432
+ if (!(0, import_node_fs10.existsSync)(filePath)) return {};
8433
+ const stat = (0, import_node_fs10.statSync)(filePath);
8434
+ const cached = tagCache.get(filePath);
8435
+ if (cached && cached.mtimeMs === stat.mtimeMs) {
8436
+ return cached.store;
8437
+ }
8438
+ try {
8439
+ const content = (0, import_node_fs10.readFileSync)(filePath, "utf-8");
8440
+ const store = JSON.parse(content);
8441
+ tagCache.set(filePath, { mtimeMs: stat.mtimeMs, store });
8442
+ return store;
8443
+ } catch {
8444
+ return {};
8445
+ }
8446
+ }
8447
+ function writeTagStore(rootDir, store) {
8448
+ const filePath = tagsFilePath(rootDir);
8449
+ const dir = (0, import_node_path12.dirname)(filePath);
8450
+ (0, import_node_fs10.mkdirSync)(dir, { recursive: true });
8451
+ const cleaned = {};
8452
+ for (const [nodeId, tags] of Object.entries(store)) {
8453
+ if (Object.keys(tags).length > 0) {
8454
+ cleaned[nodeId] = tags;
8455
+ }
8456
+ }
8457
+ (0, import_node_fs10.writeFileSync)(filePath, JSON.stringify(cleaned, null, 2) + "\n", "utf-8");
8458
+ tagCache.delete(filePath);
8459
+ }
8460
+ function setTag(rootDir, nodeId, key, value) {
8461
+ const store = readTagStore(rootDir);
8462
+ if (!store[nodeId]) store[nodeId] = {};
8463
+ store[nodeId][key] = value;
8464
+ writeTagStore(rootDir, store);
8465
+ }
8466
+ function removeTag(rootDir, nodeId, key) {
8467
+ const store = readTagStore(rootDir);
8468
+ if (!store[nodeId]) return;
8469
+ delete store[nodeId][key];
8470
+ if (Object.keys(store[nodeId]).length === 0) {
8471
+ delete store[nodeId];
8472
+ }
8473
+ writeTagStore(rootDir, store);
8474
+ }
8475
+
8476
+ // src/server/graph/index.ts
8477
+ var GRAPHS_DIR2 = ".launchsecure/graphs";
8478
+ var LAYERS = ["ui", "api", "db"];
8479
+ var graphCache = /* @__PURE__ */ new Map();
8480
+ var taggedCache = /* @__PURE__ */ new Map();
8481
+ function graphsDir(rootDir) {
8482
+ return (0, import_node_path13.join)(rootDir, GRAPHS_DIR2);
8483
+ }
8484
+ function graphFilePath(rootDir, layer) {
8485
+ return (0, import_node_path13.join)(graphsDir(rootDir), `${layer}.json`);
8486
+ }
8487
+ function tagsFilePath2(rootDir) {
8488
+ return (0, import_node_path13.join)(graphsDir(rootDir), "tags.json");
8489
+ }
8490
+ function getMtimeMs(filePath) {
8491
+ if (!(0, import_node_fs11.existsSync)(filePath)) return 0;
8492
+ return (0, import_node_fs11.statSync)(filePath).mtimeMs;
8493
+ }
8494
+ function invalidateCache(filePath) {
8495
+ graphCache.delete(filePath);
8496
+ }
8497
+ function invalidateTaggedCache(rootDir, layer) {
8498
+ taggedCache.delete(`${rootDir}:${layer}`);
8499
+ }
8500
+ function applyTags(graph, layer, rootDir) {
8501
+ const config = loadConfig(rootDir);
8502
+ const registry = createTaggerRegistry(config, rootDir);
8503
+ const manualTags = readTagStore(rootDir);
8504
+ const taggedNodes = graph.nodes.map((n) => ({ ...n }));
8505
+ const taggers = registry.getForLayer(layer);
8506
+ for (const tagger of taggers) {
8507
+ const assignments = tagger.tag(taggedNodes, layer, rootDir);
8508
+ for (const node of taggedNodes) {
8509
+ if (!node.tags) node.tags = {};
8510
+ const tags = node.tags;
8511
+ const value = assignments.get(node.id);
8512
+ if (value !== void 0) {
8513
+ tags[tagger.tagKey] = value;
8514
+ } else if (tagger.trackUntagged) {
8515
+ tags[tagger.tagKey] = "untagged";
8516
+ }
8517
+ }
8518
+ }
8519
+ for (const node of taggedNodes) {
8520
+ const manual = manualTags[node.id];
8521
+ if (manual) {
8522
+ if (!node.tags) node.tags = {};
8523
+ const tags = node.tags;
8524
+ Object.assign(tags, manual);
8525
+ }
8526
+ }
8527
+ return { ...graph, nodes: taggedNodes };
8528
+ }
8529
+ function readGraphRaw(rootDir, layer) {
8530
+ const filePath = graphFilePath(rootDir, layer);
8531
+ if (!(0, import_node_fs11.existsSync)(filePath)) return null;
8532
+ const stat = (0, import_node_fs11.statSync)(filePath);
8533
+ const cached = graphCache.get(filePath);
8534
+ if (cached && cached.mtimeMs === stat.mtimeMs) {
8535
+ return cached.graph;
8536
+ }
8537
+ const content = (0, import_node_fs11.readFileSync)(filePath, "utf-8");
8538
+ const graph = JSON.parse(content);
8539
+ graphCache.set(filePath, { mtimeMs: stat.mtimeMs, graph });
8540
+ return graph;
8541
+ }
8542
+ function readGraph(rootDir, layer) {
8543
+ const rawFilePath = graphFilePath(rootDir, layer);
8544
+ if (!(0, import_node_fs11.existsSync)(rawFilePath)) return null;
8545
+ const rawMtime = getMtimeMs(rawFilePath);
8546
+ const tagsMtime = getMtimeMs(tagsFilePath2(rootDir));
8547
+ const cacheKey = `${rootDir}:${layer}`;
8548
+ const cached = taggedCache.get(cacheKey);
8549
+ if (cached && cached.rawMtimeMs === rawMtime && cached.tagsMtimeMs === tagsMtime) {
8550
+ return cached.graph;
8551
+ }
8552
+ const raw = readGraphRaw(rootDir, layer);
8553
+ if (!raw) return null;
8554
+ const tagged = applyTags(raw, layer, rootDir);
8555
+ taggedCache.set(cacheKey, { rawMtimeMs: rawMtime, tagsMtimeMs: tagsMtime, graph: tagged });
8556
+ return tagged;
8557
+ }
8558
+ function readAllGraphs(rootDir) {
8559
+ const result = {};
8560
+ for (const layer of LAYERS) {
8561
+ const graph = readGraph(rootDir, layer);
8562
+ if (graph) result[layer] = graph;
8563
+ }
8564
+ return result;
8565
+ }
8566
+ function generateGraph(rootDir, layer) {
8567
+ const dir = graphsDir(rootDir);
8568
+ (0, import_node_fs11.mkdirSync)(dir, { recursive: true });
8569
+ const results = layer ? [generateLayer(rootDir, layer)].filter((r) => r !== null) : generateAll(rootDir);
8570
+ for (const result of results) {
8571
+ const filePath = graphFilePath(rootDir, result.layer);
8572
+ (0, import_node_fs11.writeFileSync)(filePath, JSON.stringify(result.output, null, 2) + "\n", "utf-8");
8573
+ invalidateCache(filePath);
8574
+ invalidateTaggedCache(rootDir, result.layer);
8575
+ }
8576
+ return results;
8577
+ }
8578
+
8579
+ // src/server/graph-cli.ts
8580
+ var VALID_LAYERS = ["ui", "api", "db"];
8581
+ function parseLayerFlag(args) {
8582
+ const idx = args.indexOf("--layer");
8583
+ if (idx < 0 || idx + 1 >= args.length) return void 0;
8584
+ const value = args[idx + 1];
8585
+ if (!VALID_LAYERS.includes(value)) {
8586
+ console.error(`Invalid layer "${value}". Must be one of: ${VALID_LAYERS.join(", ")}`);
8587
+ process.exit(1);
8588
+ }
8589
+ return value;
8590
+ }
8591
+ function handleGraphCommand(subcommand, args) {
7752
8592
  const rootDir = process.cwd();
7753
8593
  if (subcommand === "graph:generate") {
7754
8594
  const layer = parseLayerFlag(args);
@@ -7792,25 +8632,46 @@ function handleGraphCommand(subcommand, args) {
7792
8632
  }
7793
8633
 
7794
8634
  // src/server/graph-mcp.ts
7795
- var import_node_fs7 = require("node:fs");
7796
- var import_node_path7 = require("node:path");
8635
+ var import_node_fs13 = require("node:fs");
8636
+ var import_node_path15 = require("node:path");
8637
+ var import_node_child_process2 = require("node:child_process");
8638
+ var import_node_os2 = require("node:os");
7797
8639
 
7798
8640
  // src/server/lockfile.ts
7799
8641
  var import_node_child_process = require("node:child_process");
7800
- var import_node_fs6 = require("node:fs");
8642
+ var import_node_fs12 = require("node:fs");
7801
8643
  var import_node_os = require("node:os");
7802
- var import_node_path6 = require("node:path");
7803
- function lockDir() {
7804
- return (0, import_node_path6.join)((0, import_node_os.homedir)(), ".launchsecure");
7805
- }
7806
- function lockPath() {
7807
- return (0, import_node_path6.join)(lockDir(), "launch-chart.lock");
7808
- }
7809
- function readLock() {
7810
- const p = lockPath();
7811
- if (!(0, import_node_fs6.existsSync)(p)) return null;
8644
+ var import_node_path14 = require("node:path");
8645
+ function lockDir(projectRoot) {
8646
+ if (projectRoot) {
8647
+ return (0, import_node_path14.join)(projectRoot, ".launchsecure");
8648
+ }
8649
+ return (0, import_node_path14.join)((0, import_node_os.homedir)(), ".launchsecure");
8650
+ }
8651
+ function lockPath(projectRoot) {
8652
+ return (0, import_node_path14.join)(lockDir(projectRoot), "launch-chart.lock");
8653
+ }
8654
+ var _activeProjectRoot;
8655
+ function readLock(projectRoot) {
8656
+ const root = projectRoot ?? _activeProjectRoot;
8657
+ const p = lockPath(root);
8658
+ if (!(0, import_node_fs12.existsSync)(p)) {
8659
+ if (root) {
8660
+ const globalP = lockPath();
8661
+ if ((0, import_node_fs12.existsSync)(globalP)) {
8662
+ try {
8663
+ const data = JSON.parse((0, import_node_fs12.readFileSync)(globalP, "utf-8"));
8664
+ if (typeof data.pid === "number" && typeof data.port === "number" && data.cwd === root) {
8665
+ return data;
8666
+ }
8667
+ } catch {
8668
+ }
8669
+ }
8670
+ }
8671
+ return null;
8672
+ }
7812
8673
  try {
7813
- const data = JSON.parse((0, import_node_fs6.readFileSync)(p, "utf-8"));
8674
+ const data = JSON.parse((0, import_node_fs12.readFileSync)(p, "utf-8"));
7814
8675
  if (typeof data.pid !== "number" || typeof data.port !== "number") return null;
7815
8676
  return data;
7816
8677
  } catch {
@@ -7839,22 +8700,31 @@ function getListenerPid(port) {
7839
8700
  return null;
7840
8701
  }
7841
8702
  }
7842
- function getLiveLock() {
7843
- const lock = readLock();
8703
+ function getLiveLock(projectRoot) {
8704
+ const root = projectRoot ?? _activeProjectRoot;
8705
+ const lock = readLock(root);
7844
8706
  if (!lock) return null;
7845
8707
  const listenerPid = getListenerPid(lock.port);
7846
8708
  const live = listenerPid !== null ? listenerPid === lock.pid : isPidAlive(lock.pid);
7847
8709
  if (!live) {
7848
8710
  try {
7849
- (0, import_node_fs6.unlinkSync)(lockPath());
8711
+ (0, import_node_fs12.unlinkSync)(lockPath(root));
7850
8712
  } catch {
7851
8713
  }
7852
8714
  return null;
7853
8715
  }
7854
8716
  return lock;
7855
8717
  }
8718
+ function clearLock(projectRoot) {
8719
+ const root = projectRoot ?? _activeProjectRoot;
8720
+ try {
8721
+ (0, import_node_fs12.unlinkSync)(lockPath(root));
8722
+ } catch {
8723
+ }
8724
+ }
7856
8725
 
7857
8726
  // src/server/graph-mcp.ts
8727
+ init_config();
7858
8728
  var SERVER_INFO = {
7859
8729
  name: "launchsecure-graph",
7860
8730
  version: "0.0.1"
@@ -7876,7 +8746,7 @@ var TOOLS = [
7876
8746
  },
7877
8747
  {
7878
8748
  name: "read_graph",
7879
- description: 'Query the structural project graph \u2014 a smart Glob replacement that locates files by type/module/name and returns structural metadata (imports, renders, routes, relations). \n\nUSE THIS FOR: "where is X", "what files are in module Y", "what pages exist under /admin", "what components does Z render", "what tables relate to User", "list all hooks in auth module". \n\nDO NOT USE FOR: finding text/code content (use Grep), reading actual source code (use Read), understanding behavior/logic/patterns (graph has no code semantics \u2014 only names and edges). \n\nQUERY PARAMS (at least one required for node data \u2014 unfiltered calls return summary only to stay in context):\n- search: substring match on node id, name, or route\n- type: filter by node type (ui layer: page, layout, component, ui, hook, context, config, util; api layer: endpoint; db layer: table, enum)\n- module: filter by module (ui layer only: auth, admin, project, org, settings, integrations, shared-ui, layout, root)\n- node_id: return this node + its neighborhood (incoming+outgoing edges within `hops`)\n- hops: neighborhood radius when node_id is set (default 1)\n- minimal: return only id/type/name/module/route per node (skip heavy fields like columns, exports)\n- include_edges: return the actual edge list. Default: TRUE for neighborhood queries (node_id), FALSE for filter queries (search/type/module). Filter responses always include `edge_count`; only pass include_edges:true when you actually need to inspect individual edges (e.g. "which components render X"). This default cuts typical filter responses in half.\n\nBATCH MODE: pass `queries` (array of query objects) to run multiple independent queries in a single call. Each query object uses the same params (layer/search/type/module/node_id/hops/minimal). Returns { batch: true, count, results: [{index, query, result}, ...] }. Use this when you need multiple graph views up-front (e.g. scoping a feature across ui+api+db layers) to save round-trips. When batch mode is used, top-level params are ignored.\n\nReturns: filtered nodes + edges between them. If no filter given, returns per-layer counts and type breakdown only.\n\nWIRE FORMAT (compact): responses that include nodes/edges use short keys and edge-by-index refs to cut payload ~40-60%. Every such response carries a `_schema` legend. Quick reference:\n nodes[]: { i: id, t: type, n: name, m: module, r: route, mt: methods, x: exports, c: columns }\n edges[]: { s: source_node_index, d: target_node_index, t: type, l: label }\nedges.s / edges.d are 0-based indices into THIS response\'s nodes array. If a referenced node is not in the response (boundary case), s/d may instead contain the full node id string \u2014 always check the type.\n\nBUDGET GUARDS:\n- Neighborhood queries stop expanding when the projected response exceeds budget. The response then contains `budget_exceeded: true` plus `hops_traversed < hops_requested`. When this happens, drill into a specific neighbor with another node_id call rather than retrying with larger hops \u2014 it will just truncate again.\n- Batch mode caps total response size. Once the budget is hit, later queries return `{skipped: true, reason: "batch_budget_exhausted"}` and you must re-run them individually.',
8749
+ description: 'Query the structural project graph \u2014 a smart Glob replacement that locates files by type/module/name and returns structural metadata (imports, renders, routes, relations). \n\nUSE THIS FOR: "where is X", "what files are in module Y", "what pages exist under /admin", "what components does Z render", "what tables relate to User", "list all hooks in auth module". \n\nDO NOT USE FOR: finding text/code content (use Grep), reading actual source code (use Read), understanding behavior/logic/patterns (graph has no code semantics \u2014 only names and edges). \n\nQUERY PARAMS (at least one required for node data \u2014 unfiltered calls return summary only to stay in context):\n- search: substring match on node id, name, or route\n- type: filter by node type (ui layer: page, layout, component, ui, hook, context, config, util; api layer: endpoint; db layer: table, enum)\n- module: filter by module tag (computed from directory structure, e.g. "auth", "admin", "settings")\n- node_id: return this node + its neighborhood (incoming+outgoing edges within `hops`)\n- hops: neighborhood radius when node_id is set (default 1)\n- minimal: return only id/type/name/module/route per node (skip heavy fields like columns, exports)\n- include_edges: return the actual edge list. Default: TRUE for neighborhood queries (node_id), FALSE for filter queries (search/type/module). Filter responses always include `edge_count`; only pass include_edges:true when you actually need to inspect individual edges (e.g. "which components render X"). This default cuts typical filter responses in half.\n\nBATCH MODE: pass `queries` (array of query objects) to run multiple independent queries in a single call. Each query object uses the same params (layer/search/type/module/node_id/hops/minimal). Returns { batch: true, count, results: [{index, query, result}, ...] }. Use this when you need multiple graph views up-front (e.g. scoping a feature across ui+api+db layers) to save round-trips. When batch mode is used, top-level params are ignored.\n\nReturns: filtered nodes + edges between them. If no filter given, returns per-layer counts and type breakdown only.\n\nWIRE FORMAT (compact): responses that include nodes/edges use short keys and edge-by-index refs to cut payload ~40-60%. Every such response carries a `_schema` legend. Quick reference:\n nodes[]: { i: id, t: type, n: name, m: module, r: route, mt: methods, x: exports, c: columns }\n edges[]: { s: source_node_index, d: target_node_index, t: type, l: label }\nedges.s / edges.d are 0-based indices into THIS response\'s nodes array. If a referenced node is not in the response (boundary case), s/d may instead contain the full node id string \u2014 always check the type.\n\nBUDGET GUARDS:\n- Neighborhood queries stop expanding when the projected response exceeds budget. The response then contains `budget_exceeded: true` plus `hops_traversed < hops_requested`. When this happens, drill into a specific neighbor with another node_id call rather than retrying with larger hops \u2014 it will just truncate again.\n- Batch mode caps total response size. Once the budget is hit, later queries return `{skipped: true, reason: "batch_budget_exhausted"}` and you must re-run them individually.',
7880
8750
  inputSchema: {
7881
8751
  type: "object",
7882
8752
  properties: {
@@ -7895,7 +8765,15 @@ var TOOLS = [
7895
8765
  },
7896
8766
  module: {
7897
8767
  type: "string",
7898
- description: 'UI layer only \u2014 filter by module (e.g. "auth", "admin", "project", "org").'
8768
+ description: 'Filter by module tag (e.g. "auth", "admin", "settings"). Works across all layers.'
8769
+ },
8770
+ tag_key: {
8771
+ type: "string",
8772
+ description: "Filter by arbitrary tag key. Must be used with tag_value."
8773
+ },
8774
+ tag_value: {
8775
+ type: "string",
8776
+ description: "Filter by tag value for the given tag_key."
7899
8777
  },
7900
8778
  node_id: {
7901
8779
  type: "string",
@@ -7982,12 +8860,83 @@ Returns: { pattern, filter, files_searched, total_matches, matches: [{file, line
7982
8860
  }
7983
8861
  },
7984
8862
  {
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.',
8863
+ name: "chart_server_status",
8864
+ description: `Check whether the launch-chart UI server is running. Returns: { running: boolean, url?: string, port?: number, pid?: number, startedAt?: string, cwd?: string }.
8865
+
8866
+ Use this when the user asks "is the chart running", "show me the project graph UI", "where's the chart", etc.`,
8867
+ inputSchema: {
8868
+ type: "object",
8869
+ properties: {}
8870
+ }
8871
+ },
8872
+ {
8873
+ name: "start_chart_server",
8874
+ 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.',
8875
+ inputSchema: {
8876
+ type: "object",
8877
+ properties: {
8878
+ port: {
8879
+ type: "number",
8880
+ description: "Port to bind the server to. Defaults to 52819 with automatic fallback if in use."
8881
+ }
8882
+ }
8883
+ }
8884
+ },
8885
+ {
8886
+ name: "stop_chart_server",
8887
+ 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.',
7987
8888
  inputSchema: {
7988
8889
  type: "object",
7989
8890
  properties: {}
7990
8891
  }
8892
+ },
8893
+ {
8894
+ name: "detect_project_stack",
8895
+ 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.",
8896
+ inputSchema: {
8897
+ type: "object",
8898
+ properties: {}
8899
+ }
8900
+ },
8901
+ {
8902
+ name: "add_tag",
8903
+ description: 'Tag a graph node with a key-value pair. Tags persist in .launchsecure/graphs/tags.json and survive graph regeneration. Use for annotating nodes with arbitrary metadata (e.g. "refactor_later", "owner", "priority"). Manual tags override computed tags (like module and screen) for the same key.',
8904
+ inputSchema: {
8905
+ type: "object",
8906
+ properties: {
8907
+ node_id: {
8908
+ type: "string",
8909
+ description: 'The node id to tag (e.g. "app/(auth)/login/page.tsx").'
8910
+ },
8911
+ key: {
8912
+ type: "string",
8913
+ description: 'Tag key (e.g. "module", "owner", "refactor_later").'
8914
+ },
8915
+ value: {
8916
+ type: "string",
8917
+ description: 'Tag value (e.g. "auth", "alice", "true").'
8918
+ }
8919
+ },
8920
+ required: ["node_id", "key", "value"]
8921
+ }
8922
+ },
8923
+ {
8924
+ name: "remove_tag",
8925
+ description: "Remove a manual tag from a graph node. Only removes tags from tags.json \u2014 computed tags (module, screen) cannot be removed (they are re-derived at read time).",
8926
+ inputSchema: {
8927
+ type: "object",
8928
+ properties: {
8929
+ node_id: {
8930
+ type: "string",
8931
+ description: "The node id to remove the tag from."
8932
+ },
8933
+ key: {
8934
+ type: "string",
8935
+ description: "Tag key to remove."
8936
+ }
8937
+ },
8938
+ required: ["node_id", "key"]
8939
+ }
7991
8940
  }
7992
8941
  ];
7993
8942
  function matchesSearch(node, query) {
@@ -8001,7 +8950,7 @@ function matchesSearch(node, query) {
8001
8950
  function toMinimal(nodes) {
8002
8951
  return nodes.map((n) => {
8003
8952
  const out = { id: n.id, type: n.type, name: n.name };
8004
- if (n.module != null) out.module = n.module;
8953
+ if (n.tags != null) out.tags = n.tags;
8005
8954
  if (n.route != null) out.route = n.route;
8006
8955
  if (n.methods != null) out.methods = n.methods;
8007
8956
  return out;
@@ -8012,11 +8961,12 @@ var COMPACT_SCHEMA = {
8012
8961
  i: "id",
8013
8962
  t: "type",
8014
8963
  n: "name",
8015
- m: "module",
8964
+ m: "module (from tags)",
8016
8965
  r: "route",
8017
8966
  mt: "methods",
8018
8967
  x: "exports",
8019
- c: "columns"
8968
+ c: "columns",
8969
+ tg: "tags"
8020
8970
  },
8021
8971
  edges: {
8022
8972
  s: "source_node_index",
@@ -8034,7 +8984,8 @@ var COMPACT_NODE_KNOWN_KEYS = /* @__PURE__ */ new Set([
8034
8984
  "route",
8035
8985
  "methods",
8036
8986
  "exports",
8037
- "columns"
8987
+ "columns",
8988
+ "tags"
8038
8989
  ]);
8039
8990
  var EST_CHARS_PER_NODE_FULL = {
8040
8991
  ui: 300,
@@ -8055,11 +9006,13 @@ var NEIGHBORHOOD_BUDGET_CHARS = 55e3;
8055
9006
  var BATCH_BUDGET_CHARS = 6e4;
8056
9007
  function toCompactNode(n) {
8057
9008
  const out = { i: n.id, t: n.type, n: n.name };
8058
- if (n.module != null) out.m = n.module;
9009
+ const tags = n.tags;
9010
+ if (tags?.module) out.m = tags.module;
8059
9011
  if (n.route != null) out.r = n.route;
8060
9012
  if (n.methods != null) out.mt = n.methods;
8061
9013
  if (n.exports != null) out.x = n.exports;
8062
9014
  if (n.columns != null) out.c = n.columns;
9015
+ if (tags != null) out.tg = tags;
8063
9016
  for (const k of Object.keys(n)) {
8064
9017
  if (!COMPACT_NODE_KNOWN_KEYS.has(k) && n[k] != null) out[k] = n[k];
8065
9018
  }
@@ -8135,7 +9088,8 @@ function layerSummary(graph) {
8135
9088
  const moduleCounts = {};
8136
9089
  for (const n of graph.nodes) {
8137
9090
  typeCounts[n.type] = (typeCounts[n.type] ?? 0) + 1;
8138
- const mod = n.module;
9091
+ const tags = n.tags;
9092
+ const mod = tags?.module;
8139
9093
  if (mod) moduleCounts[mod] = (moduleCounts[mod] ?? 0) + 1;
8140
9094
  }
8141
9095
  const edgeTypeCounts = {};
@@ -8192,12 +9146,14 @@ function runReadGraphQueryRaw(rootDir, args) {
8192
9146
  const search = args.search;
8193
9147
  const type = args.type;
8194
9148
  const module_ = args.module;
9149
+ const tagKey = args.tag_key;
9150
+ const tagValue = args.tag_value;
8195
9151
  const nodeId = args.node_id;
8196
9152
  const hops = args.hops ?? 1;
8197
9153
  const layerIsDb = args.layer === "db";
8198
9154
  const minimal = args.minimal ?? layerIsDb;
8199
9155
  const includeEdges = args.include_edges;
8200
- const hasFilter = !!(search || type || module_ || nodeId);
9156
+ const hasFilter = !!(search || type || module_ || nodeId || tagKey && tagValue);
8201
9157
  if (layer && !["ui", "api", "db"].includes(layer)) {
8202
9158
  return { error: `Invalid layer "${layer}". Must be one of: ui, api, db` };
8203
9159
  }
@@ -8253,7 +9209,9 @@ function runReadGraphQueryRaw(rootDir, args) {
8253
9209
  const matched = graph.nodes.filter((n) => {
8254
9210
  if (search && !matchesSearch(n, search)) return false;
8255
9211
  if (type && n.type !== type) return false;
8256
- if (module_ && n.module !== module_) return false;
9212
+ const nodeTags = n.tags;
9213
+ if (module_ && nodeTags?.module !== module_) return false;
9214
+ if (tagKey && tagValue && nodeTags?.[tagKey] !== tagValue) return false;
8257
9215
  return true;
8258
9216
  });
8259
9217
  const matchedIds = new Set(matched.map((n) => n.id));
@@ -8340,9 +9298,9 @@ function handleReadGraph(args) {
8340
9298
  return okJson(result);
8341
9299
  }
8342
9300
  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");
9301
+ if (layer === "ui") return (0, import_node_path15.join)(rootDir, "src", nodeId);
9302
+ if (layer === "api") return (0, import_node_path15.join)(rootDir, nodeId);
9303
+ if (layer === "db") return (0, import_node_path15.join)(rootDir, "prisma", "schema.prisma");
8346
9304
  return null;
8347
9305
  }
8348
9306
  function handleGrepNodes(args) {
@@ -8402,11 +9360,11 @@ function handleGrepNodes(args) {
8402
9360
  let filesSearched = 0;
8403
9361
  let truncated = false;
8404
9362
  for (const [filePath, nodeId] of filePaths) {
8405
- if (!(0, import_node_fs7.existsSync)(filePath)) continue;
9363
+ if (!(0, import_node_fs13.existsSync)(filePath)) continue;
8406
9364
  filesSearched++;
8407
9365
  let content;
8408
9366
  try {
8409
- content = (0, import_node_fs7.readFileSync)(filePath, "utf-8");
9367
+ content = (0, import_node_fs13.readFileSync)(filePath, "utf-8");
8410
9368
  } catch {
8411
9369
  continue;
8412
9370
  }
@@ -8443,13 +9401,11 @@ function handleGrepNodes(args) {
8443
9401
  truncated
8444
9402
  });
8445
9403
  }
8446
- function handleGetGraphUiUrl() {
8447
- const lock = getLiveLock();
9404
+ function handleChartServerStatus() {
9405
+ const rootDir = process.cwd();
9406
+ const lock = getLiveLock(rootDir);
8448
9407
  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
- });
9408
+ return okJson({ running: false });
8453
9409
  }
8454
9410
  return okJson({
8455
9411
  running: true,
@@ -8460,6 +9416,146 @@ function handleGetGraphUiUrl() {
8460
9416
  startedAt: lock.startedAt
8461
9417
  });
8462
9418
  }
9419
+ function handleStartChartServer(args) {
9420
+ const rootDir = process.cwd();
9421
+ const lock = getLiveLock(rootDir);
9422
+ if (lock) {
9423
+ return okJson({
9424
+ started: false,
9425
+ reason: "already_running",
9426
+ url: lock.url,
9427
+ port: lock.port,
9428
+ pid: lock.pid
9429
+ });
9430
+ }
9431
+ const entryPath = process.argv[1];
9432
+ const logDir = (0, import_node_path15.join)((0, import_node_os2.homedir)(), ".launchsecure");
9433
+ (0, import_node_fs13.mkdirSync)(logDir, { recursive: true });
9434
+ const logPath = (0, import_node_path15.join)(logDir, "launch-chart.log");
9435
+ const out = (0, import_node_fs13.openSync)(logPath, "a");
9436
+ const err2 = (0, import_node_fs13.openSync)(logPath, "a");
9437
+ const portArgs = args.port ? ["--port", String(args.port)] : [];
9438
+ const child = (0, import_node_child_process2.spawn)(process.execPath, [entryPath, "serve", ...portArgs], {
9439
+ detached: true,
9440
+ stdio: ["ignore", out, err2],
9441
+ env: { ...process.env, LAUNCH_CHART_AUTOSERVE: "" }
9442
+ });
9443
+ child.unref();
9444
+ return okJson({
9445
+ started: true,
9446
+ pid: child.pid,
9447
+ logPath
9448
+ });
9449
+ }
9450
+ function handleStopChartServer() {
9451
+ const rootDir = process.cwd();
9452
+ const lock = getLiveLock(rootDir);
9453
+ if (!lock) {
9454
+ return okJson({ stopped: false, reason: "not_running" });
9455
+ }
9456
+ try {
9457
+ process.kill(lock.pid, "SIGTERM");
9458
+ return okJson({ stopped: true, pid: lock.pid });
9459
+ } catch (e) {
9460
+ const code = e.code;
9461
+ if (code === "ESRCH") {
9462
+ clearLock(rootDir);
9463
+ return okJson({ stopped: true, pid: lock.pid, note: "process was already gone, lock cleaned up" });
9464
+ }
9465
+ return okJson({ stopped: false, reason: `kill failed: ${code ?? e}` });
9466
+ }
9467
+ }
9468
+ function handleAddTag(args) {
9469
+ const rootDir = process.cwd();
9470
+ const nodeId = args.node_id;
9471
+ const key = args.key;
9472
+ const value = args.value;
9473
+ if (!nodeId) return err("node_id is required");
9474
+ if (!key) return err("key is required");
9475
+ if (!value) return err("value is required");
9476
+ const graphs = readAllGraphs(rootDir);
9477
+ let found = false;
9478
+ for (const graph of Object.values(graphs)) {
9479
+ if (graph && graph.nodes.some((n) => n.id === nodeId)) {
9480
+ found = true;
9481
+ break;
9482
+ }
9483
+ }
9484
+ if (!found) {
9485
+ return err(`Node "${nodeId}" not found in any graph layer. Check the node_id.`);
9486
+ }
9487
+ setTag(rootDir, nodeId, key, value);
9488
+ return okJson({ ok: true, node_id: nodeId, tag: { [key]: value } });
9489
+ }
9490
+ function handleRemoveTag(args) {
9491
+ const rootDir = process.cwd();
9492
+ const nodeId = args.node_id;
9493
+ const key = args.key;
9494
+ if (!nodeId) return err("node_id is required");
9495
+ if (!key) return err("key is required");
9496
+ removeTag(rootDir, nodeId, key);
9497
+ return okJson({ ok: true, node_id: nodeId, removed_key: key });
9498
+ }
9499
+ function handleDetectProjectStack() {
9500
+ const rootDir = process.cwd();
9501
+ const parsers = [
9502
+ { id: "react-nextjs", layer: "ui", detected: reactNextjsParser.detect(rootDir) },
9503
+ { id: "nextjs-routes", layer: "api", detected: nextjsRoutesParser.detect(rootDir) },
9504
+ { id: "prisma-schema", layer: "db", detected: prismaSchemaParser.detect(rootDir) }
9505
+ ];
9506
+ const config = loadConfig(rootDir);
9507
+ let stats = { calls_api: 0, references_api: 0, out_of_pattern: 0, annotations: 0 };
9508
+ const uiGraph = readGraph(rootDir, "ui");
9509
+ if (uiGraph) {
9510
+ for (const ref of uiGraph.cross_refs ?? []) {
9511
+ if (ref.type === "calls_api") stats.calls_api++;
9512
+ if (ref.type === "references_api") stats.references_api++;
9513
+ }
9514
+ for (const f of uiGraph.flagged_edges ?? []) {
9515
+ if (f.type === "out_of_pattern") stats.out_of_pattern++;
9516
+ }
9517
+ }
9518
+ const srcDir = (0, import_node_path15.join)(rootDir, "src");
9519
+ if ((0, import_node_fs13.existsSync)(srcDir)) {
9520
+ const scanDir = (dir) => {
9521
+ if (!(0, import_node_fs13.existsSync)(dir)) return;
9522
+ for (const entry of (0, import_node_fs13.readdirSync)(dir, { withFileTypes: true })) {
9523
+ if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
9524
+ const full = (0, import_node_path15.join)(dir, entry.name);
9525
+ if (entry.isDirectory()) {
9526
+ scanDir(full);
9527
+ continue;
9528
+ }
9529
+ if (![".ts", ".tsx"].includes((0, import_node_path15.extname)(entry.name))) continue;
9530
+ try {
9531
+ const content = (0, import_node_fs13.readFileSync)(full, "utf-8");
9532
+ const matches = content.match(/@api\s+(GET|POST|PUT|DELETE|PATCH)\s+\/\S+/g);
9533
+ if (matches) stats.annotations += matches.length;
9534
+ } catch {
9535
+ }
9536
+ }
9537
+ };
9538
+ scanDir(srcDir);
9539
+ }
9540
+ let recommendedPrimary = "fetch-resolver";
9541
+ if (stats.annotations > 0 && stats.annotations >= stats.calls_api) {
9542
+ recommendedPrimary = "api-annotations";
9543
+ } else if (stats.calls_api === 0 && stats.references_api > 0) {
9544
+ recommendedPrimary = "url-literal-scanner";
9545
+ }
9546
+ return okJson({
9547
+ parsers,
9548
+ crosslayer_parsers: [
9549
+ { id: "fetch-resolver", description: "Detects direct fetch()/api.get() calls with inline URLs" },
9550
+ { id: "api-annotations", description: "Scans for @api METHOD /path annotations in JSDoc/comments" },
9551
+ { id: "url-literal-scanner", description: "Finds /api/... string literals as fallback detection" }
9552
+ ],
9553
+ stats,
9554
+ recommended_primary: recommendedPrimary,
9555
+ current_config: Object.keys(config).length > 0 ? config : null,
9556
+ config_path: ".launchchart.json"
9557
+ });
9558
+ }
8463
9559
  function send(msg) {
8464
9560
  process.stdout.write(JSON.stringify(msg) + "\n");
8465
9561
  }
@@ -8503,8 +9599,28 @@ function handleMessage(msg) {
8503
9599
  respond(id ?? null, handleGrepNodes(args));
8504
9600
  return;
8505
9601
  }
8506
- if (toolName === "get_graph_ui_url") {
8507
- respond(id ?? null, handleGetGraphUiUrl());
9602
+ if (toolName === "chart_server_status") {
9603
+ respond(id ?? null, handleChartServerStatus());
9604
+ return;
9605
+ }
9606
+ if (toolName === "start_chart_server") {
9607
+ respond(id ?? null, handleStartChartServer(args));
9608
+ return;
9609
+ }
9610
+ if (toolName === "stop_chart_server") {
9611
+ respond(id ?? null, handleStopChartServer());
9612
+ return;
9613
+ }
9614
+ if (toolName === "detect_project_stack") {
9615
+ respond(id ?? null, handleDetectProjectStack());
9616
+ return;
9617
+ }
9618
+ if (toolName === "add_tag") {
9619
+ respond(id ?? null, handleAddTag(args));
9620
+ return;
9621
+ }
9622
+ if (toolName === "remove_tag") {
9623
+ respond(id ?? null, handleRemoveTag(args));
8508
9624
  return;
8509
9625
  }
8510
9626
  respondError(id ?? null, -32601, `Unknown tool: ${toolName}`);
@@ -8604,7 +9720,7 @@ function parseArgs() {
8604
9720
  return { port, token, serverUrl: LAUNCHSECURE_URL, subcommand };
8605
9721
  }
8606
9722
  function tryListen(server, port, maxRetries = 10) {
8607
- return new Promise((resolve, reject) => {
9723
+ return new Promise((resolve3, reject) => {
8608
9724
  let attempts = 0;
8609
9725
  function attempt(p) {
8610
9726
  server.once("error", (err2) => {
@@ -8615,7 +9731,7 @@ function tryListen(server, port, maxRetries = 10) {
8615
9731
  reject(err2);
8616
9732
  }
8617
9733
  });
8618
- server.listen(p, () => resolve(p));
9734
+ server.listen(p, () => resolve3(p));
8619
9735
  }
8620
9736
  attempt(port);
8621
9737
  });
@@ -8636,7 +9752,7 @@ function saveCredentials(creds) {
8636
9752
  });
8637
9753
  }
8638
9754
  function verifyToken(serverUrl, token) {
8639
- return new Promise((resolve) => {
9755
+ return new Promise((resolve3) => {
8640
9756
  const url = new URL("/api/mcp/verify", serverUrl);
8641
9757
  const body = JSON.stringify({ token });
8642
9758
  const mod = url.protocol === "https:" ? import_https.default : import_http.default;
@@ -8651,30 +9767,30 @@ function verifyToken(serverUrl, token) {
8651
9767
  res.on("data", (chunk) => data += chunk);
8652
9768
  res.on("end", () => {
8653
9769
  try {
8654
- resolve(JSON.parse(data));
9770
+ resolve3(JSON.parse(data));
8655
9771
  } catch {
8656
- resolve({ valid: false, error: "Invalid response from server" });
9772
+ resolve3({ valid: false, error: "Invalid response from server" });
8657
9773
  }
8658
9774
  });
8659
9775
  });
8660
9776
  req.on("error", (err2) => {
8661
- resolve({ valid: false, error: `Cannot reach server: ${err2.message}` });
9777
+ resolve3({ valid: false, error: `Cannot reach server: ${err2.message}` });
8662
9778
  });
8663
9779
  req.setTimeout(1e4, () => {
8664
9780
  req.destroy();
8665
- resolve({ valid: false, error: "Connection timed out" });
9781
+ resolve3({ valid: false, error: "Connection timed out" });
8666
9782
  });
8667
9783
  req.write(body);
8668
9784
  req.end();
8669
9785
  });
8670
9786
  }
8671
9787
  function httpRequest(reqUrl, options, body, timeout = 3e4) {
8672
- return new Promise((resolve, reject) => {
9788
+ return new Promise((resolve3, reject) => {
8673
9789
  const mod = reqUrl.protocol === "https:" ? import_https.default : import_http.default;
8674
9790
  const r = mod.request(reqUrl, options, (resp) => {
8675
9791
  let data = "";
8676
9792
  resp.on("data", (chunk) => data += chunk);
8677
- resp.on("end", () => resolve({ status: resp.statusCode || 0, headers: resp.headers, body: data }));
9793
+ resp.on("end", () => resolve3({ status: resp.statusCode || 0, headers: resp.headers, body: data }));
8678
9794
  });
8679
9795
  r.on("error", reject);
8680
9796
  r.setTimeout(timeout, () => {