@launchsecure/launch-kit 0.0.4 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -35,20 +35,41 @@ __export(chart_serve_exports, {
35
35
  });
36
36
  module.exports = __toCommonJS(chart_serve_exports);
37
37
  var import_node_http = __toESM(require("node:http"));
38
- var import_node_fs7 = __toESM(require("node:fs"));
39
- var import_node_path7 = __toESM(require("node:path"));
38
+ var import_node_fs11 = __toESM(require("node:fs"));
39
+ var import_node_path12 = __toESM(require("node:path"));
40
40
 
41
41
  // src/server/graph/index.ts
42
- var import_node_fs5 = require("node:fs");
43
- var import_node_path5 = require("node:path");
42
+ var import_node_fs9 = require("node:fs");
43
+ var import_node_path10 = require("node:path");
44
44
 
45
- // src/server/graph/parsers/ui/react-nextjs.ts
46
- var import_node_fs2 = require("node:fs");
47
- var import_node_path2 = require("node:path");
45
+ // src/server/graph/core/graph-builder.ts
46
+ var import_node_fs8 = require("node:fs");
47
+ var import_node_path9 = require("node:path");
48
48
 
49
- // src/server/graph/core/ast-helpers.ts
49
+ // src/server/graph/core/config.ts
50
50
  var import_node_fs = require("node:fs");
51
51
  var import_node_path = require("node:path");
52
+ var CONFIG_FILENAME = ".launchchart.json";
53
+ function loadConfig(rootDir) {
54
+ const configPath = (0, import_node_path.join)(rootDir, CONFIG_FILENAME);
55
+ if (!(0, import_node_fs.existsSync)(configPath)) return {};
56
+ try {
57
+ return JSON.parse((0, import_node_fs.readFileSync)(configPath, "utf-8"));
58
+ } catch {
59
+ return {};
60
+ }
61
+ }
62
+
63
+ // src/server/graph/core/parser-registry.ts
64
+ var import_node_path8 = require("node:path");
65
+
66
+ // src/server/graph/parsers/ui/react-nextjs.ts
67
+ var import_node_fs3 = require("node:fs");
68
+ var import_node_path3 = require("node:path");
69
+
70
+ // src/server/graph/core/ast-helpers.ts
71
+ var import_node_fs2 = require("node:fs");
72
+ var import_node_path2 = require("node:path");
52
73
  var tsModule;
