@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.
- package/dist/chart-client/assets/{index-BPR5akxH.js → index-BUih0oqR.js} +99 -64
- package/dist/chart-client/assets/index-DFslt72L.css +1 -0
- package/dist/chart-client/index.html +2 -2
- package/dist/client/assets/index-BCxRNp8I.css +32 -0
- package/dist/client/index.html +2 -2
- package/dist/server/chart-serve.js +796 -275
- package/dist/server/cli.js +939 -298
- package/dist/server/graph-mcp-entry.js +1028 -305
- package/package.json +1 -1
- package/dist/chart-client/assets/index-DhNl1aFF.css +0 -1
- package/dist/client/assets/index-nR-HgoHH.css +0 -32
- /package/dist/client/assets/{index-D9e81rsq.js → index-DCC--GO-.js} +0 -0
|
@@ -105,6 +105,26 @@ var init_lockfile = __esm({
|
|
|
105
105
|
}
|
|
106
106
|
});
|
|
107
107
|
|
|
108
|
+
// src/server/graph/core/config.ts
|
|
109
|
+
function loadConfig(rootDir) {
|
|
110
|
+
const configPath = (0, import_node_path2.join)(rootDir, CONFIG_FILENAME);
|
|
111
|
+
if (!(0, import_node_fs2.existsSync)(configPath)) return {};
|
|
112
|
+
try {
|
|
113
|
+
return JSON.parse((0, import_node_fs2.readFileSync)(configPath, "utf-8"));
|
|
114
|
+
} catch {
|
|
115
|
+
return {};
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
var import_node_fs2, import_node_path2, CONFIG_FILENAME;
|
|
119
|
+
var init_config = __esm({
|
|
120
|
+
"src/server/graph/core/config.ts"() {
|
|
121
|
+
"use strict";
|
|
122
|
+
import_node_fs2 = require("node:fs");
|
|
123
|
+
import_node_path2 = require("node:path");
|
|
124
|
+
CONFIG_FILENAME = ".launchchart.json";
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
108
128
|
// src/server/graph/core/ast-helpers.ts
|
|
109
129
|
function getTs() {
|
|
110
130
|
if (!tsModule) {
|
|
@@ -114,8 +134,8 @@ function getTs() {
|
|
|
114
134
|
}
|
|
115
135
|
function parseFile(absPath) {
|
|
116
136
|
const ts = getTs();
|
|
117
|
-
const content = (0,
|
|
118
|
-
const ext = (0,
|
|
137
|
+
const content = (0, import_node_fs3.readFileSync)(absPath, "utf-8");
|
|
138
|
+
const ext = (0, import_node_path3.extname)(absPath);
|
|
119
139
|
const scriptKind = ext === ".tsx" ? ts.ScriptKind.TSX : ext === ".ts" ? ts.ScriptKind.TS : ext === ".jsx" ? ts.ScriptKind.JSX : ts.ScriptKind.JS;
|
|
120
140
|
const sourceFile = ts.createSourceFile(
|
|
121
141
|
absPath,
|
|
@@ -382,8 +402,8 @@ function parseFile(absPath) {
|
|
|
382
402
|
}
|
|
383
403
|
function extractDbCalls(absPath) {
|
|
384
404
|
const ts = getTs();
|
|
385
|
-
const content = (0,
|
|
386
|
-
const ext = (0,
|
|
405
|
+
const content = (0, import_node_fs3.readFileSync)(absPath, "utf-8");
|
|
406
|
+
const ext = (0, import_node_path3.extname)(absPath);
|
|
387
407
|
const scriptKind = ext === ".tsx" ? ts.ScriptKind.TSX : ts.ScriptKind.TS;
|
|
388
408
|
const sourceFile = ts.createSourceFile(absPath, content, ts.ScriptTarget.Latest, true, scriptKind);
|
|
389
409
|
const calls = [];
|
|
@@ -412,8 +432,8 @@ function extractDbCalls(absPath) {
|
|
|
412
432
|
}
|
|
413
433
|
function extractAuthWrappers(absPath) {
|
|
414
434
|
const ts = getTs();
|
|
415
|
-
const content = (0,
|
|
416
|
-
const ext = (0,
|
|
435
|
+
const content = (0, import_node_fs3.readFileSync)(absPath, "utf-8");
|
|
436
|
+
const ext = (0, import_node_path3.extname)(absPath);
|
|
417
437
|
const scriptKind = ext === ".tsx" ? ts.ScriptKind.TSX : ts.ScriptKind.TS;
|
|
418
438
|
const sourceFile = ts.createSourceFile(absPath, content, ts.ScriptTarget.Latest, true, scriptKind);
|
|
419
439
|
const wrappers = /* @__PURE__ */ new Set();
|
|
@@ -429,12 +449,12 @@ function extractAuthWrappers(absPath) {
|
|
|
429
449
|
visit(sourceFile);
|
|
430
450
|
return wrappers;
|
|
431
451
|
}
|
|
432
|
-
var
|
|
452
|
+
var import_node_fs3, import_node_path3, tsModule, HTTP_METHODS, MUTATION_METHODS;
|
|
433
453
|
var init_ast_helpers = __esm({
|
|
434
454
|
"src/server/graph/core/ast-helpers.ts"() {
|
|
435
455
|
"use strict";
|
|
436
|
-
|
|
437
|
-
|
|
456
|
+
import_node_fs3 = require("node:fs");
|
|
457
|
+
import_node_path3 = require("node:path");
|
|
438
458
|
HTTP_METHODS = /* @__PURE__ */ new Set(["get", "post", "put", "patch", "delete", "head", "options"]);
|
|
439
459
|
MUTATION_METHODS = /* @__PURE__ */ new Set([
|
|
440
460
|
"create",
|
|
@@ -453,12 +473,12 @@ var init_ast_helpers = __esm({
|
|
|
453
473
|
// src/server/graph/parsers/ui/react-nextjs.ts
|
|
454
474
|
function walk(dir, exts) {
|
|
455
475
|
const results = [];
|
|
456
|
-
if (!(0,
|
|
457
|
-
for (const entry of (0,
|
|
458
|
-
const full = (0,
|
|
476
|
+
if (!(0, import_node_fs4.existsSync)(dir)) return results;
|
|
477
|
+
for (const entry of (0, import_node_fs4.readdirSync)(dir, { withFileTypes: true })) {
|
|
478
|
+
const full = (0, import_node_path4.join)(dir, entry.name);
|
|
459
479
|
if (entry.isDirectory()) {
|
|
460
480
|
results.push(...walk(full, exts));
|
|
461
|
-
} else if (exts.includes((0,
|
|
481
|
+
} else if (exts.includes((0, import_node_path4.extname)(entry.name))) {
|
|
462
482
|
results.push(full);
|
|
463
483
|
}
|
|
464
484
|
}
|
|
@@ -466,33 +486,33 @@ function walk(dir, exts) {
|
|
|
466
486
|
}
|
|
467
487
|
function walkWithIgnore(dir, exts, ignoreDirs) {
|
|
468
488
|
const results = [];
|
|
469
|
-
if (!(0,
|
|
470
|
-
for (const entry of (0,
|
|
489
|
+
if (!(0, import_node_fs4.existsSync)(dir)) return results;
|
|
490
|
+
for (const entry of (0, import_node_fs4.readdirSync)(dir, { withFileTypes: true })) {
|
|
471
491
|
if (entry.isDirectory()) {
|
|
472
492
|
if (ignoreDirs.has(entry.name)) continue;
|
|
473
|
-
results.push(...walkWithIgnore((0,
|
|
474
|
-
} else if (exts.includes((0,
|
|
475
|
-
results.push((0,
|
|
493
|
+
results.push(...walkWithIgnore((0, import_node_path4.join)(dir, entry.name), exts, ignoreDirs));
|
|
494
|
+
} else if (exts.includes((0, import_node_path4.extname)(entry.name))) {
|
|
495
|
+
results.push((0, import_node_path4.join)(dir, entry.name));
|
|
476
496
|
}
|
|
477
497
|
}
|
|
478
498
|
return results;
|
|
479
499
|
}
|
|
480
500
|
function toNodeId(srcDir, absPath) {
|
|
481
|
-
return (0,
|
|
501
|
+
return (0, import_node_path4.relative)(srcDir, absPath).replace(/\\/g, "/");
|
|
482
502
|
}
|
|
483
503
|
function resolveImport(srcDir, specifier) {
|
|
484
504
|
if (!specifier.startsWith("@/")) return null;
|
|
485
505
|
const rel = specifier.slice(2);
|
|
486
|
-
const base = (0,
|
|
487
|
-
for (const c of [base, base + ".ts", base + ".tsx", (0,
|
|
488
|
-
if ((0,
|
|
506
|
+
const base = (0, import_node_path4.join)(srcDir, rel);
|
|
507
|
+
for (const c of [base, base + ".ts", base + ".tsx", (0, import_node_path4.join)(base, "index.ts"), (0, import_node_path4.join)(base, "index.tsx")]) {
|
|
508
|
+
if ((0, import_node_fs4.existsSync)(c) && (0, import_node_fs4.statSync)(c).isFile()) return c;
|
|
489
509
|
}
|
|
490
510
|
return null;
|
|
491
511
|
}
|
|
492
512
|
function resolveRelativeImport(fromFile, specifier) {
|
|
493
|
-
const base = (0,
|
|
494
|
-
for (const c of [base, base + ".ts", base + ".tsx", (0,
|
|
495
|
-
if ((0,
|
|
513
|
+
const base = (0, import_node_path4.join)((0, import_node_path4.dirname)(fromFile), specifier);
|
|
514
|
+
for (const c of [base, base + ".ts", base + ".tsx", (0, import_node_path4.join)(base, "index.ts"), (0, import_node_path4.join)(base, "index.tsx")]) {
|
|
515
|
+
if ((0, import_node_fs4.existsSync)(c) && (0, import_node_fs4.statSync)(c).isFile()) return c;
|
|
496
516
|
}
|
|
497
517
|
return null;
|
|
498
518
|
}
|
|
@@ -513,7 +533,7 @@ function resolveBarrelMap(barrelAbsPath, parsedByPath, memo, visiting) {
|
|
|
513
533
|
const resolved = resolveRelativeImport(barrelAbsPath, re.from);
|
|
514
534
|
if (!resolved) continue;
|
|
515
535
|
if (re.isWildcard) {
|
|
516
|
-
const targetBn = (0,
|
|
536
|
+
const targetBn = (0, import_node_path4.basename)(resolved);
|
|
517
537
|
const targetIsBarrel = targetBn === "index.ts" || targetBn === "index.tsx";
|
|
518
538
|
if (targetIsBarrel) {
|
|
519
539
|
const nested = resolveBarrelMap(resolved, parsedByPath, memo, visiting);
|
|
@@ -540,12 +560,12 @@ function buildAllBarrelMaps(srcDir, parsedByPath) {
|
|
|
540
560
|
const barrels = /* @__PURE__ */ new Map();
|
|
541
561
|
const memo = /* @__PURE__ */ new Map();
|
|
542
562
|
for (const [absPath, parsed] of parsedByPath) {
|
|
543
|
-
const bn = (0,
|
|
563
|
+
const bn = (0, import_node_path4.basename)(absPath);
|
|
544
564
|
if (bn !== "index.ts" && bn !== "index.tsx") continue;
|
|
545
565
|
if (parsed.reExports.length === 0) continue;
|
|
546
566
|
const map = resolveBarrelMap(absPath, parsedByPath, memo, /* @__PURE__ */ new Set());
|
|
547
567
|
if (map.size > 0) {
|
|
548
|
-
const barrelId = (0,
|
|
568
|
+
const barrelId = (0, import_node_path4.relative)(srcDir, (0, import_node_path4.dirname)(absPath)).replace(/\\/g, "/");
|
|
549
569
|
barrels.set(barrelId, map);
|
|
550
570
|
}
|
|
551
571
|
}
|
|
@@ -604,7 +624,7 @@ function extractRoute(id) {
|
|
|
604
624
|
return route || "/";
|
|
605
625
|
}
|
|
606
626
|
function nameFromFilename(absPath) {
|
|
607
|
-
return (0,
|
|
627
|
+
return (0, import_node_path4.basename)(absPath, (0, import_node_path4.extname)(absPath)).replace(/[-_](\w)/g, (_, c) => c.toUpperCase()).replace(/^(\w)/, (_, c) => c.toUpperCase());
|
|
608
628
|
}
|
|
609
629
|
function resolveTemplateLiteralRoute(template, routeToNodeId) {
|
|
610
630
|
const parameterized = template.replace(/\$\{([^}]+)\}/g, (_, expr) => {
|
|
@@ -685,105 +705,6 @@ function matchRouteToPage(route, routeToNodeId) {
|
|
|
685
705
|
if (routeToNodeId.has(normalized)) return routeToNodeId.get(normalized);
|
|
686
706
|
return null;
|
|
687
707
|
}
|
|
688
|
-
function loadApiRoutes(rootDir) {
|
|
689
|
-
const apiJsonPath = (0, import_node_path3.join)(rootDir, ".launchsecure", "graphs", "api.json");
|
|
690
|
-
if (!(0, import_node_fs3.existsSync)(apiJsonPath)) return [];
|
|
691
|
-
try {
|
|
692
|
-
const parsed = JSON.parse((0, import_node_fs3.readFileSync)(apiJsonPath, "utf-8"));
|
|
693
|
-
const routes = [];
|
|
694
|
-
for (const n of parsed.nodes ?? []) {
|
|
695
|
-
const path3 = n.path;
|
|
696
|
-
if (!path3 || typeof path3 !== "string") continue;
|
|
697
|
-
routes.push({
|
|
698
|
-
path: path3,
|
|
699
|
-
nodeId: n.id,
|
|
700
|
-
segments: path3.split("/").filter(Boolean)
|
|
701
|
-
});
|
|
702
|
-
}
|
|
703
|
-
return routes;
|
|
704
|
-
} catch {
|
|
705
|
-
return [];
|
|
706
|
-
}
|
|
707
|
-
}
|
|
708
|
-
function buildApiPathMap(routes) {
|
|
709
|
-
const map = /* @__PURE__ */ new Map();
|
|
710
|
-
for (const r of routes) {
|
|
711
|
-
if (!map.has(r.path)) map.set(r.path, r.nodeId);
|
|
712
|
-
}
|
|
713
|
-
return map;
|
|
714
|
-
}
|
|
715
|
-
function normalizeFetchUrl(raw) {
|
|
716
|
-
let s = raw.replace(/^`|`$/g, "");
|
|
717
|
-
const qIdx = s.indexOf("?");
|
|
718
|
-
if (qIdx >= 0) s = s.slice(0, qIdx);
|
|
719
|
-
const hIdx = s.indexOf("#");
|
|
720
|
-
if (hIdx >= 0) s = s.slice(0, hIdx);
|
|
721
|
-
let hadInterpolation = false;
|
|
722
|
-
s = s.replace(/\$\{([^}]+)\}/g, (_, expr) => {
|
|
723
|
-
hadInterpolation = true;
|
|
724
|
-
const cleaned = expr.trim();
|
|
725
|
-
const last = cleaned.split(".").pop() ?? cleaned;
|
|
726
|
-
const name = last.replace(/[^\w]/g, "") || "param";
|
|
727
|
-
return ":" + name;
|
|
728
|
-
});
|
|
729
|
-
s = s.replace(/\/+/g, "/");
|
|
730
|
-
if (s.length > 1 && s.endsWith("/")) s = s.slice(0, -1);
|
|
731
|
-
return { path: s || "/", hadInterpolation };
|
|
732
|
-
}
|
|
733
|
-
function scoreApiRouteMatch(candidate, known) {
|
|
734
|
-
if (candidate.length !== known.length) return -1;
|
|
735
|
-
let score = 0;
|
|
736
|
-
for (let i = 0; i < candidate.length; i++) {
|
|
737
|
-
const a = candidate[i];
|
|
738
|
-
const b = known[i];
|
|
739
|
-
if (a === b) {
|
|
740
|
-
score += 3;
|
|
741
|
-
continue;
|
|
742
|
-
}
|
|
743
|
-
if (a.startsWith(":") && b.startsWith(":")) {
|
|
744
|
-
score += 2;
|
|
745
|
-
continue;
|
|
746
|
-
}
|
|
747
|
-
if (a.startsWith(":") || b.startsWith(":")) {
|
|
748
|
-
score += 1;
|
|
749
|
-
continue;
|
|
750
|
-
}
|
|
751
|
-
return -1;
|
|
752
|
-
}
|
|
753
|
-
return score;
|
|
754
|
-
}
|
|
755
|
-
function resolveFetchCall(call, apiPathMap, apiRoutes) {
|
|
756
|
-
const raw = call.url;
|
|
757
|
-
if (/^(https?:)?\/\//i.test(raw)) {
|
|
758
|
-
return { kind: "external", normalizedUrl: raw };
|
|
759
|
-
}
|
|
760
|
-
if (call.isConcat) {
|
|
761
|
-
return { kind: "dynamic", normalizedUrl: raw };
|
|
762
|
-
}
|
|
763
|
-
const { path: path3, hadInterpolation } = normalizeFetchUrl(raw);
|
|
764
|
-
if (!path3.startsWith("/")) {
|
|
765
|
-
return { kind: "unresolved", normalizedUrl: path3 };
|
|
766
|
-
}
|
|
767
|
-
const segs = path3.split("/").filter(Boolean);
|
|
768
|
-
if (hadInterpolation && segs.length > 0 && segs[0].startsWith(":")) {
|
|
769
|
-
return { kind: "dynamic", normalizedUrl: path3 };
|
|
770
|
-
}
|
|
771
|
-
const exact = apiPathMap.get(path3);
|
|
772
|
-
if (exact) return { kind: "resolved", nodeId: exact, normalizedUrl: path3 };
|
|
773
|
-
let bestScore = -1;
|
|
774
|
-
let bestId = null;
|
|
775
|
-
for (const r of apiRoutes) {
|
|
776
|
-
const score = scoreApiRouteMatch(segs, r.segments);
|
|
777
|
-
if (score > bestScore) {
|
|
778
|
-
bestScore = score;
|
|
779
|
-
bestId = r.nodeId;
|
|
780
|
-
}
|
|
781
|
-
}
|
|
782
|
-
if (bestId && bestScore > 0) {
|
|
783
|
-
return { kind: "resolved", nodeId: bestId, normalizedUrl: path3 };
|
|
784
|
-
}
|
|
785
|
-
return { kind: "unresolved", normalizedUrl: path3 };
|
|
786
|
-
}
|
|
787
708
|
function extractEdges(srcDir, absPath, sourceId, parsed, nodeIdSet, nodeTypeMap, barrelMaps, routeToNodeId) {
|
|
788
709
|
const edges = [];
|
|
789
710
|
const flagged = [];
|
|
@@ -883,26 +804,26 @@ function extractEdges(srcDir, absPath, sourceId, parsed, nodeIdSet, nodeTypeMap,
|
|
|
883
804
|
return { edges, flagged };
|
|
884
805
|
}
|
|
885
806
|
function detect(rootDir) {
|
|
886
|
-
return (0,
|
|
807
|
+
return (0, import_node_fs4.existsSync)((0, import_node_path4.join)(rootDir, "src", "app")) && (0, import_node_fs4.existsSync)((0, import_node_path4.join)(rootDir, "next.config.ts")) || (0, import_node_fs4.existsSync)((0, import_node_path4.join)(rootDir, "next.config.js")) || (0, import_node_fs4.existsSync)((0, import_node_path4.join)(rootDir, "next.config.mjs"));
|
|
887
808
|
}
|
|
888
809
|
function generate(rootDir) {
|
|
889
|
-
const srcDir = (0,
|
|
890
|
-
const appFiles = walk((0,
|
|
891
|
-
(f) => (0,
|
|
810
|
+
const srcDir = (0, import_node_path4.join)(rootDir, "src");
|
|
811
|
+
const appFiles = walk((0, import_node_path4.join)(srcDir, "app"), [".tsx", ".ts"]).filter(
|
|
812
|
+
(f) => (0, import_node_path4.basename)(f) !== "route.ts" && (0, import_node_path4.basename)(f) !== "route.tsx"
|
|
892
813
|
);
|
|
893
|
-
const clientFiles = walk((0,
|
|
894
|
-
const serverFiles = walk((0,
|
|
895
|
-
(f) => (0,
|
|
814
|
+
const clientFiles = walk((0, import_node_path4.join)(srcDir, "client"), [".tsx", ".ts"]);
|
|
815
|
+
const serverFiles = walk((0, import_node_path4.join)(srcDir, "server"), [".ts", ".tsx"]).filter(
|
|
816
|
+
(f) => (0, import_node_path4.basename)(f) !== "route.ts" && (0, import_node_path4.basename)(f) !== "route.tsx"
|
|
896
817
|
);
|
|
897
|
-
const libFiles = walk((0,
|
|
898
|
-
const configFiles = walk((0,
|
|
818
|
+
const libFiles = walk((0, import_node_path4.join)(srcDir, "lib"), [".ts", ".tsx"]);
|
|
819
|
+
const configFiles = walk((0, import_node_path4.join)(srcDir, "config"), [".ts", ".tsx"]);
|
|
899
820
|
const allDiscovered = [...appFiles, ...clientFiles, ...serverFiles, ...libFiles, ...configFiles];
|
|
900
821
|
const parsedByPath = /* @__PURE__ */ new Map();
|
|
901
822
|
for (const absPath of allDiscovered) {
|
|
902
823
|
parsedByPath.set(absPath, parseFile(absPath));
|
|
903
824
|
}
|
|
904
825
|
const barrelMaps = buildAllBarrelMaps(srcDir, parsedByPath);
|
|
905
|
-
const fileSet = allDiscovered.filter((f) => !(0,
|
|
826
|
+
const fileSet = allDiscovered.filter((f) => !(0, import_node_path4.basename)(f).startsWith("index."));
|
|
906
827
|
const nodes = [];
|
|
907
828
|
const nodeIdSet = /* @__PURE__ */ new Set();
|
|
908
829
|
const nodeTypeMap = /* @__PURE__ */ new Map();
|
|
@@ -921,7 +842,6 @@ function generate(rootDir) {
|
|
|
921
842
|
}
|
|
922
843
|
const allEdges = [];
|
|
923
844
|
const allFlagged = [];
|
|
924
|
-
const crossRefs = [];
|
|
925
845
|
for (const absPath of fileSet) {
|
|
926
846
|
const sourceId = toNodeId(srcDir, absPath);
|
|
927
847
|
const parsed = parsedByPath.get(absPath);
|
|
@@ -938,66 +858,21 @@ function generate(rootDir) {
|
|
|
938
858
|
allEdges.push(...edges);
|
|
939
859
|
allFlagged.push(...flagged);
|
|
940
860
|
}
|
|
941
|
-
const
|
|
942
|
-
const apiPathMap = buildApiPathMap(apiRoutes);
|
|
943
|
-
const includeExternalFetches = process.env.LAUNCH_CHART_INCLUDE_EXTERNAL_FETCHES === "1";
|
|
944
|
-
const fetchSeen = /* @__PURE__ */ new Set();
|
|
945
|
-
let fetchResolvedCount = 0;
|
|
946
|
-
let fetchDynamicCount = 0;
|
|
947
|
-
let fetchUnresolvedCount = 0;
|
|
948
|
-
let fetchExternalCount = 0;
|
|
861
|
+
const fetchCallEntries = [];
|
|
949
862
|
for (const absPath of fileSet) {
|
|
950
863
|
const sourceId = toNodeId(srcDir, absPath);
|
|
951
864
|
const parsed = parsedByPath.get(absPath);
|
|
952
865
|
if (parsed.fetchCalls.length === 0) continue;
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
type: "calls_api",
|
|
964
|
-
layer: "api"
|
|
965
|
-
});
|
|
966
|
-
fetchResolvedCount++;
|
|
967
|
-
continue;
|
|
968
|
-
}
|
|
969
|
-
if (result.kind === "dynamic") {
|
|
970
|
-
fetchDynamicCount++;
|
|
971
|
-
allFlagged.push({
|
|
972
|
-
source: sourceId,
|
|
973
|
-
target: "DYNAMIC",
|
|
974
|
-
type: "calls_api",
|
|
975
|
-
label: call.isConcat ? `${methodTag} fetch with concat: ${call.url}` : `${methodTag} fetch with template: ${call.url}`,
|
|
976
|
-
confidence: call.isConcat ? "low" : "medium"
|
|
977
|
-
});
|
|
978
|
-
continue;
|
|
979
|
-
}
|
|
980
|
-
if (result.kind === "external") {
|
|
981
|
-
fetchExternalCount++;
|
|
982
|
-
if (!includeExternalFetches) continue;
|
|
983
|
-
allFlagged.push({
|
|
984
|
-
source: sourceId,
|
|
985
|
-
target: "EXTERNAL",
|
|
986
|
-
type: "calls_external",
|
|
987
|
-
label: `${methodTag} external fetch: ${call.url}`,
|
|
988
|
-
confidence: "high"
|
|
989
|
-
});
|
|
990
|
-
continue;
|
|
991
|
-
}
|
|
992
|
-
fetchUnresolvedCount++;
|
|
993
|
-
allFlagged.push({
|
|
994
|
-
source: sourceId,
|
|
995
|
-
target: "UNRESOLVED",
|
|
996
|
-
type: "calls_api",
|
|
997
|
-
label: `${methodTag} fetch to unknown path: ${result.normalizedUrl}`,
|
|
998
|
-
confidence: "medium"
|
|
999
|
-
});
|
|
1000
|
-
}
|
|
866
|
+
fetchCallEntries.push({
|
|
867
|
+
nodeId: sourceId,
|
|
868
|
+
calls: parsed.fetchCalls.map((c) => ({
|
|
869
|
+
url: c.url,
|
|
870
|
+
method: c.method,
|
|
871
|
+
isTemplate: c.isTemplate,
|
|
872
|
+
isConcat: c.isConcat,
|
|
873
|
+
kind: c.kind
|
|
874
|
+
}))
|
|
875
|
+
});
|
|
1001
876
|
}
|
|
1002
877
|
const externalScanned = new Set(allDiscovered.map((f) => f.replace(/\\/g, "/")));
|
|
1003
878
|
const IGNORE_DIRS = /* @__PURE__ */ new Set([
|
|
@@ -1023,7 +898,7 @@ function generate(rootDir) {
|
|
|
1023
898
|
} catch {
|
|
1024
899
|
continue;
|
|
1025
900
|
}
|
|
1026
|
-
const externalId = (0,
|
|
901
|
+
const externalId = (0, import_node_path4.relative)(rootDir, absPath).replace(/\\/g, "/");
|
|
1027
902
|
const edgesFromThis = [];
|
|
1028
903
|
const seen = /* @__PURE__ */ new Set();
|
|
1029
904
|
for (const imp of parsed.imports) {
|
|
@@ -1114,20 +989,11 @@ function generate(rootDir) {
|
|
|
1114
989
|
layer: "ui",
|
|
1115
990
|
parser: "react-nextjs-ast",
|
|
1116
991
|
...stats,
|
|
1117
|
-
api_call_detection: {
|
|
1118
|
-
includeExternalFetches,
|
|
1119
|
-
includeConcatFetches: process.env.LAUNCH_CHART_INCLUDE_CONCAT_FETCHES === "1",
|
|
1120
|
-
apiRoutesLoaded: apiRoutes.length,
|
|
1121
|
-
resolved: fetchResolvedCount,
|
|
1122
|
-
dynamic: fetchDynamicCount,
|
|
1123
|
-
unresolved: fetchUnresolvedCount,
|
|
1124
|
-
external: fetchExternalCount
|
|
1125
|
-
},
|
|
1126
992
|
notes: "Auto-generated via TypeScript AST \u2014 edges derived from actual imports, renders from JSX usage, navigations from router/Link calls."
|
|
1127
993
|
},
|
|
1128
994
|
nodes,
|
|
1129
995
|
edges: allEdges,
|
|
1130
|
-
cross_refs:
|
|
996
|
+
cross_refs: [],
|
|
1131
997
|
contradictions: [],
|
|
1132
998
|
warnings: [],
|
|
1133
999
|
flagged_edges: dedupedFlagged,
|
|
@@ -1138,16 +1004,17 @@ function generate(rootDir) {
|
|
|
1138
1004
|
renders: allEdges.filter((e) => e.type === "renders").length,
|
|
1139
1005
|
imports: allEdges.filter((e) => e.type === "imports").length,
|
|
1140
1006
|
navigates: allEdges.filter((e) => e.type === "navigates").length
|
|
1141
|
-
}
|
|
1007
|
+
},
|
|
1008
|
+
fetch_calls: fetchCallEntries
|
|
1142
1009
|
}
|
|
1143
1010
|
};
|
|
1144
1011
|
}
|
|
1145
|
-
var
|
|
1012
|
+
var import_node_fs4, import_node_path4, RENDER_TYPES, reactNextjsParser;
|
|
1146
1013
|
var init_react_nextjs = __esm({
|
|
1147
1014
|
"src/server/graph/parsers/ui/react-nextjs.ts"() {
|
|
1148
1015
|
"use strict";
|
|
1149
|
-
|
|
1150
|
-
|
|
1016
|
+
import_node_fs4 = require("node:fs");
|
|
1017
|
+
import_node_path4 = require("node:path");
|
|
1151
1018
|
init_ast_helpers();
|
|
1152
1019
|
RENDER_TYPES = /* @__PURE__ */ new Set(["component", "ui", "layout", "context"]);
|
|
1153
1020
|
reactNextjsParser = {
|
|
@@ -1162,9 +1029,9 @@ var init_react_nextjs = __esm({
|
|
|
1162
1029
|
// src/server/graph/parsers/api/nextjs-routes.ts
|
|
1163
1030
|
function walk2(dir) {
|
|
1164
1031
|
const results = [];
|
|
1165
|
-
if (!(0,
|
|
1166
|
-
for (const entry of (0,
|
|
1167
|
-
const full = (0,
|
|
1032
|
+
if (!(0, import_node_fs5.existsSync)(dir)) return results;
|
|
1033
|
+
for (const entry of (0, import_node_fs5.readdirSync)(dir, { withFileTypes: true })) {
|
|
1034
|
+
const full = (0, import_node_path5.join)(dir, entry.name);
|
|
1168
1035
|
if (entry.isDirectory()) {
|
|
1169
1036
|
results.push(...walk2(full));
|
|
1170
1037
|
} else if (entry.name === "route.ts" || entry.name === "route.tsx") {
|
|
@@ -1174,7 +1041,7 @@ function walk2(dir) {
|
|
|
1174
1041
|
return results;
|
|
1175
1042
|
}
|
|
1176
1043
|
function filePathToRoute(apiDir, absPath) {
|
|
1177
|
-
let route = "/" + (0,
|
|
1044
|
+
let route = "/" + (0, import_node_path5.relative)(apiDir, absPath).replace(/\\/g, "/").replace(/\/route\.tsx?$/, "");
|
|
1178
1045
|
route = route.replace(/\[([^\]]+)\]/g, ":$1");
|
|
1179
1046
|
route = route.replace(/\/+/g, "/");
|
|
1180
1047
|
if (route === "/") return "/api";
|
|
@@ -1185,10 +1052,10 @@ function camelToPascal(s) {
|
|
|
1185
1052
|
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
1186
1053
|
}
|
|
1187
1054
|
function detect2(rootDir) {
|
|
1188
|
-
return (0,
|
|
1055
|
+
return (0, import_node_fs5.existsSync)((0, import_node_path5.join)(rootDir, "src", "app", "api"));
|
|
1189
1056
|
}
|
|
1190
1057
|
function generate2(rootDir) {
|
|
1191
|
-
const apiDir = (0,
|
|
1058
|
+
const apiDir = (0, import_node_path5.join)(rootDir, "src", "app", "api");
|
|
1192
1059
|
const routeFiles = walk2(apiDir);
|
|
1193
1060
|
const nodes = [];
|
|
1194
1061
|
const edges = [];
|
|
@@ -1206,7 +1073,7 @@ function generate2(rootDir) {
|
|
|
1206
1073
|
if (HTTP_METHODS2.has(exp)) methods.push(exp);
|
|
1207
1074
|
}
|
|
1208
1075
|
const routePath = filePathToRoute(apiDir, absPath);
|
|
1209
|
-
const relPath = (0,
|
|
1076
|
+
const relPath = (0, import_node_path5.relative)(rootDir, absPath).replace(/\\/g, "/");
|
|
1210
1077
|
const mutations = dbCalls.filter((c) => c.isMutation);
|
|
1211
1078
|
const reads = dbCalls.filter((c) => !c.isMutation);
|
|
1212
1079
|
const mutates = mutations.length > 0;
|
|
@@ -1283,12 +1150,12 @@ function generate2(rootDir) {
|
|
|
1283
1150
|
}
|
|
1284
1151
|
};
|
|
1285
1152
|
}
|
|
1286
|
-
var
|
|
1153
|
+
var import_node_fs5, import_node_path5, HTTP_METHODS2, nextjsRoutesParser;
|
|
1287
1154
|
var init_nextjs_routes = __esm({
|
|
1288
1155
|
"src/server/graph/parsers/api/nextjs-routes.ts"() {
|
|
1289
1156
|
"use strict";
|
|
1290
|
-
|
|
1291
|
-
|
|
1157
|
+
import_node_fs5 = require("node:fs");
|
|
1158
|
+
import_node_path5 = require("node:path");
|
|
1292
1159
|
init_ast_helpers();
|
|
1293
1160
|
HTTP_METHODS2 = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"]);
|
|
1294
1161
|
nextjsRoutesParser = {
|
|
@@ -1391,11 +1258,11 @@ function parseEnums(content) {
|
|
|
1391
1258
|
return nodes;
|
|
1392
1259
|
}
|
|
1393
1260
|
function detect3(rootDir) {
|
|
1394
|
-
return (0,
|
|
1261
|
+
return (0, import_node_fs6.existsSync)((0, import_node_path6.join)(rootDir, "prisma", "schema.prisma"));
|
|
1395
1262
|
}
|
|
1396
1263
|
function generate3(rootDir) {
|
|
1397
|
-
const schemaPath = (0,
|
|
1398
|
-
const content = (0,
|
|
1264
|
+
const schemaPath = (0, import_node_path6.join)(rootDir, "prisma", "schema.prisma");
|
|
1265
|
+
const content = (0, import_node_fs6.readFileSync)(schemaPath, "utf-8");
|
|
1399
1266
|
const { nodes: modelNodes, relations } = parseModels(content);
|
|
1400
1267
|
const enumNodes = parseEnums(content);
|
|
1401
1268
|
const allNodes = [...modelNodes, ...enumNodes];
|
|
@@ -1444,12 +1311,12 @@ function generate3(rootDir) {
|
|
|
1444
1311
|
}
|
|
1445
1312
|
};
|
|
1446
1313
|
}
|
|
1447
|
-
var
|
|
1314
|
+
var import_node_fs6, import_node_path6, prismaSchemaParser;
|
|
1448
1315
|
var init_prisma_schema = __esm({
|
|
1449
1316
|
"src/server/graph/parsers/db/prisma-schema.ts"() {
|
|
1450
1317
|
"use strict";
|
|
1451
|
-
|
|
1452
|
-
|
|
1318
|
+
import_node_fs6 = require("node:fs");
|
|
1319
|
+
import_node_path6 = require("node:path");
|
|
1453
1320
|
prismaSchemaParser = {
|
|
1454
1321
|
id: "prisma-schema",
|
|
1455
1322
|
layer: "db",
|
|
@@ -1459,66 +1326,730 @@ var init_prisma_schema = __esm({
|
|
|
1459
1326
|
}
|
|
1460
1327
|
});
|
|
1461
1328
|
|
|
1329
|
+
// src/server/graph/core/api-route-matching.ts
|
|
1330
|
+
function loadApiRoutesFromOutput(apiOutput) {
|
|
1331
|
+
const routes = [];
|
|
1332
|
+
for (const n of apiOutput.nodes) {
|
|
1333
|
+
const path3 = n.path;
|
|
1334
|
+
if (!path3 || typeof path3 !== "string") continue;
|
|
1335
|
+
routes.push({
|
|
1336
|
+
path: path3,
|
|
1337
|
+
nodeId: n.id,
|
|
1338
|
+
segments: path3.split("/").filter(Boolean)
|
|
1339
|
+
});
|
|
1340
|
+
}
|
|
1341
|
+
return routes;
|
|
1342
|
+
}
|
|
1343
|
+
function buildApiPathMap(routes) {
|
|
1344
|
+
const map = /* @__PURE__ */ new Map();
|
|
1345
|
+
for (const r of routes) {
|
|
1346
|
+
if (!map.has(r.path)) map.set(r.path, r.nodeId);
|
|
1347
|
+
}
|
|
1348
|
+
return map;
|
|
1349
|
+
}
|
|
1350
|
+
function normalizeFetchUrl(raw) {
|
|
1351
|
+
let s = raw.replace(/^`|`$/g, "");
|
|
1352
|
+
const qIdx = s.indexOf("?");
|
|
1353
|
+
if (qIdx >= 0) s = s.slice(0, qIdx);
|
|
1354
|
+
const hIdx = s.indexOf("#");
|
|
1355
|
+
if (hIdx >= 0) s = s.slice(0, hIdx);
|
|
1356
|
+
let hadInterpolation = false;
|
|
1357
|
+
s = s.replace(/\$\{([^}]+)\}/g, (_, expr) => {
|
|
1358
|
+
hadInterpolation = true;
|
|
1359
|
+
const cleaned = expr.trim();
|
|
1360
|
+
const last = cleaned.split(".").pop() ?? cleaned;
|
|
1361
|
+
const name = last.replace(/[^\w]/g, "") || "param";
|
|
1362
|
+
return ":" + name;
|
|
1363
|
+
});
|
|
1364
|
+
s = s.replace(/\/+/g, "/");
|
|
1365
|
+
if (s.length > 1 && s.endsWith("/")) s = s.slice(0, -1);
|
|
1366
|
+
return { path: s || "/", hadInterpolation };
|
|
1367
|
+
}
|
|
1368
|
+
function scoreApiRouteMatch(candidate, known) {
|
|
1369
|
+
if (candidate.length !== known.length) return -1;
|
|
1370
|
+
let score = 0;
|
|
1371
|
+
for (let i = 0; i < candidate.length; i++) {
|
|
1372
|
+
const a = candidate[i];
|
|
1373
|
+
const b = known[i];
|
|
1374
|
+
if (a === b) {
|
|
1375
|
+
score += 3;
|
|
1376
|
+
continue;
|
|
1377
|
+
}
|
|
1378
|
+
if (a.startsWith(":") && b.startsWith(":")) {
|
|
1379
|
+
score += 2;
|
|
1380
|
+
continue;
|
|
1381
|
+
}
|
|
1382
|
+
if (a.startsWith(":") || b.startsWith(":")) {
|
|
1383
|
+
score += 1;
|
|
1384
|
+
continue;
|
|
1385
|
+
}
|
|
1386
|
+
return -1;
|
|
1387
|
+
}
|
|
1388
|
+
return score;
|
|
1389
|
+
}
|
|
1390
|
+
function resolveFetchCall(call, apiPathMap, apiRoutes) {
|
|
1391
|
+
const raw = call.url;
|
|
1392
|
+
if (/^(https?:)?\/\//i.test(raw)) {
|
|
1393
|
+
return { kind: "external", normalizedUrl: raw };
|
|
1394
|
+
}
|
|
1395
|
+
if (call.isConcat) {
|
|
1396
|
+
return { kind: "dynamic", normalizedUrl: raw };
|
|
1397
|
+
}
|
|
1398
|
+
const { path: path3, hadInterpolation } = normalizeFetchUrl(raw);
|
|
1399
|
+
if (!path3.startsWith("/")) {
|
|
1400
|
+
return { kind: "unresolved", normalizedUrl: path3 };
|
|
1401
|
+
}
|
|
1402
|
+
const segs = path3.split("/").filter(Boolean);
|
|
1403
|
+
if (hadInterpolation && segs.length > 0 && segs[0].startsWith(":")) {
|
|
1404
|
+
return { kind: "dynamic", normalizedUrl: path3 };
|
|
1405
|
+
}
|
|
1406
|
+
const exact = apiPathMap.get(path3);
|
|
1407
|
+
if (exact) return { kind: "resolved", nodeId: exact, normalizedUrl: path3 };
|
|
1408
|
+
let bestScore = -1;
|
|
1409
|
+
let bestId = null;
|
|
1410
|
+
for (const r of apiRoutes) {
|
|
1411
|
+
const score = scoreApiRouteMatch(segs, r.segments);
|
|
1412
|
+
if (score > bestScore) {
|
|
1413
|
+
bestScore = score;
|
|
1414
|
+
bestId = r.nodeId;
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
if (bestId && bestScore > 0) {
|
|
1418
|
+
return { kind: "resolved", nodeId: bestId, normalizedUrl: path3 };
|
|
1419
|
+
}
|
|
1420
|
+
return { kind: "unresolved", normalizedUrl: path3 };
|
|
1421
|
+
}
|
|
1422
|
+
function resolveUrlPath(urlPath, apiPathMap, apiRoutes) {
|
|
1423
|
+
const { path: path3, hadInterpolation } = normalizeFetchUrl(urlPath);
|
|
1424
|
+
if (!path3.startsWith("/")) {
|
|
1425
|
+
return { kind: "unresolved", normalizedUrl: path3 };
|
|
1426
|
+
}
|
|
1427
|
+
const segs = path3.split("/").filter(Boolean);
|
|
1428
|
+
if (hadInterpolation && segs.length > 0 && segs[0].startsWith(":")) {
|
|
1429
|
+
return { kind: "dynamic", normalizedUrl: path3 };
|
|
1430
|
+
}
|
|
1431
|
+
const exact = apiPathMap.get(path3);
|
|
1432
|
+
if (exact) return { kind: "resolved", nodeId: exact, normalizedUrl: path3 };
|
|
1433
|
+
let bestScore = -1;
|
|
1434
|
+
let bestId = null;
|
|
1435
|
+
for (const r of apiRoutes) {
|
|
1436
|
+
const score = scoreApiRouteMatch(segs, r.segments);
|
|
1437
|
+
if (score > bestScore) {
|
|
1438
|
+
bestScore = score;
|
|
1439
|
+
bestId = r.nodeId;
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
if (bestId && bestScore > 0) {
|
|
1443
|
+
return { kind: "resolved", nodeId: bestId, normalizedUrl: path3 };
|
|
1444
|
+
}
|
|
1445
|
+
return { kind: "unresolved", normalizedUrl: path3 };
|
|
1446
|
+
}
|
|
1447
|
+
var init_api_route_matching = __esm({
|
|
1448
|
+
"src/server/graph/core/api-route-matching.ts"() {
|
|
1449
|
+
"use strict";
|
|
1450
|
+
}
|
|
1451
|
+
});
|
|
1452
|
+
|
|
1453
|
+
// src/server/graph/parsers/crosslayer/fetch-resolver.ts
|
|
1454
|
+
var fetchResolverParser;
|
|
1455
|
+
var init_fetch_resolver = __esm({
|
|
1456
|
+
"src/server/graph/parsers/crosslayer/fetch-resolver.ts"() {
|
|
1457
|
+
"use strict";
|
|
1458
|
+
init_api_route_matching();
|
|
1459
|
+
fetchResolverParser = {
|
|
1460
|
+
id: "fetch-resolver",
|
|
1461
|
+
layer: "crosslayer",
|
|
1462
|
+
detect(_rootDir) {
|
|
1463
|
+
return true;
|
|
1464
|
+
},
|
|
1465
|
+
generate(_rootDir, layerOutputs) {
|
|
1466
|
+
const uiOutput = layerOutputs.get("ui");
|
|
1467
|
+
const apiOutput = layerOutputs.get("api");
|
|
1468
|
+
if (!uiOutput || !apiOutput) {
|
|
1469
|
+
return { cross_refs: [], flagged_edges: [], warnings: [] };
|
|
1470
|
+
}
|
|
1471
|
+
const apiRoutes = loadApiRoutesFromOutput(apiOutput);
|
|
1472
|
+
const apiPathMap = buildApiPathMap(apiRoutes);
|
|
1473
|
+
const fetchCallEntries = uiOutput.patterns?.fetch_calls ?? [];
|
|
1474
|
+
if (fetchCallEntries.length === 0) {
|
|
1475
|
+
return { cross_refs: [], flagged_edges: [], warnings: [] };
|
|
1476
|
+
}
|
|
1477
|
+
const includeExternal = process.env.LAUNCH_CHART_INCLUDE_EXTERNAL_FETCHES === "1";
|
|
1478
|
+
const crossRefs = [];
|
|
1479
|
+
const flaggedEdges = [];
|
|
1480
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1481
|
+
let resolvedCount = 0;
|
|
1482
|
+
let dynamicCount = 0;
|
|
1483
|
+
let unresolvedCount = 0;
|
|
1484
|
+
let externalCount = 0;
|
|
1485
|
+
for (const entry of fetchCallEntries) {
|
|
1486
|
+
for (const call of entry.calls) {
|
|
1487
|
+
const result = resolveFetchCall(call, apiPathMap, apiRoutes);
|
|
1488
|
+
const methodTag = call.method ?? (call.kind === "fetch" ? "GET?" : "?");
|
|
1489
|
+
if (result.kind === "resolved" && result.nodeId) {
|
|
1490
|
+
const key = `${entry.nodeId}\u2192${result.nodeId}\u2192calls_api`;
|
|
1491
|
+
if (seen.has(key)) continue;
|
|
1492
|
+
seen.add(key);
|
|
1493
|
+
crossRefs.push({
|
|
1494
|
+
source: entry.nodeId,
|
|
1495
|
+
target: result.nodeId,
|
|
1496
|
+
type: "calls_api",
|
|
1497
|
+
layer: "api"
|
|
1498
|
+
});
|
|
1499
|
+
resolvedCount++;
|
|
1500
|
+
continue;
|
|
1501
|
+
}
|
|
1502
|
+
if (result.kind === "dynamic") {
|
|
1503
|
+
dynamicCount++;
|
|
1504
|
+
flaggedEdges.push({
|
|
1505
|
+
source: entry.nodeId,
|
|
1506
|
+
target: "DYNAMIC",
|
|
1507
|
+
type: "calls_api",
|
|
1508
|
+
label: call.isConcat ? `${methodTag} fetch with concat: ${call.url}` : `${methodTag} fetch with template: ${call.url}`,
|
|
1509
|
+
confidence: call.isConcat ? "low" : "medium"
|
|
1510
|
+
});
|
|
1511
|
+
continue;
|
|
1512
|
+
}
|
|
1513
|
+
if (result.kind === "external") {
|
|
1514
|
+
externalCount++;
|
|
1515
|
+
if (!includeExternal) continue;
|
|
1516
|
+
flaggedEdges.push({
|
|
1517
|
+
source: entry.nodeId,
|
|
1518
|
+
target: "EXTERNAL",
|
|
1519
|
+
type: "calls_external",
|
|
1520
|
+
label: `${methodTag} external fetch: ${call.url}`,
|
|
1521
|
+
confidence: "high"
|
|
1522
|
+
});
|
|
1523
|
+
continue;
|
|
1524
|
+
}
|
|
1525
|
+
unresolvedCount++;
|
|
1526
|
+
flaggedEdges.push({
|
|
1527
|
+
source: entry.nodeId,
|
|
1528
|
+
target: "UNRESOLVED",
|
|
1529
|
+
type: "calls_api",
|
|
1530
|
+
label: `${methodTag} fetch to unknown path: ${result.normalizedUrl}`,
|
|
1531
|
+
confidence: "medium"
|
|
1532
|
+
});
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
return {
|
|
1536
|
+
cross_refs: crossRefs,
|
|
1537
|
+
flagged_edges: flaggedEdges,
|
|
1538
|
+
warnings: [],
|
|
1539
|
+
patterns: {
|
|
1540
|
+
api_call_detection: {
|
|
1541
|
+
resolved: resolvedCount,
|
|
1542
|
+
dynamic: dynamicCount,
|
|
1543
|
+
unresolved: unresolvedCount,
|
|
1544
|
+
external: externalCount
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
};
|
|
1548
|
+
}
|
|
1549
|
+
};
|
|
1550
|
+
}
|
|
1551
|
+
});
|
|
1552
|
+
|
|
1553
|
+
// src/server/graph/parsers/crosslayer/api-annotations.ts
|
|
1554
|
+
function walk3(dir, exts) {
|
|
1555
|
+
if (!(0, import_node_fs7.existsSync)(dir)) return [];
|
|
1556
|
+
const results = [];
|
|
1557
|
+
for (const entry of (0, import_node_fs7.readdirSync)(dir, { withFileTypes: true })) {
|
|
1558
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
1559
|
+
const full = (0, import_node_path7.join)(dir, entry.name);
|
|
1560
|
+
if (entry.isDirectory()) {
|
|
1561
|
+
results.push(...walk3(full, exts));
|
|
1562
|
+
} else if (exts.includes((0, import_node_path7.extname)(entry.name))) {
|
|
1563
|
+
results.push(full);
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
return results;
|
|
1567
|
+
}
|
|
1568
|
+
function toNodeId2(srcDir, absPath) {
|
|
1569
|
+
return (0, import_node_path7.relative)(srcDir, absPath).replace(/\\/g, "/");
|
|
1570
|
+
}
|
|
1571
|
+
var import_node_fs7, import_node_path7, API_ANNOTATION_RE, apiAnnotationsParser;
|
|
1572
|
+
var init_api_annotations = __esm({
|
|
1573
|
+
"src/server/graph/parsers/crosslayer/api-annotations.ts"() {
|
|
1574
|
+
"use strict";
|
|
1575
|
+
import_node_fs7 = require("node:fs");
|
|
1576
|
+
import_node_path7 = require("node:path");
|
|
1577
|
+
init_api_route_matching();
|
|
1578
|
+
API_ANNOTATION_RE = /@api\s+(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+(\/\S+)/g;
|
|
1579
|
+
apiAnnotationsParser = {
|
|
1580
|
+
id: "api-annotations",
|
|
1581
|
+
layer: "crosslayer",
|
|
1582
|
+
detect(rootDir) {
|
|
1583
|
+
return (0, import_node_fs7.existsSync)((0, import_node_path7.join)(rootDir, "src"));
|
|
1584
|
+
},
|
|
1585
|
+
generate(rootDir, layerOutputs) {
|
|
1586
|
+
const apiOutput = layerOutputs.get("api");
|
|
1587
|
+
if (!apiOutput) {
|
|
1588
|
+
return { cross_refs: [], flagged_edges: [], warnings: [] };
|
|
1589
|
+
}
|
|
1590
|
+
const uiOutput = layerOutputs.get("ui");
|
|
1591
|
+
const uiNodeIds = new Set(uiOutput?.nodes.map((n) => n.id) ?? []);
|
|
1592
|
+
const apiRoutes = loadApiRoutesFromOutput(apiOutput);
|
|
1593
|
+
const apiPathMap = buildApiPathMap(apiRoutes);
|
|
1594
|
+
const srcDir = (0, import_node_path7.join)(rootDir, "src");
|
|
1595
|
+
const files = walk3(srcDir, [".ts", ".tsx"]);
|
|
1596
|
+
const crossRefs = [];
|
|
1597
|
+
const flaggedEdges = [];
|
|
1598
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1599
|
+
for (const absPath of files) {
|
|
1600
|
+
const content = (0, import_node_fs7.readFileSync)(absPath, "utf-8");
|
|
1601
|
+
const sourceId = toNodeId2(srcDir, absPath);
|
|
1602
|
+
if (!uiNodeIds.has(sourceId)) continue;
|
|
1603
|
+
let match;
|
|
1604
|
+
API_ANNOTATION_RE.lastIndex = 0;
|
|
1605
|
+
while ((match = API_ANNOTATION_RE.exec(content)) !== null) {
|
|
1606
|
+
const method = match[1];
|
|
1607
|
+
const urlPath = match[2];
|
|
1608
|
+
const result = resolveUrlPath(urlPath, apiPathMap, apiRoutes);
|
|
1609
|
+
if (result.kind === "resolved" && result.nodeId) {
|
|
1610
|
+
const key = `${sourceId}|${result.nodeId}|calls_api`;
|
|
1611
|
+
if (seen.has(key)) continue;
|
|
1612
|
+
seen.add(key);
|
|
1613
|
+
crossRefs.push({
|
|
1614
|
+
source: sourceId,
|
|
1615
|
+
target: result.nodeId,
|
|
1616
|
+
type: "calls_api",
|
|
1617
|
+
layer: "api"
|
|
1618
|
+
});
|
|
1619
|
+
} else {
|
|
1620
|
+
flaggedEdges.push({
|
|
1621
|
+
source: sourceId,
|
|
1622
|
+
target: "UNRESOLVED",
|
|
1623
|
+
type: "annotation_unresolved",
|
|
1624
|
+
label: `@api ${method} ${urlPath} \u2014 no matching API route found`,
|
|
1625
|
+
confidence: "high"
|
|
1626
|
+
});
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
return {
|
|
1631
|
+
cross_refs: crossRefs,
|
|
1632
|
+
flagged_edges: flaggedEdges,
|
|
1633
|
+
warnings: [],
|
|
1634
|
+
patterns: {
|
|
1635
|
+
annotations_found: crossRefs.length + flaggedEdges.length,
|
|
1636
|
+
annotations_resolved: crossRefs.length,
|
|
1637
|
+
annotations_unresolved: flaggedEdges.length
|
|
1638
|
+
}
|
|
1639
|
+
};
|
|
1640
|
+
}
|
|
1641
|
+
};
|
|
1642
|
+
}
|
|
1643
|
+
});
|
|
1644
|
+
|
|
1645
|
+
// src/server/graph/parsers/crosslayer/url-literal-scanner.ts
|
|
1646
|
+
function walk4(dir, exts) {
|
|
1647
|
+
if (!(0, import_node_fs8.existsSync)(dir)) return [];
|
|
1648
|
+
const results = [];
|
|
1649
|
+
for (const entry of (0, import_node_fs8.readdirSync)(dir, { withFileTypes: true })) {
|
|
1650
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
1651
|
+
const full = (0, import_node_path8.join)(dir, entry.name);
|
|
1652
|
+
if (entry.isDirectory()) {
|
|
1653
|
+
results.push(...walk4(full, exts));
|
|
1654
|
+
} else if (exts.includes((0, import_node_path8.extname)(entry.name))) {
|
|
1655
|
+
results.push(full);
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
return results;
|
|
1659
|
+
}
|
|
1660
|
+
function toNodeId3(srcDir, absPath) {
|
|
1661
|
+
return (0, import_node_path8.relative)(srcDir, absPath).replace(/\\/g, "/");
|
|
1662
|
+
}
|
|
1663
|
+
var import_node_fs8, import_node_path8, URL_LITERAL_RE, urlLiteralScannerParser;
|
|
1664
|
+
var init_url_literal_scanner = __esm({
|
|
1665
|
+
"src/server/graph/parsers/crosslayer/url-literal-scanner.ts"() {
|
|
1666
|
+
"use strict";
|
|
1667
|
+
import_node_fs8 = require("node:fs");
|
|
1668
|
+
import_node_path8 = require("node:path");
|
|
1669
|
+
init_api_route_matching();
|
|
1670
|
+
URL_LITERAL_RE = /['"`](\/api\/[^'"`\s]+?)['"`]/g;
|
|
1671
|
+
urlLiteralScannerParser = {
|
|
1672
|
+
id: "url-literal-scanner",
|
|
1673
|
+
layer: "crosslayer",
|
|
1674
|
+
detect(rootDir) {
|
|
1675
|
+
return (0, import_node_fs8.existsSync)((0, import_node_path8.join)(rootDir, "src"));
|
|
1676
|
+
},
|
|
1677
|
+
generate(rootDir, layerOutputs) {
|
|
1678
|
+
const apiOutput = layerOutputs.get("api");
|
|
1679
|
+
if (!apiOutput) {
|
|
1680
|
+
return { cross_refs: [], flagged_edges: [], warnings: [] };
|
|
1681
|
+
}
|
|
1682
|
+
const uiOutput = layerOutputs.get("ui");
|
|
1683
|
+
const uiNodeIds = new Set(uiOutput?.nodes.map((n) => n.id) ?? []);
|
|
1684
|
+
const apiRoutes = loadApiRoutesFromOutput(apiOutput);
|
|
1685
|
+
const apiPathMap = buildApiPathMap(apiRoutes);
|
|
1686
|
+
const srcDir = (0, import_node_path8.join)(rootDir, "src");
|
|
1687
|
+
const clientDir = (0, import_node_path8.join)(srcDir, "client");
|
|
1688
|
+
const appDir = (0, import_node_path8.join)(srcDir, "app");
|
|
1689
|
+
const files = [
|
|
1690
|
+
...walk4(clientDir, [".ts", ".tsx"]),
|
|
1691
|
+
...walk4(appDir, [".ts", ".tsx"])
|
|
1692
|
+
];
|
|
1693
|
+
const crossRefs = [];
|
|
1694
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1695
|
+
for (const absPath of files) {
|
|
1696
|
+
const sourceId = toNodeId3(srcDir, absPath);
|
|
1697
|
+
if (!uiNodeIds.has(sourceId)) continue;
|
|
1698
|
+
const content = (0, import_node_fs8.readFileSync)(absPath, "utf-8");
|
|
1699
|
+
let match;
|
|
1700
|
+
URL_LITERAL_RE.lastIndex = 0;
|
|
1701
|
+
while ((match = URL_LITERAL_RE.exec(content)) !== null) {
|
|
1702
|
+
const urlPath = match[1];
|
|
1703
|
+
const result = resolveUrlPath(urlPath, apiPathMap, apiRoutes);
|
|
1704
|
+
if (result.kind === "resolved" && result.nodeId) {
|
|
1705
|
+
const key = `${sourceId}|${result.nodeId}|references_api`;
|
|
1706
|
+
if (seen.has(key)) continue;
|
|
1707
|
+
seen.add(key);
|
|
1708
|
+
crossRefs.push({
|
|
1709
|
+
source: sourceId,
|
|
1710
|
+
target: result.nodeId,
|
|
1711
|
+
type: "references_api",
|
|
1712
|
+
layer: "api"
|
|
1713
|
+
});
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
return {
|
|
1718
|
+
cross_refs: crossRefs,
|
|
1719
|
+
flagged_edges: [],
|
|
1720
|
+
warnings: [],
|
|
1721
|
+
patterns: {
|
|
1722
|
+
url_literals_resolved: crossRefs.length
|
|
1723
|
+
}
|
|
1724
|
+
};
|
|
1725
|
+
}
|
|
1726
|
+
};
|
|
1727
|
+
}
|
|
1728
|
+
});
|
|
1729
|
+
|
|
1730
|
+
// src/server/graph/core/parser-registry.ts
|
|
1731
|
+
function registerBuiltins(registry, disabled) {
|
|
1732
|
+
const builtins = [
|
|
1733
|
+
reactNextjsParser,
|
|
1734
|
+
nextjsRoutesParser,
|
|
1735
|
+
prismaSchemaParser,
|
|
1736
|
+
fetchResolverParser,
|
|
1737
|
+
apiAnnotationsParser,
|
|
1738
|
+
urlLiteralScannerParser
|
|
1739
|
+
];
|
|
1740
|
+
for (const parser of builtins) {
|
|
1741
|
+
if (disabled.has(parser.id)) continue;
|
|
1742
|
+
registry.register(parser);
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
function loadCustomParsers(registry, config, rootDir, disabled) {
|
|
1746
|
+
for (const entry of config.parsers?.custom ?? []) {
|
|
1747
|
+
try {
|
|
1748
|
+
const absPath = (0, import_node_path9.resolve)(rootDir, entry.path);
|
|
1749
|
+
const mod = require(absPath);
|
|
1750
|
+
const parser = "default" in mod ? mod.default : mod;
|
|
1751
|
+
if (disabled.has(parser.id)) continue;
|
|
1752
|
+
if (parser.layer !== entry.layer) {
|
|
1753
|
+
process.stderr.write(
|
|
1754
|
+
`[launch-chart] custom parser "${parser.id}" declares layer "${parser.layer}" but config says "${entry.layer}" \u2014 using parser's layer
|
|
1755
|
+
`
|
|
1756
|
+
);
|
|
1757
|
+
}
|
|
1758
|
+
registry.register(parser);
|
|
1759
|
+
} catch (err2) {
|
|
1760
|
+
process.stderr.write(`[launch-chart] failed to load custom parser from ${entry.path}: ${err2}
|
|
1761
|
+
`);
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
function createRegistry(config, rootDir) {
|
|
1766
|
+
const registry = new ParserRegistry();
|
|
1767
|
+
const disabled = new Set(config.parsers?.disabled ?? []);
|
|
1768
|
+
registerBuiltins(registry, disabled);
|
|
1769
|
+
loadCustomParsers(registry, config, rootDir, disabled);
|
|
1770
|
+
return registry;
|
|
1771
|
+
}
|
|
1772
|
+
var import_node_path9, ParserRegistry;
|
|
1773
|
+
var init_parser_registry = __esm({
|
|
1774
|
+
"src/server/graph/core/parser-registry.ts"() {
|
|
1775
|
+
"use strict";
|
|
1776
|
+
import_node_path9 = require("node:path");
|
|
1777
|
+
init_react_nextjs();
|
|
1778
|
+
init_nextjs_routes();
|
|
1779
|
+
init_prisma_schema();
|
|
1780
|
+
init_fetch_resolver();
|
|
1781
|
+
init_api_annotations();
|
|
1782
|
+
init_url_literal_scanner();
|
|
1783
|
+
ParserRegistry = class {
|
|
1784
|
+
constructor() {
|
|
1785
|
+
this.parsers = /* @__PURE__ */ new Map();
|
|
1786
|
+
this.ids = /* @__PURE__ */ new Set();
|
|
1787
|
+
}
|
|
1788
|
+
register(parser) {
|
|
1789
|
+
if (this.ids.has(parser.id)) {
|
|
1790
|
+
throw new Error(`Duplicate parser id: ${parser.id}`);
|
|
1791
|
+
}
|
|
1792
|
+
this.ids.add(parser.id);
|
|
1793
|
+
const list = this.parsers.get(parser.layer) ?? [];
|
|
1794
|
+
list.push(parser);
|
|
1795
|
+
this.parsers.set(parser.layer, list);
|
|
1796
|
+
}
|
|
1797
|
+
getParsers(layer) {
|
|
1798
|
+
return this.parsers.get(layer) ?? [];
|
|
1799
|
+
}
|
|
1800
|
+
getCrossLayerParsers() {
|
|
1801
|
+
return this.parsers.get("crosslayer") ?? [];
|
|
1802
|
+
}
|
|
1803
|
+
getAll() {
|
|
1804
|
+
const all = [];
|
|
1805
|
+
for (const list of this.parsers.values()) all.push(...list);
|
|
1806
|
+
return all;
|
|
1807
|
+
}
|
|
1808
|
+
};
|
|
1809
|
+
}
|
|
1810
|
+
});
|
|
1811
|
+
|
|
1812
|
+
// src/server/graph/core/merge.ts
|
|
1813
|
+
function mergeGraphOutputs(outputs, layer) {
|
|
1814
|
+
if (outputs.length === 0) {
|
|
1815
|
+
return {
|
|
1816
|
+
metadata: { generated: (/* @__PURE__ */ new Date()).toISOString(), scope: "", layer },
|
|
1817
|
+
nodes: [],
|
|
1818
|
+
edges: [],
|
|
1819
|
+
cross_refs: [],
|
|
1820
|
+
contradictions: [],
|
|
1821
|
+
warnings: [],
|
|
1822
|
+
flagged_edges: []
|
|
1823
|
+
};
|
|
1824
|
+
}
|
|
1825
|
+
if (outputs.length === 1) return outputs[0];
|
|
1826
|
+
const seenNodes = /* @__PURE__ */ new Set();
|
|
1827
|
+
const seenEdges = /* @__PURE__ */ new Set();
|
|
1828
|
+
const seenCrossRefs = /* @__PURE__ */ new Set();
|
|
1829
|
+
const mergedNodes = [];
|
|
1830
|
+
const mergedEdges = [];
|
|
1831
|
+
const mergedCrossRefs = [];
|
|
1832
|
+
const mergedContradictions = [];
|
|
1833
|
+
const mergedWarnings = [];
|
|
1834
|
+
const mergedFlagged = [];
|
|
1835
|
+
const parserIds = [];
|
|
1836
|
+
for (const output of outputs) {
|
|
1837
|
+
if (output.metadata.parser) {
|
|
1838
|
+
parserIds.push(String(output.metadata.parser));
|
|
1839
|
+
}
|
|
1840
|
+
for (const node of output.nodes) {
|
|
1841
|
+
if (seenNodes.has(node.id)) {
|
|
1842
|
+
mergedWarnings.push({
|
|
1843
|
+
type: "merge_conflict",
|
|
1844
|
+
detail: `Node "${node.id}" produced by multiple parsers; keeping first`
|
|
1845
|
+
});
|
|
1846
|
+
continue;
|
|
1847
|
+
}
|
|
1848
|
+
seenNodes.add(node.id);
|
|
1849
|
+
mergedNodes.push(node);
|
|
1850
|
+
}
|
|
1851
|
+
for (const edge of output.edges) {
|
|
1852
|
+
const key = `${edge.source}|${edge.target}|${edge.type}`;
|
|
1853
|
+
if (seenEdges.has(key)) continue;
|
|
1854
|
+
seenEdges.add(key);
|
|
1855
|
+
mergedEdges.push(edge);
|
|
1856
|
+
}
|
|
1857
|
+
for (const ref of output.cross_refs) {
|
|
1858
|
+
const key = `${ref.source}|${ref.target}|${ref.type}`;
|
|
1859
|
+
if (seenCrossRefs.has(key)) continue;
|
|
1860
|
+
seenCrossRefs.add(key);
|
|
1861
|
+
mergedCrossRefs.push(ref);
|
|
1862
|
+
}
|
|
1863
|
+
mergedContradictions.push(...output.contradictions);
|
|
1864
|
+
mergedWarnings.push(...output.warnings);
|
|
1865
|
+
mergedFlagged.push(...output.flagged_edges);
|
|
1866
|
+
}
|
|
1867
|
+
const metadata = {
|
|
1868
|
+
...outputs[0].metadata,
|
|
1869
|
+
generated: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1870
|
+
parsers: parserIds
|
|
1871
|
+
};
|
|
1872
|
+
return {
|
|
1873
|
+
metadata,
|
|
1874
|
+
nodes: mergedNodes,
|
|
1875
|
+
edges: mergedEdges,
|
|
1876
|
+
cross_refs: mergedCrossRefs,
|
|
1877
|
+
contradictions: mergedContradictions,
|
|
1878
|
+
warnings: mergedWarnings,
|
|
1879
|
+
flagged_edges: mergedFlagged,
|
|
1880
|
+
patterns: outputs[0].patterns
|
|
1881
|
+
};
|
|
1882
|
+
}
|
|
1883
|
+
function dedupCrossRefs(refs) {
|
|
1884
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1885
|
+
const result = [];
|
|
1886
|
+
for (const ref of refs) {
|
|
1887
|
+
const key = `${ref.source}|${ref.target}|${ref.type}`;
|
|
1888
|
+
if (seen.has(key)) continue;
|
|
1889
|
+
seen.add(key);
|
|
1890
|
+
result.push(ref);
|
|
1891
|
+
}
|
|
1892
|
+
return result;
|
|
1893
|
+
}
|
|
1894
|
+
function applyCrossLayerResults(uiOutput, results, primaryId) {
|
|
1895
|
+
const allCrossRefs = [...uiOutput.cross_refs];
|
|
1896
|
+
const allFlagged = [...uiOutput.flagged_edges];
|
|
1897
|
+
const allWarnings = [...uiOutput.warnings];
|
|
1898
|
+
const primaryResult = results.find((r) => r.parserId === primaryId);
|
|
1899
|
+
const secondaryResults = results.filter((r) => r.parserId !== primaryId);
|
|
1900
|
+
if (primaryResult) {
|
|
1901
|
+
allCrossRefs.push(...primaryResult.output.cross_refs);
|
|
1902
|
+
allFlagged.push(...primaryResult.output.flagged_edges);
|
|
1903
|
+
allWarnings.push(...primaryResult.output.warnings);
|
|
1904
|
+
}
|
|
1905
|
+
const primarySet = new Set(
|
|
1906
|
+
(primaryResult?.output.cross_refs ?? []).map((r) => `${r.source}|${r.target}|${r.type}`)
|
|
1907
|
+
);
|
|
1908
|
+
for (const sec of secondaryResults) {
|
|
1909
|
+
for (const ref of sec.output.cross_refs) {
|
|
1910
|
+
const key = `${ref.source}|${ref.target}|${ref.type}`;
|
|
1911
|
+
if (primarySet.has(key)) {
|
|
1912
|
+
allCrossRefs.push(ref);
|
|
1913
|
+
} else {
|
|
1914
|
+
allFlagged.push({
|
|
1915
|
+
source: ref.source,
|
|
1916
|
+
target: ref.target,
|
|
1917
|
+
type: "out_of_pattern",
|
|
1918
|
+
label: `API call detected by ${sec.parserId} but not by primary (${primaryId})`,
|
|
1919
|
+
confidence: "medium"
|
|
1920
|
+
});
|
|
1921
|
+
allCrossRefs.push(ref);
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1924
|
+
allFlagged.push(...sec.output.flagged_edges);
|
|
1925
|
+
allWarnings.push(...sec.output.warnings);
|
|
1926
|
+
}
|
|
1927
|
+
return {
|
|
1928
|
+
...uiOutput,
|
|
1929
|
+
cross_refs: dedupCrossRefs(allCrossRefs),
|
|
1930
|
+
flagged_edges: allFlagged,
|
|
1931
|
+
warnings: allWarnings
|
|
1932
|
+
};
|
|
1933
|
+
}
|
|
1934
|
+
var init_merge = __esm({
|
|
1935
|
+
"src/server/graph/core/merge.ts"() {
|
|
1936
|
+
"use strict";
|
|
1937
|
+
}
|
|
1938
|
+
});
|
|
1939
|
+
|
|
1462
1940
|
// src/server/graph/core/graph-builder.ts
|
|
1463
|
-
function
|
|
1464
|
-
|
|
1941
|
+
function readGraphFromDisk(rootDir, layer) {
|
|
1942
|
+
const filePath = (0, import_node_path10.join)(rootDir, ".launchsecure", "graphs", `${layer}.json`);
|
|
1943
|
+
if (!(0, import_node_fs9.existsSync)(filePath)) return null;
|
|
1944
|
+
try {
|
|
1945
|
+
return JSON.parse((0, import_node_fs9.readFileSync)(filePath, "utf-8"));
|
|
1946
|
+
} catch {
|
|
1947
|
+
return null;
|
|
1948
|
+
}
|
|
1465
1949
|
}
|
|
1466
1950
|
function generateLayer(rootDir, layer) {
|
|
1467
|
-
const
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
const
|
|
1951
|
+
const config = loadConfig(rootDir);
|
|
1952
|
+
const registry = createRegistry(config, rootDir);
|
|
1953
|
+
const parsers = registry.getParsers(layer);
|
|
1954
|
+
const outputs = [];
|
|
1955
|
+
for (const parser of parsers) {
|
|
1956
|
+
if (!parser.detect(rootDir)) continue;
|
|
1957
|
+
outputs.push(parser.generate(rootDir));
|
|
1958
|
+
}
|
|
1959
|
+
if (outputs.length === 0) return null;
|
|
1960
|
+
let merged = outputs.length === 1 ? outputs[0] : mergeGraphOutputs(outputs, layer);
|
|
1961
|
+
if (layer === "ui") {
|
|
1962
|
+
const layerOutputs = /* @__PURE__ */ new Map();
|
|
1963
|
+
layerOutputs.set("ui", merged);
|
|
1964
|
+
for (const otherLayer of ["api", "db"]) {
|
|
1965
|
+
const existing = readGraphFromDisk(rootDir, otherLayer);
|
|
1966
|
+
if (existing) layerOutputs.set(otherLayer, existing);
|
|
1967
|
+
}
|
|
1968
|
+
const crossParsers = registry.getCrossLayerParsers();
|
|
1969
|
+
const primaryId = config.parsers?.primary?.crosslayer ?? crossParsers[0]?.id ?? null;
|
|
1970
|
+
const crossResults = crossParsers.filter((p) => p.detect(rootDir)).map((p) => ({ parserId: p.id, output: p.generate(rootDir, layerOutputs) }));
|
|
1971
|
+
if (crossResults.length > 0) {
|
|
1972
|
+
merged = applyCrossLayerResults(merged, crossResults, primaryId);
|
|
1973
|
+
}
|
|
1974
|
+
}
|
|
1471
1975
|
return {
|
|
1472
1976
|
layer,
|
|
1473
|
-
output,
|
|
1474
|
-
nodeCount:
|
|
1475
|
-
edgeCount:
|
|
1977
|
+
output: merged,
|
|
1978
|
+
nodeCount: merged.nodes.length,
|
|
1979
|
+
edgeCount: merged.edges.length
|
|
1476
1980
|
};
|
|
1477
1981
|
}
|
|
1478
1982
|
function generateAll(rootDir) {
|
|
1479
|
-
const
|
|
1983
|
+
const config = loadConfig(rootDir);
|
|
1984
|
+
const registry = createRegistry(config, rootDir);
|
|
1985
|
+
const layerOrder = ["api", "db", "ui"];
|
|
1986
|
+
const layerOutputs = /* @__PURE__ */ new Map();
|
|
1480
1987
|
const results = [];
|
|
1481
|
-
for (const layer of
|
|
1482
|
-
const
|
|
1483
|
-
|
|
1988
|
+
for (const layer of layerOrder) {
|
|
1989
|
+
const parsers = registry.getParsers(layer);
|
|
1990
|
+
const outputs = [];
|
|
1991
|
+
for (const parser of parsers) {
|
|
1992
|
+
if (!parser.detect(rootDir)) continue;
|
|
1993
|
+
outputs.push(parser.generate(rootDir));
|
|
1994
|
+
}
|
|
1995
|
+
if (outputs.length === 0) continue;
|
|
1996
|
+
const merged = outputs.length === 1 ? outputs[0] : mergeGraphOutputs(outputs, layer);
|
|
1997
|
+
layerOutputs.set(layer, merged);
|
|
1998
|
+
results.push({
|
|
1999
|
+
layer,
|
|
2000
|
+
output: merged,
|
|
2001
|
+
nodeCount: merged.nodes.length,
|
|
2002
|
+
edgeCount: merged.edges.length
|
|
2003
|
+
});
|
|
2004
|
+
}
|
|
2005
|
+
const crossParsers = registry.getCrossLayerParsers();
|
|
2006
|
+
const primaryId = config.parsers?.primary?.crosslayer ?? crossParsers[0]?.id ?? null;
|
|
2007
|
+
const crossResults = crossParsers.filter((p) => p.detect(rootDir)).map((p) => ({ parserId: p.id, output: p.generate(rootDir, layerOutputs) }));
|
|
2008
|
+
if (crossResults.length > 0 && layerOutputs.has("ui")) {
|
|
2009
|
+
const uiOutput = layerOutputs.get("ui");
|
|
2010
|
+
const merged = applyCrossLayerResults(uiOutput, crossResults, primaryId);
|
|
2011
|
+
layerOutputs.set("ui", merged);
|
|
2012
|
+
const uiResult = results.find((r) => r.layer === "ui");
|
|
2013
|
+
if (uiResult) {
|
|
2014
|
+
uiResult.output = merged;
|
|
2015
|
+
uiResult.nodeCount = merged.nodes.length;
|
|
2016
|
+
uiResult.edgeCount = merged.edges.length;
|
|
2017
|
+
}
|
|
1484
2018
|
}
|
|
1485
2019
|
const byLayer = new Map(results.map((r) => [r.layer, r]));
|
|
1486
2020
|
return ["ui", "api", "db"].map((l) => byLayer.get(l)).filter((r) => !!r);
|
|
1487
2021
|
}
|
|
1488
|
-
var
|
|
2022
|
+
var import_node_fs9, import_node_path10;
|
|
1489
2023
|
var init_graph_builder = __esm({
|
|
1490
2024
|
"src/server/graph/core/graph-builder.ts"() {
|
|
1491
2025
|
"use strict";
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
nextjsRoutesParser,
|
|
1498
|
-
prismaSchemaParser
|
|
1499
|
-
];
|
|
2026
|
+
import_node_fs9 = require("node:fs");
|
|
2027
|
+
import_node_path10 = require("node:path");
|
|
2028
|
+
init_config();
|
|
2029
|
+
init_parser_registry();
|
|
2030
|
+
init_merge();
|
|
1500
2031
|
}
|
|
1501
2032
|
});
|
|
1502
2033
|
|
|
1503
2034
|
// src/server/graph/index.ts
|
|
1504
2035
|
function graphsDir(rootDir) {
|
|
1505
|
-
return (0,
|
|
2036
|
+
return (0, import_node_path11.join)(rootDir, GRAPHS_DIR);
|
|
1506
2037
|
}
|
|
1507
2038
|
function graphFilePath(rootDir, layer) {
|
|
1508
|
-
return (0,
|
|
2039
|
+
return (0, import_node_path11.join)(graphsDir(rootDir), `${layer}.json`);
|
|
1509
2040
|
}
|
|
1510
2041
|
function invalidateCache(filePath) {
|
|
1511
2042
|
graphCache.delete(filePath);
|
|
1512
2043
|
}
|
|
1513
2044
|
function readGraph(rootDir, layer) {
|
|
1514
2045
|
const filePath = graphFilePath(rootDir, layer);
|
|
1515
|
-
if (!(0,
|
|
1516
|
-
const stat = (0,
|
|
2046
|
+
if (!(0, import_node_fs10.existsSync)(filePath)) return null;
|
|
2047
|
+
const stat = (0, import_node_fs10.statSync)(filePath);
|
|
1517
2048
|
const cached = graphCache.get(filePath);
|
|
1518
2049
|
if (cached && cached.mtimeMs === stat.mtimeMs) {
|
|
1519
2050
|
return cached.graph;
|
|
1520
2051
|
}
|
|
1521
|
-
const content = (0,
|
|
2052
|
+
const content = (0, import_node_fs10.readFileSync)(filePath, "utf-8");
|
|
1522
2053
|
const graph = JSON.parse(content);
|
|
1523
2054
|
graphCache.set(filePath, { mtimeMs: stat.mtimeMs, graph });
|
|
1524
2055
|
return graph;
|
|
@@ -1533,21 +2064,21 @@ function readAllGraphs(rootDir) {
|
|
|
1533
2064
|
}
|
|
1534
2065
|
function generateGraph(rootDir, layer) {
|
|
1535
2066
|
const dir = graphsDir(rootDir);
|
|
1536
|
-
(0,
|
|
2067
|
+
(0, import_node_fs10.mkdirSync)(dir, { recursive: true });
|
|
1537
2068
|
const results = layer ? [generateLayer(rootDir, layer)].filter((r) => r !== null) : generateAll(rootDir);
|
|
1538
2069
|
for (const result of results) {
|
|
1539
2070
|
const filePath = graphFilePath(rootDir, result.layer);
|
|
1540
|
-
(0,
|
|
2071
|
+
(0, import_node_fs10.writeFileSync)(filePath, JSON.stringify(result.output, null, 2) + "\n", "utf-8");
|
|
1541
2072
|
invalidateCache(filePath);
|
|
1542
2073
|
}
|
|
1543
2074
|
return results;
|
|
1544
2075
|
}
|
|
1545
|
-
var
|
|
2076
|
+
var import_node_fs10, import_node_path11, GRAPHS_DIR, LAYERS, graphCache;
|
|
1546
2077
|
var init_graph = __esm({
|
|
1547
2078
|
"src/server/graph/index.ts"() {
|
|
1548
2079
|
"use strict";
|
|
1549
|
-
|
|
1550
|
-
|
|
2080
|
+
import_node_fs10 = require("node:fs");
|
|
2081
|
+
import_node_path11 = require("node:path");
|
|
1551
2082
|
init_graph_builder();
|
|
1552
2083
|
GRAPHS_DIR = ".launchsecure/graphs";
|
|
1553
2084
|
LAYERS = ["ui", "api", "db"];
|
|
@@ -1561,19 +2092,19 @@ __export(chart_serve_exports, {
|
|
|
1561
2092
|
runServeCli: () => runServeCli,
|
|
1562
2093
|
startChartServer: () => startChartServer
|
|
1563
2094
|
});
|
|
1564
|
-
function
|
|
2095
|
+
function findProjectRoot2(startDir) {
|
|
1565
2096
|
let dir = startDir;
|
|
1566
2097
|
for (let i = 0; i < 8; i++) {
|
|
1567
|
-
const graphsDir2 =
|
|
1568
|
-
if (
|
|
1569
|
-
const parent =
|
|
2098
|
+
const graphsDir2 = import_node_path12.default.join(dir, ".launchsecure", "graphs");
|
|
2099
|
+
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;
|
|
2100
|
+
const parent = import_node_path12.default.dirname(dir);
|
|
1570
2101
|
if (parent === dir) break;
|
|
1571
2102
|
dir = parent;
|
|
1572
2103
|
}
|
|
1573
2104
|
dir = startDir;
|
|
1574
2105
|
for (let i = 0; i < 8; i++) {
|
|
1575
|
-
if (
|
|
1576
|
-
const parent =
|
|
2106
|
+
if (import_node_fs11.default.existsSync(import_node_path12.default.join(dir, ".git"))) return dir;
|
|
2107
|
+
const parent = import_node_path12.default.dirname(dir);
|
|
1577
2108
|
if (parent === dir) break;
|
|
1578
2109
|
dir = parent;
|
|
1579
2110
|
}
|
|
@@ -1625,16 +2156,16 @@ function buildMergedGraph(projectRoot) {
|
|
|
1625
2156
|
};
|
|
1626
2157
|
}
|
|
1627
2158
|
function serveStatic(res, filePath) {
|
|
1628
|
-
if (!
|
|
1629
|
-
const ext =
|
|
2159
|
+
if (!import_node_fs11.default.existsSync(filePath) || !import_node_fs11.default.statSync(filePath).isFile()) return false;
|
|
2160
|
+
const ext = import_node_path12.default.extname(filePath).toLowerCase();
|
|
1630
2161
|
const mime = MIME_TYPES[ext] ?? "application/octet-stream";
|
|
1631
2162
|
res.writeHead(200, { "Content-Type": mime, "Cache-Control": "no-cache" });
|
|
1632
|
-
|
|
2163
|
+
import_node_fs11.default.createReadStream(filePath).pipe(res);
|
|
1633
2164
|
return true;
|
|
1634
2165
|
}
|
|
1635
2166
|
function serveIndex(res, clientDir) {
|
|
1636
|
-
const indexPath =
|
|
1637
|
-
if (!
|
|
2167
|
+
const indexPath = import_node_path12.default.join(clientDir, "index.html");
|
|
2168
|
+
if (!import_node_fs11.default.existsSync(indexPath)) {
|
|
1638
2169
|
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
1639
2170
|
res.end(`LaunchChart client bundle not found at ${clientDir}. Run 'npm run build:chart-client'.`);
|
|
1640
2171
|
return;
|
|
@@ -1642,14 +2173,14 @@ function serveIndex(res, clientDir) {
|
|
|
1642
2173
|
serveStatic(res, indexPath);
|
|
1643
2174
|
}
|
|
1644
2175
|
function tryListen(server, port) {
|
|
1645
|
-
return new Promise((
|
|
2176
|
+
return new Promise((resolve2, reject) => {
|
|
1646
2177
|
const onError = (err2) => {
|
|
1647
2178
|
server.off("listening", onListening);
|
|
1648
2179
|
reject(err2);
|
|
1649
2180
|
};
|
|
1650
2181
|
const onListening = () => {
|
|
1651
2182
|
server.off("error", onError);
|
|
1652
|
-
|
|
2183
|
+
resolve2(port);
|
|
1653
2184
|
};
|
|
1654
2185
|
server.once("error", onError);
|
|
1655
2186
|
server.once("listening", onListening);
|
|
@@ -1675,7 +2206,7 @@ async function bindWithFallback(server, startPort) {
|
|
|
1675
2206
|
}
|
|
1676
2207
|
async function startChartServer(opts = {}) {
|
|
1677
2208
|
const cwd = opts.cwd ?? process.cwd();
|
|
1678
|
-
const projectRoot =
|
|
2209
|
+
const projectRoot = findProjectRoot2(cwd);
|
|
1679
2210
|
const existing = getLiveLock();
|
|
1680
2211
|
if (existing) {
|
|
1681
2212
|
if (!opts.quiet) {
|
|
@@ -1686,7 +2217,7 @@ async function startChartServer(opts = {}) {
|
|
|
1686
2217
|
}
|
|
1687
2218
|
return { port: existing.port, url: existing.url };
|
|
1688
2219
|
}
|
|
1689
|
-
const clientDir = opts.clientDir ??
|
|
2220
|
+
const clientDir = opts.clientDir ?? import_node_path12.default.join(__dirname, "..", "chart-client");
|
|
1690
2221
|
const server = import_node_http.default.createServer((req, res) => {
|
|
1691
2222
|
try {
|
|
1692
2223
|
const url2 = new URL(req.url ?? "/", `http://${req.headers.host}`);
|
|
@@ -1729,8 +2260,43 @@ async function startChartServer(opts = {}) {
|
|
|
1729
2260
|
res.end(JSON.stringify({ ok: true, projectRoot }));
|
|
1730
2261
|
return;
|
|
1731
2262
|
}
|
|
2263
|
+
if (req.method === "GET" && url2.pathname === "/api/parser-config") {
|
|
2264
|
+
const config = loadConfig(projectRoot);
|
|
2265
|
+
const detection = [
|
|
2266
|
+
{ id: "react-nextjs", layer: "ui", label: "React + Next.js", detected: reactNextjsParser.detect(projectRoot) },
|
|
2267
|
+
{ id: "nextjs-routes", layer: "api", label: "Next.js API Routes", detected: nextjsRoutesParser.detect(projectRoot) },
|
|
2268
|
+
{ id: "prisma-schema", layer: "db", label: "Prisma Schema", detected: prismaSchemaParser.detect(projectRoot) }
|
|
2269
|
+
];
|
|
2270
|
+
const crosslayerParsers = [
|
|
2271
|
+
{ id: "fetch-resolver", label: "Fetch / api.method() calls" },
|
|
2272
|
+
{ id: "api-annotations", label: "@api annotations" },
|
|
2273
|
+
{ id: "url-literal-scanner", label: "/api/... URL literals" }
|
|
2274
|
+
];
|
|
2275
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2276
|
+
res.end(JSON.stringify({ config, detection, crosslayerParsers }));
|
|
2277
|
+
return;
|
|
2278
|
+
}
|
|
2279
|
+
if (req.method === "POST" && url2.pathname === "/api/parser-config") {
|
|
2280
|
+
let body = "";
|
|
2281
|
+
req.on("data", (chunk) => {
|
|
2282
|
+
body += chunk.toString();
|
|
2283
|
+
});
|
|
2284
|
+
req.on("end", () => {
|
|
2285
|
+
try {
|
|
2286
|
+
const newConfig = JSON.parse(body);
|
|
2287
|
+
const configPath = import_node_path12.default.join(projectRoot, ".launchchart.json");
|
|
2288
|
+
import_node_fs11.default.writeFileSync(configPath, JSON.stringify(newConfig, null, 2) + "\n", "utf-8");
|
|
2289
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2290
|
+
res.end(JSON.stringify({ ok: true }));
|
|
2291
|
+
} catch (err2) {
|
|
2292
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
2293
|
+
res.end(JSON.stringify({ ok: false, error: String(err2) }));
|
|
2294
|
+
}
|
|
2295
|
+
});
|
|
2296
|
+
return;
|
|
2297
|
+
}
|
|
1732
2298
|
if (url2.pathname !== "/") {
|
|
1733
|
-
const staticPath =
|
|
2299
|
+
const staticPath = import_node_path12.default.join(clientDir, url2.pathname);
|
|
1734
2300
|
if (serveStatic(res, staticPath)) return;
|
|
1735
2301
|
}
|
|
1736
2302
|
serveIndex(res, clientDir);
|
|
@@ -1784,15 +2350,19 @@ function runServeCli(argv) {
|
|
|
1784
2350
|
process.exit(1);
|
|
1785
2351
|
});
|
|
1786
2352
|
}
|
|
1787
|
-
var import_node_http,
|
|
2353
|
+
var import_node_http, import_node_fs11, import_node_path12, DEFAULT_PORT, MAX_PORT_SCAN, MIME_TYPES;
|
|
1788
2354
|
var init_chart_serve = __esm({
|
|
1789
2355
|
"src/server/chart-serve.ts"() {
|
|
1790
2356
|
"use strict";
|
|
1791
2357
|
import_node_http = __toESM(require("node:http"));
|
|
1792
|
-
|
|
1793
|
-
|
|
2358
|
+
import_node_fs11 = __toESM(require("node:fs"));
|
|
2359
|
+
import_node_path12 = __toESM(require("node:path"));
|
|
1794
2360
|
init_graph();
|
|
1795
2361
|
init_lockfile();
|
|
2362
|
+
init_config();
|
|
2363
|
+
init_react_nextjs();
|
|
2364
|
+
init_nextjs_routes();
|
|
2365
|
+
init_prisma_schema();
|
|
1796
2366
|
DEFAULT_PORT = 52819;
|
|
1797
2367
|
MAX_PORT_SCAN = 20;
|
|
1798
2368
|
MIME_TYPES = {
|
|
@@ -2118,9 +2688,9 @@ function handleReadGraph(args) {
|
|
|
2118
2688
|
return okJson(result);
|
|
2119
2689
|
}
|
|
2120
2690
|
function nodeToFilePath(rootDir, layer, nodeId) {
|
|
2121
|
-
if (layer === "ui") return (0,
|
|
2122
|
-
if (layer === "api") return (0,
|
|
2123
|
-
if (layer === "db") return (0,
|
|
2691
|
+
if (layer === "ui") return (0, import_node_path13.join)(rootDir, "src", nodeId);
|
|
2692
|
+
if (layer === "api") return (0, import_node_path13.join)(rootDir, nodeId);
|
|
2693
|
+
if (layer === "db") return (0, import_node_path13.join)(rootDir, "prisma", "schema.prisma");
|
|
2124
2694
|
return null;
|
|
2125
2695
|
}
|
|
2126
2696
|
function handleGrepNodes(args) {
|
|
@@ -2180,11 +2750,11 @@ function handleGrepNodes(args) {
|
|
|
2180
2750
|
let filesSearched = 0;
|
|
2181
2751
|
let truncated = false;
|
|
2182
2752
|
for (const [filePath, nodeId] of filePaths) {
|
|
2183
|
-
if (!(0,
|
|
2753
|
+
if (!(0, import_node_fs12.existsSync)(filePath)) continue;
|
|
2184
2754
|
filesSearched++;
|
|
2185
2755
|
let content;
|
|
2186
2756
|
try {
|
|
2187
|
-
content = (0,
|
|
2757
|
+
content = (0, import_node_fs12.readFileSync)(filePath, "utf-8");
|
|
2188
2758
|
} catch {
|
|
2189
2759
|
continue;
|
|
2190
2760
|
}
|
|
@@ -2221,13 +2791,10 @@ function handleGrepNodes(args) {
|
|
|
2221
2791
|
truncated
|
|
2222
2792
|
});
|
|
2223
2793
|
}
|
|
2224
|
-
function
|
|
2794
|
+
function handleChartServerStatus() {
|
|
2225
2795
|
const lock = getLiveLock();
|
|
2226
2796
|
if (!lock) {
|
|
2227
|
-
return okJson({
|
|
2228
|
-
running: false,
|
|
2229
|
-
hint: "No launch-chart UI server is currently running. Start one with `launch-chart serve`, or set LAUNCH_CHART_AUTOSERVE=1 in your MCP config to auto-start it alongside the MCP server."
|
|
2230
|
-
});
|
|
2797
|
+
return okJson({ running: false });
|
|
2231
2798
|
}
|
|
2232
2799
|
return okJson({
|
|
2233
2800
|
running: true,
|
|
@@ -2238,6 +2805,113 @@ function handleGetGraphUiUrl() {
|
|
|
2238
2805
|
startedAt: lock.startedAt
|
|
2239
2806
|
});
|
|
2240
2807
|
}
|
|
2808
|
+
function handleStartChartServer(args) {
|
|
2809
|
+
const lock = getLiveLock();
|
|
2810
|
+
if (lock) {
|
|
2811
|
+
return okJson({
|
|
2812
|
+
started: false,
|
|
2813
|
+
reason: "already_running",
|
|
2814
|
+
url: lock.url,
|
|
2815
|
+
port: lock.port,
|
|
2816
|
+
pid: lock.pid
|
|
2817
|
+
});
|
|
2818
|
+
}
|
|
2819
|
+
const entryPath = process.argv[1];
|
|
2820
|
+
const logDir = (0, import_node_path13.join)((0, import_node_os2.homedir)(), ".launchsecure");
|
|
2821
|
+
(0, import_node_fs12.mkdirSync)(logDir, { recursive: true });
|
|
2822
|
+
const logPath = (0, import_node_path13.join)(logDir, "launch-chart.log");
|
|
2823
|
+
const out = (0, import_node_fs12.openSync)(logPath, "a");
|
|
2824
|
+
const err2 = (0, import_node_fs12.openSync)(logPath, "a");
|
|
2825
|
+
const portArgs = args.port ? ["--port", String(args.port)] : [];
|
|
2826
|
+
const child = (0, import_node_child_process2.spawn)(process.execPath, [entryPath, "serve", ...portArgs], {
|
|
2827
|
+
detached: true,
|
|
2828
|
+
stdio: ["ignore", out, err2],
|
|
2829
|
+
env: { ...process.env, LAUNCH_CHART_AUTOSERVE: "" }
|
|
2830
|
+
});
|
|
2831
|
+
child.unref();
|
|
2832
|
+
return okJson({
|
|
2833
|
+
started: true,
|
|
2834
|
+
pid: child.pid,
|
|
2835
|
+
logPath
|
|
2836
|
+
});
|
|
2837
|
+
}
|
|
2838
|
+
function handleStopChartServer() {
|
|
2839
|
+
const lock = getLiveLock();
|
|
2840
|
+
if (!lock) {
|
|
2841
|
+
return okJson({ stopped: false, reason: "not_running" });
|
|
2842
|
+
}
|
|
2843
|
+
try {
|
|
2844
|
+
process.kill(lock.pid, "SIGTERM");
|
|
2845
|
+
return okJson({ stopped: true, pid: lock.pid });
|
|
2846
|
+
} catch (e) {
|
|
2847
|
+
const code = e.code;
|
|
2848
|
+
if (code === "ESRCH") {
|
|
2849
|
+
clearLock();
|
|
2850
|
+
return okJson({ stopped: true, pid: lock.pid, note: "process was already gone, lock cleaned up" });
|
|
2851
|
+
}
|
|
2852
|
+
return okJson({ stopped: false, reason: `kill failed: ${code ?? e}` });
|
|
2853
|
+
}
|
|
2854
|
+
}
|
|
2855
|
+
function handleDetectProjectStack() {
|
|
2856
|
+
const rootDir = findProjectRoot(process.cwd());
|
|
2857
|
+
const parsers = [
|
|
2858
|
+
{ id: "react-nextjs", layer: "ui", detected: reactNextjsParser.detect(rootDir) },
|
|
2859
|
+
{ id: "nextjs-routes", layer: "api", detected: nextjsRoutesParser.detect(rootDir) },
|
|
2860
|
+
{ id: "prisma-schema", layer: "db", detected: prismaSchemaParser.detect(rootDir) }
|
|
2861
|
+
];
|
|
2862
|
+
const config = loadConfig(rootDir);
|
|
2863
|
+
let stats = { calls_api: 0, references_api: 0, out_of_pattern: 0, annotations: 0 };
|
|
2864
|
+
const uiGraph = readGraph(rootDir, "ui");
|
|
2865
|
+
if (uiGraph) {
|
|
2866
|
+
for (const ref of uiGraph.cross_refs ?? []) {
|
|
2867
|
+
if (ref.type === "calls_api") stats.calls_api++;
|
|
2868
|
+
if (ref.type === "references_api") stats.references_api++;
|
|
2869
|
+
}
|
|
2870
|
+
for (const f of uiGraph.flagged_edges ?? []) {
|
|
2871
|
+
if (f.type === "out_of_pattern") stats.out_of_pattern++;
|
|
2872
|
+
}
|
|
2873
|
+
}
|
|
2874
|
+
const srcDir = (0, import_node_path13.join)(rootDir, "src");
|
|
2875
|
+
if ((0, import_node_fs12.existsSync)(srcDir)) {
|
|
2876
|
+
const scanDir = (dir) => {
|
|
2877
|
+
if (!(0, import_node_fs12.existsSync)(dir)) return;
|
|
2878
|
+
for (const entry of (0, import_node_fs12.readdirSync)(dir, { withFileTypes: true })) {
|
|
2879
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
2880
|
+
const full = (0, import_node_path13.join)(dir, entry.name);
|
|
2881
|
+
if (entry.isDirectory()) {
|
|
2882
|
+
scanDir(full);
|
|
2883
|
+
continue;
|
|
2884
|
+
}
|
|
2885
|
+
if (![".ts", ".tsx"].includes((0, import_node_path13.extname)(entry.name))) continue;
|
|
2886
|
+
try {
|
|
2887
|
+
const content = (0, import_node_fs12.readFileSync)(full, "utf-8");
|
|
2888
|
+
const matches = content.match(/@api\s+(GET|POST|PUT|DELETE|PATCH)\s+\/\S+/g);
|
|
2889
|
+
if (matches) stats.annotations += matches.length;
|
|
2890
|
+
} catch {
|
|
2891
|
+
}
|
|
2892
|
+
}
|
|
2893
|
+
};
|
|
2894
|
+
scanDir(srcDir);
|
|
2895
|
+
}
|
|
2896
|
+
let recommendedPrimary = "fetch-resolver";
|
|
2897
|
+
if (stats.annotations > 0 && stats.annotations >= stats.calls_api) {
|
|
2898
|
+
recommendedPrimary = "api-annotations";
|
|
2899
|
+
} else if (stats.calls_api === 0 && stats.references_api > 0) {
|
|
2900
|
+
recommendedPrimary = "url-literal-scanner";
|
|
2901
|
+
}
|
|
2902
|
+
return okJson({
|
|
2903
|
+
parsers,
|
|
2904
|
+
crosslayer_parsers: [
|
|
2905
|
+
{ id: "fetch-resolver", description: "Detects direct fetch()/api.get() calls with inline URLs" },
|
|
2906
|
+
{ id: "api-annotations", description: "Scans for @api METHOD /path annotations in JSDoc/comments" },
|
|
2907
|
+
{ id: "url-literal-scanner", description: "Finds /api/... string literals as fallback detection" }
|
|
2908
|
+
],
|
|
2909
|
+
stats,
|
|
2910
|
+
recommended_primary: recommendedPrimary,
|
|
2911
|
+
current_config: Object.keys(config).length > 0 ? config : null,
|
|
2912
|
+
config_path: ".launchchart.json"
|
|
2913
|
+
});
|
|
2914
|
+
}
|
|
2241
2915
|
function send(msg) {
|
|
2242
2916
|
process.stdout.write(JSON.stringify(msg) + "\n");
|
|
2243
2917
|
}
|
|
@@ -2281,8 +2955,20 @@ function handleMessage(msg) {
|
|
|
2281
2955
|
respond(id ?? null, handleGrepNodes(args));
|
|
2282
2956
|
return;
|
|
2283
2957
|
}
|
|
2284
|
-
if (toolName === "
|
|
2285
|
-
respond(id ?? null,
|
|
2958
|
+
if (toolName === "chart_server_status") {
|
|
2959
|
+
respond(id ?? null, handleChartServerStatus());
|
|
2960
|
+
return;
|
|
2961
|
+
}
|
|
2962
|
+
if (toolName === "start_chart_server") {
|
|
2963
|
+
respond(id ?? null, handleStartChartServer(args));
|
|
2964
|
+
return;
|
|
2965
|
+
}
|
|
2966
|
+
if (toolName === "stop_chart_server") {
|
|
2967
|
+
respond(id ?? null, handleStopChartServer());
|
|
2968
|
+
return;
|
|
2969
|
+
}
|
|
2970
|
+
if (toolName === "detect_project_stack") {
|
|
2971
|
+
respond(id ?? null, handleDetectProjectStack());
|
|
2286
2972
|
return;
|
|
2287
2973
|
}
|
|
2288
2974
|
respondError(id ?? null, -32601, `Unknown tool: ${toolName}`);
|
|
@@ -2320,14 +3006,20 @@ function startGraphMcpServer() {
|
|
|
2320
3006
|
process.stderr.write(`[launchsecure-graph] MCP server started (cwd: ${process.cwd()})
|
|
2321
3007
|
`);
|
|
2322
3008
|
}
|
|
2323
|
-
var
|
|
3009
|
+
var import_node_fs12, import_node_path13, import_node_child_process2, import_node_os2, SERVER_INFO, TOOLS, COMPACT_SCHEMA, COMPACT_NODE_KNOWN_KEYS, EST_CHARS_PER_NODE_FULL, EST_CHARS_PER_NODE_MIN, EST_CHARS_PER_EDGE, NEIGHBORHOOD_BUDGET_CHARS, BATCH_BUDGET_CHARS;
|
|
2324
3010
|
var init_graph_mcp = __esm({
|
|
2325
3011
|
"src/server/graph-mcp.ts"() {
|
|
2326
3012
|
"use strict";
|
|
2327
|
-
|
|
2328
|
-
|
|
3013
|
+
import_node_fs12 = require("node:fs");
|
|
3014
|
+
import_node_path13 = require("node:path");
|
|
3015
|
+
import_node_child_process2 = require("node:child_process");
|
|
3016
|
+
import_node_os2 = require("node:os");
|
|
2329
3017
|
init_graph();
|
|
2330
3018
|
init_lockfile();
|
|
3019
|
+
init_config();
|
|
3020
|
+
init_react_nextjs();
|
|
3021
|
+
init_nextjs_routes();
|
|
3022
|
+
init_prisma_schema();
|
|
2331
3023
|
SERVER_INFO = {
|
|
2332
3024
|
name: "launchsecure-graph",
|
|
2333
3025
|
version: "0.0.1"
|
|
@@ -2455,8 +3147,39 @@ Returns: { pattern, filter, files_searched, total_matches, matches: [{file, line
|
|
|
2455
3147
|
}
|
|
2456
3148
|
},
|
|
2457
3149
|
{
|
|
2458
|
-
name: "
|
|
2459
|
-
description:
|
|
3150
|
+
name: "chart_server_status",
|
|
3151
|
+
description: `Check whether the launch-chart UI server is running. Returns: { running: boolean, url?: string, port?: number, pid?: number, startedAt?: string, cwd?: string }.
|
|
3152
|
+
|
|
3153
|
+
Use this when the user asks "is the chart running", "show me the project graph UI", "where's the chart", etc.`,
|
|
3154
|
+
inputSchema: {
|
|
3155
|
+
type: "object",
|
|
3156
|
+
properties: {}
|
|
3157
|
+
}
|
|
3158
|
+
},
|
|
3159
|
+
{
|
|
3160
|
+
name: "start_chart_server",
|
|
3161
|
+
description: 'Start the launch-chart UI server as a detached background process. The server serves the interactive project graph visualization at http://localhost:<port>. If the server is already running, returns the existing URL without spawning a duplicate. \n\nUse this when the user asks to "start the chart", "fire up charts", "open the graph UI", etc.',
|
|
3162
|
+
inputSchema: {
|
|
3163
|
+
type: "object",
|
|
3164
|
+
properties: {
|
|
3165
|
+
port: {
|
|
3166
|
+
type: "number",
|
|
3167
|
+
description: "Port to bind the server to. Defaults to 52819 with automatic fallback if in use."
|
|
3168
|
+
}
|
|
3169
|
+
}
|
|
3170
|
+
}
|
|
3171
|
+
},
|
|
3172
|
+
{
|
|
3173
|
+
name: "stop_chart_server",
|
|
3174
|
+
description: 'Stop the running launch-chart UI server. Sends SIGTERM to the server process and cleans up the lock file. If no server is running, returns a no-op response. \n\nUse this when the user asks to "stop the chart", "cool down charts", "kill the graph server", etc.',
|
|
3175
|
+
inputSchema: {
|
|
3176
|
+
type: "object",
|
|
3177
|
+
properties: {}
|
|
3178
|
+
}
|
|
3179
|
+
},
|
|
3180
|
+
{
|
|
3181
|
+
name: "detect_project_stack",
|
|
3182
|
+
description: "Detect project frameworks, available parsers, and recommend parser configuration. Scans the project to identify the tech stack (Next.js, Prisma, React, etc.), reports which built-in parsers are applicable, and provides cross-layer detection stats (fetch calls, @api annotations, URL literals). Returns a recommended primary parser and current .launchchart.json config if present. \n\nUse this when setting up launch-chart for a new project or reviewing parser configuration.",
|
|
2460
3183
|
inputSchema: {
|
|
2461
3184
|
type: "object",
|
|
2462
3185
|
properties: {}
|
|
@@ -2513,11 +3236,11 @@ Returns: { pattern, filter, files_searched, total_matches, matches: [{file, line
|
|
|
2513
3236
|
});
|
|
2514
3237
|
|
|
2515
3238
|
// src/server/graph-mcp-entry.ts
|
|
2516
|
-
var
|
|
2517
|
-
var
|
|
2518
|
-
var
|
|
2519
|
-
var
|
|
2520
|
-
var
|
|
3239
|
+
var import_node_child_process3 = require("node:child_process");
|
|
3240
|
+
var import_node_fs13 = require("node:fs");
|
|
3241
|
+
var import_node_path14 = __toESM(require("node:path"));
|
|
3242
|
+
var import_node_os3 = require("node:os");
|
|
3243
|
+
var import_node_fs14 = require("node:fs");
|
|
2521
3244
|
init_lockfile();
|
|
2522
3245
|
function logStderr(msg) {
|
|
2523
3246
|
process.stderr.write(`[launch-chart] ${msg}
|
|
@@ -2531,13 +3254,13 @@ function maybeAutoServe() {
|
|
|
2531
3254
|
return;
|
|
2532
3255
|
}
|
|
2533
3256
|
try {
|
|
2534
|
-
const logDir =
|
|
2535
|
-
(0,
|
|
2536
|
-
const logPath =
|
|
2537
|
-
const out = (0,
|
|
2538
|
-
const err2 = (0,
|
|
3257
|
+
const logDir = import_node_path14.default.join((0, import_node_os3.homedir)(), ".launchsecure");
|
|
3258
|
+
(0, import_node_fs14.mkdirSync)(logDir, { recursive: true });
|
|
3259
|
+
const logPath = import_node_path14.default.join(logDir, "launch-chart.log");
|
|
3260
|
+
const out = (0, import_node_fs13.openSync)(logPath, "a");
|
|
3261
|
+
const err2 = (0, import_node_fs13.openSync)(logPath, "a");
|
|
2539
3262
|
const entryPath = process.argv[1];
|
|
2540
|
-
const child = (0,
|
|
3263
|
+
const child = (0, import_node_child_process3.spawn)(process.execPath, [entryPath, "serve"], {
|
|
2541
3264
|
detached: true,
|
|
2542
3265
|
stdio: ["ignore", out, err2],
|
|
2543
3266
|
env: { ...process.env, LAUNCH_CHART_AUTOSERVE: "" }
|