@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.
@@ -5,6 +5,9 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
6
  var __getProtoOf = Object.getPrototypeOf;
7
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __esm = (fn, res) => function __init() {
9
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
10
+ };
8
11
  var __export = (target, all) => {
9
12
  for (var name in all)
10
13
  __defProp(target, name, { get: all[name], enumerable: true });
@@ -27,6 +30,30 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
27
30
  ));
28
31
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
32
 
33
+ // src/server/graph/core/config.ts
34
+ var config_exports = {};
35
+ __export(config_exports, {
36
+ loadConfig: () => loadConfig
37
+ });
38
+ function loadConfig(rootDir) {
39
+ const configPath = (0, import_node_path.join)(rootDir, CONFIG_FILENAME);
40
+ if (!(0, import_node_fs.existsSync)(configPath)) return {};
41
+ try {
42
+ return JSON.parse((0, import_node_fs.readFileSync)(configPath, "utf-8"));
43
+ } catch {
44
+ return {};
45
+ }
46
+ }
47
+ var import_node_fs, import_node_path, CONFIG_FILENAME;
48
+ var init_config = __esm({
49
+ "src/server/graph/core/config.ts"() {
50
+ "use strict";
51
+ import_node_fs = require("node:fs");
52
+ import_node_path = require("node:path");
53
+ CONFIG_FILENAME = ".launchchart.json";
54
+ }
55
+ });
56
+
30
57
  // src/server/chart-serve.ts
31
58
  var chart_serve_exports = {};