53
74
  function getTs() {
54
75
  if (!tsModule) {
@@ -59,8 +80,8 @@ function getTs() {
59
80
  var HTTP_METHODS = /* @__PURE__ */ new Set(["get", "post", "put", "patch", "delete", "head", "options"]);
60
81
  function parseFile(absPath) {
61
82
  const ts = getTs();
62
- const content = (0, import_node_fs.readFileSync)(absPath, "utf-8");
63
- const ext = (0, import_node_path.extname)(absPath);
83
+ const content = (0, import_node_fs2.readFileSync)(absPath, "utf-8");
84
+ const ext = (0, import_node_path2.extname)(absPath);
64
85
  const scriptKind = ext === ".tsx" ? ts.ScriptKind.TSX : ext === ".ts" ? ts.ScriptKind.TS : ext === ".jsx" ? ts.ScriptKind.JSX : ts.ScriptKind.JS;
65
86
  const sourceFile = ts.createSourceFile(
66
87
  absPath,
@@ -338,8 +359,8 @@ var MUTATION_METHODS = /* @__PURE__ */ new Set([
338
359
  ]);
339
360
  function extractDbCalls(absPath) {
340
361
  const ts = getTs();
341
- const content = (0, import_node_fs.readFileSync)(absPath, "utf-8");
342
- const ext = (0, import_node_path.extname)(absPath);
362
+ const content = (0, import_node_fs2.readFileSync)(absPath, "utf-8");
363
+ const ext = (0, import_node_path2.extname)(absPath);
343
364
  const scriptKind = ext === ".tsx" ? ts.ScriptKind.TSX : ts.ScriptKind.TS;
344
365
  const sourceFile = ts.createSourceFile(absPath, content, ts.ScriptTarget.Latest, true, scriptKind);
345
366
  const calls = [];
@@ -368,8 +389,8 @@ function extractDbCalls(absPath) {
368
389
  }
369
390
  function extractAuthWrappers(absPath) {
370
391
  const ts = getTs();
371
- const content = (0, import_node_fs.readFileSync)(absPath, "utf-8");
372
- const ext = (0, import_node_path.extname)(absPath);
392
+ const content = (0, import_node_fs2.readFileSync)(absPath, "utf-8");
393
+ const ext = (0, import_node_path2.extname)(absPath);
373
394
  const scriptKind = ext === ".tsx" ? ts.ScriptKind.TSX : ts.ScriptKind.TS;
374
395
  const sourceFile = ts.createSourceFile(absPath, content, ts.ScriptTarget.Latest, true, scriptKind);
375
396
  const wrappers = /* @__PURE__ */ new Set();
@@ -390,12 +411,12 @@ function extractAuthWrappers(absPath) {
390
411
  var RENDER_TYPES = /* @__PURE__ */ new Set(["component", "ui", "layout", "context"]);
391
412
  function walk(dir, exts) {
392
413
  const results = [];
393
- if (!(0, import_node_fs2.existsSync)(dir)) return results;
394
- for (const entry of (0, import_node_fs2.readdirSync)(dir, { withFileTypes: true })) {
395
- const full = (0, import_node_path2.join)(dir, entry.name);
414
+ if (!(0, import_node_fs3.existsSync)(dir)) return results;
415
+ for (const entry of (0, import_node_fs3.readdirSync)(dir, { withFileTypes: true })) {
416
+ const full = (0, import_node_path3.join)(dir, entry.name);
396
417
  if (entry.isDirectory()) {
397
418
  results.push(...walk(full, exts));
398
- } else if (exts.includes((0, import_node_path2.extname)(entry.name))) {
419
+ } else if (exts.includes((0, import_node_path3.extname)(entry.name))) {
399
420
  results.push(full);
400
421
  }
401
422
  }
@@ -403,33 +424,33 @@ function walk(dir, exts) {
403
424
  }
404
425
  function walkWithIgnore(dir, exts, ignoreDirs) {
405
426
  const results = [];
406
- if (!(0, import_node_fs2.existsSync)(dir)) return results;
407
- for (const entry of (0, import_node_fs2.readdirSync)(dir, { withFileTypes: true })) {
427
+ if (!(0, import_node_fs3.existsSync)(dir)) return results;
428
+ for (const entry of (0, import_node_fs3.readdirSync)(dir, { withFileTypes: true })) {
408
429
  if (entry.isDirectory()) {
409
430
  if (ignoreDirs.has(entry.name)) continue;
410
- results.push(...walkWithIgnore((0, import_node_path2.join)(dir, entry.name), exts, ignoreDirs));
411
- } else if (exts.includes((0, import_node_path2.extname)(entry.name))) {
412
- results.push((0, import_node_path2.join)(dir, entry.name));
431
+ results.push(...walkWithIgnore((0, import_node_path3.join)(dir, entry.name), exts, ignoreDirs));
432
+ } else if (exts.includes((0, import_node_path3.extname)(entry.name))) {
433
+ results.push((0, import_node_path3.join)(dir, entry.name));
413
434
  }
414
435
  }
415
436
  return results;
416
437
  }
417
438
  function toNodeId(srcDir, absPath) {
418
- return (0, import_node_path2.relative)(srcDir, absPath).replace(/\\/g, "/");
439
+ return (0, import_node_path3.relative)(srcDir, absPath).replace(/\\/g, "/");
419
440
  }
420
441
  function resolveImport(srcDir, specifier) {
421
442
  if (!specifier.startsWith("@/")) return null;
422
443
  const rel = specifier.slice(2);
423
- const base = (0, import_node_path2.join)(srcDir, rel);
424
- for (const c of [base, base + ".ts", base + ".tsx", (0, import_node_path2.join)(base, "index.ts"), (0, import_node_path2.join)(base, "index.tsx")]) {
425
- if ((0, import_node_fs2.existsSync)(c) && (0, import_node_fs2.statSync)(c).isFile()) return c;
444
+ const base = (0, import_node_path3.join)(srcDir, rel);
445
+ 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")]) {
446
+ if ((0, import_node_fs3.existsSync)(c) && (0, import_node_fs3.statSync)(c).isFile()) return c;
426
447
  }
427
448
  return null;
428
449
  }
429
450
  function resolveRelativeImport(fromFile, specifier) {
430
- const base = (0, import_node_path2.join)((0, import_node_path2.dirname)(fromFile), specifier);
431
- for (const c of [base, base + ".ts", base + ".tsx", (0, import_node_path2.join)(base, "index.ts"), (0, import_node_path2.join)(base, "index.tsx")]) {
432
- if ((0, import_node_fs2.existsSync)(c) && (0, import_node_fs2.statSync)(c).isFile()) return c;
451
+ const base = (0, import_node_path3.join)((0, import_node_path3.dirname)(fromFile), specifier);
452
+ 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")]) {
453
+ if ((0, import_node_fs3.existsSync)(c) && (0, import_node_fs3.statSync)(c).isFile()) return c;
433
454
  }
434
455
  return null;
435
456
  }
@@ -450,7 +471,7 @@ function resolveBarrelMap(barrelAbsPath, parsedByPath, memo, visiting) {
450
471
  const resolved = resolveRelativeImport(barrelAbsPath, re.from);
451
472
  if (!resolved) continue;
452
473
  if (re.isWildcard) {
453
- const targetBn = (0, import_node_path2.basename)(resolved);
474
+ const targetBn = (0, import_node_path3.basename)(resolved);
454
475
  const targetIsBarrel = targetBn === "index.ts" || targetBn === "index.tsx";
455
476
  if (targetIsBarrel) {
456
477
  const nested = resolveBarrelMap(resolved, parsedByPath, memo, visiting);
@@ -477,12 +498,12 @@ function buildAllBarrelMaps(srcDir, parsedByPath) {
477
498
  const barrels = /* @__PURE__ */ new Map();
478
499
  const memo = /* @__PURE__ */ new Map();
479
500
  for (const [absPath, parsed] of parsedByPath) {
480
- const bn = (0, import_node_path2.basename)(absPath);
501
+ const bn = (0, import_node_path3.basename)(absPath);
481
502
  if (bn !== "index.ts" && bn !== "index.tsx") continue;
482
503
  if (parsed.reExports.length === 0) continue;
483
504
  const map = resolveBarrelMap(absPath, parsedByPath, memo, /* @__PURE__ */ new Set());
484
505
  if (map.size > 0) {
485
- const barrelId = (0, import_node_path2.relative)(srcDir, (0, import_node_path2.dirname)(absPath)).replace(/\\/g, "/");
506
+ const barrelId = (0, import_node_path3.relative)(srcDir, (0, import_node_path3.dirname)(absPath)).replace(/\\/g, "/");
486
507
  barrels.set(barrelId, map);
487
508
  }
488
509
  }
@@ -541,7 +562,7 @@ function extractRoute(id) {
541
562
  return route || "/";
542
563
  }
543
564
  function nameFromFilename(absPath) {
544
- return (0, import_node_path2.basename)(absPath, (0, import_node_path2.extname)(absPath)).replace(/[-_](\w)/g, (_, c) => c.toUpperCase()).replace(/^(\w)/, (_, c) => c.toUpperCase());
565
+ return (0, import_node_path3.basename)(absPath, (0, import_node_path3.extname)(absPath)).replace(/[-_](\w)/g, (_, c) => c.toUpperCase()).replace(/^(\w)/, (_, c) => c.toUpperCase());
545
566
  }
546
567
  function resolveTemplateLiteralRoute(template, routeToNodeId) {
547
568
  const parameterized = template.replace(/\$\{([^}]+)\}/g, (_, expr) => {
@@ -622,105 +643,6 @@ function matchRouteToPage(route, routeToNodeId) {
622
643
  if (routeToNodeId.has(normalized)) return routeToNodeId.get(normalized);
623
644
  return null;
624
645
  }
625
- function loadApiRoutes(rootDir) {
626
- const apiJsonPath = (0, import_node_path2.join)(rootDir, ".launchsecure", "graphs", "api.json");
627
- if (!(0, import_node_fs2.existsSync)(apiJsonPath)) return [];
628
- try {
629
- const parsed = JSON.parse((0, import_node_fs2.readFileSync)(apiJsonPath, "utf-8"));
630
- const routes = [];
631
- for (const n of parsed.nodes ?? []) {
632
- const path2 = n.path;
633
- if (!path2 || typeof path2 !== "string") continue;
634
- routes.push({
635
- path: path2,
636
- nodeId: n.id,
637
- segments: path2.split("/").filter(Boolean)
638
- });
639
- }
640
- return routes;
641
- } catch {
642
- return [];
643
- }
644
- }
645
- function buildApiPathMap(routes) {
646
- const map = /* @__PURE__ */ new Map();
647
- for (const r of routes) {
648
- if (!map.has(r.path)) map.set(r.path, r.nodeId);
649
- }
650
- return map;
651
- }
652
- function normalizeFetchUrl(raw) {
653
- let s = raw.replace(/^`|`$/g, "");
654
- const qIdx = s.indexOf("?");
655
- if (qIdx >= 0) s = s.slice(0, qIdx);
656
- const hIdx = s.indexOf("#");
657
- if (hIdx >= 0) s = s.slice(0, hIdx);
658
- let hadInterpolation = false;
659
- s = s.replace(/\$\{([^}]+)\}/g, (_, expr) => {
660
- hadInterpolation = true;
661
- const cleaned = expr.trim();
662
- const last = cleaned.split(".").pop() ?? cleaned;
663
- const name = last.replace(/[^\w]/g, "") || "param";
664
- return ":" + name;
665
- });
666
- s = s.replace(/\/+/g, "/");
667
- if (s.length > 1 && s.endsWith("/")) s = s.slice(0, -1);
668
- return { path: s || "/", hadInterpolation };
669
- }
670
- function scoreApiRouteMatch(candidate, known) {
671
- if (candidate.length !== known.length) return -1;
672
- let score = 0;
673
- for (let i = 0; i < candidate.length; i++) {
674
- const a = candidate[i];
675
- const b = known[i];
676
- if (a === b) {
677
- score += 3;
678
- continue;
679
- }
680
- if (a.startsWith(":") && b.startsWith(":")) {
681
- score += 2;
682
- continue;
683
- }
684
- if (a.startsWith(":") || b.startsWith(":")) {
685
- score += 1;
686
- continue;
687
- }
688
- return -1;
689
- }
690
- return score;
691
- }
692
- function resolveFetchCall(call, apiPathMap, apiRoutes) {
693
- const raw = call.url;
694
- if (/^(https?:)?\/\//i.test(raw)) {
695
- return { kind: "external", normalizedUrl: raw };
696
- }
697
- if (call.isConcat) {
698
- return { kind: "dynamic", normalizedUrl: raw };
699
- }
700
- const { path: path2, hadInterpolation } = normalizeFetchUrl(raw);
701
- if (!path2.startsWith("/")) {
702
- return { kind: "unresolved", normalizedUrl: path2 };
703
- }
704
- const segs = path2.split("/").filter(Boolean);
705
- if (hadInterpolation && segs.length > 0 && segs[0].startsWith(":")) {
706
- return { kind: "dynamic", normalizedUrl: path2 };
707
- }
708
- const exact = apiPathMap.get(path2);
709
- if (exact) return { kind: "resolved", nodeId: exact, normalizedUrl: path2 };
710
- let bestScore = -1;
711
- let bestId = null;
712
- for (const r of apiRoutes) {
713
- const score = scoreApiRouteMatch(segs, r.segments);
714
- if (score > bestScore) {
715
- bestScore = score;
716
- bestId = r.nodeId;
717
- }
718
- }
719
- if (bestId && bestScore > 0) {
720
- return { kind: "resolved", nodeId: bestId, normalizedUrl: path2 };
721
- }
722
- return { kind: "unresolved", normalizedUrl: path2 };
723
- }
724
646
  function extractEdges(srcDir, absPath, sourceId, parsed, nodeIdSet, nodeTypeMap, barrelMaps, routeToNodeId) {
725
647
  const edges = [];
726
648
  const flagged = [];
@@ -820,26 +742,26 @@ function extractEdges(srcDir, absPath, sourceId, parsed, nodeIdSet, nodeTypeMap,
820
742
  return { edges, flagged };
821
743
  }
822
744
  function detect(rootDir) {
823
- return (0, import_node_fs2.existsSync)((0, import_node_path2.join)(rootDir, "src", "app")) && (0, import_node_fs2.existsSync)((0, import_node_path2.join)(rootDir, "next.config.ts")) || (0, import_node_fs2.existsSync)((0, import_node_path2.join)(rootDir, "next.config.js")) || (0, import_node_fs2.existsSync)((0, import_node_path2.join)(rootDir, "next.config.mjs"));
745
+ 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"));
824
746
  }
825
747
  function generate(rootDir) {
826
- const srcDir = (0, import_node_path2.join)(rootDir, "src");
827
- const appFiles = walk((0, import_node_path2.join)(srcDir, "app"), [".tsx", ".ts"]).filter(
828
- (f) => (0, import_node_path2.basename)(f) !== "route.ts" && (0, import_node_path2.basename)(f) !== "route.tsx"
748
+ const srcDir = (0, import_node_path3.join)(rootDir, "src");
749
+ const appFiles = walk((0, import_node_path3.join)(srcDir, "app"), [".tsx", ".ts"]).filter(
750
+ (f) => (0, import_node_path3.basename)(f) !== "route.ts" && (0, import_node_path3.basename)(f) !== "route.tsx"
829
751
  );
830
- const clientFiles = walk((0, import_node_path2.join)(srcDir, "client"), [".tsx", ".ts"]);
831
- const serverFiles = walk((0, import_node_path2.join)(srcDir, "server"), [".ts", ".tsx"]).filter(
832
- (f) => (0, import_node_path2.basename)(f) !== "route.ts" && (0, import_node_path2.basename)(f) !== "route.tsx"
752
+ const clientFiles = walk((0, import_node_path3.join)(srcDir, "client"), [".tsx", ".ts"]);
753
+ const serverFiles = walk((0, import_node_path3.join)(srcDir, "server"), [".ts", ".tsx"]).filter(
754
+ (f) => (0, import_node_path3.basename)(f) !== "route.ts" && (0, import_node_path3.basename)(f) !== "route.tsx"
833
755
  );
834
- const libFiles = walk((0, import_node_path2.join)(srcDir, "lib"), [".ts", ".tsx"]);
835
- const configFiles = walk((0, import_node_path2.join)(srcDir, "config"), [".ts", ".tsx"]);
756
+ const libFiles = walk((0, import_node_path3.join)(srcDir, "lib"), [".ts", ".tsx"]);
757
+ const configFiles = walk((0, import_node_path3.join)(srcDir, "config"), [".ts", ".tsx"]);
836
758
  const allDiscovered = [...appFiles, ...clientFiles, ...serverFiles, ...libFiles, ...configFiles];
837
759
  const parsedByPath = /* @__PURE__ */ new Map();
838
760
  for (const absPath of allDiscovered) {
839
761
  parsedByPath.set(absPath, parseFile(absPath));
840
762
  }
841
763
  const barrelMaps = buildAllBarrelMaps(srcDir, parsedByPath);
842
- const fileSet = allDiscovered.filter((f) => !(0, import_node_path2.basename)(f).startsWith("index."));
764
+ const fileSet = allDiscovered.filter((f) => !(0, import_node_path3.basename)(f).startsWith("index."));
843
765
  const nodes = [];
844
766
  const nodeIdSet = /* @__PURE__ */ new Set();
845
767
  const nodeTypeMap = /* @__PURE__ */ new Map();
@@ -858,7 +780,6 @@ function generate(rootDir) {
858
780
  }
859
781
  const allEdges = [];
860
782
  const allFlagged = [];
861
- const crossRefs = [];
862
783
  for (const absPath of fileSet) {
863
784
  const sourceId = toNodeId(srcDir, absPath);
864
785
  const parsed = parsedByPath.get(absPath);
@@ -875,66 +796,21 @@ function generate(rootDir) {
875
796
  allEdges.push(...edges);
876
797
  allFlagged.push(...flagged);
877
798
  }
878
- const apiRoutes = loadApiRoutes(rootDir);
879
- const apiPathMap = buildApiPathMap(apiRoutes);
880
- const includeExternalFetches = process.env.LAUNCH_CHART_INCLUDE_EXTERNAL_FETCHES === "1";
881
- const fetchSeen = /* @__PURE__ */ new Set();
882
- let fetchResolvedCount = 0;
883
- let fetchDynamicCount = 0;
884
- let fetchUnresolvedCount = 0;
885
- let fetchExternalCount = 0;
799
+ const fetchCallEntries = [];
886
800
  for (const absPath of fileSet) {
887
801
  const sourceId = toNodeId(srcDir, absPath);
888
802
  const parsed = parsedByPath.get(absPath);
889
803
  if (parsed.fetchCalls.length === 0) continue;
890
- for (const call of parsed.fetchCalls) {
891
- const result = resolveFetchCall(call, apiPathMap, apiRoutes);
892
- const methodTag = call.method ?? (call.kind === "fetch" ? "GET?" : "?");
893
- if (result.kind === "resolved" && result.nodeId) {
894
- const key = `${sourceId}\u2192${result.nodeId}\u2192calls_api`;
895
- if (fetchSeen.has(key)) continue;
896
- fetchSeen.add(key);
897
- crossRefs.push({
898
- source: sourceId,
899
- target: result.nodeId,
900
- type: "calls_api",
901
- layer: "api"
902
- });
903
- fetchResolvedCount++;
904
- continue;
905
- }
906
- if (result.kind === "dynamic") {
907
- fetchDynamicCount++;
908
- allFlagged.push({
909
- source: sourceId,
910
- target: "DYNAMIC",
911
- type: "calls_api",
912
- label: call.isConcat ? `${methodTag} fetch with concat: ${call.url}` : `${methodTag} fetch with template: ${call.url}`,
913
- confidence: call.isConcat ? "low" : "medium"
914
- });
915
- continue;
916
- }
917
- if (result.kind === "external") {
918
- fetchExternalCount++;
919
- if (!includeExternalFetches) continue;
920
- allFlagged.push({
921
- source: sourceId,
922
- target: "EXTERNAL",
923
- type: "calls_external",
924
- label: `${methodTag} external fetch: ${call.url}`,
925
- confidence: "high"
926
- });
927
- continue;
928
- }
929
- fetchUnresolvedCount++;
930
- allFlagged.push({
931
- source: sourceId,
932
- target: "UNRESOLVED",
933
- type: "calls_api",
934
- label: `${methodTag} fetch to unknown path: ${result.normalizedUrl}`,
935
- confidence: "medium"
936
- });
937
- }
804
+ fetchCallEntries.push({
805
+ nodeId: sourceId,
806
+ calls: parsed.fetchCalls.map((c) => ({
807
+ url: c.url,
808
+ method: c.method,
809
+ isTemplate: c.isTemplate,
810
+ isConcat: c.isConcat,
811
+ kind: c.kind
812
+ }))
813
+ });
938
814
  }
939
815
  const externalScanned = new Set(allDiscovered.map((f) => f.replace(/\\/g, "/")));
940
816
  const IGNORE_DIRS = /* @__PURE__ */ new Set([
@@ -960,7 +836,7 @@ function generate(rootDir) {
960
836
  } catch {
961
837
  continue;
962
838
  }
963
- const externalId = (0, import_node_path2.relative)(rootDir, absPath).replace(/\\/g, "/");
839
+ const externalId = (0, import_node_path3.relative)(rootDir, absPath).replace(/\\/g, "/");
964
840
  const edgesFromThis = [];
965
841
  const seen = /* @__PURE__ */ new Set();
966
842
  for (const imp of parsed.imports) {
@@ -1051,20 +927,11 @@ function generate(rootDir) {
1051
927
  layer: "ui",
1052
928
  parser: "react-nextjs-ast",
1053
929
  ...stats,
1054
- api_call_detection: {
1055
- includeExternalFetches,
1056
- includeConcatFetches: process.env.LAUNCH_CHART_INCLUDE_CONCAT_FETCHES === "1",
1057
- apiRoutesLoaded: apiRoutes.length,
1058
- resolved: fetchResolvedCount,
1059
- dynamic: fetchDynamicCount,
1060
- unresolved: fetchUnresolvedCount,
1061
- external: fetchExternalCount
1062
- },
1063
930
  notes: "Auto-generated via TypeScript AST \u2014 edges derived from actual imports, renders from JSX usage, navigations from router/Link calls."
1064
931
  },
1065
932
  nodes,
1066
933
  edges: allEdges,
1067
- cross_refs: crossRefs,
934
+ cross_refs: [],
1068
935
  contradictions: [],
1069
936
  warnings: [],
1070
937
  flagged_edges: dedupedFlagged,
@@ -1075,7 +942,8 @@ function generate(rootDir) {
1075
942
  renders: allEdges.filter((e) => e.type === "renders").length,
1076
943
  imports: allEdges.filter((e) => e.type === "imports").length,
1077
944
  navigates: allEdges.filter((e) => e.type === "navigates").length
1078
- }
945
+ },
946
+ fetch_calls: fetchCallEntries
1079
947
  }
1080
948
  };
1081
949
  }
@@ -1087,14 +955,14 @@ var reactNextjsParser = {
1087
955
  };
1088
956
 
1089
957
  // src/server/graph/parsers/api/nextjs-routes.ts
1090
- var import_node_fs3 = require("node:fs");
1091
- var import_node_path3 = require("node:path");
958
+ var import_node_fs4 = require("node:fs");
959
+ var import_node_path4 = require("node:path");
1092
960
  var HTTP_METHODS2 = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"]);
1093
961
  function walk2(dir) {
1094
962
  const results = [];
1095
- if (!(0, import_node_fs3.existsSync)(dir)) return results;
1096
- for (const entry of (0, import_node_fs3.readdirSync)(dir, { withFileTypes: true })) {
1097
- const full = (0, import_node_path3.join)(dir, entry.name);
963
+ if (!(0, import_node_fs4.existsSync)(dir)) return results;
964
+ for (const entry of (0, import_node_fs4.readdirSync)(dir, { withFileTypes: true })) {
965
+ const full = (0, import_node_path4.join)(dir, entry.name);
1098
966
  if (entry.isDirectory()) {
1099
967
  results.push(...walk2(full));
1100
968
  } else if (entry.name === "route.ts" || entry.name === "route.tsx") {
@@ -1104,7 +972,7 @@ function walk2(dir) {
1104
972
  return results;
1105
973
  }
1106
974
  function filePathToRoute(apiDir, absPath) {
1107
- let route = "/" + (0, import_node_path3.relative)(apiDir, absPath).replace(/\\/g, "/").replace(/\/route\.tsx?$/, "");
975
+ let route = "/" + (0, import_node_path4.relative)(apiDir, absPath).replace(/\\/g, "/").replace(/\/route\.tsx?$/, "");
1108
976
  route = route.replace(/\[([^\]]+)\]/g, ":$1");
1109
977
  route = route.replace(/\/+/g, "/");
1110
978
  if (route === "/") return "/api";
@@ -1115,10 +983,10 @@ function camelToPascal(s) {
1115
983
  return s.charAt(0).toUpperCase() + s.slice(1);
1116
984
  }
1117
985
  function detect2(rootDir) {
1118
- return (0, import_node_fs3.existsSync)((0, import_node_path3.join)(rootDir, "src", "app", "api"));
986
+ return (0, import_node_fs4.existsSync)((0, import_node_path4.join)(rootDir, "src", "app", "api"));
1119
987
  }
1120
988
  function generate2(rootDir) {
1121
- const apiDir = (0, import_node_path3.join)(rootDir, "src", "app", "api");
989
+ const apiDir = (0, import_node_path4.join)(rootDir, "src", "app", "api");
1122
990
  const routeFiles = walk2(apiDir);
1123
991
  const nodes = [];
1124
992
  const edges = [];
@@ -1136,7 +1004,7 @@ function generate2(rootDir) {
1136
1004
  if (HTTP_METHODS2.has(exp)) methods.push(exp);
1137
1005
  }
1138
1006
  const routePath = filePathToRoute(apiDir, absPath);
1139
- const relPath = (0, import_node_path3.relative)(rootDir, absPath).replace(/\\/g, "/");
1007
+ const relPath = (0, import_node_path4.relative)(rootDir, absPath).replace(/\\/g, "/");
1140
1008
  const mutations = dbCalls.filter((c) => c.isMutation);
1141
1009
  const reads = dbCalls.filter((c) => !c.isMutation);
1142
1010
  const mutates = mutations.length > 0;
@@ -1221,8 +1089,8 @@ var nextjsRoutesParser = {
1221
1089
  };
1222
1090
 
1223
1091
  // src/server/graph/parsers/db/prisma-schema.ts
1224
- var import_node_fs4 = require("node:fs");
1225
- var import_node_path4 = require("node:path");
1092
+ var import_node_fs5 = require("node:fs");
1093
+ var import_node_path5 = require("node:path");
1226
1094
  function parseModels(content) {
1227
1095
  const nodes = [];
1228
1096
  const relations = [];
@@ -1313,11 +1181,11 @@ function parseEnums(content) {
1313
1181
  return nodes;
1314
1182
  }
1315
1183
  function detect3(rootDir) {
1316
- return (0, import_node_fs4.existsSync)((0, import_node_path4.join)(rootDir, "prisma", "schema.prisma"));
1184
+ return (0, import_node_fs5.existsSync)((0, import_node_path5.join)(rootDir, "prisma", "schema.prisma"));
1317
1185
  }
1318
1186
  function generate3(rootDir) {
1319
- const schemaPath = (0, import_node_path4.join)(rootDir, "prisma", "schema.prisma");
1320
- const content = (0, import_node_fs4.readFileSync)(schemaPath, "utf-8");
1187
+ const schemaPath = (0, import_node_path5.join)(rootDir, "prisma", "schema.prisma");
1188
+ const content = (0, import_node_fs5.readFileSync)(schemaPath, "utf-8");
1321
1189
  const { nodes: modelNodes, relations } = parseModels(content);
1322
1190
  const enumNodes = parseEnums(content);
1323
1191
  const allNodes = [...modelNodes, ...enumNodes];
@@ -1373,33 +1241,651 @@ var prismaSchemaParser = {
1373
1241
  generate: generate3
1374
1242
  };
1375
1243
 
1244
+ // src/server/graph/core/api-route-matching.ts
1245
+ function loadApiRoutesFromOutput(apiOutput) {
1246
+ const routes = [];
1247
+ for (const n of apiOutput.nodes) {
1248
+ const path2 = n.path;
1249
+ if (!path2 || typeof path2 !== "string") continue;
1250
+ routes.push({
1251
+ path: path2,
1252
+ nodeId: n.id,
1253
+ segments: path2.split("/").filter(Boolean)
1254
+ });
1255
+ }
1256
+ return routes;
1257
+ }
1258
+ function buildApiPathMap(routes) {
1259
+ const map = /* @__PURE__ */ new Map();
1260
+ for (const r of routes) {
1261
+ if (!map.has(r.path)) map.set(r.path, r.nodeId);
1262
+ }
1263
+ return map;
1264
+ }
1265
+ function normalizeFetchUrl(raw) {
1266
+ let s = raw.replace(/^`|`$/g, "");
1267
+ const qIdx = s.indexOf("?");
1268
+ if (qIdx >= 0) s = s.slice(0, qIdx);
1269
+ const hIdx = s.indexOf("#");
1270
+ if (hIdx >= 0) s = s.slice(0, hIdx);
1271
+ let hadInterpolation = false;
1272
+ s = s.replace(/\$\{([^}]+)\}/g, (_, expr) => {
1273
+ hadInterpolation = true;
1274
+ const cleaned = expr.trim();
1275
+ const last = cleaned.split(".").pop() ?? cleaned;
1276
+ const name = last.replace(/[^\w]/g, "") || "param";
1277
+ return ":" + name;
1278
+ });
1279
+ s = s.replace(/\/+/g, "/");
1280
+ if (s.length > 1 && s.endsWith("/")) s = s.slice(0, -1);
1281
+ return { path: s || "/", hadInterpolation };
1282
+ }
1283
+ function scoreApiRouteMatch(candidate, known) {
1284
+ if (candidate.length !== known.length) return -1;
1285
+ let score = 0;
1286
+ for (let i = 0; i < candidate.length; i++) {
1287
+ const a = candidate[i];
1288
+ const b = known[i];
1289
+ if (a === b) {
1290
+ score += 3;
1291
+ continue;
1292
+ }
1293
+ if (a.startsWith(":") && b.startsWith(":")) {
1294
+ score += 2;
1295
+ continue;
1296
+ }
1297
+ if (a.startsWith(":") || b.startsWith(":")) {
1298
+ score += 1;
1299
+ continue;
1300
+ }
1301
+ return -1;
1302
+ }
1303
+ return score;
1304
+ }
1305
+ function resolveFetchCall(call, apiPathMap, apiRoutes) {
1306
+ const raw = call.url;
1307
+ if (/^(https?:)?\/\//i.test(raw)) {
1308
+ return { kind: "external", normalizedUrl: raw };
1309
+ }
1310
+ if (call.isConcat) {
1311
+ return { kind: "dynamic", normalizedUrl: raw };
1312
+ }
1313
+ const { path: path2, hadInterpolation } = normalizeFetchUrl(raw);
1314
+ if (!path2.startsWith("/")) {
1315
+ return { kind: "unresolved", normalizedUrl: path2 };
1316
+ }
1317
+ const segs = path2.split("/").filter(Boolean);
1318
+ if (hadInterpolation && segs.length > 0 && segs[0].startsWith(":")) {
1319
+ return { kind: "dynamic", normalizedUrl: path2 };
1320
+ }
1321
+ const exact = apiPathMap.get(path2);
1322
+ if (exact) return { kind: "resolved", nodeId: exact, normalizedUrl: path2 };
1323
+ let bestScore = -1;
1324
+ let bestId = null;
1325
+ for (const r of apiRoutes) {
1326
+ const score = scoreApiRouteMatch(segs, r.segments);
1327
+ if (score > bestScore) {
1328
+ bestScore = score;
1329
+ bestId = r.nodeId;
1330
+ }
1331
+ }
1332
+ if (bestId && bestScore > 0) {
1333
+ return { kind: "resolved", nodeId: bestId, normalizedUrl: path2 };
1334
+ }
1335
+ return { kind: "unresolved", normalizedUrl: path2 };
1336
+ }
1337
+ function resolveUrlPath(urlPath, apiPathMap, apiRoutes) {
1338
+ const { path: path2, hadInterpolation } = normalizeFetchUrl(urlPath);
1339
+ if (!path2.startsWith("/")) {
1340
+ return { kind: "unresolved", normalizedUrl: path2 };
1341
+ }
1342
+ const segs = path2.split("/").filter(Boolean);
1343
+ if (hadInterpolation && segs.length > 0 && segs[0].startsWith(":")) {
1344
+ return { kind: "dynamic", normalizedUrl: path2 };
1345
+ }
1346
+ const exact = apiPathMap.get(path2);
1347
+ if (exact) return { kind: "resolved", nodeId: exact, normalizedUrl: path2 };
1348
+ let bestScore = -1;
1349
+ let bestId = null;
1350
+ for (const r of apiRoutes) {
1351
+ const score = scoreApiRouteMatch(segs, r.segments);
1352
+ if (score > bestScore) {
1353
+ bestScore = score;
1354
+ bestId = r.nodeId;
1355
+ }
1356
+ }
1357
+ if (bestId && bestScore > 0) {
1358
+ return { kind: "resolved", nodeId: bestId, normalizedUrl: path2 };
1359
+ }
1360
+ return { kind: "unresolved", normalizedUrl: path2 };
1361
+ }
1362
+
1363
+ // src/server/graph/parsers/crosslayer/fetch-resolver.ts
1364
+ var fetchResolverParser = {
1365
+ id: "fetch-resolver",
1366
+ layer: "crosslayer",
1367
+ detect(_rootDir) {
1368
+ return true;
1369
+ },
1370
+ generate(_rootDir, layerOutputs) {
1371
+ const uiOutput = layerOutputs.get("ui");
1372
+ const apiOutput = layerOutputs.get("api");
1373
+ if (!uiOutput || !apiOutput) {
1374
+ return { cross_refs: [], flagged_edges: [], warnings: [] };
1375
+ }
1376
+ const apiRoutes = loadApiRoutesFromOutput(apiOutput);
1377
+ const apiPathMap = buildApiPathMap(apiRoutes);
1378
+ const fetchCallEntries = uiOutput.patterns?.fetch_calls ?? [];
1379
+ if (fetchCallEntries.length === 0) {
1380
+ return { cross_refs: [], flagged_edges: [], warnings: [] };
1381
+ }
1382
+ const includeExternal = process.env.LAUNCH_CHART_INCLUDE_EXTERNAL_FETCHES === "1";
1383
+ const crossRefs = [];
1384
+ const flaggedEdges = [];
1385
+ const seen = /* @__PURE__ */ new Set();
1386
+ let resolvedCount = 0;
1387
+ let dynamicCount = 0;
1388
+ let unresolvedCount = 0;
1389
+ let externalCount = 0;
1390
+ for (const entry of fetchCallEntries) {
1391
+ for (const call of entry.calls) {
1392
+ const result = resolveFetchCall(call, apiPathMap, apiRoutes);
1393
+ const methodTag = call.method ?? (call.kind === "fetch" ? "GET?" : "?");
1394
+ if (result.kind === "resolved" && result.nodeId) {
1395
+ const key = `${entry.nodeId}\u2192${result.nodeId}\u2192calls_api`;
1396
+ if (seen.has(key)) continue;
1397
+ seen.add(key);
1398
+ crossRefs.push({
1399
+ source: entry.nodeId,
1400
+ target: result.nodeId,
1401
+ type: "calls_api",
1402
+ layer: "api"
1403
+ });
1404
+ resolvedCount++;
1405
+ continue;
1406
+ }
1407
+ if (result.kind === "dynamic") {
1408
+ dynamicCount++;
1409
+ flaggedEdges.push({
1410
+ source: entry.nodeId,
1411
+ target: "DYNAMIC",
1412
+ type: "calls_api",
1413
+ label: call.isConcat ? `${methodTag} fetch with concat: ${call.url}` : `${methodTag} fetch with template: ${call.url}`,
1414
+ confidence: call.isConcat ? "low" : "medium"
1415
+ });
1416
+ continue;
1417
+ }
1418
+ if (result.kind === "external") {
1419
+ externalCount++;
1420
+ if (!includeExternal) continue;
1421
+ flaggedEdges.push({
1422
+ source: entry.nodeId,
1423
+ target: "EXTERNAL",
1424
+ type: "calls_external",
1425
+ label: `${methodTag} external fetch: ${call.url}`,
1426
+ confidence: "high"
1427
+ });
1428
+ continue;
1429
+ }
1430
+ unresolvedCount++;
1431
+ flaggedEdges.push({
1432
+ source: entry.nodeId,
1433
+ target: "UNRESOLVED",
1434
+ type: "calls_api",
1435
+ label: `${methodTag} fetch to unknown path: ${result.normalizedUrl}`,
1436
+ confidence: "medium"
1437
+ });
1438
+ }
1439
+ }
1440
+ return {
1441
+ cross_refs: crossRefs,
1442
+ flagged_edges: flaggedEdges,
1443
+ warnings: [],
1444
+ patterns: {
1445
+ api_call_detection: {
1446
+ resolved: resolvedCount,
1447
+ dynamic: dynamicCount,
1448
+ unresolved: unresolvedCount,
1449
+ external: externalCount
1450
+ }
1451
+ }
1452
+ };
1453
+ }
1454
+ };
1455
+
1456
+ // src/server/graph/parsers/crosslayer/api-annotations.ts
1457
+ var import_node_fs6 = require("node:fs");
1458
+ var import_node_path6 = require("node:path");
1459
+ var API_ANNOTATION_RE = /@api\s+(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+(\/\S+)/g;
1460
+ function walk3(dir, exts) {
1461
+ if (!(0, import_node_fs6.existsSync)(dir)) return [];
1462
+ const results = [];
1463
+ for (const entry of (0, import_node_fs6.readdirSync)(dir, { withFileTypes: true })) {
1464
+ if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
1465
+ const full = (0, import_node_path6.join)(dir, entry.name);
1466
+ if (entry.isDirectory()) {
1467
+ results.push(...walk3(full, exts));
1468
+ } else if (exts.includes((0, import_node_path6.extname)(entry.name))) {
1469
+ results.push(full);
1470
+ }
1471
+ }
1472
+ return results;
1473
+ }
1474
+ function toNodeId2(srcDir, absPath) {
1475
+ return (0, import_node_path6.relative)(srcDir, absPath).replace(/\\/g, "/");
1476
+ }
1477
+ var apiAnnotationsParser = {
1478
+ id: "api-annotations",
1479
+ layer: "crosslayer",
1480
+ detect(rootDir) {
1481
+ return (0, import_node_fs6.existsSync)((0, import_node_path6.join)(rootDir, "src"));
1482
+ },
1483
+ generate(rootDir, layerOutputs) {
1484
+ const apiOutput = layerOutputs.get("api");
1485
+ if (!apiOutput) {
1486
+ return { cross_refs: [], flagged_edges: [], warnings: [] };
1487
+ }
1488
+ const uiOutput = layerOutputs.get("ui");
1489
+ const uiNodeIds = new Set(uiOutput?.nodes.map((n) => n.id) ?? []);
1490
+ const apiRoutes = loadApiRoutesFromOutput(apiOutput);
1491
+ const apiPathMap = buildApiPathMap(apiRoutes);
1492
+ const srcDir = (0, import_node_path6.join)(rootDir, "src");
1493
+ const files = walk3(srcDir, [".ts", ".tsx"]);
1494
+ const crossRefs = [];
1495
+ const flaggedEdges = [];
1496
+ const seen = /* @__PURE__ */ new Set();
1497
+ for (const absPath of files) {
1498
+ const content = (0, import_node_fs6.readFileSync)(absPath, "utf-8");
1499
+ const sourceId = toNodeId2(srcDir, absPath);
1500
+ if (!uiNodeIds.has(sourceId)) continue;
1501
+ let match;
1502
+ API_ANNOTATION_RE.lastIndex = 0;
1503
+ while ((match = API_ANNOTATION_RE.exec(content)) !== null) {
1504
+ const method = match[1];
1505
+ const urlPath = match[2];
1506
+ const result = resolveUrlPath(urlPath, apiPathMap, apiRoutes);
1507
+ if (result.kind === "resolved" && result.nodeId) {
1508
+ const key = `${sourceId}|${result.nodeId}|calls_api`;
1509
+ if (seen.has(key)) continue;
1510
+ seen.add(key);
1511
+ crossRefs.push({
1512
+ source: sourceId,
1513
+ target: result.nodeId,
1514
+ type: "calls_api",
1515
+ layer: "api"
1516
+ });
1517
+ } else {
1518
+ flaggedEdges.push({
1519
+ source: sourceId,
1520
+ target: "UNRESOLVED",
1521
+ type: "annotation_unresolved",
1522
+ label: `@api ${method} ${urlPath} \u2014 no matching API route found`,
1523
+ confidence: "high"
1524
+ });
1525
+ }
1526
+ }
1527
+ }
1528
+ return {
1529
+ cross_refs: crossRefs,
1530
+ flagged_edges: flaggedEdges,
1531
+ warnings: [],
1532
+ patterns: {
1533
+ annotations_found: crossRefs.length + flaggedEdges.length,
1534
+ annotations_resolved: crossRefs.length,
1535
+ annotations_unresolved: flaggedEdges.length
1536
+ }
1537
+ };
1538
+ }
1539
+ };
1540
+
1541
+ // src/server/graph/parsers/crosslayer/url-literal-scanner.ts
1542
+ var import_node_fs7 = require("node:fs");
1543
+ var import_node_path7 = require("node:path");
1544
+ var URL_LITERAL_RE = /['"`](\/api\/[^'"`\s]+?)['"`]/g;
1545
+ function walk4(dir, exts) {
1546
+ if (!(0, import_node_fs7.existsSync)(dir)) return [];
1547
+ const results = [];
1548
+ for (const entry of (0, import_node_fs7.readdirSync)(dir, { withFileTypes: true })) {
1549
+ if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
1550
+ const full = (0, import_node_path7.join)(dir, entry.name);
1551
+ if (entry.isDirectory()) {
1552
+ results.push(...walk4(full, exts));
1553
+ } else if (exts.includes((0, import_node_path7.extname)(entry.name))) {
1554
+ results.push(full);
1555
+ }
1556
+ }
1557
+ return results;
1558
+ }
1559
+ function toNodeId3(srcDir, absPath) {
1560
+ return (0, import_node_path7.relative)(srcDir, absPath).replace(/\\/g, "/");
1561
+ }
1562
+ var urlLiteralScannerParser = {
1563
+ id: "url-literal-scanner",
1564
+ layer: "crosslayer",
1565
+ detect(rootDir) {
1566
+ return (0, import_node_fs7.existsSync)((0, import_node_path7.join)(rootDir, "src"));
1567
+ },
1568
+ generate(rootDir, layerOutputs) {
1569
+ const apiOutput = layerOutputs.get("api");
1570
+ if (!apiOutput) {
1571
+ return { cross_refs: [], flagged_edges: [], warnings: [] };
1572
+ }
1573
+ const uiOutput = layerOutputs.get("ui");
1574
+ const uiNodeIds = new Set(uiOutput?.nodes.map((n) => n.id) ?? []);
1575
+ const apiRoutes = loadApiRoutesFromOutput(apiOutput);
1576
+ const apiPathMap = buildApiPathMap(apiRoutes);
1577
+ const srcDir = (0, import_node_path7.join)(rootDir, "src");
1578
+ const clientDir = (0, import_node_path7.join)(srcDir, "client");
1579
+ const appDir = (0, import_node_path7.join)(srcDir, "app");
1580
+ const files = [
1581
+ ...walk4(clientDir, [".ts", ".tsx"]),
1582
+ ...walk4(appDir, [".ts", ".tsx"])
1583
+ ];
1584
+ const crossRefs = [];
1585
+ const seen = /* @__PURE__ */ new Set();
1586
+ for (const absPath of files) {
1587
+ const sourceId = toNodeId3(srcDir, absPath);
1588
+ if (!uiNodeIds.has(sourceId)) continue;
1589
+ const content = (0, import_node_fs7.readFileSync)(absPath, "utf-8");
1590
+ let match;
1591
+ URL_LITERAL_RE.lastIndex = 0;
1592
+ while ((match = URL_LITERAL_RE.exec(content)) !== null) {
1593
+ const urlPath = match[1];
1594
+ const result = resolveUrlPath(urlPath, apiPathMap, apiRoutes);
1595
+ if (result.kind === "resolved" && result.nodeId) {
1596
+ const key = `${sourceId}|${result.nodeId}|references_api`;
1597
+ if (seen.has(key)) continue;
1598
+ seen.add(key);
1599
+ crossRefs.push({
1600
+ source: sourceId,
1601
+ target: result.nodeId,
1602
+ type: "references_api",
1603
+ layer: "api"
1604
+ });
1605
+ }
1606
+ }
1607
+ }
1608
+ return {
1609
+ cross_refs: crossRefs,
1610
+ flagged_edges: [],
1611
+ warnings: [],
1612
+ patterns: {
1613
+ url_literals_resolved: crossRefs.length
1614
+ }
1615
+ };
1616
+ }
1617
+ };
1618
+
1619
+ // src/server/graph/core/parser-registry.ts
1620
+ var ParserRegistry = class {
1621
+ constructor() {
1622
+ this.parsers = /* @__PURE__ */ new Map();
1623
+ this.ids = /* @__PURE__ */ new Set();
1624
+ }
1625
+ register(parser) {
1626
+ if (this.ids.has(parser.id)) {
1627
+ throw new Error(`Duplicate parser id: ${parser.id}`);
1628
+ }
1629
+ this.ids.add(parser.id);
1630
+ const list = this.parsers.get(parser.layer) ?? [];
1631
+ list.push(parser);
1632
+ this.parsers.set(parser.layer, list);
1633
+ }
1634
+ getParsers(layer) {
1635
+ return this.parsers.get(layer) ?? [];
1636
+ }
1637
+ getCrossLayerParsers() {
1638
+ return this.parsers.get("crosslayer") ?? [];
1639
+ }
1640
+ getAll() {
1641
+ const all = [];
1642
+ for (const list of this.parsers.values()) all.push(...list);
1643
+ return all;
1644
+ }
1645
+ };
1646
+ function registerBuiltins(registry, disabled) {
1647
+ const builtins = [
1648
+ reactNextjsParser,
1649
+ nextjsRoutesParser,
1650
+ prismaSchemaParser,
1651
+ fetchResolverParser,
1652
+ apiAnnotationsParser,
1653
+ urlLiteralScannerParser
1654
+ ];
1655
+ for (const parser of builtins) {
1656
+ if (disabled.has(parser.id)) continue;
1657
+ registry.register(parser);
1658
+ }
1659
+ }
1660
+ function loadCustomParsers(registry, config, rootDir, disabled) {
1661
+ for (const entry of config.parsers?.custom ?? []) {
1662
+ try {
1663
+ const absPath = (0, import_node_path8.resolve)(rootDir, entry.path);
1664
+ const mod = require(absPath);
1665
+ const parser = "default" in mod ? mod.default : mod;
1666
+ if (disabled.has(parser.id)) continue;
1667
+ if (parser.layer !== entry.layer) {
1668
+ process.stderr.write(
1669
+ `[launch-chart] custom parser "${parser.id}" declares layer "${parser.layer}" but config says "${entry.layer}" \u2014 using parser's layer
1670
+ `
1671
+ );
1672
+ }
1673
+ registry.register(parser);
1674
+ } catch (err) {
1675
+ process.stderr.write(`[launch-chart] failed to load custom parser from ${entry.path}: ${err}
1676
+ `);
1677
+ }
1678
+ }
1679
+ }
1680
+ function createRegistry(config, rootDir) {
1681
+ const registry = new ParserRegistry();
1682
+ const disabled = new Set(config.parsers?.disabled ?? []);
1683
+ registerBuiltins(registry, disabled);
1684
+ loadCustomParsers(registry, config, rootDir, disabled);
1685
+ return registry;
1686
+ }
1687
+
1688
+ // src/server/graph/core/merge.ts
1689
+ function mergeGraphOutputs(outputs, layer) {
1690
+ if (outputs.length === 0) {
1691
+ return {
1692
+ metadata: { generated: (/* @__PURE__ */ new Date()).toISOString(), scope: "", layer },
1693
+ nodes: [],
1694
+ edges: [],
1695
+ cross_refs: [],
1696
+ contradictions: [],
1697
+ warnings: [],
1698
+ flagged_edges: []
1699
+ };
1700
+ }
1701
+ if (outputs.length === 1) return outputs[0];
1702
+ const seenNodes = /* @__PURE__ */ new Set();
1703
+ const seenEdges = /* @__PURE__ */ new Set();
1704
+ const seenCrossRefs = /* @__PURE__ */ new Set();
1705
+ const mergedNodes = [];
1706
+ const mergedEdges = [];
1707
+ const mergedCrossRefs = [];
1708
+ const mergedContradictions = [];
1709
+ const mergedWarnings = [];
1710
+ const mergedFlagged = [];
1711
+ const parserIds = [];
1712
+ for (const output of outputs) {
1713
+ if (output.metadata.parser) {
1714
+ parserIds.push(String(output.metadata.parser));
1715
+ }
1716
+ for (const node of output.nodes) {
1717
+ if (seenNodes.has(node.id)) {
1718
+ mergedWarnings.push({
1719
+ type: "merge_conflict",
1720
+ detail: `Node "${node.id}" produced by multiple parsers; keeping first`
1721
+ });
1722
+ continue;
1723
+ }
1724
+ seenNodes.add(node.id);
1725
+ mergedNodes.push(node);
1726
+ }
1727
+ for (const edge of output.edges) {
1728
+ const key = `${edge.source}|${edge.target}|${edge.type}`;
1729
+ if (seenEdges.has(key)) continue;
1730
+ seenEdges.add(key);
1731
+ mergedEdges.push(edge);
1732
+ }
1733
+ for (const ref of output.cross_refs) {
1734
+ const key = `${ref.source}|${ref.target}|${ref.type}`;
1735
+ if (seenCrossRefs.has(key)) continue;
1736
+ seenCrossRefs.add(key);
1737
+ mergedCrossRefs.push(ref);
1738
+ }
1739
+ mergedContradictions.push(...output.contradictions);
1740
+ mergedWarnings.push(...output.warnings);
1741
+ mergedFlagged.push(...output.flagged_edges);
1742
+ }
1743
+ const metadata = {
1744
+ ...outputs[0].metadata,
1745
+ generated: (/* @__PURE__ */ new Date()).toISOString(),
1746
+ parsers: parserIds
1747
+ };
1748
+ return {
1749
+ metadata,
1750
+ nodes: mergedNodes,
1751
+ edges: mergedEdges,
1752
+ cross_refs: mergedCrossRefs,
1753
+ contradictions: mergedContradictions,
1754
+ warnings: mergedWarnings,
1755
+ flagged_edges: mergedFlagged,
1756
+ patterns: outputs[0].patterns
1757
+ };
1758
+ }
1759
+ function dedupCrossRefs(refs) {
1760
+ const seen = /* @__PURE__ */ new Set();
1761
+ const result = [];
1762
+ for (const ref of refs) {
1763
+ const key = `${ref.source}|${ref.target}|${ref.type}`;
1764
+ if (seen.has(key)) continue;
1765
+ seen.add(key);
1766
+ result.push(ref);
1767
+ }
1768
+ return result;
1769
+ }
1770
+ function applyCrossLayerResults(uiOutput, results, primaryId) {
1771
+ const allCrossRefs = [...uiOutput.cross_refs];
1772
+ const allFlagged = [...uiOutput.flagged_edges];
1773
+ const allWarnings = [...uiOutput.warnings];
1774
+ const primaryResult = results.find((r) => r.parserId === primaryId);
1775
+ const secondaryResults = results.filter((r) => r.parserId !== primaryId);
1776
+ if (primaryResult) {
1777
+ allCrossRefs.push(...primaryResult.output.cross_refs);
1778
+ allFlagged.push(...primaryResult.output.flagged_edges);
1779
+ allWarnings.push(...primaryResult.output.warnings);
1780
+ }
1781
+ const primarySet = new Set(
1782
+ (primaryResult?.output.cross_refs ?? []).map((r) => `${r.source}|${r.target}|${r.type}`)
1783
+ );
1784
+ for (const sec of secondaryResults) {
1785
+ for (const ref of sec.output.cross_refs) {
1786
+ const key = `${ref.source}|${ref.target}|${ref.type}`;
1787
+ if (primarySet.has(key)) {
1788
+ allCrossRefs.push(ref);
1789
+ } else {
1790
+ allFlagged.push({
1791
+ source: ref.source,
1792
+ target: ref.target,
1793
+ type: "out_of_pattern",
1794
+ label: `API call detected by ${sec.parserId} but not by primary (${primaryId})`,
1795
+ confidence: "medium"
1796
+ });
1797
+ allCrossRefs.push(ref);
1798
+ }
1799
+ }
1800
+ allFlagged.push(...sec.output.flagged_edges);
1801
+ allWarnings.push(...sec.output.warnings);
1802
+ }
1803
+ return {
1804
+ ...uiOutput,
1805
+ cross_refs: dedupCrossRefs(allCrossRefs),
1806
+ flagged_edges: allFlagged,
1807
+ warnings: allWarnings
1808
+ };
1809
+ }
1810
+
1376
1811
  // src/server/graph/core/graph-builder.ts
1377
- var ALL_PARSERS = [
1378
- reactNextjsParser,
1379
- nextjsRoutesParser,
1380
- prismaSchemaParser
1381
- ];
1382
- function getParser(layer) {
1383
- return ALL_PARSERS.find((p) => p.layer === layer);
1812
+ function readGraphFromDisk(rootDir, layer) {
1813
+ const filePath = (0, import_node_path9.join)(rootDir, ".launchsecure", "graphs", `${layer}.json`);
1814
+ if (!(0, import_node_fs8.existsSync)(filePath)) return null;
1815
+ try {
1816
+ return JSON.parse((0, import_node_fs8.readFileSync)(filePath, "utf-8"));
1817
+ } catch {
1818
+ return null;
1819
+ }
1384
1820
  }
1385
1821
  function generateLayer(rootDir, layer) {
1386
- const parser = getParser(layer);
1387
- if (!parser) return null;
1388
- if (!parser.detect(rootDir)) return null;
1389
- const output = parser.generate(rootDir);
1822
+ const config = loadConfig(rootDir);
1823
+ const registry = createRegistry(config, rootDir);
1824
+ const parsers = registry.getParsers(layer);
1825
+ const outputs = [];
1826
+ for (const parser of parsers) {
1827
+ if (!parser.detect(rootDir)) continue;
1828
+ outputs.push(parser.generate(rootDir));
1829
+ }
1830
+ if (outputs.length === 0) return null;
1831
+ let merged = outputs.length === 1 ? outputs[0] : mergeGraphOutputs(outputs, layer);
1832
+ if (layer === "ui") {
1833
+ const layerOutputs = /* @__PURE__ */ new Map();
1834
+ layerOutputs.set("ui", merged);
1835
+ for (const otherLayer of ["api", "db"]) {
1836
+ const existing = readGraphFromDisk(rootDir, otherLayer);
1837
+ if (existing) layerOutputs.set(otherLayer, existing);
1838
+ }
1839
+ const crossParsers = registry.getCrossLayerParsers();
1840
+ const primaryId = config.parsers?.primary?.crosslayer ?? crossParsers[0]?.id ?? null;
1841
+ const crossResults = crossParsers.filter((p) => p.detect(rootDir)).map((p) => ({ parserId: p.id, output: p.generate(rootDir, layerOutputs) }));
1842
+ if (crossResults.length > 0) {
1843
+ merged = applyCrossLayerResults(merged, crossResults, primaryId);
1844
+ }
1845
+ }
1390
1846
  return {
1391
1847
  layer,
1392
- output,
1393
- nodeCount: output.nodes.length,
1394
- edgeCount: output.edges.length
1848
+ output: merged,
1849
+ nodeCount: merged.nodes.length,
1850
+ edgeCount: merged.edges.length
1395
1851
  };
1396
1852
  }
1397
1853
  function generateAll(rootDir) {
1398
- const layers = ["api", "db", "ui"];
1854
+ const config = loadConfig(rootDir);
1855
+ const registry = createRegistry(config, rootDir);
1856
+ const layerOrder = ["api", "db", "ui"];
1857
+ const layerOutputs = /* @__PURE__ */ new Map();
1399
1858
  const results = [];
1400
- for (const layer of layers) {
1401
- const result = generateLayer(rootDir, layer);
1402
- if (result) results.push(result);
1859
+ for (const layer of layerOrder) {
1860
+ const parsers = registry.getParsers(layer);
1861
+ const outputs = [];
1862
+ for (const parser of parsers) {
1863
+ if (!parser.detect(rootDir)) continue;
1864
+ outputs.push(parser.generate(rootDir));
1865
+ }
1866
+ if (outputs.length === 0) continue;
1867
+ const merged = outputs.length === 1 ? outputs[0] : mergeGraphOutputs(outputs, layer);
1868
+ layerOutputs.set(layer, merged);
1869
+ results.push({
1870
+ layer,
1871
+ output: merged,
1872
+ nodeCount: merged.nodes.length,
1873
+ edgeCount: merged.edges.length
1874
+ });
1875
+ }
1876
+ const crossParsers = registry.getCrossLayerParsers();
1877
+ const primaryId = config.parsers?.primary?.crosslayer ?? crossParsers[0]?.id ?? null;
1878
+ const crossResults = crossParsers.filter((p) => p.detect(rootDir)).map((p) => ({ parserId: p.id, output: p.generate(rootDir, layerOutputs) }));
1879
+ if (crossResults.length > 0 && layerOutputs.has("ui")) {
1880
+ const uiOutput = layerOutputs.get("ui");
1881
+ const merged = applyCrossLayerResults(uiOutput, crossResults, primaryId);
1882
+ layerOutputs.set("ui", merged);
1883
+ const uiResult = results.find((r) => r.layer === "ui");
1884
+ if (uiResult) {
1885
+ uiResult.output = merged;
1886
+ uiResult.nodeCount = merged.nodes.length;
1887
+ uiResult.edgeCount = merged.edges.length;
1888
+ }
1403
1889
  }
1404
1890
  const byLayer = new Map(results.map((r) => [r.layer, r]));
1405
1891
  return ["ui", "api", "db"].map((l) => byLayer.get(l)).filter((r) => !!r);
@@ -1410,23 +1896,23 @@ var GRAPHS_DIR = ".launchsecure/graphs";
1410
1896
  var LAYERS = ["ui", "api", "db"];
1411
1897
  var graphCache = /* @__PURE__ */ new Map();
1412
1898
  function graphsDir(rootDir) {
1413
- return (0, import_node_path5.join)(rootDir, GRAPHS_DIR);
1899
+ return (0, import_node_path10.join)(rootDir, GRAPHS_DIR);
1414
1900
  }
1415
1901
  function graphFilePath(rootDir, layer) {
1416
- return (0, import_node_path5.join)(graphsDir(rootDir), `${layer}.json`);
1902
+ return (0, import_node_path10.join)(graphsDir(rootDir), `${layer}.json`);
1417
1903
  }
1418
1904
  function invalidateCache(filePath) {
1419
1905
  graphCache.delete(filePath);
1420
1906
  }
1421
1907
  function readGraph(rootDir, layer) {
1422
1908
  const filePath = graphFilePath(rootDir, layer);
1423
- if (!(0, import_node_fs5.existsSync)(filePath)) return null;
1424
- const stat = (0, import_node_fs5.statSync)(filePath);
1909
+ if (!(0, import_node_fs9.existsSync)(filePath)) return null;
1910
+ const stat = (0, import_node_fs9.statSync)(filePath);
1425
1911
  const cached = graphCache.get(filePath);
1426
1912
  if (cached && cached.mtimeMs === stat.mtimeMs) {
1427
1913
  return cached.graph;
1428
1914
  }
1429
- const content = (0, import_node_fs5.readFileSync)(filePath, "utf-8");
1915
+ const content = (0, import_node_fs9.readFileSync)(filePath, "utf-8");
1430
1916
  const graph = JSON.parse(content);
1431
1917
  graphCache.set(filePath, { mtimeMs: stat.mtimeMs, graph });
1432
1918
  return graph;
@@ -1441,11 +1927,11 @@ function readAllGraphs(rootDir) {
1441
1927
  }
1442
1928
  function generateGraph(rootDir, layer) {
1443
1929
  const dir = graphsDir(rootDir);
1444
- (0, import_node_fs5.mkdirSync)(dir, { recursive: true });
1930
+ (0, import_node_fs9.mkdirSync)(dir, { recursive: true });
1445
1931
  const results = layer ? [generateLayer(rootDir, layer)].filter((r) => r !== null) : generateAll(rootDir);
1446
1932
  for (const result of results) {
1447
1933
  const filePath = graphFilePath(rootDir, result.layer);
1448
- (0, import_node_fs5.writeFileSync)(filePath, JSON.stringify(result.output, null, 2) + "\n", "utf-8");
1934
+ (0, import_node_fs9.writeFileSync)(filePath, JSON.stringify(result.output, null, 2) + "\n", "utf-8");
1449
1935
  invalidateCache(filePath);
1450
1936
  }
1451
1937
  return results;
@@ -1453,20 +1939,20 @@ function generateGraph(rootDir, layer) {
1453
1939
 
1454
1940
  // src/server/lockfile.ts
1455
1941
  var import_node_child_process = require("node:child_process");
1456
- var import_node_fs6 = require("node:fs");
1942
+ var import_node_fs10 = require("node:fs");
1457
1943
  var import_node_os = require("node:os");
1458
- var import_node_path6 = require("node:path");
1944
+ var import_node_path11 = require("node:path");
1459
1945
  function lockDir() {
1460
- return (0, import_node_path6.join)((0, import_node_os.homedir)(), ".launchsecure");
1946
+ return (0, import_node_path11.join)((0, import_node_os.homedir)(), ".launchsecure");
1461
1947
  }
1462
1948
  function lockPath() {
1463
- return (0, import_node_path6.join)(lockDir(), "launch-chart.lock");
1949
+ return (0, import_node_path11.join)(lockDir(), "launch-chart.lock");
1464
1950
  }
1465
1951
  function readLock() {
1466
1952
  const p = lockPath();
1467
- if (!(0, import_node_fs6.existsSync)(p)) return null;
1953
+ if (!(0, import_node_fs10.existsSync)(p)) return null;
1468
1954
  try {
1469
- const data = JSON.parse((0, import_node_fs6.readFileSync)(p, "utf-8"));
1955
+ const data = JSON.parse((0, import_node_fs10.readFileSync)(p, "utf-8"));
1470
1956
  if (typeof data.pid !== "number" || typeof data.port !== "number") return null;
1471
1957
  return data;
1472
1958
  } catch {
@@ -1502,7 +1988,7 @@ function getLiveLock() {
1502
1988
  const live = listenerPid !== null ? listenerPid === lock.pid : isPidAlive(lock.pid);
1503
1989
  if (!live) {
1504
1990
  try {
1505
- (0, import_node_fs6.unlinkSync)(lockPath());
1991
+ (0, import_node_fs10.unlinkSync)(lockPath());
1506
1992
  } catch {
1507
1993
  }
1508
1994
  return null;
@@ -1510,12 +1996,12 @@ function getLiveLock() {
1510
1996
  return lock;
1511
1997
  }
1512
1998
  function writeLock(data) {
1513
- (0, import_node_fs6.mkdirSync)(lockDir(), { recursive: true });
1514
- (0, import_node_fs6.writeFileSync)(lockPath(), JSON.stringify(data, null, 2) + "\n", "utf-8");
1999
+ (0, import_node_fs10.mkdirSync)(lockDir(), { recursive: true });
2000
+ (0, import_node_fs10.writeFileSync)(lockPath(), JSON.stringify(data, null, 2) + "\n", "utf-8");
1515
2001
  }
1516
2002
  function clearLock() {
1517
2003
  try {
1518
- (0, import_node_fs6.unlinkSync)(lockPath());
2004
+ (0, import_node_fs10.unlinkSync)(lockPath());
1519
2005
  } catch {
1520
2006
  }
1521
2007
  }
@@ -1537,16 +2023,16 @@ var MIME_TYPES = {
1537
2023
  function findProjectRoot(startDir) {
1538
2024
  let dir = startDir;
1539
2025
  for (let i = 0; i < 8; i++) {
1540
- const graphsDir2 = import_node_path7.default.join(dir, ".launchsecure", "graphs");
1541
- 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;
1542
- const parent = import_node_path7.default.dirname(dir);
2026
+ const graphsDir2 = import_node_path12.default.join(dir, ".launchsecure", "graphs");
2027
+ if (import_node_fs11.default.existsSync(import_node_path12.default.join(graphsDir2, "ui.json")) || import_node_fs11.default.existsSync(import_node_path12.default.join(graphsDir2, "api.json")) || import_node_fs11.default.existsSync(import_node_path12.default.join(graphsDir2, "db.json"))) return dir;
2028
+ const parent = import_node_path12.default.dirname(dir);
1543
2029
  if (parent === dir) break;
1544
2030
  dir = parent;
1545
2031
  }
1546
2032
  dir = startDir;
1547
2033
  for (let i = 0; i < 8; i++) {
1548
- if (import_node_fs7.default.existsSync(import_node_path7.default.join(dir, ".git"))) return dir;
1549
- const parent = import_node_path7.default.dirname(dir);
2034
+ if (import_node_fs11.default.existsSync(import_node_path12.default.join(dir, ".git"))) return dir;
2035
+ const parent = import_node_path12.default.dirname(dir);
1550
2036
  if (parent === dir) break;
1551
2037
  dir = parent;
1552
2038
  }
@@ -1598,16 +2084,16 @@ function buildMergedGraph(projectRoot) {
1598
2084
  };
1599
2085
  }
1600
2086
  function serveStatic(res, filePath) {
1601
- if (!import_node_fs7.default.existsSync(filePath) || !import_node_fs7.default.statSync(filePath).isFile()) return false;
1602
- const ext = import_node_path7.default.extname(filePath).toLowerCase();
2087
+ if (!import_node_fs11.default.existsSync(filePath) || !import_node_fs11.default.statSync(filePath).isFile()) return false;
2088
+ const ext = import_node_path12.default.extname(filePath).toLowerCase();
1603
2089
  const mime = MIME_TYPES[ext] ?? "application/octet-stream";
1604
2090
  res.writeHead(200, { "Content-Type": mime, "Cache-Control": "no-cache" });
1605
- import_node_fs7.default.createReadStream(filePath).pipe(res);
2091
+ import_node_fs11.default.createReadStream(filePath).pipe(res);
1606
2092
  return true;
1607
2093
  }
1608
2094
  function serveIndex(res, clientDir) {
1609
- const indexPath = import_node_path7.default.join(clientDir, "index.html");
1610
- if (!import_node_fs7.default.existsSync(indexPath)) {
2095
+ const indexPath = import_node_path12.default.join(clientDir, "index.html");
2096
+ if (!import_node_fs11.default.existsSync(indexPath)) {
1611
2097
  res.writeHead(500, { "Content-Type": "text/plain" });
1612
2098
  res.end(`LaunchChart client bundle not found at ${clientDir}. Run 'npm run build:chart-client'.`);
1613
2099
  return;
@@ -1615,14 +2101,14 @@ function serveIndex(res, clientDir) {
1615
2101
  serveStatic(res, indexPath);
1616
2102
  }
1617
2103
  function tryListen(server, port) {
1618
- return new Promise((resolve, reject) => {
2104
+ return new Promise((resolve2, reject) => {
1619
2105
  const onError = (err) => {
1620
2106
  server.off("listening", onListening);
1621
2107
  reject(err);
1622
2108
  };
1623
2109
  const onListening = () => {
1624
2110
  server.off("error", onError);
1625
- resolve(port);
2111
+ resolve2(port);
1626
2112
  };
1627
2113
  server.once("error", onError);
1628
2114
  server.once("listening", onListening);
@@ -1659,7 +2145,7 @@ async function startChartServer(opts = {}) {
1659
2145
  }
1660
2146
  return { port: existing.port, url: existing.url };
1661
2147
  }
1662
- const clientDir = opts.clientDir ?? import_node_path7.default.join(__dirname, "..", "chart-client");
2148
+ const clientDir = opts.clientDir ?? import_node_path12.default.join(__dirname, "..", "chart-client");
1663
2149
  const server = import_node_http.default.createServer((req, res) => {
1664
2150
  try {
1665
2151
  const url2 = new URL(req.url ?? "/", `http://${req.headers.host}`);
@@ -1702,8 +2188,43 @@ async function startChartServer(opts = {}) {
1702
2188
  res.end(JSON.stringify({ ok: true, projectRoot }));
1703
2189
  return;
1704
2190
  }
2191
+ if (req.method === "GET" && url2.pathname === "/api/parser-config") {
2192
+ const config = loadConfig(projectRoot);
2193
+ const detection = [
2194
+ { id: "react-nextjs", layer: "ui", label: "React + Next.js", detected: reactNextjsParser.detect(projectRoot) },
2195
+ { id: "nextjs-routes", layer: "api", label: "Next.js API Routes", detected: nextjsRoutesParser.detect(projectRoot) },
2196
+ { id: "prisma-schema", layer: "db", label: "Prisma Schema", detected: prismaSchemaParser.detect(projectRoot) }
2197
+ ];
2198
+ const crosslayerParsers = [
2199
+ { id: "fetch-resolver", label: "Fetch / api.method() calls" },
2200
+ { id: "api-annotations", label: "@api annotations" },
2201
+ { id: "url-literal-scanner", label: "/api/... URL literals" }
2202
+ ];
2203
+ res.writeHead(200, { "Content-Type": "application/json" });
2204
+ res.end(JSON.stringify({ config, detection, crosslayerParsers }));
2205
+ return;
2206
+ }
2207
+ if (req.method === "POST" && url2.pathname === "/api/parser-config") {
2208
+ let body = "";
2209
+ req.on("data", (chunk) => {
2210
+ body += chunk.toString();
2211
+ });
2212
+ req.on("end", () => {
2213
+ try {
2214
+ const newConfig = JSON.parse(body);
2215
+ const configPath = import_node_path12.default.join(projectRoot, ".launchchart.json");
2216
+ import_node_fs11.default.writeFileSync(configPath, JSON.stringify(newConfig, null, 2) + "\n", "utf-8");
2217
+ res.writeHead(200, { "Content-Type": "application/json" });
2218
+ res.end(JSON.stringify({ ok: true }));
2219
+ } catch (err) {
2220
+ res.writeHead(400, { "Content-Type": "application/json" });
2221
+ res.end(JSON.stringify({ ok: false, error: String(err) }));
2222
+ }
2223
+ });
2224
+ return;
2225
+ }
1705
2226
  if (url2.pathname !== "/") {
1706
- const staticPath = import_node_path7.default.join(clientDir, url2.pathname);
2227
+ const staticPath = import_node_path12.default.join(clientDir, url2.pathname);
1707
2228
  if (serveStatic(res, staticPath)) return;
1708
2229
  }
1709
2230
  serveIndex(res, clientDir);