@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.
@@ -29,17 +29,36 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
29
29
  isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
30
30
  mod
31
31
  ));
32
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
32
33
 
33
34
  // src/server/lockfile.ts
34
- function lockDir() {
35
+ function lockDir(projectRoot) {
36
+ if (projectRoot) {
37
+ return (0, import_node_path.join)(projectRoot, ".launchsecure");
38
+ }
35
39
  return (0, import_node_path.join)((0, import_node_os.homedir)(), ".launchsecure");
36
40
  }
37
- function lockPath() {
38
- return (0, import_node_path.join)(lockDir(), "launch-chart.lock");
41
+ function lockPath(projectRoot) {
42
+ return (0, import_node_path.join)(lockDir(projectRoot), "launch-chart.lock");
39
43
  }
40
- function readLock() {
41
- const p = lockPath();
42
- if (!(0, import_node_fs.existsSync)(p)) return null;
44
+ function readLock(projectRoot) {
45
+ const root = projectRoot ?? _activeProjectRoot;
46
+ const p = lockPath(root);
47
+ if (!(0, import_node_fs.existsSync)(p)) {
48
+ if (root) {
49
+ const globalP = lockPath();
50
+ if ((0, import_node_fs.existsSync)(globalP)) {
51
+ try {
52
+ const data = JSON.parse((0, import_node_fs.readFileSync)(globalP, "utf-8"));
53
+ if (typeof data.pid === "number" && typeof data.port === "number" && data.cwd === root) {
54
+ return data;
55
+ }
56
+ } catch {
57
+ }
58
+ }
59
+ }
60
+ return null;
61
+ }
43
62
  try {
44
63
  const data = JSON.parse((0, import_node_fs.readFileSync)(p, "utf-8"));
45
64
  if (typeof data.pid !== "number" || typeof data.port !== "number") return null;
@@ -70,31 +89,35 @@ function getListenerPid(port) {
70
89
  return null;
71
90
  }
72
91
  }
73
- function getLiveLock() {
74
- const lock = readLock();
92
+ function getLiveLock(projectRoot) {
93
+ const root = projectRoot ?? _activeProjectRoot;
94
+ const lock = readLock(root);
75
95
  if (!lock) return null;
76
96
  const listenerPid = getListenerPid(lock.port);
77
97
  const live = listenerPid !== null ? listenerPid === lock.pid : isPidAlive(lock.pid);
78
98
  if (!live) {
79
99
  try {
80
- (0, import_node_fs.unlinkSync)(lockPath());
100
+ (0, import_node_fs.unlinkSync)(lockPath(root));
81
101
  } catch {
82
102
  }
83
103
  return null;
84
104
  }
85
105
  return lock;
86
106
  }
87
- function writeLock(data) {
88
- (0, import_node_fs.mkdirSync)(lockDir(), { recursive: true });
89
- (0, import_node_fs.writeFileSync)(lockPath(), JSON.stringify(data, null, 2) + "\n", "utf-8");
107
+ function writeLock(data, projectRoot) {
108
+ const root = projectRoot ?? _activeProjectRoot;
109
+ (0, import_node_fs.mkdirSync)(lockDir(root), { recursive: true });
110
+ (0, import_node_fs.writeFileSync)(lockPath(root), JSON.stringify(data, null, 2) + "\n", "utf-8");
111
+ if (root) _activeProjectRoot = root;
90
112
  }
91
- function clearLock() {
113
+ function clearLock(projectRoot) {
114
+ const root = projectRoot ?? _activeProjectRoot;
92
115
  try {
93
- (0, import_node_fs.unlinkSync)(lockPath());
116
+ (0, import_node_fs.unlinkSync)(lockPath(root));
94
117
  } catch {
95
118
  }
96
119
  }
97
- var import_node_child_process, import_node_fs, import_node_os, import_node_path;
120
+ var import_node_child_process, import_node_fs, import_node_os, import_node_path, _activeProjectRoot;
98
121
  var init_lockfile = __esm({
99
122
  "src/server/lockfile.ts"() {
100
123
  "use strict";
@@ -105,6 +128,30 @@ var init_lockfile = __esm({
105
128
  }
106
129
  });
107
130
 
131
+ // src/server/graph/core/config.ts
132
+ var config_exports = {};
133
+ __export(config_exports, {
134
+ loadConfig: () => loadConfig
135
+ });
136
+ function loadConfig(rootDir) {
137
+ const configPath = (0, import_node_path2.join)(rootDir, CONFIG_FILENAME);
138
+ if (!(0, import_node_fs2.existsSync)(configPath)) return {};
139
+ try {
140
+ return JSON.parse((0, import_node_fs2.readFileSync)(configPath, "utf-8"));
141
+ } catch {
142
+ return {};
143
+ }
144
+ }
145
+ var import_node_fs2, import_node_path2, CONFIG_FILENAME;
146
+ var init_config = __esm({
147
+ "src/server/graph/core/config.ts"() {
148
+ "use strict";
149
+ import_node_fs2 = require("node:fs");
150
+ import_node_path2 = require("node:path");
151
+ CONFIG_FILENAME = ".launchchart.json";
152
+ }
153
+ });
154
+
108
155
  // src/server/graph/core/ast-helpers.ts
109
156
  function getTs() {
110
157
  if (!tsModule) {
@@ -114,8 +161,8 @@ function getTs() {
114
161
  }
115
162
  function parseFile(absPath) {
116
163
  const ts = getTs();
117
- const content = (0, import_node_fs2.readFileSync)(absPath, "utf-8");
118
- const ext = (0, import_node_path2.extname)(absPath);
164
+ const content = (0, import_node_fs3.readFileSync)(absPath, "utf-8");
165
+ const ext = (0, import_node_path3.extname)(absPath);
119
166
  const scriptKind = ext === ".tsx" ? ts.ScriptKind.TSX : ext === ".ts" ? ts.ScriptKind.TS : ext === ".jsx" ? ts.ScriptKind.JSX : ts.ScriptKind.JS;
120
167
  const sourceFile = ts.createSourceFile(
121
168
  absPath,
@@ -382,8 +429,8 @@ function parseFile(absPath) {
382
429
  }
383
430
  function extractDbCalls(absPath) {
384
431
  const ts = getTs();
385
- const content = (0, import_node_fs2.readFileSync)(absPath, "utf-8");
386
- const ext = (0, import_node_path2.extname)(absPath);
432
+ const content = (0, import_node_fs3.readFileSync)(absPath, "utf-8");
433
+ const ext = (0, import_node_path3.extname)(absPath);
387
434
  const scriptKind = ext === ".tsx" ? ts.ScriptKind.TSX : ts.ScriptKind.TS;
388
435
  const sourceFile = ts.createSourceFile(absPath, content, ts.ScriptTarget.Latest, true, scriptKind);
389
436
  const calls = [];
@@ -412,8 +459,8 @@ function extractDbCalls(absPath) {
412
459
  }
413
460
  function extractAuthWrappers(absPath) {
414
461
  const ts = getTs();
415
- const content = (0, import_node_fs2.readFileSync)(absPath, "utf-8");
416
- const ext = (0, import_node_path2.extname)(absPath);
462
+ const content = (0, import_node_fs3.readFileSync)(absPath, "utf-8");
463
+ const ext = (0, import_node_path3.extname)(absPath);
417
464
  const scriptKind = ext === ".tsx" ? ts.ScriptKind.TSX : ts.ScriptKind.TS;
418
465
  const sourceFile = ts.createSourceFile(absPath, content, ts.ScriptTarget.Latest, true, scriptKind);
419
466
  const wrappers = /* @__PURE__ */ new Set();
@@ -429,12 +476,12 @@ function extractAuthWrappers(absPath) {
429
476
  visit(sourceFile);
430
477
  return wrappers;
431
478
  }
432
- var import_node_fs2, import_node_path2, tsModule, HTTP_METHODS, MUTATION_METHODS;
479
+ var import_node_fs3, import_node_path3, tsModule, HTTP_METHODS, MUTATION_METHODS;
433
480
  var init_ast_helpers = __esm({
434
481
  "src/server/graph/core/ast-helpers.ts"() {
435
482
  "use strict";
436
- import_node_fs2 = require("node:fs");
437
- import_node_path2 = require("node:path");
483
+ import_node_fs3 = require("node:fs");
484
+ import_node_path3 = require("node:path");
438
485
  HTTP_METHODS = /* @__PURE__ */ new Set(["get", "post", "put", "patch", "delete", "head", "options"]);
439
486
  MUTATION_METHODS = /* @__PURE__ */ new Set([
440
487
  "create",
@@ -453,12 +500,12 @@ var init_ast_helpers = __esm({
453
500
  // src/server/graph/parsers/ui/react-nextjs.ts
454
501
  function walk(dir, exts) {
455
502
  const results = [];
456
- if (!(0, import_node_fs3.existsSync)(dir)) return results;
457
- for (const entry of (0, import_node_fs3.readdirSync)(dir, { withFileTypes: true })) {
458
- const full = (0, import_node_path3.join)(dir, entry.name);
503
+ if (!(0, import_node_fs4.existsSync)(dir)) return results;
504
+ for (const entry of (0, import_node_fs4.readdirSync)(dir, { withFileTypes: true })) {
505
+ const full = (0, import_node_path4.join)(dir, entry.name);
459
506
  if (entry.isDirectory()) {
460
507
  results.push(...walk(full, exts));
461
- } else if (exts.includes((0, import_node_path3.extname)(entry.name))) {
508
+ } else if (exts.includes((0, import_node_path4.extname)(entry.name))) {
462
509
  results.push(full);
463
510
  }
464
511
  }
@@ -466,33 +513,33 @@ function walk(dir, exts) {
466
513
  }
467
514
  function walkWithIgnore(dir, exts, ignoreDirs) {
468
515
  const results = [];
469
- if (!(0, import_node_fs3.existsSync)(dir)) return results;
470
- for (const entry of (0, import_node_fs3.readdirSync)(dir, { withFileTypes: true })) {
516
+ if (!(0, import_node_fs4.existsSync)(dir)) return results;
517
+ for (const entry of (0, import_node_fs4.readdirSync)(dir, { withFileTypes: true })) {
471
518
  if (entry.isDirectory()) {
472
519
  if (ignoreDirs.has(entry.name)) continue;
473
- results.push(...walkWithIgnore((0, import_node_path3.join)(dir, entry.name), exts, ignoreDirs));
474
- } else if (exts.includes((0, import_node_path3.extname)(entry.name))) {
475
- results.push((0, import_node_path3.join)(dir, entry.name));
520
+ results.push(...walkWithIgnore((0, import_node_path4.join)(dir, entry.name), exts, ignoreDirs));
521
+ } else if (exts.includes((0, import_node_path4.extname)(entry.name))) {
522
+ results.push((0, import_node_path4.join)(dir, entry.name));
476
523
  }
477
524
  }
478
525
  return results;
479
526
  }
480
527
  function toNodeId(srcDir, absPath) {
481
- return (0, import_node_path3.relative)(srcDir, absPath).replace(/\\/g, "/");
528
+ return (0, import_node_path4.relative)(srcDir, absPath).replace(/\\/g, "/");
482
529
  }
483
530
  function resolveImport(srcDir, specifier) {
484
531
  if (!specifier.startsWith("@/")) return null;
485
532
  const rel = specifier.slice(2);
486
- const base = (0, import_node_path3.join)(srcDir, rel);
487
- for (const c of [base, base + ".ts", base + ".tsx", (0, import_node_path3.join)(base, "index.ts"), (0, import_node_path3.join)(base, "index.tsx")]) {
488
- if ((0, import_node_fs3.existsSync)(c) && (0, import_node_fs3.statSync)(c).isFile()) return c;
533
+ const base = (0, import_node_path4.join)(srcDir, rel);
534
+ for (const c of [base, base + ".ts", base + ".tsx", (0, import_node_path4.join)(base, "index.ts"), (0, import_node_path4.join)(base, "index.tsx")]) {
535
+ if ((0, import_node_fs4.existsSync)(c) && (0, import_node_fs4.statSync)(c).isFile()) return c;
489
536
  }
490
537
  return null;
491
538
  }
492
539
  function resolveRelativeImport(fromFile, specifier) {
493
- const base = (0, import_node_path3.join)((0, import_node_path3.dirname)(fromFile), specifier);
494
- for (const c of [base, base + ".ts", base + ".tsx", (0, import_node_path3.join)(base, "index.ts"), (0, import_node_path3.join)(base, "index.tsx")]) {
495
- if ((0, import_node_fs3.existsSync)(c) && (0, import_node_fs3.statSync)(c).isFile()) return c;
540
+ const base = (0, import_node_path4.join)((0, import_node_path4.dirname)(fromFile), specifier);
541
+ for (const c of [base, base + ".ts", base + ".tsx", (0, import_node_path4.join)(base, "index.ts"), (0, import_node_path4.join)(base, "index.tsx")]) {
542
+ if ((0, import_node_fs4.existsSync)(c) && (0, import_node_fs4.statSync)(c).isFile()) return c;
496
543
  }
497
544
  return null;
498
545
  }
@@ -513,7 +560,7 @@ function resolveBarrelMap(barrelAbsPath, parsedByPath, memo, visiting) {
513
560
  const resolved = resolveRelativeImport(barrelAbsPath, re.from);
514
561
  if (!resolved) continue;
515
562
  if (re.isWildcard) {
516
- const targetBn = (0, import_node_path3.basename)(resolved);
563
+ const targetBn = (0, import_node_path4.basename)(resolved);
517
564
  const targetIsBarrel = targetBn === "index.ts" || targetBn === "index.tsx";
518
565
  if (targetIsBarrel) {
519
566
  const nested = resolveBarrelMap(resolved, parsedByPath, memo, visiting);
@@ -540,12 +587,12 @@ function buildAllBarrelMaps(srcDir, parsedByPath) {
540
587
  const barrels = /* @__PURE__ */ new Map();
541
588
  const memo = /* @__PURE__ */ new Map();
542
589
  for (const [absPath, parsed] of parsedByPath) {
543
- const bn = (0, import_node_path3.basename)(absPath);
590
+ const bn = (0, import_node_path4.basename)(absPath);
544
591
  if (bn !== "index.ts" && bn !== "index.tsx") continue;
545
592
  if (parsed.reExports.length === 0) continue;
546
593
  const map = resolveBarrelMap(absPath, parsedByPath, memo, /* @__PURE__ */ new Set());
547
594
  if (map.size > 0) {
548
- const barrelId = (0, import_node_path3.relative)(srcDir, (0, import_node_path3.dirname)(absPath)).replace(/\\/g, "/");
595
+ const barrelId = (0, import_node_path4.relative)(srcDir, (0, import_node_path4.dirname)(absPath)).replace(/\\/g, "/");
549
596
  barrels.set(barrelId, map);
550
597
  }
551
598
  }
@@ -566,34 +613,6 @@ function classifyType(id) {
566
613
  if (id.startsWith("lib/") || id.startsWith("config/")) return "lib";
567
614
  return "component";
568
615
  }
569
- function classifyModule(id) {
570
- if (/app\/\(auth\)\//.test(id)) return "auth";
571
- if (/app\/\(admin\)\//.test(id)) return "admin";
572
- if (/app\/\(settings\)\//.test(id)) return "settings";
573
- if (/app\/\(app\)\/\[orgSlug\]\/\(project-pages\)\//.test(id)) return "project";
574
- if (/app\/\(app\)\/\[orgSlug\]\/\(org-pages\)\//.test(id)) return "org";
575
- if (/app\/\(app\)\/\[orgSlug\]\//.test(id)) return "org";
576
- if (id.startsWith("app/integrations/")) return "integrations";
577
- if (id.startsWith("app/docs/")) return "admin";
578
- if (id.startsWith("client/components/ui/")) return "shared-ui";
579
- if (id.startsWith("client/components/layout/") || /client\/lib\/navigation/.test(id)) return "layout";
580
- 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";
581
- if (/client\/components\/prd-/.test(id) || /client\/hooks\/use-admin/.test(id)) return "admin";
582
- if (/client\/components\/org-/.test(id) || /client\/hooks\/use-org-/.test(id) || /client\/hooks\/use-provider-def/.test(id)) return "org";
583
- 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";
584
- if (/client\/hooks\/use-(profile|sessions|organizations|notification)/.test(id)) return "settings";
585
- if (id.startsWith("server/auth/")) return "auth";
586
- if (id.startsWith("server/mcp/")) return "mcp";
587
- if (id.startsWith("server/lib/")) return "server-lib";
588
- if (id.startsWith("server/middleware")) return "middleware";
589
- if (id.startsWith("server/services/")) return "services";
590
- if (id.startsWith("server/db")) return "db";
591
- if (id.startsWith("server/errors")) return "errors";
592
- if (id.startsWith("server/")) return "server-lib";
593
- if (id.startsWith("config/")) return "config";
594
- if (id.startsWith("lib/")) return "lib";
595
- return "root";
596
- }
597
616
  function extractRoute(id) {
598
617
  if (!id.endsWith("/page.tsx")) return null;
599
618
  let route = id.replace(/^app\//, "/").replace(/\/page\.tsx$/, "");
@@ -604,7 +623,7 @@ function extractRoute(id) {
604
623
  return route || "/";
605
624
  }
606
625
  function nameFromFilename(absPath) {
607
- return (0, import_node_path3.basename)(absPath, (0, import_node_path3.extname)(absPath)).replace(/[-_](\w)/g, (_, c) => c.toUpperCase()).replace(/^(\w)/, (_, c) => c.toUpperCase());
626
+ return (0, import_node_path4.basename)(absPath, (0, import_node_path4.extname)(absPath)).replace(/[-_](\w)/g, (_, c) => c.toUpperCase()).replace(/^(\w)/, (_, c) => c.toUpperCase());
608
627
  }
609
628
  function resolveTemplateLiteralRoute(template, routeToNodeId) {
610
629
  const parameterized = template.replace(/\$\{([^}]+)\}/g, (_, expr) => {
@@ -685,105 +704,6 @@ function matchRouteToPage(route, routeToNodeId) {
685
704
  if (routeToNodeId.has(normalized)) return routeToNodeId.get(normalized);
686
705
  return null;
687
706
  }
688
- function loadApiRoutes(rootDir) {
689
- const apiJsonPath = (0, import_node_path3.join)(rootDir, ".launchsecure", "graphs", "api.json");
690
- if (!(0, import_node_fs3.existsSync)(apiJsonPath)) return [];
691
- try {
692
- const parsed = JSON.parse((0, import_node_fs3.readFileSync)(apiJsonPath, "utf-8"));
693
- const routes = [];
694
- for (const n of parsed.nodes ?? []) {
695
- const path3 = n.path;
696
- if (!path3 || typeof path3 !== "string") continue;
697
- routes.push({
698
- path: path3,
699
- nodeId: n.id,
700
- segments: path3.split("/").filter(Boolean)
701
- });
702
- }
703
- return routes;
704
- } catch {
705
- return [];
706
- }
707
- }
708
- function buildApiPathMap(routes) {
709
- const map = /* @__PURE__ */ new Map();
710
- for (const r of routes) {
711
- if (!map.has(r.path)) map.set(r.path, r.nodeId);
712
- }
713
- return map;
714
- }
715
- function normalizeFetchUrl(raw) {
716
- let s = raw.replace(/^`|`$/g, "");
717
- const qIdx = s.indexOf("?");
718
- if (qIdx >= 0) s = s.slice(0, qIdx);
719
- const hIdx = s.indexOf("#");
720
- if (hIdx >= 0) s = s.slice(0, hIdx);
721
- let hadInterpolation = false;
722
- s = s.replace(/\$\{([^}]+)\}/g, (_, expr) => {
723
- hadInterpolation = true;
724
- const cleaned = expr.trim();
725
- const last = cleaned.split(".").pop() ?? cleaned;
726
- const name = last.replace(/[^\w]/g, "") || "param";
727
- return ":" + name;
728
- });
729
- s = s.replace(/\/+/g, "/");
730
- if (s.length > 1 && s.endsWith("/")) s = s.slice(0, -1);
731
- return { path: s || "/", hadInterpolation };
732
- }
733
- function scoreApiRouteMatch(candidate, known) {
734
- if (candidate.length !== known.length) return -1;
735
- let score = 0;
736
- for (let i = 0; i < candidate.length; i++) {
737
- const a = candidate[i];
738
- const b = known[i];
739
- if (a === b) {
740
- score += 3;
741
- continue;
742
- }
743
- if (a.startsWith(":") && b.startsWith(":")) {
744
- score += 2;
745
- continue;
746
- }
747
- if (a.startsWith(":") || b.startsWith(":")) {
748
- score += 1;
749
- continue;
750
- }
751
- return -1;
752
- }
753
- return score;
754
- }
755
- function resolveFetchCall(call, apiPathMap, apiRoutes) {
756
- const raw = call.url;
757
- if (/^(https?:)?\/\//i.test(raw)) {
758
- return { kind: "external", normalizedUrl: raw };
759
- }
760
- if (call.isConcat) {
761
- return { kind: "dynamic", normalizedUrl: raw };
762
- }
763
- const { path: path3, hadInterpolation } = normalizeFetchUrl(raw);
764
- if (!path3.startsWith("/")) {
765
- return { kind: "unresolved", normalizedUrl: path3 };
766
- }
767
- const segs = path3.split("/").filter(Boolean);
768
- if (hadInterpolation && segs.length > 0 && segs[0].startsWith(":")) {
769
- return { kind: "dynamic", normalizedUrl: path3 };
770
- }
771
- const exact = apiPathMap.get(path3);
772
- if (exact) return { kind: "resolved", nodeId: exact, normalizedUrl: path3 };
773
- let bestScore = -1;
774
- let bestId = null;
775
- for (const r of apiRoutes) {
776
- const score = scoreApiRouteMatch(segs, r.segments);
777
- if (score > bestScore) {
778
- bestScore = score;
779
- bestId = r.nodeId;
780
- }
781
- }
782
- if (bestId && bestScore > 0) {
783
- return { kind: "resolved", nodeId: bestId, normalizedUrl: path3 };
784
- }
785
- return { kind: "unresolved", normalizedUrl: path3 };
786
- }
787
707
  function extractEdges(srcDir, absPath, sourceId, parsed, nodeIdSet, nodeTypeMap, barrelMaps, routeToNodeId) {
788
708
  const edges = [];
789
709
  const flagged = [];
@@ -883,26 +803,26 @@ function extractEdges(srcDir, absPath, sourceId, parsed, nodeIdSet, nodeTypeMap,
883
803
  return { edges, flagged };
884
804
  }
885
805
  function detect(rootDir) {
886
- return (0, import_node_fs3.existsSync)((0, import_node_path3.join)(rootDir, "src", "app")) && (0, import_node_fs3.existsSync)((0, import_node_path3.join)(rootDir, "next.config.ts")) || (0, import_node_fs3.existsSync)((0, import_node_path3.join)(rootDir, "next.config.js")) || (0, import_node_fs3.existsSync)((0, import_node_path3.join)(rootDir, "next.config.mjs"));
806
+ return (0, import_node_fs4.existsSync)((0, import_node_path4.join)(rootDir, "src", "app")) && (0, import_node_fs4.existsSync)((0, import_node_path4.join)(rootDir, "next.config.ts")) || (0, import_node_fs4.existsSync)((0, import_node_path4.join)(rootDir, "next.config.js")) || (0, import_node_fs4.existsSync)((0, import_node_path4.join)(rootDir, "next.config.mjs"));
887
807
  }
888
808
  function generate(rootDir) {
889
- const srcDir = (0, import_node_path3.join)(rootDir, "src");
890
- const appFiles = walk((0, import_node_path3.join)(srcDir, "app"), [".tsx", ".ts"]).filter(
891
- (f) => (0, import_node_path3.basename)(f) !== "route.ts" && (0, import_node_path3.basename)(f) !== "route.tsx"
809
+ const srcDir = (0, import_node_path4.join)(rootDir, "src");
810
+ const appFiles = walk((0, import_node_path4.join)(srcDir, "app"), [".tsx", ".ts"]).filter(
811
+ (f) => (0, import_node_path4.basename)(f) !== "route.ts" && (0, import_node_path4.basename)(f) !== "route.tsx"
892
812
  );
893
- const clientFiles = walk((0, import_node_path3.join)(srcDir, "client"), [".tsx", ".ts"]);
894
- const serverFiles = walk((0, import_node_path3.join)(srcDir, "server"), [".ts", ".tsx"]).filter(
895
- (f) => (0, import_node_path3.basename)(f) !== "route.ts" && (0, import_node_path3.basename)(f) !== "route.tsx"
813
+ const clientFiles = walk((0, import_node_path4.join)(srcDir, "client"), [".tsx", ".ts"]);
814
+ const serverFiles = walk((0, import_node_path4.join)(srcDir, "server"), [".ts", ".tsx"]).filter(
815
+ (f) => (0, import_node_path4.basename)(f) !== "route.ts" && (0, import_node_path4.basename)(f) !== "route.tsx"
896
816
  );
897
- const libFiles = walk((0, import_node_path3.join)(srcDir, "lib"), [".ts", ".tsx"]);
898
- const configFiles = walk((0, import_node_path3.join)(srcDir, "config"), [".ts", ".tsx"]);
817
+ const libFiles = walk((0, import_node_path4.join)(srcDir, "lib"), [".ts", ".tsx"]);
818
+ const configFiles = walk((0, import_node_path4.join)(srcDir, "config"), [".ts", ".tsx"]);
899
819
  const allDiscovered = [...appFiles, ...clientFiles, ...serverFiles, ...libFiles, ...configFiles];
900
820
  const parsedByPath = /* @__PURE__ */ new Map();
901
821
  for (const absPath of allDiscovered) {
902
822
  parsedByPath.set(absPath, parseFile(absPath));
903
823
  }
904
824
  const barrelMaps = buildAllBarrelMaps(srcDir, parsedByPath);
905
- const fileSet = allDiscovered.filter((f) => !(0, import_node_path3.basename)(f).startsWith("index."));
825
+ const fileSet = allDiscovered.filter((f) => !(0, import_node_path4.basename)(f).startsWith("index."));
906
826
  const nodes = [];
907
827
  const nodeIdSet = /* @__PURE__ */ new Set();
908
828
  const nodeTypeMap = /* @__PURE__ */ new Map();
@@ -913,15 +833,13 @@ function generate(rootDir) {
913
833
  const parsed = parsedByPath.get(absPath);
914
834
  const name = parsed.name || nameFromFilename(absPath);
915
835
  const route = extractRoute(id);
916
- const module_ = classifyModule(id);
917
- nodes.push({ id, type, name, route, module: module_, exports: parsed.exports });
836
+ nodes.push({ id, type, name, route, exports: parsed.exports });
918
837
  nodeIdSet.add(id);
919
838
  nodeTypeMap.set(id, type);
920
839
  if (route) routeToNodeId.set(route, id);
921
840
  }
922
841
  const allEdges = [];
923
842
  const allFlagged = [];
924
- const crossRefs = [];
925
843
  for (const absPath of fileSet) {
926
844
  const sourceId = toNodeId(srcDir, absPath);
927
845
  const parsed = parsedByPath.get(absPath);
@@ -938,66 +856,21 @@ function generate(rootDir) {
938
856
  allEdges.push(...edges);
939
857
  allFlagged.push(...flagged);
940
858
  }
941
- const apiRoutes = loadApiRoutes(rootDir);
942
- const apiPathMap = buildApiPathMap(apiRoutes);
943
- const includeExternalFetches = process.env.LAUNCH_CHART_INCLUDE_EXTERNAL_FETCHES === "1";
944
- const fetchSeen = /* @__PURE__ */ new Set();
945
- let fetchResolvedCount = 0;
946
- let fetchDynamicCount = 0;
947
- let fetchUnresolvedCount = 0;
948
- let fetchExternalCount = 0;
859
+ const fetchCallEntries = [];
949
860
  for (const absPath of fileSet) {
950
861
  const sourceId = toNodeId(srcDir, absPath);
951
862
  const parsed = parsedByPath.get(absPath);
952
863
  if (parsed.fetchCalls.length === 0) continue;
953
- for (const call of parsed.fetchCalls) {
954
- const result = resolveFetchCall(call, apiPathMap, apiRoutes);
955
- const methodTag = call.method ?? (call.kind === "fetch" ? "GET?" : "?");
956
- if (result.kind === "resolved" && result.nodeId) {
957
- const key = `${sourceId}\u2192${result.nodeId}\u2192calls_api`;
958
- if (fetchSeen.has(key)) continue;
959
- fetchSeen.add(key);
960
- crossRefs.push({
961
- source: sourceId,
962
- target: result.nodeId,
963
- type: "calls_api",
964
- layer: "api"
965
- });
966
- fetchResolvedCount++;
967
- continue;
968
- }
969
- if (result.kind === "dynamic") {
970
- fetchDynamicCount++;
971
- allFlagged.push({
972
- source: sourceId,
973
- target: "DYNAMIC",
974
- type: "calls_api",
975
- label: call.isConcat ? `${methodTag} fetch with concat: ${call.url}` : `${methodTag} fetch with template: ${call.url}`,
976
- confidence: call.isConcat ? "low" : "medium"
977
- });
978
- continue;
979
- }
980
- if (result.kind === "external") {
981
- fetchExternalCount++;
982
- if (!includeExternalFetches) continue;
983
- allFlagged.push({
984
- source: sourceId,
985
- target: "EXTERNAL",
986
- type: "calls_external",
987
- label: `${methodTag} external fetch: ${call.url}`,
988
- confidence: "high"
989
- });
990
- continue;
991
- }
992
- fetchUnresolvedCount++;
993
- allFlagged.push({
994
- source: sourceId,
995
- target: "UNRESOLVED",
996
- type: "calls_api",
997
- label: `${methodTag} fetch to unknown path: ${result.normalizedUrl}`,
998
- confidence: "medium"
999
- });
1000
- }
864
+ fetchCallEntries.push({
865
+ nodeId: sourceId,
866
+ calls: parsed.fetchCalls.map((c) => ({
867
+ url: c.url,
868
+ method: c.method,
869
+ isTemplate: c.isTemplate,
870
+ isConcat: c.isConcat,
871
+ kind: c.kind
872
+ }))
873
+ });
1001
874
  }
1002
875
  const externalScanned = new Set(allDiscovered.map((f) => f.replace(/\\/g, "/")));
1003
876
  const IGNORE_DIRS = /* @__PURE__ */ new Set([
@@ -1023,7 +896,7 @@ function generate(rootDir) {
1023
896
  } catch {
1024
897
  continue;
1025
898
  }
1026
- const externalId = (0, import_node_path3.relative)(rootDir, absPath).replace(/\\/g, "/");
899
+ const externalId = (0, import_node_path4.relative)(rootDir, absPath).replace(/\\/g, "/");
1027
900
  const edgesFromThis = [];
1028
901
  const seen = /* @__PURE__ */ new Set();
1029
902
  for (const imp of parsed.imports) {
@@ -1064,7 +937,6 @@ function generate(rootDir) {
1064
937
  type: "external",
1065
938
  name: parsed.name || nameFromFilename(absPath),
1066
939
  route: null,
1067
- module: "external",
1068
940
  exports: parsed.exports
1069
941
  });
1070
942
  nodeIdSet.add(externalId);
@@ -1114,20 +986,11 @@ function generate(rootDir) {
1114
986
  layer: "ui",
1115
987
  parser: "react-nextjs-ast",
1116
988
  ...stats,
1117
- api_call_detection: {
1118
- includeExternalFetches,
1119
- includeConcatFetches: process.env.LAUNCH_CHART_INCLUDE_CONCAT_FETCHES === "1",
1120
- apiRoutesLoaded: apiRoutes.length,
1121
- resolved: fetchResolvedCount,
1122
- dynamic: fetchDynamicCount,
1123
- unresolved: fetchUnresolvedCount,
1124
- external: fetchExternalCount
1125
- },
1126
989
  notes: "Auto-generated via TypeScript AST \u2014 edges derived from actual imports, renders from JSX usage, navigations from router/Link calls."
1127
990
  },
1128
991
  nodes,
1129
992
  edges: allEdges,
1130
- cross_refs: crossRefs,
993
+ cross_refs: [],
1131
994
  contradictions: [],
1132
995
  warnings: [],
1133
996
  flagged_edges: dedupedFlagged,
@@ -1138,16 +1001,17 @@ function generate(rootDir) {
1138
1001
  renders: allEdges.filter((e) => e.type === "renders").length,
1139
1002
  imports: allEdges.filter((e) => e.type === "imports").length,
1140
1003
  navigates: allEdges.filter((e) => e.type === "navigates").length
1141
- }
1004
+ },
1005
+ fetch_calls: fetchCallEntries
1142
1006
  }
1143
1007
  };
1144
1008
  }
1145
- var import_node_fs3, import_node_path3, RENDER_TYPES, reactNextjsParser;
1009
+ var import_node_fs4, import_node_path4, RENDER_TYPES, reactNextjsParser;
1146
1010
  var init_react_nextjs = __esm({
1147
1011
  "src/server/graph/parsers/ui/react-nextjs.ts"() {
1148
1012
  "use strict";
1149
- import_node_fs3 = require("node:fs");
1150
- import_node_path3 = require("node:path");
1013
+ import_node_fs4 = require("node:fs");
1014
+ import_node_path4 = require("node:path");
1151
1015
  init_ast_helpers();
1152
1016
  RENDER_TYPES = /* @__PURE__ */ new Set(["component", "ui", "layout", "context"]);
1153
1017
  reactNextjsParser = {
@@ -1162,9 +1026,9 @@ var init_react_nextjs = __esm({
1162
1026
  // src/server/graph/parsers/api/nextjs-routes.ts
1163
1027
  function walk2(dir) {
1164
1028
  const results = [];
1165
- if (!(0, import_node_fs4.existsSync)(dir)) return results;
1166
- for (const entry of (0, import_node_fs4.readdirSync)(dir, { withFileTypes: true })) {
1167
- const full = (0, import_node_path4.join)(dir, entry.name);
1029
+ if (!(0, import_node_fs5.existsSync)(dir)) return results;
1030
+ for (const entry of (0, import_node_fs5.readdirSync)(dir, { withFileTypes: true })) {
1031
+ const full = (0, import_node_path5.join)(dir, entry.name);
1168
1032
  if (entry.isDirectory()) {
1169
1033
  results.push(...walk2(full));
1170
1034
  } else if (entry.name === "route.ts" || entry.name === "route.tsx") {
@@ -1174,7 +1038,7 @@ function walk2(dir) {
1174
1038
  return results;
1175
1039
  }
1176
1040
  function filePathToRoute(apiDir, absPath) {
1177
- let route = "/" + (0, import_node_path4.relative)(apiDir, absPath).replace(/\\/g, "/").replace(/\/route\.tsx?$/, "");
1041
+ let route = "/" + (0, import_node_path5.relative)(apiDir, absPath).replace(/\\/g, "/").replace(/\/route\.tsx?$/, "");
1178
1042
  route = route.replace(/\[([^\]]+)\]/g, ":$1");
1179
1043
  route = route.replace(/\/+/g, "/");
1180
1044
  if (route === "/") return "/api";
@@ -1185,10 +1049,10 @@ function camelToPascal(s) {
1185
1049
  return s.charAt(0).toUpperCase() + s.slice(1);
1186
1050
  }
1187
1051
  function detect2(rootDir) {
1188
- return (0, import_node_fs4.existsSync)((0, import_node_path4.join)(rootDir, "src", "app", "api"));
1052
+ return (0, import_node_fs5.existsSync)((0, import_node_path5.join)(rootDir, "src", "app", "api"));
1189
1053
  }
1190
1054
  function generate2(rootDir) {
1191
- const apiDir = (0, import_node_path4.join)(rootDir, "src", "app", "api");
1055
+ const apiDir = (0, import_node_path5.join)(rootDir, "src", "app", "api");
1192
1056
  const routeFiles = walk2(apiDir);
1193
1057
  const nodes = [];
1194
1058
  const edges = [];
@@ -1206,7 +1070,7 @@ function generate2(rootDir) {
1206
1070
  if (HTTP_METHODS2.has(exp)) methods.push(exp);
1207
1071
  }
1208
1072
  const routePath = filePathToRoute(apiDir, absPath);
1209
- const relPath = (0, import_node_path4.relative)(rootDir, absPath).replace(/\\/g, "/");
1073
+ const relPath = (0, import_node_path5.relative)(rootDir, absPath).replace(/\\/g, "/");
1210
1074
  const mutations = dbCalls.filter((c) => c.isMutation);
1211
1075
  const reads = dbCalls.filter((c) => !c.isMutation);
1212
1076
  const mutates = mutations.length > 0;
@@ -1283,12 +1147,12 @@ function generate2(rootDir) {
1283
1147
  }
1284
1148
  };
1285
1149
  }
1286
- var import_node_fs4, import_node_path4, HTTP_METHODS2, nextjsRoutesParser;
1150
+ var import_node_fs5, import_node_path5, HTTP_METHODS2, nextjsRoutesParser;
1287
1151
  var init_nextjs_routes = __esm({
1288
1152
  "src/server/graph/parsers/api/nextjs-routes.ts"() {
1289
1153
  "use strict";
1290
- import_node_fs4 = require("node:fs");
1291
- import_node_path4 = require("node:path");
1154
+ import_node_fs5 = require("node:fs");
1155
+ import_node_path5 = require("node:path");
1292
1156
  init_ast_helpers();
1293
1157
  HTTP_METHODS2 = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"]);
1294
1158
  nextjsRoutesParser = {
@@ -1391,11 +1255,11 @@ function parseEnums(content) {
1391
1255
  return nodes;
1392
1256
  }
1393
1257
  function detect3(rootDir) {
1394
- return (0, import_node_fs5.existsSync)((0, import_node_path5.join)(rootDir, "prisma", "schema.prisma"));
1258
+ return (0, import_node_fs6.existsSync)((0, import_node_path6.join)(rootDir, "prisma", "schema.prisma"));
1395
1259
  }
1396
1260
  function generate3(rootDir) {
1397
- const schemaPath = (0, import_node_path5.join)(rootDir, "prisma", "schema.prisma");
1398
- const content = (0, import_node_fs5.readFileSync)(schemaPath, "utf-8");
1261
+ const schemaPath = (0, import_node_path6.join)(rootDir, "prisma", "schema.prisma");
1262
+ const content = (0, import_node_fs6.readFileSync)(schemaPath, "utf-8");
1399
1263
  const { nodes: modelNodes, relations } = parseModels(content);
1400
1264
  const enumNodes = parseEnums(content);
1401
1265
  const allNodes = [...modelNodes, ...enumNodes];
@@ -1444,12 +1308,12 @@ function generate3(rootDir) {
1444
1308
  }
1445
1309
  };
1446
1310
  }
1447
- var import_node_fs5, import_node_path5, prismaSchemaParser;
1311
+ var import_node_fs6, import_node_path6, prismaSchemaParser;
1448
1312
  var init_prisma_schema = __esm({
1449
1313
  "src/server/graph/parsers/db/prisma-schema.ts"() {
1450
1314
  "use strict";
1451
- import_node_fs5 = require("node:fs");
1452
- import_node_path5 = require("node:path");
1315
+ import_node_fs6 = require("node:fs");
1316
+ import_node_path6 = require("node:path");
1453
1317
  prismaSchemaParser = {
1454
1318
  id: "prisma-schema",
1455
1319
  layer: "db",
@@ -1459,99 +1323,1161 @@ var init_prisma_schema = __esm({
1459
1323
  }
1460
1324
  });
1461
1325
 
1462
- // src/server/graph/core/graph-builder.ts
1463
- function getParser(layer) {
1464
- return ALL_PARSERS.find((p) => p.layer === layer);
1465
- }
1466
- function generateLayer(rootDir, layer) {
1467
- const parser = getParser(layer);
1468
- if (!parser) return null;
1469
- if (!parser.detect(rootDir)) return null;
1470
- const output = parser.generate(rootDir);
1471
- return {
1472
- layer,
1473
- output,
1474
- nodeCount: output.nodes.length,
1475
- edgeCount: output.edges.length
1476
- };
1477
- }
1478
- function generateAll(rootDir) {
1479
- const layers = ["api", "db", "ui"];
1480
- const results = [];
1481
- for (const layer of layers) {
1482
- const result = generateLayer(rootDir, layer);
1483
- if (result) results.push(result);
1326
+ // src/server/graph/core/api-route-matching.ts
1327
+ function loadApiRoutesFromOutput(apiOutput) {
1328
+ const routes = [];
1329
+ for (const n of apiOutput.nodes) {
1330
+ const path3 = n.path;
1331
+ if (!path3 || typeof path3 !== "string") continue;
1332
+ routes.push({
1333
+ path: path3,
1334
+ nodeId: n.id,
1335
+ segments: path3.split("/").filter(Boolean)
1336
+ });
1484
1337
  }
1485
- const byLayer = new Map(results.map((r) => [r.layer, r]));
1486
- return ["ui", "api", "db"].map((l) => byLayer.get(l)).filter((r) => !!r);
1338
+ return routes;
1487
1339
  }
1488
- var ALL_PARSERS;
1489
- var init_graph_builder = __esm({
1490
- "src/server/graph/core/graph-builder.ts"() {
1491
- "use strict";
1492
- init_react_nextjs();
1493
- init_nextjs_routes();
1494
- init_prisma_schema();
1495
- ALL_PARSERS = [
1496
- reactNextjsParser,
1497
- nextjsRoutesParser,
1498
- prismaSchemaParser
1499
- ];
1340
+ function buildApiPathMap(routes) {
1341
+ const map = /* @__PURE__ */ new Map();
1342
+ for (const r of routes) {
1343
+ if (!map.has(r.path)) map.set(r.path, r.nodeId);
1500
1344
  }
1501
- });
1502
-
1503
- // src/server/graph/index.ts
1504
- function graphsDir(rootDir) {
1505
- return (0, import_node_path6.join)(rootDir, GRAPHS_DIR);
1506
- }
1507
- function graphFilePath(rootDir, layer) {
1508
- return (0, import_node_path6.join)(graphsDir(rootDir), `${layer}.json`);
1345
+ return map;
1509
1346
  }
1510
- function invalidateCache(filePath) {
1511
- graphCache.delete(filePath);
1347
+ function normalizeFetchUrl(raw) {
1348
+ let s = raw.replace(/^`|`$/g, "");
1349
+ const qIdx = s.indexOf("?");
1350
+ if (qIdx >= 0) s = s.slice(0, qIdx);
1351
+ const hIdx = s.indexOf("#");
1352
+ if (hIdx >= 0) s = s.slice(0, hIdx);
1353
+ let hadInterpolation = false;
1354
+ s = s.replace(/\$\{([^}]+)\}/g, (_, expr) => {
1355
+ hadInterpolation = true;
1356
+ const cleaned = expr.trim();
1357
+ const last = cleaned.split(".").pop() ?? cleaned;
1358
+ const name = last.replace(/[^\w]/g, "") || "param";
1359
+ return ":" + name;
1360
+ });
1361
+ s = s.replace(/\/+/g, "/");
1362
+ if (s.length > 1 && s.endsWith("/")) s = s.slice(0, -1);
1363
+ return { path: s || "/", hadInterpolation };
1512
1364
  }
1513
- function readGraph(rootDir, layer) {
1514
- const filePath = graphFilePath(rootDir, layer);
1515
- if (!(0, import_node_fs6.existsSync)(filePath)) return null;
1516
- const stat = (0, import_node_fs6.statSync)(filePath);
1517
- const cached = graphCache.get(filePath);
1518
- if (cached && cached.mtimeMs === stat.mtimeMs) {
1519
- return cached.graph;
1365
+ function scoreApiRouteMatch(candidate, known) {
1366
+ if (candidate.length !== known.length) return -1;
1367
+ let score = 0;
1368
+ for (let i = 0; i < candidate.length; i++) {
1369
+ const a = candidate[i];
1370
+ const b = known[i];
1371
+ if (a === b) {
1372
+ score += 3;
1373
+ continue;
1374
+ }
1375
+ if (a.startsWith(":") && b.startsWith(":")) {
1376
+ score += 2;
1377
+ continue;
1378
+ }
1379
+ if (a.startsWith(":") || b.startsWith(":")) {
1380
+ score += 1;
1381
+ continue;
1382
+ }
1383
+ return -1;
1520
1384
  }
1521
- const content = (0, import_node_fs6.readFileSync)(filePath, "utf-8");
1522
- const graph = JSON.parse(content);
1523
- graphCache.set(filePath, { mtimeMs: stat.mtimeMs, graph });
1524
- return graph;
1385
+ return score;
1525
1386
  }
1526
- function readAllGraphs(rootDir) {
1527
- const result = {};
1528
- for (const layer of LAYERS) {
1529
- const graph = readGraph(rootDir, layer);
1530
- if (graph) result[layer] = graph;
1387
+ function resolveFetchCall(call, apiPathMap, apiRoutes) {
1388
+ const raw = call.url;
1389
+ if (/^(https?:)?\/\//i.test(raw)) {
1390
+ return { kind: "external", normalizedUrl: raw };
1531
1391
  }
1532
- return result;
1533
- }
1534
- function generateGraph(rootDir, layer) {
1535
- const dir = graphsDir(rootDir);
1536
- (0, import_node_fs6.mkdirSync)(dir, { recursive: true });
1537
- const results = layer ? [generateLayer(rootDir, layer)].filter((r) => r !== null) : generateAll(rootDir);
1538
- for (const result of results) {
1539
- const filePath = graphFilePath(rootDir, result.layer);
1540
- (0, import_node_fs6.writeFileSync)(filePath, JSON.stringify(result.output, null, 2) + "\n", "utf-8");
1541
- invalidateCache(filePath);
1392
+ if (call.isConcat) {
1393
+ return { kind: "dynamic", normalizedUrl: raw };
1542
1394
  }
1543
- return results;
1544
- }
1545
- var import_node_fs6, import_node_path6, GRAPHS_DIR, LAYERS, graphCache;
1546
- var init_graph = __esm({
1547
- "src/server/graph/index.ts"() {
1548
- "use strict";
1549
- import_node_fs6 = require("node:fs");
1550
- import_node_path6 = require("node:path");
1551
- init_graph_builder();
1395
+ const { path: path3, hadInterpolation } = normalizeFetchUrl(raw);
1396
+ if (!path3.startsWith("/")) {
1397
+ return { kind: "unresolved", normalizedUrl: path3 };
1398
+ }
1399
+ const segs = path3.split("/").filter(Boolean);
1400
+ if (hadInterpolation && segs.length > 0 && segs[0].startsWith(":")) {
1401
+ return { kind: "dynamic", normalizedUrl: path3 };
1402
+ }
1403
+ const exact = apiPathMap.get(path3);
1404
+ if (exact) return { kind: "resolved", nodeId: exact, normalizedUrl: path3 };
1405
+ let bestScore = -1;
1406
+ let bestId = null;
1407
+ for (const r of apiRoutes) {
1408
+ const score = scoreApiRouteMatch(segs, r.segments);
1409
+ if (score > bestScore) {
1410
+ bestScore = score;
1411
+ bestId = r.nodeId;
1412
+ }
1413
+ }
1414
+ if (bestId && bestScore > 0) {
1415
+ return { kind: "resolved", nodeId: bestId, normalizedUrl: path3 };
1416
+ }
1417
+ return { kind: "unresolved", normalizedUrl: path3 };
1418
+ }
1419
+ function resolveUrlPath(urlPath, apiPathMap, apiRoutes) {
1420
+ const { path: path3, hadInterpolation } = normalizeFetchUrl(urlPath);
1421
+ if (!path3.startsWith("/")) {
1422
+ return { kind: "unresolved", normalizedUrl: path3 };
1423
+ }
1424
+ const segs = path3.split("/").filter(Boolean);
1425
+ if (hadInterpolation && segs.length > 0 && segs[0].startsWith(":")) {
1426
+ return { kind: "dynamic", normalizedUrl: path3 };
1427
+ }
1428
+ const exact = apiPathMap.get(path3);
1429
+ if (exact) return { kind: "resolved", nodeId: exact, normalizedUrl: path3 };
1430
+ let bestScore = -1;
1431
+ let bestId = null;
1432
+ for (const r of apiRoutes) {
1433
+ const score = scoreApiRouteMatch(segs, r.segments);
1434
+ if (score > bestScore) {
1435
+ bestScore = score;
1436
+ bestId = r.nodeId;
1437
+ }
1438
+ }
1439
+ if (bestId && bestScore > 0) {
1440
+ return { kind: "resolved", nodeId: bestId, normalizedUrl: path3 };
1441
+ }
1442
+ return { kind: "unresolved", normalizedUrl: path3 };
1443
+ }
1444
+ var init_api_route_matching = __esm({
1445
+ "src/server/graph/core/api-route-matching.ts"() {
1446
+ "use strict";
1447
+ }
1448
+ });
1449
+
1450
+ // src/server/graph/parsers/crosslayer/fetch-resolver.ts
1451
+ var fetchResolverParser;
1452
+ var init_fetch_resolver = __esm({
1453
+ "src/server/graph/parsers/crosslayer/fetch-resolver.ts"() {
1454
+ "use strict";
1455
+ init_api_route_matching();
1456
+ fetchResolverParser = {
1457
+ id: "fetch-resolver",
1458
+ layer: "crosslayer",
1459
+ detect(_rootDir) {
1460
+ return true;
1461
+ },
1462
+ generate(_rootDir, layerOutputs) {
1463
+ const uiOutput = layerOutputs.get("ui");
1464
+ const apiOutput = layerOutputs.get("api");
1465
+ if (!uiOutput || !apiOutput) {
1466
+ return { cross_refs: [], flagged_edges: [], warnings: [] };
1467
+ }
1468
+ const apiRoutes = loadApiRoutesFromOutput(apiOutput);
1469
+ const apiPathMap = buildApiPathMap(apiRoutes);
1470
+ const fetchCallEntries = uiOutput.patterns?.fetch_calls ?? [];
1471
+ if (fetchCallEntries.length === 0) {
1472
+ return { cross_refs: [], flagged_edges: [], warnings: [] };
1473
+ }
1474
+ const includeExternal = process.env.LAUNCH_CHART_INCLUDE_EXTERNAL_FETCHES === "1";
1475
+ const crossRefs = [];
1476
+ const flaggedEdges = [];
1477
+ const seen = /* @__PURE__ */ new Set();
1478
+ let resolvedCount = 0;
1479
+ let dynamicCount = 0;
1480
+ let unresolvedCount = 0;
1481
+ let externalCount = 0;
1482
+ for (const entry of fetchCallEntries) {
1483
+ for (const call of entry.calls) {
1484
+ const result = resolveFetchCall(call, apiPathMap, apiRoutes);
1485
+ const methodTag = call.method ?? (call.kind === "fetch" ? "GET?" : "?");
1486
+ if (result.kind === "resolved" && result.nodeId) {
1487
+ const key = `${entry.nodeId}\u2192${result.nodeId}\u2192calls_api`;
1488
+ if (seen.has(key)) continue;
1489
+ seen.add(key);
1490
+ crossRefs.push({
1491
+ source: entry.nodeId,
1492
+ target: result.nodeId,
1493
+ type: "calls_api",
1494
+ layer: "api"
1495
+ });
1496
+ resolvedCount++;
1497
+ continue;
1498
+ }
1499
+ if (result.kind === "dynamic") {
1500
+ dynamicCount++;
1501
+ flaggedEdges.push({
1502
+ source: entry.nodeId,
1503
+ target: "DYNAMIC",
1504
+ type: "calls_api",
1505
+ label: call.isConcat ? `${methodTag} fetch with concat: ${call.url}` : `${methodTag} fetch with template: ${call.url}`,
1506
+ confidence: call.isConcat ? "low" : "medium"
1507
+ });
1508
+ continue;
1509
+ }
1510
+ if (result.kind === "external") {
1511
+ externalCount++;
1512
+ if (!includeExternal) continue;
1513
+ flaggedEdges.push({
1514
+ source: entry.nodeId,
1515
+ target: "EXTERNAL",
1516
+ type: "calls_external",
1517
+ label: `${methodTag} external fetch: ${call.url}`,
1518
+ confidence: "high"
1519
+ });
1520
+ continue;
1521
+ }
1522
+ unresolvedCount++;
1523
+ flaggedEdges.push({
1524
+ source: entry.nodeId,
1525
+ target: "UNRESOLVED",
1526
+ type: "calls_api",
1527
+ label: `${methodTag} fetch to unknown path: ${result.normalizedUrl}`,
1528
+ confidence: "medium"
1529
+ });
1530
+ }
1531
+ }
1532
+ return {
1533
+ cross_refs: crossRefs,
1534
+ flagged_edges: flaggedEdges,
1535
+ warnings: [],
1536
+ patterns: {
1537
+ api_call_detection: {
1538
+ resolved: resolvedCount,
1539
+ dynamic: dynamicCount,
1540
+ unresolved: unresolvedCount,
1541
+ external: externalCount
1542
+ }
1543
+ }
1544
+ };
1545
+ }
1546
+ };
1547
+ }
1548
+ });
1549
+
1550
+ // src/server/graph/parsers/crosslayer/api-annotations.ts
1551
+ function walk3(dir, exts) {
1552
+ if (!(0, import_node_fs7.existsSync)(dir)) return [];
1553
+ const results = [];
1554
+ for (const entry of (0, import_node_fs7.readdirSync)(dir, { withFileTypes: true })) {
1555
+ if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
1556
+ const full = (0, import_node_path7.join)(dir, entry.name);
1557
+ if (entry.isDirectory()) {
1558
+ results.push(...walk3(full, exts));
1559
+ } else if (exts.includes((0, import_node_path7.extname)(entry.name))) {
1560
+ results.push(full);
1561
+ }
1562
+ }
1563
+ return results;
1564
+ }
1565
+ function toNodeId2(srcDir, absPath) {
1566
+ return (0, import_node_path7.relative)(srcDir, absPath).replace(/\\/g, "/");
1567
+ }
1568
+ var import_node_fs7, import_node_path7, API_ANNOTATION_RE, apiAnnotationsParser;
1569
+ var init_api_annotations = __esm({
1570
+ "src/server/graph/parsers/crosslayer/api-annotations.ts"() {
1571
+ "use strict";
1572
+ import_node_fs7 = require("node:fs");
1573
+ import_node_path7 = require("node:path");
1574
+ init_api_route_matching();
1575
+ API_ANNOTATION_RE = /@api\s+(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+(\/\S+)/g;
1576
+ apiAnnotationsParser = {
1577
+ id: "api-annotations",
1578
+ layer: "crosslayer",
1579
+ detect(rootDir) {
1580
+ return (0, import_node_fs7.existsSync)((0, import_node_path7.join)(rootDir, "src"));
1581
+ },
1582
+ generate(rootDir, layerOutputs) {
1583
+ const apiOutput = layerOutputs.get("api");
1584
+ if (!apiOutput) {
1585
+ return { cross_refs: [], flagged_edges: [], warnings: [] };
1586
+ }
1587
+ const uiOutput = layerOutputs.get("ui");
1588
+ const uiNodeIds = new Set(uiOutput?.nodes.map((n) => n.id) ?? []);
1589
+ const apiRoutes = loadApiRoutesFromOutput(apiOutput);
1590
+ const apiPathMap = buildApiPathMap(apiRoutes);
1591
+ const srcDir = (0, import_node_path7.join)(rootDir, "src");
1592
+ const files = walk3(srcDir, [".ts", ".tsx"]);
1593
+ const crossRefs = [];
1594
+ const flaggedEdges = [];
1595
+ const seen = /* @__PURE__ */ new Set();
1596
+ for (const absPath of files) {
1597
+ const content = (0, import_node_fs7.readFileSync)(absPath, "utf-8");
1598
+ const sourceId = toNodeId2(srcDir, absPath);
1599
+ if (!uiNodeIds.has(sourceId)) continue;
1600
+ let match;
1601
+ API_ANNOTATION_RE.lastIndex = 0;
1602
+ while ((match = API_ANNOTATION_RE.exec(content)) !== null) {
1603
+ const method = match[1];
1604
+ const urlPath = match[2];
1605
+ const result = resolveUrlPath(urlPath, apiPathMap, apiRoutes);
1606
+ if (result.kind === "resolved" && result.nodeId) {
1607
+ const key = `${sourceId}|${result.nodeId}|calls_api`;
1608
+ if (seen.has(key)) continue;
1609
+ seen.add(key);
1610
+ crossRefs.push({
1611
+ source: sourceId,
1612
+ target: result.nodeId,
1613
+ type: "calls_api",
1614
+ layer: "api"
1615
+ });
1616
+ } else {
1617
+ flaggedEdges.push({
1618
+ source: sourceId,
1619
+ target: "UNRESOLVED",
1620
+ type: "annotation_unresolved",
1621
+ label: `@api ${method} ${urlPath} \u2014 no matching API route found`,
1622
+ confidence: "high"
1623
+ });
1624
+ }
1625
+ }
1626
+ }
1627
+ return {
1628
+ cross_refs: crossRefs,
1629
+ flagged_edges: flaggedEdges,
1630
+ warnings: [],
1631
+ patterns: {
1632
+ annotations_found: crossRefs.length + flaggedEdges.length,
1633
+ annotations_resolved: crossRefs.length,
1634
+ annotations_unresolved: flaggedEdges.length
1635
+ }
1636
+ };
1637
+ }
1638
+ };
1639
+ }
1640
+ });
1641
+
1642
+ // src/server/graph/parsers/crosslayer/url-literal-scanner.ts
1643
+ function walk4(dir, exts) {
1644
+ if (!(0, import_node_fs8.existsSync)(dir)) return [];
1645
+ const results = [];
1646
+ for (const entry of (0, import_node_fs8.readdirSync)(dir, { withFileTypes: true })) {
1647
+ if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
1648
+ const full = (0, import_node_path8.join)(dir, entry.name);
1649
+ if (entry.isDirectory()) {
1650
+ results.push(...walk4(full, exts));
1651
+ } else if (exts.includes((0, import_node_path8.extname)(entry.name))) {
1652
+ results.push(full);
1653
+ }
1654
+ }
1655
+ return results;
1656
+ }
1657
+ function toNodeId3(srcDir, absPath) {
1658
+ return (0, import_node_path8.relative)(srcDir, absPath).replace(/\\/g, "/");
1659
+ }
1660
+ var import_node_fs8, import_node_path8, URL_LITERAL_RE, urlLiteralScannerParser;
1661
+ var init_url_literal_scanner = __esm({
1662
+ "src/server/graph/parsers/crosslayer/url-literal-scanner.ts"() {
1663
+ "use strict";
1664
+ import_node_fs8 = require("node:fs");
1665
+ import_node_path8 = require("node:path");
1666
+ init_api_route_matching();
1667
+ URL_LITERAL_RE = /['"`](\/api\/[^'"`\s]+?)['"`]/g;
1668
+ urlLiteralScannerParser = {
1669
+ id: "url-literal-scanner",
1670
+ layer: "crosslayer",
1671
+ detect(rootDir) {
1672
+ return (0, import_node_fs8.existsSync)((0, import_node_path8.join)(rootDir, "src"));
1673
+ },
1674
+ generate(rootDir, layerOutputs) {
1675
+ const apiOutput = layerOutputs.get("api");
1676
+ if (!apiOutput) {
1677
+ return { cross_refs: [], flagged_edges: [], warnings: [] };
1678
+ }
1679
+ const uiOutput = layerOutputs.get("ui");
1680
+ const uiNodeIds = new Set(uiOutput?.nodes.map((n) => n.id) ?? []);
1681
+ const apiRoutes = loadApiRoutesFromOutput(apiOutput);
1682
+ const apiPathMap = buildApiPathMap(apiRoutes);
1683
+ const srcDir = (0, import_node_path8.join)(rootDir, "src");
1684
+ const clientDir = (0, import_node_path8.join)(srcDir, "client");
1685
+ const appDir = (0, import_node_path8.join)(srcDir, "app");
1686
+ const files = [
1687
+ ...walk4(clientDir, [".ts", ".tsx"]),
1688
+ ...walk4(appDir, [".ts", ".tsx"])
1689
+ ];
1690
+ const crossRefs = [];
1691
+ const seen = /* @__PURE__ */ new Set();
1692
+ for (const absPath of files) {
1693
+ const sourceId = toNodeId3(srcDir, absPath);
1694
+ if (!uiNodeIds.has(sourceId)) continue;
1695
+ const content = (0, import_node_fs8.readFileSync)(absPath, "utf-8");
1696
+ let match;
1697
+ URL_LITERAL_RE.lastIndex = 0;
1698
+ while ((match = URL_LITERAL_RE.exec(content)) !== null) {
1699
+ const urlPath = match[1];
1700
+ const result = resolveUrlPath(urlPath, apiPathMap, apiRoutes);
1701
+ if (result.kind === "resolved" && result.nodeId) {
1702
+ const key = `${sourceId}|${result.nodeId}|references_api`;
1703
+ if (seen.has(key)) continue;
1704
+ seen.add(key);
1705
+ crossRefs.push({
1706
+ source: sourceId,
1707
+ target: result.nodeId,
1708
+ type: "references_api",
1709
+ layer: "api"
1710
+ });
1711
+ }
1712
+ }
1713
+ }
1714
+ return {
1715
+ cross_refs: crossRefs,
1716
+ flagged_edges: [],
1717
+ warnings: [],
1718
+ patterns: {
1719
+ url_literals_resolved: crossRefs.length
1720
+ }
1721
+ };
1722
+ }
1723
+ };
1724
+ }
1725
+ });
1726
+
1727
+ // src/server/graph/core/parser-registry.ts
1728
+ function registerBuiltins(registry, disabled) {
1729
+ const builtins = [
1730
+ reactNextjsParser,
1731
+ nextjsRoutesParser,
1732
+ prismaSchemaParser,
1733
+ fetchResolverParser,
1734
+ apiAnnotationsParser,
1735
+ urlLiteralScannerParser
1736
+ ];
1737
+ for (const parser of builtins) {
1738
+ if (disabled.has(parser.id)) continue;
1739
+ registry.register(parser);
1740
+ }
1741
+ }
1742
+ function loadCustomParsers(registry, config, rootDir, disabled) {
1743
+ for (const entry of config.parsers?.custom ?? []) {
1744
+ try {
1745
+ const absPath = (0, import_node_path9.resolve)(rootDir, entry.path);
1746
+ const mod = require(absPath);
1747
+ const parser = "default" in mod ? mod.default : mod;
1748
+ if (disabled.has(parser.id)) continue;
1749
+ if (parser.layer !== entry.layer) {
1750
+ process.stderr.write(
1751
+ `[launch-chart] custom parser "${parser.id}" declares layer "${parser.layer}" but config says "${entry.layer}" \u2014 using parser's layer
1752
+ `
1753
+ );
1754
+ }
1755
+ registry.register(parser);
1756
+ } catch (err2) {
1757
+ process.stderr.write(`[launch-chart] failed to load custom parser from ${entry.path}: ${err2}
1758
+ `);
1759
+ }
1760
+ }
1761
+ }
1762
+ function createRegistry(config, rootDir) {
1763
+ const registry = new ParserRegistry();
1764
+ const disabled = new Set(config.parsers?.disabled ?? []);
1765
+ registerBuiltins(registry, disabled);
1766
+ loadCustomParsers(registry, config, rootDir, disabled);
1767
+ return registry;
1768
+ }
1769
+ var import_node_path9, ParserRegistry;
1770
+ var init_parser_registry = __esm({
1771
+ "src/server/graph/core/parser-registry.ts"() {
1772
+ "use strict";
1773
+ import_node_path9 = require("node:path");
1774
+ init_react_nextjs();
1775
+ init_nextjs_routes();
1776
+ init_prisma_schema();
1777
+ init_fetch_resolver();
1778
+ init_api_annotations();
1779
+ init_url_literal_scanner();
1780
+ ParserRegistry = class {
1781
+ constructor() {
1782
+ this.parsers = /* @__PURE__ */ new Map();
1783
+ this.ids = /* @__PURE__ */ new Set();
1784
+ }
1785
+ register(parser) {
1786
+ if (this.ids.has(parser.id)) {
1787
+ throw new Error(`Duplicate parser id: ${parser.id}`);
1788
+ }
1789
+ this.ids.add(parser.id);
1790
+ const list = this.parsers.get(parser.layer) ?? [];
1791
+ list.push(parser);
1792
+ this.parsers.set(parser.layer, list);
1793
+ }
1794
+ getParsers(layer) {
1795
+ return this.parsers.get(layer) ?? [];
1796
+ }
1797
+ getCrossLayerParsers() {
1798
+ return this.parsers.get("crosslayer") ?? [];
1799
+ }
1800
+ getAll() {
1801
+ const all = [];
1802
+ for (const list of this.parsers.values()) all.push(...list);
1803
+ return all;
1804
+ }
1805
+ };
1806
+ }
1807
+ });
1808
+
1809
+ // src/server/graph/core/merge.ts
1810
+ function mergeGraphOutputs(outputs, layer) {
1811
+ if (outputs.length === 0) {
1812
+ return {
1813
+ metadata: { generated: (/* @__PURE__ */ new Date()).toISOString(), scope: "", layer },
1814
+ nodes: [],
1815
+ edges: [],
1816
+ cross_refs: [],
1817
+ contradictions: [],
1818
+ warnings: [],
1819
+ flagged_edges: []
1820
+ };
1821
+ }
1822
+ if (outputs.length === 1) return outputs[0];
1823
+ const seenNodes = /* @__PURE__ */ new Set();
1824
+ const seenEdges = /* @__PURE__ */ new Set();
1825
+ const seenCrossRefs = /* @__PURE__ */ new Set();
1826
+ const mergedNodes = [];
1827
+ const mergedEdges = [];
1828
+ const mergedCrossRefs = [];
1829
+ const mergedContradictions = [];
1830
+ const mergedWarnings = [];
1831
+ const mergedFlagged = [];
1832
+ const parserIds = [];
1833
+ for (const output of outputs) {
1834
+ if (output.metadata.parser) {
1835
+ parserIds.push(String(output.metadata.parser));
1836
+ }
1837
+ for (const node of output.nodes) {
1838
+ if (seenNodes.has(node.id)) {
1839
+ mergedWarnings.push({
1840
+ type: "merge_conflict",
1841
+ detail: `Node "${node.id}" produced by multiple parsers; keeping first`
1842
+ });
1843
+ continue;
1844
+ }
1845
+ seenNodes.add(node.id);
1846
+ mergedNodes.push(node);
1847
+ }
1848
+ for (const edge of output.edges) {
1849
+ const key = `${edge.source}|${edge.target}|${edge.type}`;
1850
+ if (seenEdges.has(key)) continue;
1851
+ seenEdges.add(key);
1852
+ mergedEdges.push(edge);
1853
+ }
1854
+ for (const ref of output.cross_refs) {
1855
+ const key = `${ref.source}|${ref.target}|${ref.type}`;
1856
+ if (seenCrossRefs.has(key)) continue;
1857
+ seenCrossRefs.add(key);
1858
+ mergedCrossRefs.push(ref);
1859
+ }
1860
+ mergedContradictions.push(...output.contradictions);
1861
+ mergedWarnings.push(...output.warnings);
1862
+ mergedFlagged.push(...output.flagged_edges);
1863
+ }
1864
+ const metadata = {
1865
+ ...outputs[0].metadata,
1866
+ generated: (/* @__PURE__ */ new Date()).toISOString(),
1867
+ parsers: parserIds
1868
+ };
1869
+ return {
1870
+ metadata,
1871
+ nodes: mergedNodes,
1872
+ edges: mergedEdges,
1873
+ cross_refs: mergedCrossRefs,
1874
+ contradictions: mergedContradictions,
1875
+ warnings: mergedWarnings,
1876
+ flagged_edges: mergedFlagged,
1877
+ patterns: outputs[0].patterns
1878
+ };
1879
+ }
1880
+ function dedupCrossRefs(refs) {
1881
+ const seen = /* @__PURE__ */ new Set();
1882
+ const result = [];
1883
+ for (const ref of refs) {
1884
+ const key = `${ref.source}|${ref.target}|${ref.type}`;
1885
+ if (seen.has(key)) continue;
1886
+ seen.add(key);
1887
+ result.push(ref);
1888
+ }
1889
+ return result;
1890
+ }
1891
+ function applyCrossLayerResults(uiOutput, results, primaryId) {
1892
+ const allCrossRefs = [...uiOutput.cross_refs];
1893
+ const allFlagged = [...uiOutput.flagged_edges];
1894
+ const allWarnings = [...uiOutput.warnings];
1895
+ const primaryResult = results.find((r) => r.parserId === primaryId);
1896
+ const secondaryResults = results.filter((r) => r.parserId !== primaryId);
1897
+ if (primaryResult) {
1898
+ allCrossRefs.push(...primaryResult.output.cross_refs);
1899
+ allFlagged.push(...primaryResult.output.flagged_edges);
1900
+ allWarnings.push(...primaryResult.output.warnings);
1901
+ }
1902
+ const primarySet = new Set(
1903
+ (primaryResult?.output.cross_refs ?? []).map((r) => `${r.source}|${r.target}|${r.type}`)
1904
+ );
1905
+ for (const sec of secondaryResults) {
1906
+ for (const ref of sec.output.cross_refs) {
1907
+ const key = `${ref.source}|${ref.target}|${ref.type}`;
1908
+ if (primarySet.has(key)) {
1909
+ allCrossRefs.push(ref);
1910
+ } else {
1911
+ allFlagged.push({
1912
+ source: ref.source,
1913
+ target: ref.target,
1914
+ type: "out_of_pattern",
1915
+ label: `API call detected by ${sec.parserId} but not by primary (${primaryId})`,
1916
+ confidence: "medium"
1917
+ });
1918
+ allCrossRefs.push(ref);
1919
+ }
1920
+ }
1921
+ allFlagged.push(...sec.output.flagged_edges);
1922
+ allWarnings.push(...sec.output.warnings);
1923
+ }
1924
+ return {
1925
+ ...uiOutput,
1926
+ cross_refs: dedupCrossRefs(allCrossRefs),
1927
+ flagged_edges: allFlagged,
1928
+ warnings: allWarnings
1929
+ };
1930
+ }
1931
+ var init_merge = __esm({
1932
+ "src/server/graph/core/merge.ts"() {
1933
+ "use strict";
1934
+ }
1935
+ });
1936
+
1937
+ // src/server/graph/core/graph-builder.ts
1938
+ function readGraphFromDisk(rootDir, layer) {
1939
+ const filePath = (0, import_node_path10.join)(rootDir, ".launchsecure", "graphs", `${layer}.json`);
1940
+ if (!(0, import_node_fs9.existsSync)(filePath)) return null;
1941
+ try {
1942
+ return JSON.parse((0, import_node_fs9.readFileSync)(filePath, "utf-8"));
1943
+ } catch {
1944
+ return null;
1945
+ }
1946
+ }
1947
+ function generateLayer(rootDir, layer) {
1948
+ const config = loadConfig(rootDir);
1949
+ const registry = createRegistry(config, rootDir);
1950
+ const parsers = registry.getParsers(layer);
1951
+ const outputs = [];
1952
+ for (const parser of parsers) {
1953
+ if (!parser.detect(rootDir)) continue;
1954
+ outputs.push(parser.generate(rootDir));
1955
+ }
1956
+ if (outputs.length === 0) return null;
1957
+ let merged = outputs.length === 1 ? outputs[0] : mergeGraphOutputs(outputs, layer);
1958
+ if (layer === "ui") {
1959
+ const layerOutputs = /* @__PURE__ */ new Map();
1960
+ layerOutputs.set("ui", merged);
1961
+ for (const otherLayer of ["api", "db"]) {
1962
+ const existing = readGraphFromDisk(rootDir, otherLayer);
1963
+ if (existing) layerOutputs.set(otherLayer, existing);
1964
+ }
1965
+ const crossParsers = registry.getCrossLayerParsers();
1966
+ const primaryId = config.parsers?.primary?.crosslayer ?? crossParsers[0]?.id ?? null;
1967
+ const crossResults = crossParsers.filter((p) => p.detect(rootDir)).map((p) => ({ parserId: p.id, output: p.generate(rootDir, layerOutputs) }));
1968
+ if (crossResults.length > 0) {
1969
+ merged = applyCrossLayerResults(merged, crossResults, primaryId);
1970
+ }
1971
+ }
1972
+ return {
1973
+ layer,
1974
+ output: merged,
1975
+ nodeCount: merged.nodes.length,
1976
+ edgeCount: merged.edges.length
1977
+ };
1978
+ }
1979
+ function generateAll(rootDir) {
1980
+ const config = loadConfig(rootDir);
1981
+ const registry = createRegistry(config, rootDir);
1982
+ const layerOrder = ["api", "db", "ui"];
1983
+ const layerOutputs = /* @__PURE__ */ new Map();
1984
+ const results = [];
1985
+ for (const layer of layerOrder) {
1986
+ const parsers = registry.getParsers(layer);
1987
+ const outputs = [];
1988
+ for (const parser of parsers) {
1989
+ if (!parser.detect(rootDir)) continue;
1990
+ outputs.push(parser.generate(rootDir));
1991
+ }
1992
+ if (outputs.length === 0) continue;
1993
+ const merged = outputs.length === 1 ? outputs[0] : mergeGraphOutputs(outputs, layer);
1994
+ layerOutputs.set(layer, merged);
1995
+ results.push({
1996
+ layer,
1997
+ output: merged,
1998
+ nodeCount: merged.nodes.length,
1999
+ edgeCount: merged.edges.length
2000
+ });
2001
+ }
2002
+ const crossParsers = registry.getCrossLayerParsers();
2003
+ const primaryId = config.parsers?.primary?.crosslayer ?? crossParsers[0]?.id ?? null;
2004
+ const crossResults = crossParsers.filter((p) => p.detect(rootDir)).map((p) => ({ parserId: p.id, output: p.generate(rootDir, layerOutputs) }));
2005
+ if (crossResults.length > 0 && layerOutputs.has("ui")) {
2006
+ const uiOutput = layerOutputs.get("ui");
2007
+ const merged = applyCrossLayerResults(uiOutput, crossResults, primaryId);
2008
+ layerOutputs.set("ui", merged);
2009
+ const uiResult = results.find((r) => r.layer === "ui");
2010
+ if (uiResult) {
2011
+ uiResult.output = merged;
2012
+ uiResult.nodeCount = merged.nodes.length;
2013
+ uiResult.edgeCount = merged.edges.length;
2014
+ }
2015
+ }
2016
+ const byLayer = new Map(results.map((r) => [r.layer, r]));
2017
+ return ["ui", "api", "db"].map((l) => byLayer.get(l)).filter((r) => !!r);
2018
+ }
2019
+ var import_node_fs9, import_node_path10;
2020
+ var init_graph_builder = __esm({
2021
+ "src/server/graph/core/graph-builder.ts"() {
2022
+ "use strict";
2023
+ import_node_fs9 = require("node:fs");
2024
+ import_node_path10 = require("node:path");
2025
+ init_config();
2026
+ init_parser_registry();
2027
+ init_merge();
2028
+ }
2029
+ });
2030
+
2031
+ // src/server/graph/taggers/module-tagger.ts
2032
+ function matchGlob(pattern, id) {
2033
+ const patParts = pattern.split("/");
2034
+ const idParts = id.split("/");
2035
+ return matchParts(patParts, 0, idParts, 0);
2036
+ }
2037
+ function matchParts(pat, pi, id, ii) {
2038
+ while (pi < pat.length && ii < id.length) {
2039
+ const p = pat[pi];
2040
+ if (p === "**") {
2041
+ for (let skip = ii; skip <= id.length; skip++) {
2042
+ if (matchParts(pat, pi + 1, id, skip)) return true;
2043
+ }
2044
+ return false;
2045
+ }
2046
+ if (p === "*") {
2047
+ pi++;
2048
+ ii++;
2049
+ continue;
2050
+ }
2051
+ if (p !== id[ii]) return false;
2052
+ pi++;
2053
+ ii++;
2054
+ }
2055
+ while (pi < pat.length && pat[pi] === "**") pi++;
2056
+ return pi === pat.length && ii === id.length;
2057
+ }
2058
+ function detectConventionDirs(rootDir) {
2059
+ const result = /* @__PURE__ */ new Map();
2060
+ const searchDirs = [
2061
+ rootDir,
2062
+ (0, import_node_path11.join)(rootDir, "src"),
2063
+ (0, import_node_path11.join)(rootDir, "app"),
2064
+ (0, import_node_path11.join)(rootDir, "lib")
2065
+ ];
2066
+ for (const base of searchDirs) {
2067
+ for (const convention of CONVENTION_DIRS) {
2068
+ const dir = (0, import_node_path11.join)(base, convention);
2069
+ if (!(0, import_node_fs10.existsSync)(dir)) continue;
2070
+ try {
2071
+ const stat = (0, import_node_fs10.statSync)(dir);
2072
+ if (!stat.isDirectory()) continue;
2073
+ const entries = (0, import_node_fs10.readdirSync)(dir, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => e.name);
2074
+ if (entries.length > 0) {
2075
+ const relPath = dir.replace(rootDir + "/", "").replace(rootDir + "\\", "");
2076
+ result.set(relPath, entries);
2077
+ }
2078
+ } catch {
2079
+ }
2080
+ }
2081
+ }
2082
+ return result;
2083
+ }
2084
+ function extractRouteGroups(id) {
2085
+ const groups = [];
2086
+ const re = /\(([^)]+)\)/g;
2087
+ let m;
2088
+ while ((m = re.exec(id)) !== null) {
2089
+ groups.push(m[1]);
2090
+ }
2091
+ return groups;
2092
+ }
2093
+ function isRouteGroup(segment) {
2094
+ return segment.startsWith("(") && segment.endsWith(")");
2095
+ }
2096
+ function isDynamicSegment(segment) {
2097
+ return segment.startsWith("[") || segment.startsWith(":");
2098
+ }
2099
+ function isDomainDir(segment) {
2100
+ return segment.includes(".") && !segment.endsWith(".tsx") && !segment.endsWith(".ts") && !segment.endsWith(".js") && !segment.endsWith(".jsx") && !segment.endsWith(".vue");
2101
+ }
2102
+ function isTrivialGroup(name) {
2103
+ if (TRIVIAL_GROUPS.has(name)) return true;
2104
+ const lower = name.toLowerCase();
2105
+ const wrapperPatterns = [
2106
+ /^.*-?wrapper$/,
2107
+ // "page-wrapper", "use-page-wrapper"
2108
+ /^.*-?layout$/,
2109
+ // "admin-layout", "settings-layout"
2110
+ /^use-/,
2111
+ // "use-page-wrapper"
2112
+ /^default$/
2113
+ ];
2114
+ return wrapperPatterns.some((p) => p.test(lower));
2115
+ }
2116
+ function normalizeGroupName(name) {
2117
+ return name.replace(/-pages?$/, "").replace(/-layout$/, "").replace(/-wrapper$/, "");
2118
+ }
2119
+ function extractModuleFromPath(id) {
2120
+ const segments = id.split("/");
2121
+ const routeGroups = extractRouteGroups(id);
2122
+ const moduleGroups = routeGroups.filter((g) => !isTrivialGroup(g)).map(normalizeGroupName);
2123
+ if (moduleGroups.length > 0) {
2124
+ return moduleGroups[moduleGroups.length - 1];
2125
+ }
2126
+ const meaningful = [];
2127
+ for (const seg of segments) {
2128
+ if (seg.includes(".")) continue;
2129
+ if (isRouteGroup(seg)) continue;
2130
+ if (isDynamicSegment(seg)) continue;
2131
+ if (isDomainDir(seg)) continue;
2132
+ if (SKIP_SEGMENTS.has(seg)) continue;
2133
+ meaningful.push(seg);
2134
+ }
2135
+ if (meaningful.length > 0) {
2136
+ return meaningful[0];
2137
+ }
2138
+ return "root";
2139
+ }
2140
+ var import_node_fs10, import_node_path11, CONVENTION_DIRS, SKIP_SEGMENTS, TRIVIAL_GROUPS, cachedRootDir, cachedConventionDirs, moduleTagger;
2141
+ var init_module_tagger = __esm({
2142
+ "src/server/graph/taggers/module-tagger.ts"() {
2143
+ "use strict";
2144
+ import_node_fs10 = require("node:fs");
2145
+ import_node_path11 = require("node:path");
2146
+ CONVENTION_DIRS = ["features", "modules", "domains", "areas"];
2147
+ SKIP_SEGMENTS = /* @__PURE__ */ new Set([
2148
+ "src",
2149
+ "app",
2150
+ "client",
2151
+ "server",
2152
+ "lib",
2153
+ "config"
2154
+ ]);
2155
+ TRIVIAL_GROUPS = /* @__PURE__ */ new Set([
2156
+ "app",
2157
+ "all",
2158
+ "ee",
2159
+ "home",
2160
+ "root"
2161
+ ]);
2162
+ cachedRootDir = null;
2163
+ cachedConventionDirs = /* @__PURE__ */ new Map();
2164
+ moduleTagger = {
2165
+ id: "module",
2166
+ tagKey: "module",
2167
+ trackUntagged: true,
2168
+ layers: null,
2169
+ // applies to all layers
2170
+ tag(nodes, layer, rootDir) {
2171
+ if (cachedRootDir !== rootDir) {
2172
+ cachedConventionDirs = detectConventionDirs(rootDir);
2173
+ cachedRootDir = rootDir;
2174
+ }
2175
+ let configRules = [];
2176
+ try {
2177
+ const { loadConfig: loadConfig2 } = (init_config(), __toCommonJS(config_exports));
2178
+ const config = loadConfig2(rootDir);
2179
+ configRules = config.taggers?.module?.rules ?? [];
2180
+ } catch {
2181
+ }
2182
+ const result = /* @__PURE__ */ new Map();
2183
+ for (const node of nodes) {
2184
+ const id = node.id;
2185
+ let matched = false;
2186
+ for (const rule of configRules) {
2187
+ if (matchGlob(rule.match, id)) {
2188
+ result.set(id, rule.module);
2189
+ matched = true;
2190
+ break;
2191
+ }
2192
+ }
2193
+ if (matched) continue;
2194
+ matched = false;
2195
+ for (const [convDir, moduleNames] of cachedConventionDirs) {
2196
+ if (id.startsWith(convDir + "/")) {
2197
+ const rest = id.slice(convDir.length + 1);
2198
+ const firstSeg = rest.split("/")[0];
2199
+ if (moduleNames.includes(firstSeg)) {
2200
+ result.set(id, firstSeg);
2201
+ matched = true;
2202
+ break;
2203
+ }
2204
+ }
2205
+ }
2206
+ if (matched) continue;
2207
+ const module2 = extractModuleFromPath(id);
2208
+ result.set(id, module2);
2209
+ }
2210
+ return result;
2211
+ }
2212
+ };
2213
+ }
2214
+ });
2215
+
2216
+ // src/server/graph/taggers/screen-tagger.ts
2217
+ var SCREEN_TYPES, screenTagger;
2218
+ var init_screen_tagger = __esm({
2219
+ "src/server/graph/taggers/screen-tagger.ts"() {
2220
+ "use strict";
2221
+ SCREEN_TYPES = /* @__PURE__ */ new Set(["page", "layout"]);
2222
+ screenTagger = {
2223
+ id: "screen",
2224
+ tagKey: "screen",
2225
+ trackUntagged: true,
2226
+ layers: ["ui"],
2227
+ tag(nodes, layer) {
2228
+ if (layer !== "ui") return /* @__PURE__ */ new Map();
2229
+ const result = /* @__PURE__ */ new Map();
2230
+ for (const node of nodes) {
2231
+ if (SCREEN_TYPES.has(node.type)) {
2232
+ result.set(node.id, "true");
2233
+ }
2234
+ }
2235
+ return result;
2236
+ }
2237
+ };
2238
+ }
2239
+ });
2240
+
2241
+ // src/server/graph/core/tagger-registry.ts
2242
+ function registerBuiltins2(registry, disabled, config) {
2243
+ for (const tagger of BUILTIN_TAGGERS) {
2244
+ if (disabled.has(tagger.id)) continue;
2245
+ const override = config.taggers?.trackUntagged?.[tagger.id];
2246
+ if (override !== void 0) {
2247
+ tagger.trackUntagged = override;
2248
+ }
2249
+ registry.register(tagger);
2250
+ }
2251
+ }
2252
+ function loadCustomTaggers(registry, config, rootDir, disabled) {
2253
+ for (const entry of config.taggers?.custom ?? []) {
2254
+ if (disabled.has(entry.id)) continue;
2255
+ try {
2256
+ const absPath = (0, import_node_path12.resolve)(rootDir, entry.path);
2257
+ const mod = require(absPath);
2258
+ const tagger = "default" in mod ? mod.default : mod;
2259
+ const override = config.taggers?.trackUntagged?.[tagger.id];
2260
+ if (override !== void 0) {
2261
+ tagger.trackUntagged = override;
2262
+ }
2263
+ registry.register(tagger);
2264
+ } catch (err2) {
2265
+ process.stderr.write(`[launch-chart] failed to load custom tagger from ${entry.path}: ${err2}
2266
+ `);
2267
+ }
2268
+ }
2269
+ }
2270
+ function createTaggerRegistry(config, rootDir) {
2271
+ const registry = new TaggerRegistry();
2272
+ const disabled = new Set(config.taggers?.disabled ?? []);
2273
+ registerBuiltins2(registry, disabled, config);
2274
+ loadCustomTaggers(registry, config, rootDir, disabled);
2275
+ return registry;
2276
+ }
2277
+ var import_node_path12, TaggerRegistry, BUILTIN_TAGGERS;
2278
+ var init_tagger_registry = __esm({
2279
+ "src/server/graph/core/tagger-registry.ts"() {
2280
+ "use strict";
2281
+ import_node_path12 = require("node:path");
2282
+ init_module_tagger();
2283
+ init_screen_tagger();
2284
+ TaggerRegistry = class {
2285
+ constructor() {
2286
+ this.taggers = [];
2287
+ this.ids = /* @__PURE__ */ new Set();
2288
+ }
2289
+ register(tagger) {
2290
+ if (this.ids.has(tagger.id)) {
2291
+ throw new Error(`Duplicate tagger id: ${tagger.id}`);
2292
+ }
2293
+ this.ids.add(tagger.id);
2294
+ this.taggers.push(tagger);
2295
+ }
2296
+ getAll() {
2297
+ return this.taggers;
2298
+ }
2299
+ getForLayer(layer) {
2300
+ return this.taggers.filter((t) => t.layers === null || t.layers.includes(layer));
2301
+ }
2302
+ };
2303
+ BUILTIN_TAGGERS = [moduleTagger, screenTagger];
2304
+ }
2305
+ });
2306
+
2307
+ // src/server/graph/core/tag-store.ts
2308
+ function tagsFilePath(rootDir) {
2309
+ return (0, import_node_path13.join)(rootDir, GRAPHS_DIR, TAGS_FILENAME);
2310
+ }
2311
+ function readTagStore(rootDir) {
2312
+ const filePath = tagsFilePath(rootDir);
2313
+ if (!(0, import_node_fs11.existsSync)(filePath)) return {};
2314
+ const stat = (0, import_node_fs11.statSync)(filePath);
2315
+ const cached = tagCache.get(filePath);
2316
+ if (cached && cached.mtimeMs === stat.mtimeMs) {
2317
+ return cached.store;
2318
+ }
2319
+ try {
2320
+ const content = (0, import_node_fs11.readFileSync)(filePath, "utf-8");
2321
+ const store = JSON.parse(content);
2322
+ tagCache.set(filePath, { mtimeMs: stat.mtimeMs, store });
2323
+ return store;
2324
+ } catch {
2325
+ return {};
2326
+ }
2327
+ }
2328
+ function writeTagStore(rootDir, store) {
2329
+ const filePath = tagsFilePath(rootDir);
2330
+ const dir = (0, import_node_path13.dirname)(filePath);
2331
+ (0, import_node_fs11.mkdirSync)(dir, { recursive: true });
2332
+ const cleaned = {};
2333
+ for (const [nodeId, tags] of Object.entries(store)) {
2334
+ if (Object.keys(tags).length > 0) {
2335
+ cleaned[nodeId] = tags;
2336
+ }
2337
+ }
2338
+ (0, import_node_fs11.writeFileSync)(filePath, JSON.stringify(cleaned, null, 2) + "\n", "utf-8");
2339
+ tagCache.delete(filePath);
2340
+ }
2341
+ function setTag(rootDir, nodeId, key, value) {
2342
+ const store = readTagStore(rootDir);
2343
+ if (!store[nodeId]) store[nodeId] = {};
2344
+ store[nodeId][key] = value;
2345
+ writeTagStore(rootDir, store);
2346
+ }
2347
+ function removeTag(rootDir, nodeId, key) {
2348
+ const store = readTagStore(rootDir);
2349
+ if (!store[nodeId]) return;
2350
+ delete store[nodeId][key];
2351
+ if (Object.keys(store[nodeId]).length === 0) {
2352
+ delete store[nodeId];
2353
+ }
2354
+ writeTagStore(rootDir, store);
2355
+ }
2356
+ var import_node_fs11, import_node_path13, TAGS_FILENAME, GRAPHS_DIR, tagCache;
2357
+ var init_tag_store = __esm({
2358
+ "src/server/graph/core/tag-store.ts"() {
2359
+ "use strict";
2360
+ import_node_fs11 = require("node:fs");
2361
+ import_node_path13 = require("node:path");
2362
+ TAGS_FILENAME = "tags.json";
1552
2363
  GRAPHS_DIR = ".launchsecure/graphs";
2364
+ tagCache = /* @__PURE__ */ new Map();
2365
+ }
2366
+ });
2367
+
2368
+ // src/server/graph/index.ts
2369
+ function graphsDir(rootDir) {
2370
+ return (0, import_node_path14.join)(rootDir, GRAPHS_DIR2);
2371
+ }
2372
+ function graphFilePath(rootDir, layer) {
2373
+ return (0, import_node_path14.join)(graphsDir(rootDir), `${layer}.json`);
2374
+ }
2375
+ function tagsFilePath2(rootDir) {
2376
+ return (0, import_node_path14.join)(graphsDir(rootDir), "tags.json");
2377
+ }
2378
+ function getMtimeMs(filePath) {
2379
+ if (!(0, import_node_fs12.existsSync)(filePath)) return 0;
2380
+ return (0, import_node_fs12.statSync)(filePath).mtimeMs;
2381
+ }
2382
+ function invalidateCache(filePath) {
2383
+ graphCache.delete(filePath);
2384
+ }
2385
+ function invalidateTaggedCache(rootDir, layer) {
2386
+ taggedCache.delete(`${rootDir}:${layer}`);
2387
+ }
2388
+ function applyTags(graph, layer, rootDir) {
2389
+ const config = loadConfig(rootDir);
2390
+ const registry = createTaggerRegistry(config, rootDir);
2391
+ const manualTags = readTagStore(rootDir);
2392
+ const taggedNodes = graph.nodes.map((n) => ({ ...n }));
2393
+ const taggers = registry.getForLayer(layer);
2394
+ for (const tagger of taggers) {
2395
+ const assignments = tagger.tag(taggedNodes, layer, rootDir);
2396
+ for (const node of taggedNodes) {
2397
+ if (!node.tags) node.tags = {};
2398
+ const tags = node.tags;
2399
+ const value = assignments.get(node.id);
2400
+ if (value !== void 0) {
2401
+ tags[tagger.tagKey] = value;
2402
+ } else if (tagger.trackUntagged) {
2403
+ tags[tagger.tagKey] = "untagged";
2404
+ }
2405
+ }
2406
+ }
2407
+ for (const node of taggedNodes) {
2408
+ const manual = manualTags[node.id];
2409
+ if (manual) {
2410
+ if (!node.tags) node.tags = {};
2411
+ const tags = node.tags;
2412
+ Object.assign(tags, manual);
2413
+ }
2414
+ }
2415
+ return { ...graph, nodes: taggedNodes };
2416
+ }
2417
+ function readGraphRaw(rootDir, layer) {
2418
+ const filePath = graphFilePath(rootDir, layer);
2419
+ if (!(0, import_node_fs12.existsSync)(filePath)) return null;
2420
+ const stat = (0, import_node_fs12.statSync)(filePath);
2421
+ const cached = graphCache.get(filePath);
2422
+ if (cached && cached.mtimeMs === stat.mtimeMs) {
2423
+ return cached.graph;
2424
+ }
2425
+ const content = (0, import_node_fs12.readFileSync)(filePath, "utf-8");
2426
+ const graph = JSON.parse(content);
2427
+ graphCache.set(filePath, { mtimeMs: stat.mtimeMs, graph });
2428
+ return graph;
2429
+ }
2430
+ function readGraph(rootDir, layer) {
2431
+ const rawFilePath = graphFilePath(rootDir, layer);
2432
+ if (!(0, import_node_fs12.existsSync)(rawFilePath)) return null;
2433
+ const rawMtime = getMtimeMs(rawFilePath);
2434
+ const tagsMtime = getMtimeMs(tagsFilePath2(rootDir));
2435
+ const cacheKey = `${rootDir}:${layer}`;
2436
+ const cached = taggedCache.get(cacheKey);
2437
+ if (cached && cached.rawMtimeMs === rawMtime && cached.tagsMtimeMs === tagsMtime) {
2438
+ return cached.graph;
2439
+ }
2440
+ const raw = readGraphRaw(rootDir, layer);
2441
+ if (!raw) return null;
2442
+ const tagged = applyTags(raw, layer, rootDir);
2443
+ taggedCache.set(cacheKey, { rawMtimeMs: rawMtime, tagsMtimeMs: tagsMtime, graph: tagged });
2444
+ return tagged;
2445
+ }
2446
+ function readAllGraphs(rootDir) {
2447
+ const result = {};
2448
+ for (const layer of LAYERS) {
2449
+ const graph = readGraph(rootDir, layer);
2450
+ if (graph) result[layer] = graph;
2451
+ }
2452
+ return result;
2453
+ }
2454
+ function generateGraph(rootDir, layer) {
2455
+ const dir = graphsDir(rootDir);
2456
+ (0, import_node_fs12.mkdirSync)(dir, { recursive: true });
2457
+ const results = layer ? [generateLayer(rootDir, layer)].filter((r) => r !== null) : generateAll(rootDir);
2458
+ for (const result of results) {
2459
+ const filePath = graphFilePath(rootDir, result.layer);
2460
+ (0, import_node_fs12.writeFileSync)(filePath, JSON.stringify(result.output, null, 2) + "\n", "utf-8");
2461
+ invalidateCache(filePath);
2462
+ invalidateTaggedCache(rootDir, result.layer);
2463
+ }
2464
+ return results;
2465
+ }
2466
+ var import_node_fs12, import_node_path14, GRAPHS_DIR2, LAYERS, graphCache, taggedCache;
2467
+ var init_graph = __esm({
2468
+ "src/server/graph/index.ts"() {
2469
+ "use strict";
2470
+ import_node_fs12 = require("node:fs");
2471
+ import_node_path14 = require("node:path");
2472
+ init_graph_builder();
2473
+ init_config();
2474
+ init_tagger_registry();
2475
+ init_tag_store();
2476
+ init_tag_store();
2477
+ GRAPHS_DIR2 = ".launchsecure/graphs";
1553
2478
  LAYERS = ["ui", "api", "db"];
1554
2479
  graphCache = /* @__PURE__ */ new Map();
2480
+ taggedCache = /* @__PURE__ */ new Map();
1555
2481
  }
1556
2482
  });
1557
2483
 
@@ -1561,19 +2487,22 @@ __export(chart_serve_exports, {
1561
2487
  runServeCli: () => runServeCli,
1562
2488
  startChartServer: () => startChartServer
1563
2489
  });
2490
+ function randomPort() {
2491
+ return 49152 + Math.floor(Math.random() * (65535 - 49152));
2492
+ }
1564
2493
  function findProjectRoot(startDir) {
1565
2494
  let dir = startDir;
1566
2495
  for (let i = 0; i < 8; i++) {
1567
- const graphsDir2 = import_node_path7.default.join(dir, ".launchsecure", "graphs");
1568
- if (import_node_fs7.default.existsSync(import_node_path7.default.join(graphsDir2, "ui.json")) || import_node_fs7.default.existsSync(import_node_path7.default.join(graphsDir2, "api.json")) || import_node_fs7.default.existsSync(import_node_path7.default.join(graphsDir2, "db.json"))) return dir;
1569
- const parent = import_node_path7.default.dirname(dir);
2496
+ const graphsDir2 = import_node_path15.default.join(dir, ".launchsecure", "graphs");
2497
+ if (import_node_fs13.default.existsSync(import_node_path15.default.join(graphsDir2, "ui.json")) || import_node_fs13.default.existsSync(import_node_path15.default.join(graphsDir2, "api.json")) || import_node_fs13.default.existsSync(import_node_path15.default.join(graphsDir2, "db.json"))) return dir;
2498
+ const parent = import_node_path15.default.dirname(dir);
1570
2499
  if (parent === dir) break;
1571
2500
  dir = parent;
1572
2501
  }
1573
2502
  dir = startDir;
1574
2503
  for (let i = 0; i < 8; i++) {
1575
- if (import_node_fs7.default.existsSync(import_node_path7.default.join(dir, ".git"))) return dir;
1576
- const parent = import_node_path7.default.dirname(dir);
2504
+ if (import_node_fs13.default.existsSync(import_node_path15.default.join(dir, ".git"))) return dir;
2505
+ const parent = import_node_path15.default.dirname(dir);
1577
2506
  if (parent === dir) break;
1578
2507
  dir = parent;
1579
2508
  }
@@ -1625,16 +2554,16 @@ function buildMergedGraph(projectRoot) {
1625
2554
  };
1626
2555
  }
1627
2556
  function serveStatic(res, filePath) {
1628
- if (!import_node_fs7.default.existsSync(filePath) || !import_node_fs7.default.statSync(filePath).isFile()) return false;
1629
- const ext = import_node_path7.default.extname(filePath).toLowerCase();
2557
+ if (!import_node_fs13.default.existsSync(filePath) || !import_node_fs13.default.statSync(filePath).isFile()) return false;
2558
+ const ext = import_node_path15.default.extname(filePath).toLowerCase();
1630
2559
  const mime = MIME_TYPES[ext] ?? "application/octet-stream";
1631
2560
  res.writeHead(200, { "Content-Type": mime, "Cache-Control": "no-cache" });
1632
- import_node_fs7.default.createReadStream(filePath).pipe(res);
2561
+ import_node_fs13.default.createReadStream(filePath).pipe(res);
1633
2562
  return true;
1634
2563
  }
1635
2564
  function serveIndex(res, clientDir) {
1636
- const indexPath = import_node_path7.default.join(clientDir, "index.html");
1637
- if (!import_node_fs7.default.existsSync(indexPath)) {
2565
+ const indexPath = import_node_path15.default.join(clientDir, "index.html");
2566
+ if (!import_node_fs13.default.existsSync(indexPath)) {
1638
2567
  res.writeHead(500, { "Content-Type": "text/plain" });
1639
2568
  res.end(`LaunchChart client bundle not found at ${clientDir}. Run 'npm run build:chart-client'.`);
1640
2569
  return;
@@ -1642,14 +2571,14 @@ function serveIndex(res, clientDir) {
1642
2571
  serveStatic(res, indexPath);
1643
2572
  }
1644
2573
  function tryListen(server, port) {
1645
- return new Promise((resolve, reject) => {
2574
+ return new Promise((resolve3, reject) => {
1646
2575
  const onError = (err2) => {
1647
2576
  server.off("listening", onListening);
1648
2577
  reject(err2);
1649
2578
  };
1650
2579
  const onListening = () => {
1651
2580
  server.off("error", onError);
1652
- resolve(port);
2581
+ resolve3(port);
1653
2582
  };
1654
2583
  server.once("error", onError);
1655
2584
  server.once("listening", onListening);
@@ -1676,7 +2605,7 @@ async function bindWithFallback(server, startPort) {
1676
2605
  async function startChartServer(opts = {}) {
1677
2606
  const cwd = opts.cwd ?? process.cwd();
1678
2607
  const projectRoot = findProjectRoot(cwd);
1679
- const existing = getLiveLock();
2608
+ const existing = getLiveLock(projectRoot);
1680
2609
  if (existing) {
1681
2610
  if (!opts.quiet) {
1682
2611
  process.stderr.write(
@@ -1686,7 +2615,7 @@ async function startChartServer(opts = {}) {
1686
2615
  }
1687
2616
  return { port: existing.port, url: existing.url };
1688
2617
  }
1689
- const clientDir = opts.clientDir ?? import_node_path7.default.join(__dirname, "..", "chart-client");
2618
+ const clientDir = opts.clientDir ?? import_node_path15.default.join(__dirname, "..", "chart-client");
1690
2619
  const server = import_node_http.default.createServer((req, res) => {
1691
2620
  try {
1692
2621
  const url2 = new URL(req.url ?? "/", `http://${req.headers.host}`);
@@ -1724,13 +2653,154 @@ async function startChartServer(opts = {}) {
1724
2653
  }
1725
2654
  return;
1726
2655
  }
2656
+ if (req.method === "GET" && url2.pathname === "/api/file-content") {
2657
+ const relPath = url2.searchParams.get("path");
2658
+ if (!relPath || relPath.includes("..") || import_node_path15.default.isAbsolute(relPath)) {
2659
+ res.writeHead(400, { "Content-Type": "application/json" });
2660
+ res.end(JSON.stringify({ error: "Invalid path" }));
2661
+ return;
2662
+ }
2663
+ const filePath = import_node_path15.default.join(projectRoot, relPath);
2664
+ if (!filePath.startsWith(projectRoot) || !import_node_fs13.default.existsSync(filePath) || !import_node_fs13.default.statSync(filePath).isFile()) {
2665
+ res.writeHead(404, { "Content-Type": "application/json" });
2666
+ res.end(JSON.stringify({ error: "File not found" }));
2667
+ return;
2668
+ }
2669
+ const ext = import_node_path15.default.extname(filePath).toLowerCase();
2670
+ const langMap = { ".ts": "typescript", ".tsx": "tsx", ".js": "javascript", ".jsx": "jsx", ".prisma": "prisma", ".json": "json", ".css": "css" };
2671
+ const content = import_node_fs13.default.readFileSync(filePath, "utf-8");
2672
+ res.writeHead(200, { "Content-Type": "application/json" });
2673
+ res.end(JSON.stringify({ content, language: langMap[ext] ?? "text", path: relPath }));
2674
+ return;
2675
+ }
1727
2676
  if (req.method === "GET" && url2.pathname === "/api/health") {
1728
2677
  res.writeHead(200, { "Content-Type": "application/json" });
1729
2678
  res.end(JSON.stringify({ ok: true, projectRoot }));
1730
2679
  return;
1731
2680
  }
2681
+ if (req.method === "GET" && url2.pathname === "/api/parser-config") {
2682
+ const config = loadConfig(projectRoot);
2683
+ const detection = [
2684
+ { id: "react-nextjs", layer: "ui", label: "React + Next.js", detected: reactNextjsParser.detect(projectRoot) },
2685
+ { id: "nextjs-routes", layer: "api", label: "Next.js API Routes", detected: nextjsRoutesParser.detect(projectRoot) },
2686
+ { id: "prisma-schema", layer: "db", label: "Prisma Schema", detected: prismaSchemaParser.detect(projectRoot) }
2687
+ ];
2688
+ const crosslayerParsers = [
2689
+ { id: "fetch-resolver", label: "Fetch / api.method() calls" },
2690
+ { id: "api-annotations", label: "@api annotations" },
2691
+ { id: "url-literal-scanner", label: "/api/... URL literals" }
2692
+ ];
2693
+ res.writeHead(200, { "Content-Type": "application/json" });
2694
+ res.end(JSON.stringify({ config, detection, crosslayerParsers }));
2695
+ return;
2696
+ }
2697
+ if (req.method === "POST" && url2.pathname === "/api/parser-config") {
2698
+ let body = "";
2699
+ req.on("data", (chunk) => {
2700
+ body += chunk.toString();
2701
+ });
2702
+ req.on("end", () => {
2703
+ try {
2704
+ const newConfig = JSON.parse(body);
2705
+ const configPath = import_node_path15.default.join(projectRoot, ".launchchart.json");
2706
+ import_node_fs13.default.writeFileSync(configPath, JSON.stringify(newConfig, null, 2) + "\n", "utf-8");
2707
+ res.writeHead(200, { "Content-Type": "application/json" });
2708
+ res.end(JSON.stringify({ ok: true }));
2709
+ } catch (err2) {
2710
+ res.writeHead(400, { "Content-Type": "application/json" });
2711
+ res.end(JSON.stringify({ ok: false, error: String(err2) }));
2712
+ }
2713
+ });
2714
+ return;
2715
+ }
2716
+ if (req.method === "GET" && url2.pathname === "/api/tagger-config") {
2717
+ const config = loadConfig(projectRoot);
2718
+ const builtinTaggers = [
2719
+ { id: "module", tagKey: "module", trackUntagged: config.taggers?.trackUntagged?.module ?? true },
2720
+ { id: "screen", tagKey: "screen", trackUntagged: config.taggers?.trackUntagged?.screen ?? true }
2721
+ ];
2722
+ const disabled = config.taggers?.disabled ?? [];
2723
+ const customTaggers = config.taggers?.custom ?? [];
2724
+ const moduleRules = config.taggers?.module?.rules ?? [];
2725
+ res.writeHead(200, { "Content-Type": "application/json" });
2726
+ res.end(JSON.stringify({ builtinTaggers, disabled, customTaggers, moduleRules }));
2727
+ return;
2728
+ }
2729
+ if (req.method === "POST" && url2.pathname === "/api/tagger-config") {
2730
+ let body = "";
2731
+ req.on("data", (chunk) => {
2732
+ body += chunk.toString();
2733
+ });
2734
+ req.on("end", () => {
2735
+ try {
2736
+ const taggerConfig = JSON.parse(body);
2737
+ const config = loadConfig(projectRoot);
2738
+ config.taggers = taggerConfig;
2739
+ const configPath = import_node_path15.default.join(projectRoot, ".launchchart.json");
2740
+ import_node_fs13.default.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
2741
+ res.writeHead(200, { "Content-Type": "application/json" });
2742
+ res.end(JSON.stringify({ ok: true }));
2743
+ } catch (err2) {
2744
+ res.writeHead(400, { "Content-Type": "application/json" });
2745
+ res.end(JSON.stringify({ ok: false, error: String(err2) }));
2746
+ }
2747
+ });
2748
+ return;
2749
+ }
2750
+ if (req.method === "GET" && url2.pathname === "/api/tags") {
2751
+ const store = readTagStore(projectRoot);
2752
+ res.writeHead(200, { "Content-Type": "application/json" });
2753
+ res.end(JSON.stringify(store));
2754
+ return;
2755
+ }
2756
+ if (req.method === "POST" && url2.pathname === "/api/tags") {
2757
+ let body = "";
2758
+ req.on("data", (chunk) => {
2759
+ body += chunk.toString();
2760
+ });
2761
+ req.on("end", () => {
2762
+ try {
2763
+ const { nodeId, key, value } = JSON.parse(body);
2764
+ if (!nodeId || !key || !value) {
2765
+ res.writeHead(400, { "Content-Type": "application/json" });
2766
+ res.end(JSON.stringify({ ok: false, error: "nodeId, key, and value are required" }));
2767
+ return;
2768
+ }
2769
+ setTag(projectRoot, nodeId, key, value);
2770
+ res.writeHead(200, { "Content-Type": "application/json" });
2771
+ res.end(JSON.stringify({ ok: true }));
2772
+ } catch (err2) {
2773
+ res.writeHead(400, { "Content-Type": "application/json" });
2774
+ res.end(JSON.stringify({ ok: false, error: String(err2) }));
2775
+ }
2776
+ });
2777
+ return;
2778
+ }
2779
+ if (req.method === "DELETE" && url2.pathname === "/api/tags") {
2780
+ let body = "";
2781
+ req.on("data", (chunk) => {
2782
+ body += chunk.toString();
2783
+ });
2784
+ req.on("end", () => {
2785
+ try {
2786
+ const { nodeId, key } = JSON.parse(body);
2787
+ if (!nodeId || !key) {
2788
+ res.writeHead(400, { "Content-Type": "application/json" });
2789
+ res.end(JSON.stringify({ ok: false, error: "nodeId and key are required" }));
2790
+ return;
2791
+ }
2792
+ removeTag(projectRoot, nodeId, key);
2793
+ res.writeHead(200, { "Content-Type": "application/json" });
2794
+ res.end(JSON.stringify({ ok: true }));
2795
+ } catch (err2) {
2796
+ res.writeHead(400, { "Content-Type": "application/json" });
2797
+ res.end(JSON.stringify({ ok: false, error: String(err2) }));
2798
+ }
2799
+ });
2800
+ return;
2801
+ }
1732
2802
  if (url2.pathname !== "/") {
1733
- const staticPath = import_node_path7.default.join(clientDir, url2.pathname);
2803
+ const staticPath = import_node_path15.default.join(clientDir, url2.pathname);
1734
2804
  if (serveStatic(res, staticPath)) return;
1735
2805
  }
1736
2806
  serveIndex(res, clientDir);
@@ -1739,7 +2809,8 @@ async function startChartServer(opts = {}) {
1739
2809
  res.end(JSON.stringify({ error: String(err2) }));
1740
2810
  }
1741
2811
  });
1742
- const port = await bindWithFallback(server, opts.port ?? DEFAULT_PORT);
2812
+ const startPort = opts.port ?? randomPort();
2813
+ const port = await bindWithFallback(server, startPort);
1743
2814
  const url = `http://localhost:${port}`;
1744
2815
  writeLock({
1745
2816
  pid: process.pid,
@@ -1747,9 +2818,9 @@ async function startChartServer(opts = {}) {
1747
2818
  cwd,
1748
2819
  url,
1749
2820
  startedAt: (/* @__PURE__ */ new Date()).toISOString()
1750
- });
2821
+ }, projectRoot);
1751
2822
  const cleanup = () => {
1752
- clearLock();
2823
+ clearLock(projectRoot);
1753
2824
  server.close();
1754
2825
  };
1755
2826
  process.once("SIGINT", () => {
@@ -1784,17 +2855,20 @@ function runServeCli(argv) {
1784
2855
  process.exit(1);
1785
2856
  });
1786
2857
  }
1787
- var import_node_http, import_node_fs7, import_node_path7, DEFAULT_PORT, MAX_PORT_SCAN, MIME_TYPES;
2858
+ var import_node_http, import_node_fs13, import_node_path15, MAX_PORT_SCAN, MIME_TYPES;
1788
2859
  var init_chart_serve = __esm({
1789
2860
  "src/server/chart-serve.ts"() {
1790
2861
  "use strict";
1791
2862
  import_node_http = __toESM(require("node:http"));
1792
- import_node_fs7 = __toESM(require("node:fs"));
1793
- import_node_path7 = __toESM(require("node:path"));
2863
+ import_node_fs13 = __toESM(require("node:fs"));
2864
+ import_node_path15 = __toESM(require("node:path"));
1794
2865
  init_graph();
1795
2866
  init_lockfile();
1796
- DEFAULT_PORT = 52819;
1797
- MAX_PORT_SCAN = 20;
2867
+ init_config();
2868
+ init_react_nextjs();
2869
+ init_nextjs_routes();
2870
+ init_prisma_schema();
2871
+ MAX_PORT_SCAN = 3;
1798
2872
  MIME_TYPES = {
1799
2873
  ".html": "text/html; charset=utf-8",
1800
2874
  ".js": "application/javascript; charset=utf-8",
@@ -1825,7 +2899,7 @@ function matchesSearch(node, query) {
1825
2899
  function toMinimal(nodes) {
1826
2900
  return nodes.map((n) => {
1827
2901
  const out = { id: n.id, type: n.type, name: n.name };
1828
- if (n.module != null) out.module = n.module;
2902
+ if (n.tags != null) out.tags = n.tags;
1829
2903
  if (n.route != null) out.route = n.route;
1830
2904
  if (n.methods != null) out.methods = n.methods;
1831
2905
  return out;
@@ -1833,11 +2907,13 @@ function toMinimal(nodes) {
1833
2907
  }
1834
2908
  function toCompactNode(n) {
1835
2909
  const out = { i: n.id, t: n.type, n: n.name };
1836
- if (n.module != null) out.m = n.module;
2910
+ const tags = n.tags;
2911
+ if (tags?.module) out.m = tags.module;
1837
2912
  if (n.route != null) out.r = n.route;
1838
2913
  if (n.methods != null) out.mt = n.methods;
1839
2914
  if (n.exports != null) out.x = n.exports;
1840
2915
  if (n.columns != null) out.c = n.columns;
2916
+ if (tags != null) out.tg = tags;
1841
2917
  for (const k of Object.keys(n)) {
1842
2918
  if (!COMPACT_NODE_KNOWN_KEYS.has(k) && n[k] != null) out[k] = n[k];
1843
2919
  }
@@ -1913,7 +2989,8 @@ function layerSummary(graph) {
1913
2989
  const moduleCounts = {};
1914
2990
  for (const n of graph.nodes) {
1915
2991
  typeCounts[n.type] = (typeCounts[n.type] ?? 0) + 1;
1916
- const mod = n.module;
2992
+ const tags = n.tags;
2993
+ const mod = tags?.module;
1917
2994
  if (mod) moduleCounts[mod] = (moduleCounts[mod] ?? 0) + 1;
1918
2995
  }
1919
2996
  const edgeTypeCounts = {};
@@ -1970,12 +3047,14 @@ function runReadGraphQueryRaw(rootDir, args) {
1970
3047
  const search = args.search;
1971
3048
  const type = args.type;
1972
3049
  const module_ = args.module;
3050
+ const tagKey = args.tag_key;
3051
+ const tagValue = args.tag_value;
1973
3052
  const nodeId = args.node_id;
1974
3053
  const hops = args.hops ?? 1;
1975
3054
  const layerIsDb = args.layer === "db";
1976
3055
  const minimal = args.minimal ?? layerIsDb;
1977
3056
  const includeEdges = args.include_edges;
1978
- const hasFilter = !!(search || type || module_ || nodeId);
3057
+ const hasFilter = !!(search || type || module_ || nodeId || tagKey && tagValue);
1979
3058
  if (layer && !["ui", "api", "db"].includes(layer)) {
1980
3059
  return { error: `Invalid layer "${layer}". Must be one of: ui, api, db` };
1981
3060
  }
@@ -2031,7 +3110,9 @@ function runReadGraphQueryRaw(rootDir, args) {
2031
3110
  const matched = graph.nodes.filter((n) => {
2032
3111
  if (search && !matchesSearch(n, search)) return false;
2033
3112
  if (type && n.type !== type) return false;
2034
- if (module_ && n.module !== module_) return false;
3113
+ const nodeTags = n.tags;
3114
+ if (module_ && nodeTags?.module !== module_) return false;
3115
+ if (tagKey && tagValue && nodeTags?.[tagKey] !== tagValue) return false;
2035
3116
  return true;
2036
3117
  });
2037
3118
  const matchedIds = new Set(matched.map((n) => n.id));
@@ -2118,9 +3199,9 @@ function handleReadGraph(args) {
2118
3199
  return okJson(result);
2119
3200
  }
2120
3201
  function nodeToFilePath(rootDir, layer, nodeId) {
2121
- if (layer === "ui") return (0, import_node_path8.join)(rootDir, "src", nodeId);
2122
- if (layer === "api") return (0, import_node_path8.join)(rootDir, nodeId);
2123
- if (layer === "db") return (0, import_node_path8.join)(rootDir, "prisma", "schema.prisma");
3202
+ if (layer === "ui") return (0, import_node_path16.join)(rootDir, "src", nodeId);
3203
+ if (layer === "api") return (0, import_node_path16.join)(rootDir, nodeId);
3204
+ if (layer === "db") return (0, import_node_path16.join)(rootDir, "prisma", "schema.prisma");
2124
3205
  return null;
2125
3206
  }
2126
3207
  function handleGrepNodes(args) {
@@ -2180,11 +3261,11 @@ function handleGrepNodes(args) {
2180
3261
  let filesSearched = 0;
2181
3262
  let truncated = false;
2182
3263
  for (const [filePath, nodeId] of filePaths) {
2183
- if (!(0, import_node_fs8.existsSync)(filePath)) continue;
3264
+ if (!(0, import_node_fs14.existsSync)(filePath)) continue;
2184
3265
  filesSearched++;
2185
3266
  let content;
2186
3267
  try {
2187
- content = (0, import_node_fs8.readFileSync)(filePath, "utf-8");
3268
+ content = (0, import_node_fs14.readFileSync)(filePath, "utf-8");
2188
3269
  } catch {
2189
3270
  continue;
2190
3271
  }
@@ -2221,13 +3302,11 @@ function handleGrepNodes(args) {
2221
3302
  truncated
2222
3303
  });
2223
3304
  }
2224
- function handleGetGraphUiUrl() {
2225
- const lock = getLiveLock();
3305
+ function handleChartServerStatus() {
3306
+ const rootDir = process.cwd();
3307
+ const lock = getLiveLock(rootDir);
2226
3308
  if (!lock) {
2227
- return okJson({
2228
- running: false,
2229
- hint: "No launch-chart UI server is currently running. Start one with `launch-chart serve`, or set LAUNCH_CHART_AUTOSERVE=1 in your MCP config to auto-start it alongside the MCP server."
2230
- });
3309
+ return okJson({ running: false });
2231
3310
  }
2232
3311
  return okJson({
2233
3312
  running: true,
@@ -2238,6 +3317,146 @@ function handleGetGraphUiUrl() {
2238
3317
  startedAt: lock.startedAt
2239
3318
  });
2240
3319
  }
3320
+ function handleStartChartServer(args) {
3321
+ const rootDir = process.cwd();
3322
+ const lock = getLiveLock(rootDir);
3323
+ if (lock) {
3324
+ return okJson({
3325
+ started: false,
3326
+ reason: "already_running",
3327
+ url: lock.url,
3328
+ port: lock.port,
3329
+ pid: lock.pid
3330
+ });
3331
+ }
3332
+ const entryPath = process.argv[1];
3333
+ const logDir = (0, import_node_path16.join)((0, import_node_os2.homedir)(), ".launchsecure");
3334
+ (0, import_node_fs14.mkdirSync)(logDir, { recursive: true });
3335
+ const logPath = (0, import_node_path16.join)(logDir, "launch-chart.log");
3336
+ const out = (0, import_node_fs14.openSync)(logPath, "a");
3337
+ const err2 = (0, import_node_fs14.openSync)(logPath, "a");
3338
+ const portArgs = args.port ? ["--port", String(args.port)] : [];
3339
+ const child = (0, import_node_child_process2.spawn)(process.execPath, [entryPath, "serve", ...portArgs], {
3340
+ detached: true,
3341
+ stdio: ["ignore", out, err2],
3342
+ env: { ...process.env, LAUNCH_CHART_AUTOSERVE: "" }
3343
+ });
3344
+ child.unref();
3345
+ return okJson({
3346
+ started: true,
3347
+ pid: child.pid,
3348
+ logPath
3349
+ });
3350
+ }
3351
+ function handleStopChartServer() {
3352
+ const rootDir = process.cwd();
3353
+ const lock = getLiveLock(rootDir);
3354
+ if (!lock) {
3355
+ return okJson({ stopped: false, reason: "not_running" });
3356
+ }
3357
+ try {
3358
+ process.kill(lock.pid, "SIGTERM");
3359
+ return okJson({ stopped: true, pid: lock.pid });
3360
+ } catch (e) {
3361
+ const code = e.code;
3362
+ if (code === "ESRCH") {
3363
+ clearLock(rootDir);
3364
+ return okJson({ stopped: true, pid: lock.pid, note: "process was already gone, lock cleaned up" });
3365
+ }
3366
+ return okJson({ stopped: false, reason: `kill failed: ${code ?? e}` });
3367
+ }
3368
+ }
3369
+ function handleAddTag(args) {
3370
+ const rootDir = process.cwd();
3371
+ const nodeId = args.node_id;
3372
+ const key = args.key;
3373
+ const value = args.value;
3374
+ if (!nodeId) return err("node_id is required");
3375
+ if (!key) return err("key is required");
3376
+ if (!value) return err("value is required");
3377
+ const graphs = readAllGraphs(rootDir);
3378
+ let found = false;
3379
+ for (const graph of Object.values(graphs)) {
3380
+ if (graph && graph.nodes.some((n) => n.id === nodeId)) {
3381
+ found = true;
3382
+ break;
3383
+ }
3384
+ }
3385
+ if (!found) {
3386
+ return err(`Node "${nodeId}" not found in any graph layer. Check the node_id.`);
3387
+ }
3388
+ setTag(rootDir, nodeId, key, value);
3389
+ return okJson({ ok: true, node_id: nodeId, tag: { [key]: value } });
3390
+ }
3391
+ function handleRemoveTag(args) {
3392
+ const rootDir = process.cwd();
3393
+ const nodeId = args.node_id;
3394
+ const key = args.key;
3395
+ if (!nodeId) return err("node_id is required");
3396
+ if (!key) return err("key is required");
3397
+ removeTag(rootDir, nodeId, key);
3398
+ return okJson({ ok: true, node_id: nodeId, removed_key: key });
3399
+ }
3400
+ function handleDetectProjectStack() {
3401
+ const rootDir = process.cwd();
3402
+ const parsers = [
3403
+ { id: "react-nextjs", layer: "ui", detected: reactNextjsParser.detect(rootDir) },
3404
+ { id: "nextjs-routes", layer: "api", detected: nextjsRoutesParser.detect(rootDir) },
3405
+ { id: "prisma-schema", layer: "db", detected: prismaSchemaParser.detect(rootDir) }
3406
+ ];
3407
+ const config = loadConfig(rootDir);
3408
+ let stats = { calls_api: 0, references_api: 0, out_of_pattern: 0, annotations: 0 };
3409
+ const uiGraph = readGraph(rootDir, "ui");
3410
+ if (uiGraph) {
3411
+ for (const ref of uiGraph.cross_refs ?? []) {
3412
+ if (ref.type === "calls_api") stats.calls_api++;
3413
+ if (ref.type === "references_api") stats.references_api++;
3414
+ }
3415
+ for (const f of uiGraph.flagged_edges ?? []) {
3416
+ if (f.type === "out_of_pattern") stats.out_of_pattern++;
3417
+ }
3418
+ }
3419
+ const srcDir = (0, import_node_path16.join)(rootDir, "src");
3420
+ if ((0, import_node_fs14.existsSync)(srcDir)) {
3421
+ const scanDir = (dir) => {
3422
+ if (!(0, import_node_fs14.existsSync)(dir)) return;
3423
+ for (const entry of (0, import_node_fs14.readdirSync)(dir, { withFileTypes: true })) {
3424
+ if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
3425
+ const full = (0, import_node_path16.join)(dir, entry.name);
3426
+ if (entry.isDirectory()) {
3427
+ scanDir(full);
3428
+ continue;
3429
+ }
3430
+ if (![".ts", ".tsx"].includes((0, import_node_path16.extname)(entry.name))) continue;
3431
+ try {
3432
+ const content = (0, import_node_fs14.readFileSync)(full, "utf-8");
3433
+ const matches = content.match(/@api\s+(GET|POST|PUT|DELETE|PATCH)\s+\/\S+/g);
3434
+ if (matches) stats.annotations += matches.length;
3435
+ } catch {
3436
+ }
3437
+ }
3438
+ };
3439
+ scanDir(srcDir);
3440
+ }
3441
+ let recommendedPrimary = "fetch-resolver";
3442
+ if (stats.annotations > 0 && stats.annotations >= stats.calls_api) {
3443
+ recommendedPrimary = "api-annotations";
3444
+ } else if (stats.calls_api === 0 && stats.references_api > 0) {
3445
+ recommendedPrimary = "url-literal-scanner";
3446
+ }
3447
+ return okJson({
3448
+ parsers,
3449
+ crosslayer_parsers: [
3450
+ { id: "fetch-resolver", description: "Detects direct fetch()/api.get() calls with inline URLs" },
3451
+ { id: "api-annotations", description: "Scans for @api METHOD /path annotations in JSDoc/comments" },
3452
+ { id: "url-literal-scanner", description: "Finds /api/... string literals as fallback detection" }
3453
+ ],
3454
+ stats,
3455
+ recommended_primary: recommendedPrimary,
3456
+ current_config: Object.keys(config).length > 0 ? config : null,
3457
+ config_path: ".launchchart.json"
3458
+ });
3459
+ }
2241
3460
  function send(msg) {
2242
3461
  process.stdout.write(JSON.stringify(msg) + "\n");
2243
3462
  }
@@ -2281,8 +3500,28 @@ function handleMessage(msg) {
2281
3500
  respond(id ?? null, handleGrepNodes(args));
2282
3501
  return;
2283
3502
  }
2284
- if (toolName === "get_graph_ui_url") {
2285
- respond(id ?? null, handleGetGraphUiUrl());
3503
+ if (toolName === "chart_server_status") {
3504
+ respond(id ?? null, handleChartServerStatus());
3505
+ return;
3506
+ }
3507
+ if (toolName === "start_chart_server") {
3508
+ respond(id ?? null, handleStartChartServer(args));
3509
+ return;
3510
+ }
3511
+ if (toolName === "stop_chart_server") {
3512
+ respond(id ?? null, handleStopChartServer());
3513
+ return;
3514
+ }
3515
+ if (toolName === "detect_project_stack") {
3516
+ respond(id ?? null, handleDetectProjectStack());
3517
+ return;
3518
+ }
3519
+ if (toolName === "add_tag") {
3520
+ respond(id ?? null, handleAddTag(args));
3521
+ return;
3522
+ }
3523
+ if (toolName === "remove_tag") {
3524
+ respond(id ?? null, handleRemoveTag(args));
2286
3525
  return;
2287
3526
  }
2288
3527
  respondError(id ?? null, -32601, `Unknown tool: ${toolName}`);
@@ -2320,14 +3559,20 @@ function startGraphMcpServer() {
2320
3559
  process.stderr.write(`[launchsecure-graph] MCP server started (cwd: ${process.cwd()})
2321
3560
  `);
2322
3561
  }
2323
- var import_node_fs8, import_node_path8, SERVER_INFO, TOOLS, COMPACT_SCHEMA, COMPACT_NODE_KNOWN_KEYS, EST_CHARS_PER_NODE_FULL, EST_CHARS_PER_NODE_MIN, EST_CHARS_PER_EDGE, NEIGHBORHOOD_BUDGET_CHARS, BATCH_BUDGET_CHARS;
3562
+ var import_node_fs14, import_node_path16, import_node_child_process2, import_node_os2, SERVER_INFO, TOOLS, COMPACT_SCHEMA, COMPACT_NODE_KNOWN_KEYS, EST_CHARS_PER_NODE_FULL, EST_CHARS_PER_NODE_MIN, EST_CHARS_PER_EDGE, NEIGHBORHOOD_BUDGET_CHARS, BATCH_BUDGET_CHARS;
2324
3563
  var init_graph_mcp = __esm({
2325
3564
  "src/server/graph-mcp.ts"() {
2326
3565
  "use strict";
2327
- import_node_fs8 = require("node:fs");
2328
- import_node_path8 = require("node:path");
3566
+ import_node_fs14 = require("node:fs");
3567
+ import_node_path16 = require("node:path");
3568
+ import_node_child_process2 = require("node:child_process");
3569
+ import_node_os2 = require("node:os");
2329
3570
  init_graph();
2330
3571
  init_lockfile();
3572
+ init_config();
3573
+ init_react_nextjs();
3574
+ init_nextjs_routes();
3575
+ init_prisma_schema();
2331
3576
  SERVER_INFO = {
2332
3577
  name: "launchsecure-graph",
2333
3578
  version: "0.0.1"
@@ -2349,7 +3594,7 @@ var init_graph_mcp = __esm({
2349
3594
  },
2350
3595
  {
2351
3596
  name: "read_graph",
2352
- description: 'Query the structural project graph \u2014 a smart Glob replacement that locates files by type/module/name and returns structural metadata (imports, renders, routes, relations). \n\nUSE THIS FOR: "where is X", "what files are in module Y", "what pages exist under /admin", "what components does Z render", "what tables relate to User", "list all hooks in auth module". \n\nDO NOT USE FOR: finding text/code content (use Grep), reading actual source code (use Read), understanding behavior/logic/patterns (graph has no code semantics \u2014 only names and edges). \n\nQUERY PARAMS (at least one required for node data \u2014 unfiltered calls return summary only to stay in context):\n- search: substring match on node id, name, or route\n- type: filter by node type (ui layer: page, layout, component, ui, hook, context, config, util; api layer: endpoint; db layer: table, enum)\n- module: filter by module (ui layer only: auth, admin, project, org, settings, integrations, shared-ui, layout, root)\n- node_id: return this node + its neighborhood (incoming+outgoing edges within `hops`)\n- hops: neighborhood radius when node_id is set (default 1)\n- minimal: return only id/type/name/module/route per node (skip heavy fields like columns, exports)\n- include_edges: return the actual edge list. Default: TRUE for neighborhood queries (node_id), FALSE for filter queries (search/type/module). Filter responses always include `edge_count`; only pass include_edges:true when you actually need to inspect individual edges (e.g. "which components render X"). This default cuts typical filter responses in half.\n\nBATCH MODE: pass `queries` (array of query objects) to run multiple independent queries in a single call. Each query object uses the same params (layer/search/type/module/node_id/hops/minimal). Returns { batch: true, count, results: [{index, query, result}, ...] }. Use this when you need multiple graph views up-front (e.g. scoping a feature across ui+api+db layers) to save round-trips. When batch mode is used, top-level params are ignored.\n\nReturns: filtered nodes + edges between them. If no filter given, returns per-layer counts and type breakdown only.\n\nWIRE FORMAT (compact): responses that include nodes/edges use short keys and edge-by-index refs to cut payload ~40-60%. Every such response carries a `_schema` legend. Quick reference:\n nodes[]: { i: id, t: type, n: name, m: module, r: route, mt: methods, x: exports, c: columns }\n edges[]: { s: source_node_index, d: target_node_index, t: type, l: label }\nedges.s / edges.d are 0-based indices into THIS response\'s nodes array. If a referenced node is not in the response (boundary case), s/d may instead contain the full node id string \u2014 always check the type.\n\nBUDGET GUARDS:\n- Neighborhood queries stop expanding when the projected response exceeds budget. The response then contains `budget_exceeded: true` plus `hops_traversed < hops_requested`. When this happens, drill into a specific neighbor with another node_id call rather than retrying with larger hops \u2014 it will just truncate again.\n- Batch mode caps total response size. Once the budget is hit, later queries return `{skipped: true, reason: "batch_budget_exhausted"}` and you must re-run them individually.',
3597
+ 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.',
2353
3598
  inputSchema: {
2354
3599
  type: "object",
2355
3600
  properties: {
@@ -2368,7 +3613,15 @@ var init_graph_mcp = __esm({
2368
3613
  },
2369
3614
  module: {
2370
3615
  type: "string",
2371
- description: 'UI layer only \u2014 filter by module (e.g. "auth", "admin", "project", "org").'
3616
+ description: 'Filter by module tag (e.g. "auth", "admin", "settings"). Works across all layers.'
3617
+ },
3618
+ tag_key: {
3619
+ type: "string",
3620
+ description: "Filter by arbitrary tag key. Must be used with tag_value."
3621
+ },
3622
+ tag_value: {
3623
+ type: "string",
3624
+ description: "Filter by tag value for the given tag_key."
2372
3625
  },
2373
3626
  node_id: {
2374
3627
  type: "string",
@@ -2455,12 +3708,83 @@ Returns: { pattern, filter, files_searched, total_matches, matches: [{file, line
2455
3708
  }
2456
3709
  },
2457
3710
  {
2458
- name: "get_graph_ui_url",
2459
- description: 'Return the URL of a running launch-chart UI server if one exists. The UI is a visual, interactive view of the merged UI+API+DB project graph served by `launch-chart serve` (or auto-started via LAUNCH_CHART_AUTOSERVE=1). \n\nReturns: { running: boolean, url?: string, port?: number, pid?: number, startedAt?: string, cwd?: string }. If running is false, no server is currently live \u2014 suggest the user run `launch-chart serve` to start one. \n\nUse this when the user asks "open the graph", "show me the project graph UI", "where\'s the chart", etc.',
3711
+ name: "chart_server_status",
3712
+ description: `Check whether the launch-chart UI server is running. Returns: { running: boolean, url?: string, port?: number, pid?: number, startedAt?: string, cwd?: string }.
3713
+
3714
+ Use this when the user asks "is the chart running", "show me the project graph UI", "where's the chart", etc.`,
3715
+ inputSchema: {
3716
+ type: "object",
3717
+ properties: {}
3718
+ }
3719
+ },
3720
+ {
3721
+ name: "start_chart_server",
3722
+ 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.',
3723
+ inputSchema: {
3724
+ type: "object",
3725
+ properties: {
3726
+ port: {
3727
+ type: "number",
3728
+ description: "Port to bind the server to. Defaults to 52819 with automatic fallback if in use."
3729
+ }
3730
+ }
3731
+ }
3732
+ },
3733
+ {
3734
+ name: "stop_chart_server",
3735
+ 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.',
2460
3736
  inputSchema: {
2461
3737
  type: "object",
2462
3738
  properties: {}
2463
3739
  }
3740
+ },
3741
+ {
3742
+ name: "detect_project_stack",
3743
+ 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.",
3744
+ inputSchema: {
3745
+ type: "object",
3746
+ properties: {}
3747
+ }
3748
+ },
3749
+ {
3750
+ name: "add_tag",
3751
+ 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.',
3752
+ inputSchema: {
3753
+ type: "object",
3754
+ properties: {
3755
+ node_id: {
3756
+ type: "string",
3757
+ description: 'The node id to tag (e.g. "app/(auth)/login/page.tsx").'
3758
+ },
3759
+ key: {
3760
+ type: "string",
3761
+ description: 'Tag key (e.g. "module", "owner", "refactor_later").'
3762
+ },
3763
+ value: {
3764
+ type: "string",
3765
+ description: 'Tag value (e.g. "auth", "alice", "true").'
3766
+ }
3767
+ },
3768
+ required: ["node_id", "key", "value"]
3769
+ }
3770
+ },
3771
+ {
3772
+ name: "remove_tag",
3773
+ 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).",
3774
+ inputSchema: {
3775
+ type: "object",
3776
+ properties: {
3777
+ node_id: {
3778
+ type: "string",
3779
+ description: "The node id to remove the tag from."
3780
+ },
3781
+ key: {
3782
+ type: "string",
3783
+ description: "Tag key to remove."
3784
+ }
3785
+ },
3786
+ required: ["node_id", "key"]
3787
+ }
2464
3788
  }
2465
3789
  ];
2466
3790
  COMPACT_SCHEMA = {
@@ -2468,11 +3792,12 @@ Returns: { pattern, filter, files_searched, total_matches, matches: [{file, line
2468
3792
  i: "id",
2469
3793
  t: "type",
2470
3794
  n: "name",
2471
- m: "module",
3795
+ m: "module (from tags)",
2472
3796
  r: "route",
2473
3797
  mt: "methods",
2474
3798
  x: "exports",
2475
- c: "columns"
3799
+ c: "columns",
3800
+ tg: "tags"
2476
3801
  },
2477
3802
  edges: {
2478
3803
  s: "source_node_index",
@@ -2490,7 +3815,8 @@ Returns: { pattern, filter, files_searched, total_matches, matches: [{file, line
2490
3815
  "route",
2491
3816
  "methods",
2492
3817
  "exports",
2493
- "columns"
3818
+ "columns",
3819
+ "tags"
2494
3820
  ]);
2495
3821
  EST_CHARS_PER_NODE_FULL = {
2496
3822
  ui: 300,
@@ -2513,11 +3839,11 @@ Returns: { pattern, filter, files_searched, total_matches, matches: [{file, line
2513
3839
  });
2514
3840
 
2515
3841
  // src/server/graph-mcp-entry.ts
2516
- var import_node_child_process2 = require("node:child_process");
2517
- var import_node_fs9 = require("node:fs");
2518
- var import_node_path9 = __toESM(require("node:path"));
2519
- var import_node_os2 = require("node:os");
2520
- var import_node_fs10 = require("node:fs");
3842
+ var import_node_child_process3 = require("node:child_process");
3843
+ var import_node_fs15 = require("node:fs");
3844
+ var import_node_path17 = __toESM(require("node:path"));
3845
+ var import_node_os3 = require("node:os");
3846
+ var import_node_fs16 = require("node:fs");
2521
3847
  init_lockfile();
2522
3848
  function logStderr(msg) {
2523
3849
  process.stderr.write(`[launch-chart] ${msg}
@@ -2531,13 +3857,13 @@ function maybeAutoServe() {
2531
3857
  return;
2532
3858
  }
2533
3859
  try {
2534
- const logDir = import_node_path9.default.join((0, import_node_os2.homedir)(), ".launchsecure");
2535
- (0, import_node_fs10.mkdirSync)(logDir, { recursive: true });
2536
- const logPath = import_node_path9.default.join(logDir, "launch-chart.log");
2537
- const out = (0, import_node_fs9.openSync)(logPath, "a");
2538
- const err2 = (0, import_node_fs9.openSync)(logPath, "a");
3860
+ const logDir = import_node_path17.default.join((0, import_node_os3.homedir)(), ".launchsecure");
3861
+ (0, import_node_fs16.mkdirSync)(logDir, { recursive: true });
3862
+ const logPath = import_node_path17.default.join(logDir, "launch-chart.log");
3863
+ const out = (0, import_node_fs15.openSync)(logPath, "a");
3864
+ const err2 = (0, import_node_fs15.openSync)(logPath, "a");
2539
3865
  const entryPath = process.argv[1];
2540
- const child = (0, import_node_child_process2.spawn)(process.execPath, [entryPath, "serve"], {
3866
+ const child = (0, import_node_child_process3.spawn)(process.execPath, [entryPath, "serve"], {
2541
3867
  detached: true,
2542
3868
  stdio: ["ignore", out, err2],
2543
3869
  env: { ...process.env, LAUNCH_CHART_AUTOSERVE: "" }