32
59
  __export(chart_serve_exports, {
@@ -35,20 +62,28 @@ __export(chart_serve_exports, {
35
62
  });
36
63
  module.exports = __toCommonJS(chart_serve_exports);
37
64
  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"));
65
+ var import_node_fs13 = __toESM(require("node:fs"));
66
+ var import_node_path15 = __toESM(require("node:path"));
40
67
 
41
68
  // src/server/graph/index.ts
42
- var import_node_fs5 = require("node:fs");
43
- var import_node_path5 = require("node:path");
69
+ var import_node_fs11 = require("node:fs");
70
+ var import_node_path13 = require("node:path");
71
+
72
+ // src/server/graph/core/graph-builder.ts
73
+ var import_node_fs8 = require("node:fs");
74
+ var import_node_path9 = require("node:path");
75
+ init_config();
76
+
77
+ // src/server/graph/core/parser-registry.ts
78
+ var import_node_path8 = require("node:path");
44
79
 
45
80
  // src/server/graph/parsers/ui/react-nextjs.ts
46
- var import_node_fs2 = require("node:fs");
47
- var import_node_path2 = require("node:path");
81
+ var import_node_fs3 = require("node:fs");
82
+ var import_node_path3 = require("node:path");
48
83
 
49
84
  // src/server/graph/core/ast-helpers.ts
50
- var import_node_fs = require("node:fs");
51
- var import_node_path = require("node:path");
85
+ var import_node_fs2 = require("node:fs");
86
+ var import_node_path2 = require("node:path");
52
87
  var tsModule;
53
88
  function getTs() {
54
89
  if (!tsModule) {
@@ -59,8 +94,8 @@ function getTs() {
59
94
  var HTTP_METHODS = /* @__PURE__ */ new Set(["get", "post", "put", "patch", "delete", "head", "options"]);
60
95
  function parseFile(absPath) {
61
96
  const ts = getTs();
62
- const content = (0, import_node_fs.readFileSync)(absPath, "utf-8");
63
- const ext = (0, import_node_path.extname)(absPath);
97
+ const content = (0, import_node_fs2.readFileSync)(absPath, "utf-8");
98
+ const ext = (0, import_node_path2.extname)(absPath);
64
99
  const scriptKind = ext === ".tsx" ? ts.ScriptKind.TSX : ext === ".ts" ? ts.ScriptKind.TS : ext === ".jsx" ? ts.ScriptKind.JSX : ts.ScriptKind.JS;
65
100
  const sourceFile = ts.createSourceFile(
66
101
  absPath,
@@ -338,8 +373,8 @@ var MUTATION_METHODS = /* @__PURE__ */ new Set([
338
373
  ]);
339
374
  function extractDbCalls(absPath) {
340
375
  const ts = getTs();
341
- const content = (0, import_node_fs.readFileSync)(absPath, "utf-8");
342
- const ext = (0, import_node_path.extname)(absPath);
376
+ const content = (0, import_node_fs2.readFileSync)(absPath, "utf-8");
377
+ const ext = (0, import_node_path2.extname)(absPath);
343
378
  const scriptKind = ext === ".tsx" ? ts.ScriptKind.TSX : ts.ScriptKind.TS;
344
379
  const sourceFile = ts.createSourceFile(absPath, content, ts.ScriptTarget.Latest, true, scriptKind);
345
380
  const calls = [];
@@ -368,8 +403,8 @@ function extractDbCalls(absPath) {
368
403
  }
369
404
  function extractAuthWrappers(absPath) {
370
405
  const ts = getTs();
371
- const content = (0, import_node_fs.readFileSync)(absPath, "utf-8");
372
- const ext = (0, import_node_path.extname)(absPath);
406
+ const content = (0, import_node_fs2.readFileSync)(absPath, "utf-8");
407
+ const ext = (0, import_node_path2.extname)(absPath);
373
408
  const scriptKind = ext === ".tsx" ? ts.ScriptKind.TSX : ts.ScriptKind.TS;
374
409
  const sourceFile = ts.createSourceFile(absPath, content, ts.ScriptTarget.Latest, true, scriptKind);
375
410
  const wrappers = /* @__PURE__ */ new Set();
@@ -390,12 +425,12 @@ function extractAuthWrappers(absPath) {
390
425
  var RENDER_TYPES = /* @__PURE__ */ new Set(["component", "ui", "layout", "context"]);
391
426
  function walk(dir, exts) {
392
427
  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);
428
+ if (!(0, import_node_fs3.existsSync)(dir)) return results;
429
+ for (const entry of (0, import_node_fs3.readdirSync)(dir, { withFileTypes: true })) {
430
+ const full = (0, import_node_path3.join)(dir, entry.name);
396
431
  if (entry.isDirectory()) {
397
432
  results.push(...walk(full, exts));
398
- } else if (exts.includes((0, import_node_path2.extname)(entry.name))) {
433
+ } else if (exts.includes((0, import_node_path3.extname)(entry.name))) {
399
434
  results.push(full);
400
435
  }
401
436
  }
@@ -403,33 +438,33 @@ function walk(dir, exts) {
403
438
  }
404
439
  function walkWithIgnore(dir, exts, ignoreDirs) {
405
440
  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 })) {
441
+ if (!(0, import_node_fs3.existsSync)(dir)) return results;
442
+ for (const entry of (0, import_node_fs3.readdirSync)(dir, { withFileTypes: true })) {
408
443
  if (entry.isDirectory()) {
409
444
  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));
445
+ results.push(...walkWithIgnore((0, import_node_path3.join)(dir, entry.name), exts, ignoreDirs));
446
+ } else if (exts.includes((0, import_node_path3.extname)(entry.name))) {
447
+ results.push((0, import_node_path3.join)(dir, entry.name));
413
448
  }
414
449
  }
415
450
  return results;
416
451
  }
417
452
  function toNodeId(srcDir, absPath) {
418
- return (0, import_node_path2.relative)(srcDir, absPath).replace(/\\/g, "/");
453
+ return (0, import_node_path3.relative)(srcDir, absPath).replace(/\\/g, "/");
419
454
  }
420
455
  function resolveImport(srcDir, specifier) {
421
456
  if (!specifier.startsWith("@/")) return null;
422
457
  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;
458
+ const base = (0, import_node_path3.join)(srcDir, rel);
459
+ 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")]) {
460
+ if ((0, import_node_fs3.existsSync)(c) && (0, import_node_fs3.statSync)(c).isFile()) return c;
426
461
  }
427
462
  return null;
428
463
  }
429
464
  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;
465
+ const base = (0, import_node_path3.join)((0, import_node_path3.dirname)(fromFile), specifier);
466
+ 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")]) {
467
+ if ((0, import_node_fs3.existsSync)(c) && (0, import_node_fs3.statSync)(c).isFile()) return c;
433
468
  }
434
469
  return null;
435
470
  }
@@ -450,7 +485,7 @@ function resolveBarrelMap(barrelAbsPath, parsedByPath, memo, visiting) {
450
485
  const resolved = resolveRelativeImport(barrelAbsPath, re.from);
451
486
  if (!resolved) continue;
452
487
  if (re.isWildcard) {
453
- const targetBn = (0, import_node_path2.basename)(resolved);
488
+ const targetBn = (0, import_node_path3.basename)(resolved);
454
489
  const targetIsBarrel = targetBn === "index.ts" || targetBn === "index.tsx";
455
490
  if (targetIsBarrel) {
456
491
  const nested = resolveBarrelMap(resolved, parsedByPath, memo, visiting);
@@ -477,12 +512,12 @@ function buildAllBarrelMaps(srcDir, parsedByPath) {
477
512
  const barrels = /* @__PURE__ */ new Map();
478
513
  const memo = /* @__PURE__ */ new Map();
479
514
  for (const [absPath, parsed] of parsedByPath) {
480
- const bn = (0, import_node_path2.basename)(absPath);
515
+ const bn = (0, import_node_path3.basename)(absPath);
481
516
  if (bn !== "index.ts" && bn !== "index.tsx") continue;
482
517
  if (parsed.reExports.length === 0) continue;
483
518
  const map = resolveBarrelMap(absPath, parsedByPath, memo, /* @__PURE__ */ new Set());
484
519
  if (map.size > 0) {
485
- const barrelId = (0, import_node_path2.relative)(srcDir, (0, import_node_path2.dirname)(absPath)).replace(/\\/g, "/");
520
+ const barrelId = (0, import_node_path3.relative)(srcDir, (0, import_node_path3.dirname)(absPath)).replace(/\\/g, "/");
486
521
  barrels.set(barrelId, map);
487
522
  }
488
523
  }
@@ -503,34 +538,6 @@ function classifyType(id) {
503
538
  if (id.startsWith("lib/") || id.startsWith("config/")) return "lib";
504
539
  return "component";
505
540
  }
506
- function classifyModule(id) {
507
- if (/app\/\(auth\)\//.test(id)) return "auth";
508
- if (/app\/\(admin\)\//.test(id)) return "admin";
509
- if (/app\/\(settings\)\//.test(id)) return "settings";
510
- if (/app\/\(app\)\/\[orgSlug\]\/\(project-pages\)\//.test(id)) return "project";
511
- if (/app\/\(app\)\/\[orgSlug\]\/\(org-pages\)\//.test(id)) return "org";
512
- if (/app\/\(app\)\/\[orgSlug\]\//.test(id)) return "org";
513
- if (id.startsWith("app/integrations/")) return "integrations";
514
- if (id.startsWith("app/docs/")) return "admin";
515
- if (id.startsWith("client/components/ui/")) return "shared-ui";
516
- if (id.startsWith("client/components/layout/") || /client\/lib\/navigation/.test(id)) return "layout";
517
- 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";
518
- if (/client\/components\/prd-/.test(id) || /client\/hooks\/use-admin/.test(id)) return "admin";
519
- if (/client\/components\/org-/.test(id) || /client\/hooks\/use-org-/.test(id) || /client\/hooks\/use-provider-def/.test(id)) return "org";
520
- 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";
521
- if (/client\/hooks\/use-(profile|sessions|organizations|notification)/.test(id)) return "settings";
522
- if (id.startsWith("server/auth/")) return "auth";
523
- if (id.startsWith("server/mcp/")) return "mcp";
524
- if (id.startsWith("server/lib/")) return "server-lib";
525
- if (id.startsWith("server/middleware")) return "middleware";
526
- if (id.startsWith("server/services/")) return "services";
527
- if (id.startsWith("server/db")) return "db";
528
- if (id.startsWith("server/errors")) return "errors";
529
- if (id.startsWith("server/")) return "server-lib";
530
- if (id.startsWith("config/")) return "config";
531
- if (id.startsWith("lib/")) return "lib";
532
- return "root";
533
- }
534
541
  function extractRoute(id) {
535
542
  if (!id.endsWith("/page.tsx")) return null;
536
543
  let route = id.replace(/^app\//, "/").replace(/\/page\.tsx$/, "");
@@ -541,7 +548,7 @@ function extractRoute(id) {
541
548
  return route || "/";
542
549
  }
543
550
  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());
551
+ 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
552
  }
546
553
  function resolveTemplateLiteralRoute(template, routeToNodeId) {
547
554
  const parameterized = template.replace(/\$\{([^}]+)\}/g, (_, expr) => {
@@ -622,105 +629,6 @@ function matchRouteToPage(route, routeToNodeId) {
622
629
  if (routeToNodeId.has(normalized)) return routeToNodeId.get(normalized);
623
630
  return null;
624
631
  }
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
632
  function extractEdges(srcDir, absPath, sourceId, parsed, nodeIdSet, nodeTypeMap, barrelMaps, routeToNodeId) {
725
633
  const edges = [];
726
634
  const flagged = [];
@@ -820,26 +728,26 @@ function extractEdges(srcDir, absPath, sourceId, parsed, nodeIdSet, nodeTypeMap,
820
728
  return { edges, flagged };
821
729
  }
822
730
  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"));
731
+ 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
732
  }
825
733
  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"
734
+ const srcDir = (0, import_node_path3.join)(rootDir, "src");
735
+ const appFiles = walk((0, import_node_path3.join)(srcDir, "app"), [".tsx", ".ts"]).filter(
736
+ (f) => (0, import_node_path3.basename)(f) !== "route.ts" && (0, import_node_path3.basename)(f) !== "route.tsx"
829
737
  );
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"
738
+ const clientFiles = walk((0, import_node_path3.join)(srcDir, "client"), [".tsx", ".ts"]);
739
+ const serverFiles = walk((0, import_node_path3.join)(srcDir, "server"), [".ts", ".tsx"]).filter(
740
+ (f) => (0, import_node_path3.basename)(f) !== "route.ts" && (0, import_node_path3.basename)(f) !== "route.tsx"
833
741
  );
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"]);
742
+ const libFiles = walk((0, import_node_path3.join)(srcDir, "lib"), [".ts", ".tsx"]);
743
+ const configFiles = walk((0, import_node_path3.join)(srcDir, "config"), [".ts", ".tsx"]);
836
744
  const allDiscovered = [...appFiles, ...clientFiles, ...serverFiles, ...libFiles, ...configFiles];
837
745
  const parsedByPath = /* @__PURE__ */ new Map();
838
746
  for (const absPath of allDiscovered) {
839
747
  parsedByPath.set(absPath, parseFile(absPath));
840
748
  }
841
749
  const barrelMaps = buildAllBarrelMaps(srcDir, parsedByPath);
842
- const fileSet = allDiscovered.filter((f) => !(0, import_node_path2.basename)(f).startsWith("index."));
750
+ const fileSet = allDiscovered.filter((f) => !(0, import_node_path3.basename)(f).startsWith("index."));
843
751
  const nodes = [];
844
752
  const nodeIdSet = /* @__PURE__ */ new Set();
845
753
  const nodeTypeMap = /* @__PURE__ */ new Map();
@@ -850,15 +758,13 @@ function generate(rootDir) {
850
758
  const parsed = parsedByPath.get(absPath);
851
759
  const name = parsed.name || nameFromFilename(absPath);
852
760
  const route = extractRoute(id);
853
- const module_ = classifyModule(id);
854
- nodes.push({ id, type, name, route, module: module_, exports: parsed.exports });
761
+ nodes.push({ id, type, name, route, exports: parsed.exports });
855
762
  nodeIdSet.add(id);
856
763
  nodeTypeMap.set(id, type);
857
764
  if (route) routeToNodeId.set(route, id);
858
765
  }
859
766
  const allEdges = [];
860
767
  const allFlagged = [];
861
- const crossRefs = [];
862
768
  for (const absPath of fileSet) {
863
769
  const sourceId = toNodeId(srcDir, absPath);
864
770
  const parsed = parsedByPath.get(absPath);
@@ -875,66 +781,21 @@ function generate(rootDir) {
875
781
  allEdges.push(...edges);
876
782
  allFlagged.push(...flagged);
877
783
  }
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;
784
+ const fetchCallEntries = [];
886
785
  for (const absPath of fileSet) {
887
786
  const sourceId = toNodeId(srcDir, absPath);
888
787
  const parsed = parsedByPath.get(absPath);
889
788
  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
- }
789
+ fetchCallEntries.push({
790
+ nodeId: sourceId,
791
+ calls: parsed.fetchCalls.map((c) => ({
792
+ url: c.url,
793
+ method: c.method,
794
+ isTemplate: c.isTemplate,
795
+ isConcat: c.isConcat,
796
+ kind: c.kind
797
+ }))
798
+ });
938
799
  }
939
800
  const externalScanned = new Set(allDiscovered.map((f) => f.replace(/\\/g, "/")));
940
801
  const IGNORE_DIRS = /* @__PURE__ */ new Set([
@@ -960,7 +821,7 @@ function generate(rootDir) {
960
821
  } catch {
961
822
  continue;
962
823
  }
963
- const externalId = (0, import_node_path2.relative)(rootDir, absPath).replace(/\\/g, "/");
824
+ const externalId = (0, import_node_path3.relative)(rootDir, absPath).replace(/\\/g, "/");
964
825
  const edgesFromThis = [];
965
826
  const seen = /* @__PURE__ */ new Set();
966
827
  for (const imp of parsed.imports) {
@@ -1001,7 +862,6 @@ function generate(rootDir) {
1001
862
  type: "external",
1002
863
  name: parsed.name || nameFromFilename(absPath),
1003
864
  route: null,
1004
- module: "external",
1005
865
  exports: parsed.exports
1006
866
  });
1007
867
  nodeIdSet.add(externalId);
@@ -1051,20 +911,11 @@ function generate(rootDir) {
1051
911
  layer: "ui",
1052
912
  parser: "react-nextjs-ast",
1053
913
  ...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
914
  notes: "Auto-generated via TypeScript AST \u2014 edges derived from actual imports, renders from JSX usage, navigations from router/Link calls."
1064
915
  },
1065
916
  nodes,
1066
917
  edges: allEdges,
1067
- cross_refs: crossRefs,
918
+ cross_refs: [],
1068
919
  contradictions: [],
1069
920
  warnings: [],
1070
921
  flagged_edges: dedupedFlagged,
@@ -1075,7 +926,8 @@ function generate(rootDir) {
1075
926
  renders: allEdges.filter((e) => e.type === "renders").length,
1076
927
  imports: allEdges.filter((e) => e.type === "imports").length,
1077
928
  navigates: allEdges.filter((e) => e.type === "navigates").length
1078
- }
929
+ },
930
+ fetch_calls: fetchCallEntries
1079
931
  }
1080
932
  };
1081
933
  }
@@ -1087,14 +939,14 @@ var reactNextjsParser = {
1087
939
  };
1088
940
 
1089
941
  // src/server/graph/parsers/api/nextjs-routes.ts
1090
- var import_node_fs3 = require("node:fs");
1091
- var import_node_path3 = require("node:path");
942
+ var import_node_fs4 = require("node:fs");
943
+ var import_node_path4 = require("node:path");
1092
944
  var HTTP_METHODS2 = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"]);
1093
945
  function walk2(dir) {
1094
946
  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);
947
+ if (!(0, import_node_fs4.existsSync)(dir)) return results;
948
+ for (const entry of (0, import_node_fs4.readdirSync)(dir, { withFileTypes: true })) {
949
+ const full = (0, import_node_path4.join)(dir, entry.name);
1098
950
  if (entry.isDirectory()) {
1099
951
  results.push(...walk2(full));
1100
952
  } else if (entry.name === "route.ts" || entry.name === "route.tsx") {
@@ -1104,7 +956,7 @@ function walk2(dir) {
1104
956
  return results;
1105
957
  }
1106
958
  function filePathToRoute(apiDir, absPath) {
1107
- let route = "/" + (0, import_node_path3.relative)(apiDir, absPath).replace(/\\/g, "/").replace(/\/route\.tsx?$/, "");
959
+ let route = "/" + (0, import_node_path4.relative)(apiDir, absPath).replace(/\\/g, "/").replace(/\/route\.tsx?$/, "");
1108
960
  route = route.replace(/\[([^\]]+)\]/g, ":$1");
1109
961
  route = route.replace(/\/+/g, "/");
1110
962
  if (route === "/") return "/api";
@@ -1115,10 +967,10 @@ function camelToPascal(s) {
1115
967
  return s.charAt(0).toUpperCase() + s.slice(1);
1116
968
  }
1117
969
  function detect2(rootDir) {
1118
- return (0, import_node_fs3.existsSync)((0, import_node_path3.join)(rootDir, "src", "app", "api"));
970
+ return (0, import_node_fs4.existsSync)((0, import_node_path4.join)(rootDir, "src", "app", "api"));
1119
971
  }
1120
972
  function generate2(rootDir) {
1121
- const apiDir = (0, import_node_path3.join)(rootDir, "src", "app", "api");
973
+ const apiDir = (0, import_node_path4.join)(rootDir, "src", "app", "api");
1122
974
  const routeFiles = walk2(apiDir);
1123
975
  const nodes = [];
1124
976
  const edges = [];
@@ -1136,7 +988,7 @@ function generate2(rootDir) {
1136
988
  if (HTTP_METHODS2.has(exp)) methods.push(exp);
1137
989
  }
1138
990
  const routePath = filePathToRoute(apiDir, absPath);
1139
- const relPath = (0, import_node_path3.relative)(rootDir, absPath).replace(/\\/g, "/");
991
+ const relPath = (0, import_node_path4.relative)(rootDir, absPath).replace(/\\/g, "/");
1140
992
  const mutations = dbCalls.filter((c) => c.isMutation);
1141
993
  const reads = dbCalls.filter((c) => !c.isMutation);
1142
994
  const mutates = mutations.length > 0;
@@ -1221,8 +1073,8 @@ var nextjsRoutesParser = {
1221
1073
  };
1222
1074
 
1223
1075
  // src/server/graph/parsers/db/prisma-schema.ts
1224
- var import_node_fs4 = require("node:fs");
1225
- var import_node_path4 = require("node:path");
1076
+ var import_node_fs5 = require("node:fs");
1077
+ var import_node_path5 = require("node:path");
1226
1078
  function parseModels(content) {
1227
1079
  const nodes = [];
1228
1080
  const relations = [];
@@ -1313,11 +1165,11 @@ function parseEnums(content) {
1313
1165
  return nodes;
1314
1166
  }
1315
1167
  function detect3(rootDir) {
1316
- return (0, import_node_fs4.existsSync)((0, import_node_path4.join)(rootDir, "prisma", "schema.prisma"));
1168
+ return (0, import_node_fs5.existsSync)((0, import_node_path5.join)(rootDir, "prisma", "schema.prisma"));
1317
1169
  }
1318
1170
  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");
1171
+ const schemaPath = (0, import_node_path5.join)(rootDir, "prisma", "schema.prisma");
1172
+ const content = (0, import_node_fs5.readFileSync)(schemaPath, "utf-8");
1321
1173
  const { nodes: modelNodes, relations } = parseModels(content);
1322
1174
  const enumNodes = parseEnums(content);
1323
1175
  const allNodes = [...modelNodes, ...enumNodes];
@@ -1373,106 +1225,1116 @@ var prismaSchemaParser = {
1373
1225
  generate: generate3
1374
1226
  };
1375
1227
 
1376
- // 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);
1384
- }
1385
- 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);
1390
- return {
1391
- layer,
1392
- output,
1393
- nodeCount: output.nodes.length,
1394
- edgeCount: output.edges.length
1395
- };
1396
- }
1397
- function generateAll(rootDir) {
1398
- const layers = ["api", "db", "ui"];
1399
- const results = [];
1400
- for (const layer of layers) {
1401
- const result = generateLayer(rootDir, layer);
1402
- if (result) results.push(result);
1228
+ // src/server/graph/core/api-route-matching.ts
1229
+ function loadApiRoutesFromOutput(apiOutput) {
1230
+ const routes = [];
1231
+ for (const n of apiOutput.nodes) {
1232
+ const path2 = n.path;
1233
+ if (!path2 || typeof path2 !== "string") continue;
1234
+ routes.push({
1235
+ path: path2,
1236
+ nodeId: n.id,
1237
+ segments: path2.split("/").filter(Boolean)
1238
+ });
1403
1239
  }
1404
- const byLayer = new Map(results.map((r) => [r.layer, r]));
1405
- return ["ui", "api", "db"].map((l) => byLayer.get(l)).filter((r) => !!r);
1406
- }
1407
-
1408
- // src/server/graph/index.ts
1409
- var GRAPHS_DIR = ".launchsecure/graphs";
1410
- var LAYERS = ["ui", "api", "db"];
1411
- var graphCache = /* @__PURE__ */ new Map();
1412
- function graphsDir(rootDir) {
1413
- return (0, import_node_path5.join)(rootDir, GRAPHS_DIR);
1414
- }
1415
- function graphFilePath(rootDir, layer) {
1416
- return (0, import_node_path5.join)(graphsDir(rootDir), `${layer}.json`);
1417
- }
1418
- function invalidateCache(filePath) {
1419
- graphCache.delete(filePath);
1240
+ return routes;
1420
1241
  }
1421
- function readGraph(rootDir, layer) {
1422
- const filePath = graphFilePath(rootDir, layer);
1423
- if (!(0, import_node_fs5.existsSync)(filePath)) return null;
1424
- const stat = (0, import_node_fs5.statSync)(filePath);
1425
- const cached = graphCache.get(filePath);
1426
- if (cached && cached.mtimeMs === stat.mtimeMs) {
1427
- return cached.graph;
1242
+ function buildApiPathMap(routes) {
1243
+ const map = /* @__PURE__ */ new Map();
1244
+ for (const r of routes) {
1245
+ if (!map.has(r.path)) map.set(r.path, r.nodeId);
1428
1246
  }
1429
- const content = (0, import_node_fs5.readFileSync)(filePath, "utf-8");
1430
- const graph = JSON.parse(content);
1431
- graphCache.set(filePath, { mtimeMs: stat.mtimeMs, graph });
1432
- return graph;
1247
+ return map;
1433
1248
  }
1434
- function readAllGraphs(rootDir) {
1435
- const result = {};
1436
- for (const layer of LAYERS) {
1437
- const graph = readGraph(rootDir, layer);
1438
- if (graph) result[layer] = graph;
1439
- }
1440
- return result;
1249
+ function normalizeFetchUrl(raw) {
1250
+ let s = raw.replace(/^`|`$/g, "");
1251
+ const qIdx = s.indexOf("?");
1252
+ if (qIdx >= 0) s = s.slice(0, qIdx);
1253
+ const hIdx = s.indexOf("#");
1254
+ if (hIdx >= 0) s = s.slice(0, hIdx);
1255
+ let hadInterpolation = false;
1256
+ s = s.replace(/\$\{([^}]+)\}/g, (_, expr) => {
1257
+ hadInterpolation = true;
1258
+ const cleaned = expr.trim();
1259
+ const last = cleaned.split(".").pop() ?? cleaned;
1260
+ const name = last.replace(/[^\w]/g, "") || "param";
1261
+ return ":" + name;
1262
+ });
1263
+ s = s.replace(/\/+/g, "/");
1264
+ if (s.length > 1 && s.endsWith("/")) s = s.slice(0, -1);
1265
+ return { path: s || "/", hadInterpolation };
1441
1266
  }
1442
- function generateGraph(rootDir, layer) {
1443
- const dir = graphsDir(rootDir);
1444
- (0, import_node_fs5.mkdirSync)(dir, { recursive: true });
1445
- const results = layer ? [generateLayer(rootDir, layer)].filter((r) => r !== null) : generateAll(rootDir);
1446
- for (const result of results) {
1447
- const filePath = graphFilePath(rootDir, result.layer);
1448
- (0, import_node_fs5.writeFileSync)(filePath, JSON.stringify(result.output, null, 2) + "\n", "utf-8");
1449
- invalidateCache(filePath);
1267
+ function scoreApiRouteMatch(candidate, known) {
1268
+ if (candidate.length !== known.length) return -1;
1269
+ let score = 0;
1270
+ for (let i = 0; i < candidate.length; i++) {
1271
+ const a = candidate[i];
1272
+ const b = known[i];
1273
+ if (a === b) {
1274
+ score += 3;
1275
+ continue;
1276
+ }
1277
+ if (a.startsWith(":") && b.startsWith(":")) {
1278
+ score += 2;
1279
+ continue;
1280
+ }
1281
+ if (a.startsWith(":") || b.startsWith(":")) {
1282
+ score += 1;
1283
+ continue;
1284
+ }
1285
+ return -1;
1450
1286
  }
1451
- return results;
1452
- }
1453
-
1454
- // src/server/lockfile.ts
1455
- var import_node_child_process = require("node:child_process");
1456
- var import_node_fs6 = require("node:fs");
1457
- var import_node_os = require("node:os");
1458
- var import_node_path6 = require("node:path");
1459
- function lockDir() {
1460
- return (0, import_node_path6.join)((0, import_node_os.homedir)(), ".launchsecure");
1461
- }
1462
- function lockPath() {
1463
- return (0, import_node_path6.join)(lockDir(), "launch-chart.lock");
1287
+ return score;
1464
1288
  }
1465
- function readLock() {
1466
- const p = lockPath();
1467
- if (!(0, import_node_fs6.existsSync)(p)) return null;
1468
- try {
1469
- const data = JSON.parse((0, import_node_fs6.readFileSync)(p, "utf-8"));
1470
- if (typeof data.pid !== "number" || typeof data.port !== "number") return null;
1471
- return data;
1472
- } catch {
1473
- return null;
1289
+ function resolveFetchCall(call, apiPathMap, apiRoutes) {
1290
+ const raw = call.url;
1291
+ if (/^(https?:)?\/\//i.test(raw)) {
1292
+ return { kind: "external", normalizedUrl: raw };
1474
1293
  }
1475
- }
1294
+ if (call.isConcat) {
1295
+ return { kind: "dynamic", normalizedUrl: raw };
1296
+ }
1297
+ const { path: path2, hadInterpolation } = normalizeFetchUrl(raw);
1298
+ if (!path2.startsWith("/")) {
1299
+ return { kind: "unresolved", normalizedUrl: path2 };
1300
+ }
1301
+ const segs = path2.split("/").filter(Boolean);
1302
+ if (hadInterpolation && segs.length > 0 && segs[0].startsWith(":")) {
1303
+ return { kind: "dynamic", normalizedUrl: path2 };
1304
+ }
1305
+ const exact = apiPathMap.get(path2);
1306
+ if (exact) return { kind: "resolved", nodeId: exact, normalizedUrl: path2 };
1307
+ let bestScore = -1;
1308
+ let bestId = null;
1309
+ for (const r of apiRoutes) {
1310
+ const score = scoreApiRouteMatch(segs, r.segments);
1311
+ if (score > bestScore) {
1312
+ bestScore = score;
1313
+ bestId = r.nodeId;
1314
+ }
1315
+ }
1316
+ if (bestId && bestScore > 0) {
1317
+ return { kind: "resolved", nodeId: bestId, normalizedUrl: path2 };
1318
+ }
1319
+ return { kind: "unresolved", normalizedUrl: path2 };
1320
+ }
1321
+ function resolveUrlPath(urlPath, apiPathMap, apiRoutes) {
1322
+ const { path: path2, hadInterpolation } = normalizeFetchUrl(urlPath);
1323
+ if (!path2.startsWith("/")) {
1324
+ return { kind: "unresolved", normalizedUrl: path2 };
1325
+ }
1326
+ const segs = path2.split("/").filter(Boolean);
1327
+ if (hadInterpolation && segs.length > 0 && segs[0].startsWith(":")) {
1328
+ return { kind: "dynamic", normalizedUrl: path2 };
1329
+ }
1330
+ const exact = apiPathMap.get(path2);
1331
+ if (exact) return { kind: "resolved", nodeId: exact, normalizedUrl: path2 };
1332
+ let bestScore = -1;
1333
+ let bestId = null;
1334
+ for (const r of apiRoutes) {
1335
+ const score = scoreApiRouteMatch(segs, r.segments);
1336
+ if (score > bestScore) {
1337
+ bestScore = score;
1338
+ bestId = r.nodeId;
1339
+ }
1340
+ }
1341
+ if (bestId && bestScore > 0) {
1342
+ return { kind: "resolved", nodeId: bestId, normalizedUrl: path2 };
1343
+ }
1344
+ return { kind: "unresolved", normalizedUrl: path2 };
1345
+ }
1346
+
1347
+ // src/server/graph/parsers/crosslayer/fetch-resolver.ts
1348
+ var fetchResolverParser = {
1349
+ id: "fetch-resolver",
1350
+ layer: "crosslayer",
1351
+ detect(_rootDir) {
1352
+ return true;
1353
+ },
1354
+ generate(_rootDir, layerOutputs) {
1355
+ const uiOutput = layerOutputs.get("ui");
1356
+ const apiOutput = layerOutputs.get("api");
1357
+ if (!uiOutput || !apiOutput) {
1358
+ return { cross_refs: [], flagged_edges: [], warnings: [] };
1359
+ }
1360
+ const apiRoutes = loadApiRoutesFromOutput(apiOutput);
1361
+ const apiPathMap = buildApiPathMap(apiRoutes);
1362
+ const fetchCallEntries = uiOutput.patterns?.fetch_calls ?? [];
1363
+ if (fetchCallEntries.length === 0) {
1364
+ return { cross_refs: [], flagged_edges: [], warnings: [] };
1365
+ }
1366
+ const includeExternal = process.env.LAUNCH_CHART_INCLUDE_EXTERNAL_FETCHES === "1";
1367
+ const crossRefs = [];
1368
+ const flaggedEdges = [];
1369
+ const seen = /* @__PURE__ */ new Set();
1370
+ let resolvedCount = 0;
1371
+ let dynamicCount = 0;
1372
+ let unresolvedCount = 0;
1373
+ let externalCount = 0;
1374
+ for (const entry of fetchCallEntries) {
1375
+ for (const call of entry.calls) {
1376
+ const result = resolveFetchCall(call, apiPathMap, apiRoutes);
1377
+ const methodTag = call.method ?? (call.kind === "fetch" ? "GET?" : "?");
1378
+ if (result.kind === "resolved" && result.nodeId) {
1379
+ const key = `${entry.nodeId}\u2192${result.nodeId}\u2192calls_api`;
1380
+ if (seen.has(key)) continue;
1381
+ seen.add(key);
1382
+ crossRefs.push({
1383
+ source: entry.nodeId,
1384
+ target: result.nodeId,
1385
+ type: "calls_api",
1386
+ layer: "api"
1387
+ });
1388
+ resolvedCount++;
1389
+ continue;
1390
+ }
1391
+ if (result.kind === "dynamic") {
1392
+ dynamicCount++;
1393
+ flaggedEdges.push({
1394
+ source: entry.nodeId,
1395
+ target: "DYNAMIC",
1396
+ type: "calls_api",
1397
+ label: call.isConcat ? `${methodTag} fetch with concat: ${call.url}` : `${methodTag} fetch with template: ${call.url}`,
1398
+ confidence: call.isConcat ? "low" : "medium"
1399
+ });
1400
+ continue;
1401
+ }
1402
+ if (result.kind === "external") {
1403
+ externalCount++;
1404
+ if (!includeExternal) continue;
1405
+ flaggedEdges.push({
1406
+ source: entry.nodeId,
1407
+ target: "EXTERNAL",
1408
+ type: "calls_external",
1409
+ label: `${methodTag} external fetch: ${call.url}`,
1410
+ confidence: "high"
1411
+ });
1412
+ continue;
1413
+ }
1414
+ unresolvedCount++;
1415
+ flaggedEdges.push({
1416
+ source: entry.nodeId,
1417
+ target: "UNRESOLVED",
1418
+ type: "calls_api",
1419
+ label: `${methodTag} fetch to unknown path: ${result.normalizedUrl}`,
1420
+ confidence: "medium"
1421
+ });
1422
+ }
1423
+ }
1424
+ return {
1425
+ cross_refs: crossRefs,
1426
+ flagged_edges: flaggedEdges,
1427
+ warnings: [],
1428
+ patterns: {
1429
+ api_call_detection: {
1430
+ resolved: resolvedCount,
1431
+ dynamic: dynamicCount,
1432
+ unresolved: unresolvedCount,
1433
+ external: externalCount
1434
+ }
1435
+ }
1436
+ };
1437
+ }
1438
+ };
1439
+
1440
+ // src/server/graph/parsers/crosslayer/api-annotations.ts
1441
+ var import_node_fs6 = require("node:fs");
1442
+ var import_node_path6 = require("node:path");
1443
+ var API_ANNOTATION_RE = /@api\s+(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+(\/\S+)/g;
1444
+ function walk3(dir, exts) {
1445
+ if (!(0, import_node_fs6.existsSync)(dir)) return [];
1446
+ const results = [];
1447
+ for (const entry of (0, import_node_fs6.readdirSync)(dir, { withFileTypes: true })) {
1448
+ if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
1449
+ const full = (0, import_node_path6.join)(dir, entry.name);
1450
+ if (entry.isDirectory()) {
1451
+ results.push(...walk3(full, exts));
1452
+ } else if (exts.includes((0, import_node_path6.extname)(entry.name))) {
1453
+ results.push(full);
1454
+ }
1455
+ }
1456
+ return results;
1457
+ }
1458
+ function toNodeId2(srcDir, absPath) {
1459
+ return (0, import_node_path6.relative)(srcDir, absPath).replace(/\\/g, "/");
1460
+ }
1461
+ var apiAnnotationsParser = {
1462
+ id: "api-annotations",
1463
+ layer: "crosslayer",
1464
+ detect(rootDir) {
1465
+ return (0, import_node_fs6.existsSync)((0, import_node_path6.join)(rootDir, "src"));
1466
+ },
1467
+ generate(rootDir, layerOutputs) {
1468
+ const apiOutput = layerOutputs.get("api");
1469
+ if (!apiOutput) {
1470
+ return { cross_refs: [], flagged_edges: [], warnings: [] };
1471
+ }
1472
+ const uiOutput = layerOutputs.get("ui");
1473
+ const uiNodeIds = new Set(uiOutput?.nodes.map((n) => n.id) ?? []);
1474
+ const apiRoutes = loadApiRoutesFromOutput(apiOutput);
1475
+ const apiPathMap = buildApiPathMap(apiRoutes);
1476
+ const srcDir = (0, import_node_path6.join)(rootDir, "src");
1477
+ const files = walk3(srcDir, [".ts", ".tsx"]);
1478
+ const crossRefs = [];
1479
+ const flaggedEdges = [];
1480
+ const seen = /* @__PURE__ */ new Set();
1481
+ for (const absPath of files) {
1482
+ const content = (0, import_node_fs6.readFileSync)(absPath, "utf-8");
1483
+ const sourceId = toNodeId2(srcDir, absPath);
1484
+ if (!uiNodeIds.has(sourceId)) continue;
1485
+ let match;
1486
+ API_ANNOTATION_RE.lastIndex = 0;
1487
+ while ((match = API_ANNOTATION_RE.exec(content)) !== null) {
1488
+ const method = match[1];
1489
+ const urlPath = match[2];
1490
+ const result = resolveUrlPath(urlPath, apiPathMap, apiRoutes);
1491
+ if (result.kind === "resolved" && result.nodeId) {
1492
+ const key = `${sourceId}|${result.nodeId}|calls_api`;
1493
+ if (seen.has(key)) continue;
1494
+ seen.add(key);
1495
+ crossRefs.push({
1496
+ source: sourceId,
1497
+ target: result.nodeId,
1498
+ type: "calls_api",
1499
+ layer: "api"
1500
+ });
1501
+ } else {
1502
+ flaggedEdges.push({
1503
+ source: sourceId,
1504
+ target: "UNRESOLVED",
1505
+ type: "annotation_unresolved",
1506
+ label: `@api ${method} ${urlPath} \u2014 no matching API route found`,
1507
+ confidence: "high"
1508
+ });
1509
+ }
1510
+ }
1511
+ }
1512
+ return {
1513
+ cross_refs: crossRefs,
1514
+ flagged_edges: flaggedEdges,
1515
+ warnings: [],
1516
+ patterns: {
1517
+ annotations_found: crossRefs.length + flaggedEdges.length,
1518
+ annotations_resolved: crossRefs.length,
1519
+ annotations_unresolved: flaggedEdges.length
1520
+ }
1521
+ };
1522
+ }
1523
+ };
1524
+
1525
+ // src/server/graph/parsers/crosslayer/url-literal-scanner.ts
1526
+ var import_node_fs7 = require("node:fs");
1527
+ var import_node_path7 = require("node:path");
1528
+ var URL_LITERAL_RE = /['"`](\/api\/[^'"`\s]+?)['"`]/g;
1529
+ function walk4(dir, exts) {
1530
+ if (!(0, import_node_fs7.existsSync)(dir)) return [];
1531
+ const results = [];
1532
+ for (const entry of (0, import_node_fs7.readdirSync)(dir, { withFileTypes: true })) {
1533
+ if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
1534
+ const full = (0, import_node_path7.join)(dir, entry.name);
1535
+ if (entry.isDirectory()) {
1536
+ results.push(...walk4(full, exts));
1537
+ } else if (exts.includes((0, import_node_path7.extname)(entry.name))) {
1538
+ results.push(full);
1539
+ }
1540
+ }
1541
+ return results;
1542
+ }
1543
+ function toNodeId3(srcDir, absPath) {
1544
+ return (0, import_node_path7.relative)(srcDir, absPath).replace(/\\/g, "/");
1545
+ }
1546
+ var urlLiteralScannerParser = {
1547
+ id: "url-literal-scanner",
1548
+ layer: "crosslayer",
1549
+ detect(rootDir) {
1550
+ return (0, import_node_fs7.existsSync)((0, import_node_path7.join)(rootDir, "src"));
1551
+ },
1552
+ generate(rootDir, layerOutputs) {
1553
+ const apiOutput = layerOutputs.get("api");
1554
+ if (!apiOutput) {
1555
+ return { cross_refs: [], flagged_edges: [], warnings: [] };
1556
+ }
1557
+ const uiOutput = layerOutputs.get("ui");
1558
+ const uiNodeIds = new Set(uiOutput?.nodes.map((n) => n.id) ?? []);
1559
+ const apiRoutes = loadApiRoutesFromOutput(apiOutput);
1560
+ const apiPathMap = buildApiPathMap(apiRoutes);
1561
+ const srcDir = (0, import_node_path7.join)(rootDir, "src");
1562
+ const clientDir = (0, import_node_path7.join)(srcDir, "client");
1563
+ const appDir = (0, import_node_path7.join)(srcDir, "app");
1564
+ const files = [
1565
+ ...walk4(clientDir, [".ts", ".tsx"]),
1566
+ ...walk4(appDir, [".ts", ".tsx"])
1567
+ ];
1568
+ const crossRefs = [];
1569
+ const seen = /* @__PURE__ */ new Set();
1570
+ for (const absPath of files) {
1571
+ const sourceId = toNodeId3(srcDir, absPath);
1572
+ if (!uiNodeIds.has(sourceId)) continue;
1573
+ const content = (0, import_node_fs7.readFileSync)(absPath, "utf-8");
1574
+ let match;
1575
+ URL_LITERAL_RE.lastIndex = 0;
1576
+ while ((match = URL_LITERAL_RE.exec(content)) !== null) {
1577
+ const urlPath = match[1];
1578
+ const result = resolveUrlPath(urlPath, apiPathMap, apiRoutes);
1579
+ if (result.kind === "resolved" && result.nodeId) {
1580
+ const key = `${sourceId}|${result.nodeId}|references_api`;
1581
+ if (seen.has(key)) continue;
1582
+ seen.add(key);
1583
+ crossRefs.push({
1584
+ source: sourceId,
1585
+ target: result.nodeId,
1586
+ type: "references_api",
1587
+ layer: "api"
1588
+ });
1589
+ }
1590
+ }
1591
+ }
1592
+ return {
1593
+ cross_refs: crossRefs,
1594
+ flagged_edges: [],
1595
+ warnings: [],
1596
+ patterns: {
1597
+ url_literals_resolved: crossRefs.length
1598
+ }
1599
+ };
1600
+ }
1601
+ };
1602
+
1603
+ // src/server/graph/core/parser-registry.ts
1604
+ var ParserRegistry = class {
1605
+ constructor() {
1606
+ this.parsers = /* @__PURE__ */ new Map();
1607
+ this.ids = /* @__PURE__ */ new Set();
1608
+ }
1609
+ register(parser) {
1610
+ if (this.ids.has(parser.id)) {
1611
+ throw new Error(`Duplicate parser id: ${parser.id}`);
1612
+ }
1613
+ this.ids.add(parser.id);
1614
+ const list = this.parsers.get(parser.layer) ?? [];
1615
+ list.push(parser);
1616
+ this.parsers.set(parser.layer, list);
1617
+ }
1618
+ getParsers(layer) {
1619
+ return this.parsers.get(layer) ?? [];
1620
+ }
1621
+ getCrossLayerParsers() {
1622
+ return this.parsers.get("crosslayer") ?? [];
1623
+ }
1624
+ getAll() {
1625
+ const all = [];
1626
+ for (const list of this.parsers.values()) all.push(...list);
1627
+ return all;
1628
+ }
1629
+ };
1630
+ function registerBuiltins(registry, disabled) {
1631
+ const builtins = [
1632
+ reactNextjsParser,
1633
+ nextjsRoutesParser,
1634
+ prismaSchemaParser,
1635
+ fetchResolverParser,
1636
+ apiAnnotationsParser,
1637
+ urlLiteralScannerParser
1638
+ ];
1639
+ for (const parser of builtins) {
1640
+ if (disabled.has(parser.id)) continue;
1641
+ registry.register(parser);
1642
+ }
1643
+ }
1644
+ function loadCustomParsers(registry, config, rootDir, disabled) {
1645
+ for (const entry of config.parsers?.custom ?? []) {
1646
+ try {
1647
+ const absPath = (0, import_node_path8.resolve)(rootDir, entry.path);
1648
+ const mod = require(absPath);
1649
+ const parser = "default" in mod ? mod.default : mod;
1650
+ if (disabled.has(parser.id)) continue;
1651
+ if (parser.layer !== entry.layer) {
1652
+ process.stderr.write(
1653
+ `[launch-chart] custom parser "${parser.id}" declares layer "${parser.layer}" but config says "${entry.layer}" \u2014 using parser's layer
1654
+ `
1655
+ );
1656
+ }
1657
+ registry.register(parser);
1658
+ } catch (err) {
1659
+ process.stderr.write(`[launch-chart] failed to load custom parser from ${entry.path}: ${err}
1660
+ `);
1661
+ }
1662
+ }
1663
+ }
1664
+ function createRegistry(config, rootDir) {
1665
+ const registry = new ParserRegistry();
1666
+ const disabled = new Set(config.parsers?.disabled ?? []);
1667
+ registerBuiltins(registry, disabled);
1668
+ loadCustomParsers(registry, config, rootDir, disabled);
1669
+ return registry;
1670
+ }
1671
+
1672
+ // src/server/graph/core/merge.ts
1673
+ function mergeGraphOutputs(outputs, layer) {
1674
+ if (outputs.length === 0) {
1675
+ return {
1676
+ metadata: { generated: (/* @__PURE__ */ new Date()).toISOString(), scope: "", layer },
1677
+ nodes: [],
1678
+ edges: [],
1679
+ cross_refs: [],
1680
+ contradictions: [],
1681
+ warnings: [],
1682
+ flagged_edges: []
1683
+ };
1684
+ }
1685
+ if (outputs.length === 1) return outputs[0];
1686
+ const seenNodes = /* @__PURE__ */ new Set();
1687
+ const seenEdges = /* @__PURE__ */ new Set();
1688
+ const seenCrossRefs = /* @__PURE__ */ new Set();
1689
+ const mergedNodes = [];
1690
+ const mergedEdges = [];
1691
+ const mergedCrossRefs = [];
1692
+ const mergedContradictions = [];
1693
+ const mergedWarnings = [];
1694
+ const mergedFlagged = [];
1695
+ const parserIds = [];
1696
+ for (const output of outputs) {
1697
+ if (output.metadata.parser) {
1698
+ parserIds.push(String(output.metadata.parser));
1699
+ }
1700
+ for (const node of output.nodes) {
1701
+ if (seenNodes.has(node.id)) {
1702
+ mergedWarnings.push({
1703
+ type: "merge_conflict",
1704
+ detail: `Node "${node.id}" produced by multiple parsers; keeping first`
1705
+ });
1706
+ continue;
1707
+ }
1708
+ seenNodes.add(node.id);
1709
+ mergedNodes.push(node);
1710
+ }
1711
+ for (const edge of output.edges) {
1712
+ const key = `${edge.source}|${edge.target}|${edge.type}`;
1713
+ if (seenEdges.has(key)) continue;
1714
+ seenEdges.add(key);
1715
+ mergedEdges.push(edge);
1716
+ }
1717
+ for (const ref of output.cross_refs) {
1718
+ const key = `${ref.source}|${ref.target}|${ref.type}`;
1719
+ if (seenCrossRefs.has(key)) continue;
1720
+ seenCrossRefs.add(key);
1721
+ mergedCrossRefs.push(ref);
1722
+ }
1723
+ mergedContradictions.push(...output.contradictions);
1724
+ mergedWarnings.push(...output.warnings);
1725
+ mergedFlagged.push(...output.flagged_edges);
1726
+ }
1727
+ const metadata = {
1728
+ ...outputs[0].metadata,
1729
+ generated: (/* @__PURE__ */ new Date()).toISOString(),
1730
+ parsers: parserIds
1731
+ };
1732
+ return {
1733
+ metadata,
1734
+ nodes: mergedNodes,
1735
+ edges: mergedEdges,
1736
+ cross_refs: mergedCrossRefs,
1737
+ contradictions: mergedContradictions,
1738
+ warnings: mergedWarnings,
1739
+ flagged_edges: mergedFlagged,
1740
+ patterns: outputs[0].patterns
1741
+ };
1742
+ }
1743
+ function dedupCrossRefs(refs) {
1744
+ const seen = /* @__PURE__ */ new Set();
1745
+ const result = [];
1746
+ for (const ref of refs) {
1747
+ const key = `${ref.source}|${ref.target}|${ref.type}`;
1748
+ if (seen.has(key)) continue;
1749
+ seen.add(key);
1750
+ result.push(ref);
1751
+ }
1752
+ return result;
1753
+ }
1754
+ function applyCrossLayerResults(uiOutput, results, primaryId) {
1755
+ const allCrossRefs = [...uiOutput.cross_refs];
1756
+ const allFlagged = [...uiOutput.flagged_edges];
1757
+ const allWarnings = [...uiOutput.warnings];
1758
+ const primaryResult = results.find((r) => r.parserId === primaryId);
1759
+ const secondaryResults = results.filter((r) => r.parserId !== primaryId);
1760
+ if (primaryResult) {
1761
+ allCrossRefs.push(...primaryResult.output.cross_refs);
1762
+ allFlagged.push(...primaryResult.output.flagged_edges);
1763
+ allWarnings.push(...primaryResult.output.warnings);
1764
+ }
1765
+ const primarySet = new Set(
1766
+ (primaryResult?.output.cross_refs ?? []).map((r) => `${r.source}|${r.target}|${r.type}`)
1767
+ );
1768
+ for (const sec of secondaryResults) {
1769
+ for (const ref of sec.output.cross_refs) {
1770
+ const key = `${ref.source}|${ref.target}|${ref.type}`;
1771
+ if (primarySet.has(key)) {
1772
+ allCrossRefs.push(ref);
1773
+ } else {
1774
+ allFlagged.push({
1775
+ source: ref.source,
1776
+ target: ref.target,
1777
+ type: "out_of_pattern",
1778
+ label: `API call detected by ${sec.parserId} but not by primary (${primaryId})`,
1779
+ confidence: "medium"
1780
+ });
1781
+ allCrossRefs.push(ref);
1782
+ }
1783
+ }
1784
+ allFlagged.push(...sec.output.flagged_edges);
1785
+ allWarnings.push(...sec.output.warnings);
1786
+ }
1787
+ return {
1788
+ ...uiOutput,
1789
+ cross_refs: dedupCrossRefs(allCrossRefs),
1790
+ flagged_edges: allFlagged,
1791
+ warnings: allWarnings
1792
+ };
1793
+ }
1794
+
1795
+ // src/server/graph/core/graph-builder.ts
1796
+ function readGraphFromDisk(rootDir, layer) {
1797
+ const filePath = (0, import_node_path9.join)(rootDir, ".launchsecure", "graphs", `${layer}.json`);
1798
+ if (!(0, import_node_fs8.existsSync)(filePath)) return null;
1799
+ try {
1800
+ return JSON.parse((0, import_node_fs8.readFileSync)(filePath, "utf-8"));
1801
+ } catch {
1802
+ return null;
1803
+ }
1804
+ }
1805
+ function generateLayer(rootDir, layer) {
1806
+ const config = loadConfig(rootDir);
1807
+ const registry = createRegistry(config, rootDir);
1808
+ const parsers = registry.getParsers(layer);
1809
+ const outputs = [];
1810
+ for (const parser of parsers) {
1811
+ if (!parser.detect(rootDir)) continue;
1812
+ outputs.push(parser.generate(rootDir));
1813
+ }
1814
+ if (outputs.length === 0) return null;
1815
+ let merged = outputs.length === 1 ? outputs[0] : mergeGraphOutputs(outputs, layer);
1816
+ if (layer === "ui") {
1817
+ const layerOutputs = /* @__PURE__ */ new Map();
1818
+ layerOutputs.set("ui", merged);
1819
+ for (const otherLayer of ["api", "db"]) {
1820
+ const existing = readGraphFromDisk(rootDir, otherLayer);
1821
+ if (existing) layerOutputs.set(otherLayer, existing);
1822
+ }
1823
+ const crossParsers = registry.getCrossLayerParsers();
1824
+ const primaryId = config.parsers?.primary?.crosslayer ?? crossParsers[0]?.id ?? null;
1825
+ const crossResults = crossParsers.filter((p) => p.detect(rootDir)).map((p) => ({ parserId: p.id, output: p.generate(rootDir, layerOutputs) }));
1826
+ if (crossResults.length > 0) {
1827
+ merged = applyCrossLayerResults(merged, crossResults, primaryId);
1828
+ }
1829
+ }
1830
+ return {
1831
+ layer,
1832
+ output: merged,
1833
+ nodeCount: merged.nodes.length,
1834
+ edgeCount: merged.edges.length
1835
+ };
1836
+ }
1837
+ function generateAll(rootDir) {
1838
+ const config = loadConfig(rootDir);
1839
+ const registry = createRegistry(config, rootDir);
1840
+ const layerOrder = ["api", "db", "ui"];
1841
+ const layerOutputs = /* @__PURE__ */ new Map();
1842
+ const results = [];
1843
+ for (const layer of layerOrder) {
1844
+ const parsers = registry.getParsers(layer);
1845
+ const outputs = [];
1846
+ for (const parser of parsers) {
1847
+ if (!parser.detect(rootDir)) continue;
1848
+ outputs.push(parser.generate(rootDir));
1849
+ }
1850
+ if (outputs.length === 0) continue;
1851
+ const merged = outputs.length === 1 ? outputs[0] : mergeGraphOutputs(outputs, layer);
1852
+ layerOutputs.set(layer, merged);
1853
+ results.push({
1854
+ layer,
1855
+ output: merged,
1856
+ nodeCount: merged.nodes.length,
1857
+ edgeCount: merged.edges.length
1858
+ });
1859
+ }
1860
+ const crossParsers = registry.getCrossLayerParsers();
1861
+ const primaryId = config.parsers?.primary?.crosslayer ?? crossParsers[0]?.id ?? null;
1862
+ const crossResults = crossParsers.filter((p) => p.detect(rootDir)).map((p) => ({ parserId: p.id, output: p.generate(rootDir, layerOutputs) }));
1863
+ if (crossResults.length > 0 && layerOutputs.has("ui")) {
1864
+ const uiOutput = layerOutputs.get("ui");
1865
+ const merged = applyCrossLayerResults(uiOutput, crossResults, primaryId);
1866
+ layerOutputs.set("ui", merged);
1867
+ const uiResult = results.find((r) => r.layer === "ui");
1868
+ if (uiResult) {
1869
+ uiResult.output = merged;
1870
+ uiResult.nodeCount = merged.nodes.length;
1871
+ uiResult.edgeCount = merged.edges.length;
1872
+ }
1873
+ }
1874
+ const byLayer = new Map(results.map((r) => [r.layer, r]));
1875
+ return ["ui", "api", "db"].map((l) => byLayer.get(l)).filter((r) => !!r);
1876
+ }
1877
+
1878
+ // src/server/graph/index.ts
1879
+ init_config();
1880
+
1881
+ // src/server/graph/core/tagger-registry.ts
1882
+ var import_node_path11 = require("node:path");
1883
+
1884
+ // src/server/graph/taggers/module-tagger.ts
1885
+ var import_node_fs9 = require("node:fs");
1886
+ var import_node_path10 = require("node:path");
1887
+ function matchGlob(pattern, id) {
1888
+ const patParts = pattern.split("/");
1889
+ const idParts = id.split("/");
1890
+ return matchParts(patParts, 0, idParts, 0);
1891
+ }
1892
+ function matchParts(pat, pi, id, ii) {
1893
+ while (pi < pat.length && ii < id.length) {
1894
+ const p = pat[pi];
1895
+ if (p === "**") {
1896
+ for (let skip = ii; skip <= id.length; skip++) {
1897
+ if (matchParts(pat, pi + 1, id, skip)) return true;
1898
+ }
1899
+ return false;
1900
+ }
1901
+ if (p === "*") {
1902
+ pi++;
1903
+ ii++;
1904
+ continue;
1905
+ }
1906
+ if (p !== id[ii]) return false;
1907
+ pi++;
1908
+ ii++;
1909
+ }
1910
+ while (pi < pat.length && pat[pi] === "**") pi++;
1911
+ return pi === pat.length && ii === id.length;
1912
+ }
1913
+ var CONVENTION_DIRS = ["features", "modules", "domains", "areas"];
1914
+ function detectConventionDirs(rootDir) {
1915
+ const result = /* @__PURE__ */ new Map();
1916
+ const searchDirs = [
1917
+ rootDir,
1918
+ (0, import_node_path10.join)(rootDir, "src"),
1919
+ (0, import_node_path10.join)(rootDir, "app"),
1920
+ (0, import_node_path10.join)(rootDir, "lib")
1921
+ ];
1922
+ for (const base of searchDirs) {
1923
+ for (const convention of CONVENTION_DIRS) {
1924
+ const dir = (0, import_node_path10.join)(base, convention);
1925
+ if (!(0, import_node_fs9.existsSync)(dir)) continue;
1926
+ try {
1927
+ const stat = (0, import_node_fs9.statSync)(dir);
1928
+ if (!stat.isDirectory()) continue;
1929
+ const entries = (0, import_node_fs9.readdirSync)(dir, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => e.name);
1930
+ if (entries.length > 0) {
1931
+ const relPath = dir.replace(rootDir + "/", "").replace(rootDir + "\\", "");
1932
+ result.set(relPath, entries);
1933
+ }
1934
+ } catch {
1935
+ }
1936
+ }
1937
+ }
1938
+ return result;
1939
+ }
1940
+ function extractRouteGroups(id) {
1941
+ const groups = [];
1942
+ const re = /\(([^)]+)\)/g;
1943
+ let m;
1944
+ while ((m = re.exec(id)) !== null) {
1945
+ groups.push(m[1]);
1946
+ }
1947
+ return groups;
1948
+ }
1949
+ var SKIP_SEGMENTS = /* @__PURE__ */ new Set([
1950
+ "src",
1951
+ "app",
1952
+ "client",
1953
+ "server",
1954
+ "lib",
1955
+ "config"
1956
+ ]);
1957
+ function isRouteGroup(segment) {
1958
+ return segment.startsWith("(") && segment.endsWith(")");
1959
+ }
1960
+ function isDynamicSegment(segment) {
1961
+ return segment.startsWith("[") || segment.startsWith(":");
1962
+ }
1963
+ function isDomainDir(segment) {
1964
+ return segment.includes(".") && !segment.endsWith(".tsx") && !segment.endsWith(".ts") && !segment.endsWith(".js") && !segment.endsWith(".jsx") && !segment.endsWith(".vue");
1965
+ }
1966
+ var TRIVIAL_GROUPS = /* @__PURE__ */ new Set([
1967
+ "app",
1968
+ "all",
1969
+ "ee",
1970
+ "home",
1971
+ "root"
1972
+ ]);
1973
+ function isTrivialGroup(name) {
1974
+ if (TRIVIAL_GROUPS.has(name)) return true;
1975
+ const lower = name.toLowerCase();
1976
+ const wrapperPatterns = [
1977
+ /^.*-?wrapper$/,
1978
+ // "page-wrapper", "use-page-wrapper"
1979
+ /^.*-?layout$/,
1980
+ // "admin-layout", "settings-layout"
1981
+ /^use-/,
1982
+ // "use-page-wrapper"
1983
+ /^default$/
1984
+ ];
1985
+ return wrapperPatterns.some((p) => p.test(lower));
1986
+ }
1987
+ function normalizeGroupName(name) {
1988
+ return name.replace(/-pages?$/, "").replace(/-layout$/, "").replace(/-wrapper$/, "");
1989
+ }
1990
+ function extractModuleFromPath(id) {
1991
+ const segments = id.split("/");
1992
+ const routeGroups = extractRouteGroups(id);
1993
+ const moduleGroups = routeGroups.filter((g) => !isTrivialGroup(g)).map(normalizeGroupName);
1994
+ if (moduleGroups.length > 0) {
1995
+ return moduleGroups[moduleGroups.length - 1];
1996
+ }
1997
+ const meaningful = [];
1998
+ for (const seg of segments) {
1999
+ if (seg.includes(".")) continue;
2000
+ if (isRouteGroup(seg)) continue;
2001
+ if (isDynamicSegment(seg)) continue;
2002
+ if (isDomainDir(seg)) continue;
2003
+ if (SKIP_SEGMENTS.has(seg)) continue;
2004
+ meaningful.push(seg);
2005
+ }
2006
+ if (meaningful.length > 0) {
2007
+ return meaningful[0];
2008
+ }
2009
+ return "root";
2010
+ }
2011
+ var cachedRootDir = null;
2012
+ var cachedConventionDirs = /* @__PURE__ */ new Map();
2013
+ var moduleTagger = {
2014
+ id: "module",
2015
+ tagKey: "module",
2016
+ trackUntagged: true,
2017
+ layers: null,
2018
+ // applies to all layers
2019
+ tag(nodes, layer, rootDir) {
2020
+ if (cachedRootDir !== rootDir) {
2021
+ cachedConventionDirs = detectConventionDirs(rootDir);
2022
+ cachedRootDir = rootDir;
2023
+ }
2024
+ let configRules = [];
2025
+ try {
2026
+ const { loadConfig: loadConfig2 } = (init_config(), __toCommonJS(config_exports));
2027
+ const config = loadConfig2(rootDir);
2028
+ configRules = config.taggers?.module?.rules ?? [];
2029
+ } catch {
2030
+ }
2031
+ const result = /* @__PURE__ */ new Map();
2032
+ for (const node of nodes) {
2033
+ const id = node.id;
2034
+ let matched = false;
2035
+ for (const rule of configRules) {
2036
+ if (matchGlob(rule.match, id)) {
2037
+ result.set(id, rule.module);
2038
+ matched = true;
2039
+ break;
2040
+ }
2041
+ }
2042
+ if (matched) continue;
2043
+ matched = false;
2044
+ for (const [convDir, moduleNames] of cachedConventionDirs) {
2045
+ if (id.startsWith(convDir + "/")) {
2046
+ const rest = id.slice(convDir.length + 1);
2047
+ const firstSeg = rest.split("/")[0];
2048
+ if (moduleNames.includes(firstSeg)) {
2049
+ result.set(id, firstSeg);
2050
+ matched = true;
2051
+ break;
2052
+ }
2053
+ }
2054
+ }
2055
+ if (matched) continue;
2056
+ const module2 = extractModuleFromPath(id);
2057
+ result.set(id, module2);
2058
+ }
2059
+ return result;
2060
+ }
2061
+ };
2062
+
2063
+ // src/server/graph/taggers/screen-tagger.ts
2064
+ var SCREEN_TYPES = /* @__PURE__ */ new Set(["page", "layout"]);
2065
+ var screenTagger = {
2066
+ id: "screen",
2067
+ tagKey: "screen",
2068
+ trackUntagged: true,
2069
+ layers: ["ui"],
2070
+ tag(nodes, layer) {
2071
+ if (layer !== "ui") return /* @__PURE__ */ new Map();
2072
+ const result = /* @__PURE__ */ new Map();
2073
+ for (const node of nodes) {
2074
+ if (SCREEN_TYPES.has(node.type)) {
2075
+ result.set(node.id, "true");
2076
+ }
2077
+ }
2078
+ return result;
2079
+ }
2080
+ };
2081
+
2082
+ // src/server/graph/core/tagger-registry.ts
2083
+ var TaggerRegistry = class {
2084
+ constructor() {
2085
+ this.taggers = [];
2086
+ this.ids = /* @__PURE__ */ new Set();
2087
+ }
2088
+ register(tagger) {
2089
+ if (this.ids.has(tagger.id)) {
2090
+ throw new Error(`Duplicate tagger id: ${tagger.id}`);
2091
+ }
2092
+ this.ids.add(tagger.id);
2093
+ this.taggers.push(tagger);
2094
+ }
2095
+ getAll() {
2096
+ return this.taggers;
2097
+ }
2098
+ getForLayer(layer) {
2099
+ return this.taggers.filter((t) => t.layers === null || t.layers.includes(layer));
2100
+ }
2101
+ };
2102
+ var BUILTIN_TAGGERS = [moduleTagger, screenTagger];
2103
+ function registerBuiltins2(registry, disabled, config) {
2104
+ for (const tagger of BUILTIN_TAGGERS) {
2105
+ if (disabled.has(tagger.id)) continue;
2106
+ const override = config.taggers?.trackUntagged?.[tagger.id];
2107
+ if (override !== void 0) {
2108
+ tagger.trackUntagged = override;
2109
+ }
2110
+ registry.register(tagger);
2111
+ }
2112
+ }
2113
+ function loadCustomTaggers(registry, config, rootDir, disabled) {
2114
+ for (const entry of config.taggers?.custom ?? []) {
2115
+ if (disabled.has(entry.id)) continue;
2116
+ try {
2117
+ const absPath = (0, import_node_path11.resolve)(rootDir, entry.path);
2118
+ const mod = require(absPath);
2119
+ const tagger = "default" in mod ? mod.default : mod;
2120
+ const override = config.taggers?.trackUntagged?.[tagger.id];
2121
+ if (override !== void 0) {
2122
+ tagger.trackUntagged = override;
2123
+ }
2124
+ registry.register(tagger);
2125
+ } catch (err) {
2126
+ process.stderr.write(`[launch-chart] failed to load custom tagger from ${entry.path}: ${err}
2127
+ `);
2128
+ }
2129
+ }
2130
+ }
2131
+ function createTaggerRegistry(config, rootDir) {
2132
+ const registry = new TaggerRegistry();
2133
+ const disabled = new Set(config.taggers?.disabled ?? []);
2134
+ registerBuiltins2(registry, disabled, config);
2135
+ loadCustomTaggers(registry, config, rootDir, disabled);
2136
+ return registry;
2137
+ }
2138
+
2139
+ // src/server/graph/core/tag-store.ts
2140
+ var import_node_fs10 = require("node:fs");
2141
+ var import_node_path12 = require("node:path");
2142
+ var TAGS_FILENAME = "tags.json";
2143
+ var GRAPHS_DIR = ".launchsecure/graphs";
2144
+ var tagCache = /* @__PURE__ */ new Map();
2145
+ function tagsFilePath(rootDir) {
2146
+ return (0, import_node_path12.join)(rootDir, GRAPHS_DIR, TAGS_FILENAME);
2147
+ }
2148
+ function readTagStore(rootDir) {
2149
+ const filePath = tagsFilePath(rootDir);
2150
+ if (!(0, import_node_fs10.existsSync)(filePath)) return {};
2151
+ const stat = (0, import_node_fs10.statSync)(filePath);
2152
+ const cached = tagCache.get(filePath);
2153
+ if (cached && cached.mtimeMs === stat.mtimeMs) {
2154
+ return cached.store;
2155
+ }
2156
+ try {
2157
+ const content = (0, import_node_fs10.readFileSync)(filePath, "utf-8");
2158
+ const store = JSON.parse(content);
2159
+ tagCache.set(filePath, { mtimeMs: stat.mtimeMs, store });
2160
+ return store;
2161
+ } catch {
2162
+ return {};
2163
+ }
2164
+ }
2165
+ function writeTagStore(rootDir, store) {
2166
+ const filePath = tagsFilePath(rootDir);
2167
+ const dir = (0, import_node_path12.dirname)(filePath);
2168
+ (0, import_node_fs10.mkdirSync)(dir, { recursive: true });
2169
+ const cleaned = {};
2170
+ for (const [nodeId, tags] of Object.entries(store)) {
2171
+ if (Object.keys(tags).length > 0) {
2172
+ cleaned[nodeId] = tags;
2173
+ }
2174
+ }
2175
+ (0, import_node_fs10.writeFileSync)(filePath, JSON.stringify(cleaned, null, 2) + "\n", "utf-8");
2176
+ tagCache.delete(filePath);
2177
+ }
2178
+ function setTag(rootDir, nodeId, key, value) {
2179
+ const store = readTagStore(rootDir);
2180
+ if (!store[nodeId]) store[nodeId] = {};
2181
+ store[nodeId][key] = value;
2182
+ writeTagStore(rootDir, store);
2183
+ }
2184
+ function removeTag(rootDir, nodeId, key) {
2185
+ const store = readTagStore(rootDir);
2186
+ if (!store[nodeId]) return;
2187
+ delete store[nodeId][key];
2188
+ if (Object.keys(store[nodeId]).length === 0) {
2189
+ delete store[nodeId];
2190
+ }
2191
+ writeTagStore(rootDir, store);
2192
+ }
2193
+
2194
+ // src/server/graph/index.ts
2195
+ var GRAPHS_DIR2 = ".launchsecure/graphs";
2196
+ var LAYERS = ["ui", "api", "db"];
2197
+ var graphCache = /* @__PURE__ */ new Map();
2198
+ var taggedCache = /* @__PURE__ */ new Map();
2199
+ function graphsDir(rootDir) {
2200
+ return (0, import_node_path13.join)(rootDir, GRAPHS_DIR2);
2201
+ }
2202
+ function graphFilePath(rootDir, layer) {
2203
+ return (0, import_node_path13.join)(graphsDir(rootDir), `${layer}.json`);
2204
+ }
2205
+ function tagsFilePath2(rootDir) {
2206
+ return (0, import_node_path13.join)(graphsDir(rootDir), "tags.json");
2207
+ }
2208
+ function getMtimeMs(filePath) {
2209
+ if (!(0, import_node_fs11.existsSync)(filePath)) return 0;
2210
+ return (0, import_node_fs11.statSync)(filePath).mtimeMs;
2211
+ }
2212
+ function invalidateCache(filePath) {
2213
+ graphCache.delete(filePath);
2214
+ }
2215
+ function invalidateTaggedCache(rootDir, layer) {
2216
+ taggedCache.delete(`${rootDir}:${layer}`);
2217
+ }
2218
+ function applyTags(graph, layer, rootDir) {
2219
+ const config = loadConfig(rootDir);
2220
+ const registry = createTaggerRegistry(config, rootDir);
2221
+ const manualTags = readTagStore(rootDir);
2222
+ const taggedNodes = graph.nodes.map((n) => ({ ...n }));
2223
+ const taggers = registry.getForLayer(layer);
2224
+ for (const tagger of taggers) {
2225
+ const assignments = tagger.tag(taggedNodes, layer, rootDir);
2226
+ for (const node of taggedNodes) {
2227
+ if (!node.tags) node.tags = {};
2228
+ const tags = node.tags;
2229
+ const value = assignments.get(node.id);
2230
+ if (value !== void 0) {
2231
+ tags[tagger.tagKey] = value;
2232
+ } else if (tagger.trackUntagged) {
2233
+ tags[tagger.tagKey] = "untagged";
2234
+ }
2235
+ }
2236
+ }
2237
+ for (const node of taggedNodes) {
2238
+ const manual = manualTags[node.id];
2239
+ if (manual) {
2240
+ if (!node.tags) node.tags = {};
2241
+ const tags = node.tags;
2242
+ Object.assign(tags, manual);
2243
+ }
2244
+ }
2245
+ return { ...graph, nodes: taggedNodes };
2246
+ }
2247
+ function readGraphRaw(rootDir, layer) {
2248
+ const filePath = graphFilePath(rootDir, layer);
2249
+ if (!(0, import_node_fs11.existsSync)(filePath)) return null;
2250
+ const stat = (0, import_node_fs11.statSync)(filePath);
2251
+ const cached = graphCache.get(filePath);
2252
+ if (cached && cached.mtimeMs === stat.mtimeMs) {
2253
+ return cached.graph;
2254
+ }
2255
+ const content = (0, import_node_fs11.readFileSync)(filePath, "utf-8");
2256
+ const graph = JSON.parse(content);
2257
+ graphCache.set(filePath, { mtimeMs: stat.mtimeMs, graph });
2258
+ return graph;
2259
+ }
2260
+ function readGraph(rootDir, layer) {
2261
+ const rawFilePath = graphFilePath(rootDir, layer);
2262
+ if (!(0, import_node_fs11.existsSync)(rawFilePath)) return null;
2263
+ const rawMtime = getMtimeMs(rawFilePath);
2264
+ const tagsMtime = getMtimeMs(tagsFilePath2(rootDir));
2265
+ const cacheKey = `${rootDir}:${layer}`;
2266
+ const cached = taggedCache.get(cacheKey);
2267
+ if (cached && cached.rawMtimeMs === rawMtime && cached.tagsMtimeMs === tagsMtime) {
2268
+ return cached.graph;
2269
+ }
2270
+ const raw = readGraphRaw(rootDir, layer);
2271
+ if (!raw) return null;
2272
+ const tagged = applyTags(raw, layer, rootDir);
2273
+ taggedCache.set(cacheKey, { rawMtimeMs: rawMtime, tagsMtimeMs: tagsMtime, graph: tagged });
2274
+ return tagged;
2275
+ }
2276
+ function readAllGraphs(rootDir) {
2277
+ const result = {};
2278
+ for (const layer of LAYERS) {
2279
+ const graph = readGraph(rootDir, layer);
2280
+ if (graph) result[layer] = graph;
2281
+ }
2282
+ return result;
2283
+ }
2284
+ function generateGraph(rootDir, layer) {
2285
+ const dir = graphsDir(rootDir);
2286
+ (0, import_node_fs11.mkdirSync)(dir, { recursive: true });
2287
+ const results = layer ? [generateLayer(rootDir, layer)].filter((r) => r !== null) : generateAll(rootDir);
2288
+ for (const result of results) {
2289
+ const filePath = graphFilePath(rootDir, result.layer);
2290
+ (0, import_node_fs11.writeFileSync)(filePath, JSON.stringify(result.output, null, 2) + "\n", "utf-8");
2291
+ invalidateCache(filePath);
2292
+ invalidateTaggedCache(rootDir, result.layer);
2293
+ }
2294
+ return results;
2295
+ }
2296
+
2297
+ // src/server/lockfile.ts
2298
+ var import_node_child_process = require("node:child_process");
2299
+ var import_node_fs12 = require("node:fs");
2300
+ var import_node_os = require("node:os");
2301
+ var import_node_path14 = require("node:path");
2302
+ function lockDir(projectRoot) {
2303
+ if (projectRoot) {
2304
+ return (0, import_node_path14.join)(projectRoot, ".launchsecure");
2305
+ }
2306
+ return (0, import_node_path14.join)((0, import_node_os.homedir)(), ".launchsecure");
2307
+ }
2308
+ function lockPath(projectRoot) {
2309
+ return (0, import_node_path14.join)(lockDir(projectRoot), "launch-chart.lock");
2310
+ }
2311
+ var _activeProjectRoot;
2312
+ function readLock(projectRoot) {
2313
+ const root = projectRoot ?? _activeProjectRoot;
2314
+ const p = lockPath(root);
2315
+ if (!(0, import_node_fs12.existsSync)(p)) {
2316
+ if (root) {
2317
+ const globalP = lockPath();
2318
+ if ((0, import_node_fs12.existsSync)(globalP)) {
2319
+ try {
2320
+ const data = JSON.parse((0, import_node_fs12.readFileSync)(globalP, "utf-8"));
2321
+ if (typeof data.pid === "number" && typeof data.port === "number" && data.cwd === root) {
2322
+ return data;
2323
+ }
2324
+ } catch {
2325
+ }
2326
+ }
2327
+ }
2328
+ return null;
2329
+ }
2330
+ try {
2331
+ const data = JSON.parse((0, import_node_fs12.readFileSync)(p, "utf-8"));
2332
+ if (typeof data.pid !== "number" || typeof data.port !== "number") return null;
2333
+ return data;
2334
+ } catch {
2335
+ return null;
2336
+ }
2337
+ }
1476
2338
  function isPidAlive(pid) {
1477
2339
  try {
1478
2340
  process.kill(pid, 0);
@@ -1495,34 +2357,41 @@ function getListenerPid(port) {
1495
2357
  return null;
1496
2358
  }
1497
2359
  }
1498
- function getLiveLock() {
1499
- const lock = readLock();
2360
+ function getLiveLock(projectRoot) {
2361
+ const root = projectRoot ?? _activeProjectRoot;
2362
+ const lock = readLock(root);
1500
2363
  if (!lock) return null;
1501
2364
  const listenerPid = getListenerPid(lock.port);
1502
2365
  const live = listenerPid !== null ? listenerPid === lock.pid : isPidAlive(lock.pid);
1503
2366
  if (!live) {
1504
2367
  try {
1505
- (0, import_node_fs6.unlinkSync)(lockPath());
2368
+ (0, import_node_fs12.unlinkSync)(lockPath(root));
1506
2369
  } catch {
1507
2370
  }
1508
2371
  return null;
1509
2372
  }
1510
2373
  return lock;
1511
2374
  }
1512
- 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");
2375
+ function writeLock(data, projectRoot) {
2376
+ const root = projectRoot ?? _activeProjectRoot;
2377
+ (0, import_node_fs12.mkdirSync)(lockDir(root), { recursive: true });
2378
+ (0, import_node_fs12.writeFileSync)(lockPath(root), JSON.stringify(data, null, 2) + "\n", "utf-8");
2379
+ if (root) _activeProjectRoot = root;
1515
2380
  }
1516
- function clearLock() {
2381
+ function clearLock(projectRoot) {
2382
+ const root = projectRoot ?? _activeProjectRoot;
1517
2383
  try {
1518
- (0, import_node_fs6.unlinkSync)(lockPath());
2384
+ (0, import_node_fs12.unlinkSync)(lockPath(root));
1519
2385
  } catch {
1520
2386
  }
1521
2387
  }
1522
2388
 
1523
2389
  // src/server/chart-serve.ts
1524
- var DEFAULT_PORT = 52819;
1525
- var MAX_PORT_SCAN = 20;
2390
+ init_config();
2391
+ var MAX_PORT_SCAN = 3;
2392
+ function randomPort() {
2393
+ return 49152 + Math.floor(Math.random() * (65535 - 49152));
2394
+ }
1526
2395
  var MIME_TYPES = {
1527
2396
  ".html": "text/html; charset=utf-8",
1528
2397
  ".js": "application/javascript; charset=utf-8",
@@ -1537,16 +2406,16 @@ var MIME_TYPES = {
1537
2406
  function findProjectRoot(startDir) {
1538
2407
  let dir = startDir;
1539
2408
  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);
2409
+ const graphsDir2 = import_node_path15.default.join(dir, ".launchsecure", "graphs");
2410
+ 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;
2411
+ const parent = import_node_path15.default.dirname(dir);
1543
2412
  if (parent === dir) break;
1544
2413
  dir = parent;
1545
2414
  }
1546
2415
  dir = startDir;
1547
2416
  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);
2417
+ if (import_node_fs13.default.existsSync(import_node_path15.default.join(dir, ".git"))) return dir;
2418
+ const parent = import_node_path15.default.dirname(dir);
1550
2419
  if (parent === dir) break;
1551
2420
  dir = parent;
1552
2421
  }
@@ -1598,16 +2467,16 @@ function buildMergedGraph(projectRoot) {
1598
2467
  };
1599
2468
  }
1600
2469
  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();
2470
+ if (!import_node_fs13.default.existsSync(filePath) || !import_node_fs13.default.statSync(filePath).isFile()) return false;
2471
+ const ext = import_node_path15.default.extname(filePath).toLowerCase();
1603
2472
  const mime = MIME_TYPES[ext] ?? "application/octet-stream";
1604
2473
  res.writeHead(200, { "Content-Type": mime, "Cache-Control": "no-cache" });
1605
- import_node_fs7.default.createReadStream(filePath).pipe(res);
2474
+ import_node_fs13.default.createReadStream(filePath).pipe(res);
1606
2475
  return true;
1607
2476
  }
1608
2477
  function serveIndex(res, clientDir) {
1609
- const indexPath = import_node_path7.default.join(clientDir, "index.html");
1610
- if (!import_node_fs7.default.existsSync(indexPath)) {
2478
+ const indexPath = import_node_path15.default.join(clientDir, "index.html");
2479
+ if (!import_node_fs13.default.existsSync(indexPath)) {
1611
2480
  res.writeHead(500, { "Content-Type": "text/plain" });
1612
2481
  res.end(`LaunchChart client bundle not found at ${clientDir}. Run 'npm run build:chart-client'.`);
1613
2482
  return;
@@ -1615,14 +2484,14 @@ function serveIndex(res, clientDir) {
1615
2484
  serveStatic(res, indexPath);
1616
2485
  }
1617
2486
  function tryListen(server, port) {
1618
- return new Promise((resolve, reject) => {
2487
+ return new Promise((resolve3, reject) => {
1619
2488
  const onError = (err) => {
1620
2489
  server.off("listening", onListening);
1621
2490
  reject(err);
1622
2491
  };
1623
2492
  const onListening = () => {
1624
2493
  server.off("error", onError);
1625
- resolve(port);
2494
+ resolve3(port);
1626
2495
  };
1627
2496
  server.once("error", onError);
1628
2497
  server.once("listening", onListening);
@@ -1649,7 +2518,7 @@ async function bindWithFallback(server, startPort) {
1649
2518
  async function startChartServer(opts = {}) {
1650
2519
  const cwd = opts.cwd ?? process.cwd();
1651
2520
  const projectRoot = findProjectRoot(cwd);
1652
- const existing = getLiveLock();
2521
+ const existing = getLiveLock(projectRoot);
1653
2522
  if (existing) {
1654
2523
  if (!opts.quiet) {
1655
2524
  process.stderr.write(
@@ -1659,7 +2528,7 @@ async function startChartServer(opts = {}) {
1659
2528
  }
1660
2529
  return { port: existing.port, url: existing.url };
1661
2530
  }
1662
- const clientDir = opts.clientDir ?? import_node_path7.default.join(__dirname, "..", "chart-client");
2531
+ const clientDir = opts.clientDir ?? import_node_path15.default.join(__dirname, "..", "chart-client");
1663
2532
  const server = import_node_http.default.createServer((req, res) => {
1664
2533
  try {
1665
2534
  const url2 = new URL(req.url ?? "/", `http://${req.headers.host}`);
@@ -1697,13 +2566,154 @@ async function startChartServer(opts = {}) {
1697
2566
  }
1698
2567
  return;
1699
2568
  }
2569
+ if (req.method === "GET" && url2.pathname === "/api/file-content") {
2570
+ const relPath = url2.searchParams.get("path");
2571
+ if (!relPath || relPath.includes("..") || import_node_path15.default.isAbsolute(relPath)) {
2572
+ res.writeHead(400, { "Content-Type": "application/json" });
2573
+ res.end(JSON.stringify({ error: "Invalid path" }));
2574
+ return;
2575
+ }
2576
+ const filePath = import_node_path15.default.join(projectRoot, relPath);
2577
+ if (!filePath.startsWith(projectRoot) || !import_node_fs13.default.existsSync(filePath) || !import_node_fs13.default.statSync(filePath).isFile()) {
2578
+ res.writeHead(404, { "Content-Type": "application/json" });
2579
+ res.end(JSON.stringify({ error: "File not found" }));
2580
+ return;
2581
+ }
2582
+ const ext = import_node_path15.default.extname(filePath).toLowerCase();
2583
+ const langMap = { ".ts": "typescript", ".tsx": "tsx", ".js": "javascript", ".jsx": "jsx", ".prisma": "prisma", ".json": "json", ".css": "css" };
2584
+ const content = import_node_fs13.default.readFileSync(filePath, "utf-8");
2585
+ res.writeHead(200, { "Content-Type": "application/json" });
2586
+ res.end(JSON.stringify({ content, language: langMap[ext] ?? "text", path: relPath }));
2587
+ return;
2588
+ }
1700
2589
  if (req.method === "GET" && url2.pathname === "/api/health") {
1701
2590
  res.writeHead(200, { "Content-Type": "application/json" });
1702
2591
  res.end(JSON.stringify({ ok: true, projectRoot }));
1703
2592
  return;
1704
2593
  }
2594
+ if (req.method === "GET" && url2.pathname === "/api/parser-config") {
2595
+ const config = loadConfig(projectRoot);
2596
+ const detection = [
2597
+ { id: "react-nextjs", layer: "ui", label: "React + Next.js", detected: reactNextjsParser.detect(projectRoot) },
2598
+ { id: "nextjs-routes", layer: "api", label: "Next.js API Routes", detected: nextjsRoutesParser.detect(projectRoot) },
2599
+ { id: "prisma-schema", layer: "db", label: "Prisma Schema", detected: prismaSchemaParser.detect(projectRoot) }
2600
+ ];
2601
+ const crosslayerParsers = [
2602
+ { id: "fetch-resolver", label: "Fetch / api.method() calls" },
2603
+ { id: "api-annotations", label: "@api annotations" },
2604
+ { id: "url-literal-scanner", label: "/api/... URL literals" }
2605
+ ];
2606
+ res.writeHead(200, { "Content-Type": "application/json" });
2607
+ res.end(JSON.stringify({ config, detection, crosslayerParsers }));
2608
+ return;
2609
+ }
2610
+ if (req.method === "POST" && url2.pathname === "/api/parser-config") {
2611
+ let body = "";
2612
+ req.on("data", (chunk) => {
2613
+ body += chunk.toString();
2614
+ });
2615
+ req.on("end", () => {
2616
+ try {
2617
+ const newConfig = JSON.parse(body);
2618
+ const configPath = import_node_path15.default.join(projectRoot, ".launchchart.json");
2619
+ import_node_fs13.default.writeFileSync(configPath, JSON.stringify(newConfig, null, 2) + "\n", "utf-8");
2620
+ res.writeHead(200, { "Content-Type": "application/json" });
2621
+ res.end(JSON.stringify({ ok: true }));
2622
+ } catch (err) {
2623
+ res.writeHead(400, { "Content-Type": "application/json" });
2624
+ res.end(JSON.stringify({ ok: false, error: String(err) }));
2625
+ }
2626
+ });
2627
+ return;
2628
+ }
2629
+ if (req.method === "GET" && url2.pathname === "/api/tagger-config") {
2630
+ const config = loadConfig(projectRoot);
2631
+ const builtinTaggers = [
2632
+ { id: "module", tagKey: "module", trackUntagged: config.taggers?.trackUntagged?.module ?? true },
2633
+ { id: "screen", tagKey: "screen", trackUntagged: config.taggers?.trackUntagged?.screen ?? true }
2634
+ ];
2635
+ const disabled = config.taggers?.disabled ?? [];
2636
+ const customTaggers = config.taggers?.custom ?? [];
2637
+ const moduleRules = config.taggers?.module?.rules ?? [];
2638
+ res.writeHead(200, { "Content-Type": "application/json" });
2639
+ res.end(JSON.stringify({ builtinTaggers, disabled, customTaggers, moduleRules }));
2640
+ return;
2641
+ }
2642
+ if (req.method === "POST" && url2.pathname === "/api/tagger-config") {
2643
+ let body = "";
2644
+ req.on("data", (chunk) => {
2645
+ body += chunk.toString();
2646
+ });
2647
+ req.on("end", () => {
2648
+ try {
2649
+ const taggerConfig = JSON.parse(body);
2650
+ const config = loadConfig(projectRoot);
2651
+ config.taggers = taggerConfig;
2652
+ const configPath = import_node_path15.default.join(projectRoot, ".launchchart.json");
2653
+ import_node_fs13.default.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
2654
+ res.writeHead(200, { "Content-Type": "application/json" });
2655
+ res.end(JSON.stringify({ ok: true }));
2656
+ } catch (err) {
2657
+ res.writeHead(400, { "Content-Type": "application/json" });
2658
+ res.end(JSON.stringify({ ok: false, error: String(err) }));
2659
+ }
2660
+ });
2661
+ return;
2662
+ }
2663
+ if (req.method === "GET" && url2.pathname === "/api/tags") {
2664
+ const store = readTagStore(projectRoot);
2665
+ res.writeHead(200, { "Content-Type": "application/json" });
2666
+ res.end(JSON.stringify(store));
2667
+ return;
2668
+ }
2669
+ if (req.method === "POST" && url2.pathname === "/api/tags") {
2670
+ let body = "";
2671
+ req.on("data", (chunk) => {
2672
+ body += chunk.toString();
2673
+ });
2674
+ req.on("end", () => {
2675
+ try {
2676
+ const { nodeId, key, value } = JSON.parse(body);
2677
+ if (!nodeId || !key || !value) {
2678
+ res.writeHead(400, { "Content-Type": "application/json" });
2679
+ res.end(JSON.stringify({ ok: false, error: "nodeId, key, and value are required" }));
2680
+ return;
2681
+ }
2682
+ setTag(projectRoot, nodeId, key, value);
2683
+ res.writeHead(200, { "Content-Type": "application/json" });
2684
+ res.end(JSON.stringify({ ok: true }));
2685
+ } catch (err) {
2686
+ res.writeHead(400, { "Content-Type": "application/json" });
2687
+ res.end(JSON.stringify({ ok: false, error: String(err) }));
2688
+ }
2689
+ });
2690
+ return;
2691
+ }
2692
+ if (req.method === "DELETE" && url2.pathname === "/api/tags") {
2693
+ let body = "";
2694
+ req.on("data", (chunk) => {
2695
+ body += chunk.toString();
2696
+ });
2697
+ req.on("end", () => {
2698
+ try {
2699
+ const { nodeId, key } = JSON.parse(body);
2700
+ if (!nodeId || !key) {
2701
+ res.writeHead(400, { "Content-Type": "application/json" });
2702
+ res.end(JSON.stringify({ ok: false, error: "nodeId and key are required" }));
2703
+ return;
2704
+ }
2705
+ removeTag(projectRoot, nodeId, key);
2706
+ res.writeHead(200, { "Content-Type": "application/json" });
2707
+ res.end(JSON.stringify({ ok: true }));
2708
+ } catch (err) {
2709
+ res.writeHead(400, { "Content-Type": "application/json" });
2710
+ res.end(JSON.stringify({ ok: false, error: String(err) }));
2711
+ }
2712
+ });
2713
+ return;
2714
+ }
1705
2715
  if (url2.pathname !== "/") {
1706
- const staticPath = import_node_path7.default.join(clientDir, url2.pathname);
2716
+ const staticPath = import_node_path15.default.join(clientDir, url2.pathname);
1707
2717
  if (serveStatic(res, staticPath)) return;
1708
2718
  }
1709
2719
  serveIndex(res, clientDir);
@@ -1712,7 +2722,8 @@ async function startChartServer(opts = {}) {
1712
2722
  res.end(JSON.stringify({ error: String(err) }));
1713
2723
  }
1714
2724
  });
1715
- const port = await bindWithFallback(server, opts.port ?? DEFAULT_PORT);
2725
+ const startPort = opts.port ?? randomPort();
2726
+ const port = await bindWithFallback(server, startPort);
1716
2727
  const url = `http://localhost:${port}`;
1717
2728
  writeLock({
1718
2729
  pid: process.pid,
@@ -1720,9 +2731,9 @@ async function startChartServer(opts = {}) {
1720
2731
  cwd,
1721
2732
  url,
1722
2733
  startedAt: (/* @__PURE__ */ new Date()).toISOString()
1723
- });
2734
+ }, projectRoot);
1724
2735
  const cleanup = () => {
1725
- clearLock();
2736
+ clearLock(projectRoot);
1726
2737
  server.close();
1727
2738
  };
1728
2739
  process.once("SIGINT", () => {