@launchsecure/launch-kit 0.0.13 → 0.0.14
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-Dm6IBkiC.js +441 -0
- package/dist/chart-client/assets/index-l-yyLDX5.css +1 -0
- package/dist/chart-client/index.html +2 -2
- package/dist/client/assets/index-rlw8dmPR.css +32 -0
- package/dist/client/index.html +2 -2
- package/dist/server/chart-serve.js +2030 -460
- package/dist/server/cli.js +4987 -3302
- package/dist/server/graph-mcp-entry.js +2327 -486
- package/package.json +1 -1
- package/dist/chart-client/assets/index-DIPKWwEJ.js +0 -404
- package/dist/chart-client/assets/index-DjnSR-Hf.css +0 -1
- package/dist/client/assets/index-BIpeUMYR.css +0 -32
- /package/dist/client/assets/{index-qzK1AD2G.js → index-CyML1UiJ.js} +0 -0
|
@@ -155,7 +155,45 @@ var init_config = __esm({
|
|
|
155
155
|
}
|
|
156
156
|
});
|
|
157
157
|
|
|
158
|
+
// src/server/graph/core/resolve-paths.ts
|
|
159
|
+
function resolveProjectPaths(rootDir, config) {
|
|
160
|
+
if (config.paths?.appDir) {
|
|
161
|
+
const appDir = (0, import_node_path3.join)(rootDir, config.paths.appDir);
|
|
162
|
+
const srcDir = config.paths.srcDir ? (0, import_node_path3.join)(rootDir, config.paths.srcDir) : (0, import_node_path3.dirname)(appDir);
|
|
163
|
+
return { srcDir, appDir, apiDir: (0, import_node_path3.join)(appDir, "api") };
|
|
164
|
+
}
|
|
165
|
+
const srcApp = (0, import_node_path3.join)(rootDir, "src", "app");
|
|
166
|
+
if ((0, import_node_fs3.existsSync)(srcApp)) {
|
|
167
|
+
return { srcDir: (0, import_node_path3.join)(rootDir, "src"), appDir: srcApp, apiDir: (0, import_node_path3.join)(srcApp, "api") };
|
|
168
|
+
}
|
|
169
|
+
const rootApp = (0, import_node_path3.join)(rootDir, "app");
|
|
170
|
+
if ((0, import_node_fs3.existsSync)(rootApp)) {
|
|
171
|
+
return { srcDir: rootDir, appDir: rootApp, apiDir: (0, import_node_path3.join)(rootApp, "api") };
|
|
172
|
+
}
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
var import_node_fs3, import_node_path3;
|
|
176
|
+
var init_resolve_paths = __esm({
|
|
177
|
+
"src/server/graph/core/resolve-paths.ts"() {
|
|
178
|
+
"use strict";
|
|
179
|
+
import_node_fs3 = require("node:fs");
|
|
180
|
+
import_node_path3 = require("node:path");
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
|
|
158
184
|
// src/server/graph/core/ts-extractor.ts
|
|
185
|
+
var ts_extractor_exports = {};
|
|
186
|
+
__export(ts_extractor_exports, {
|
|
187
|
+
classifyFile: () => classifyFile,
|
|
188
|
+
createQuery: () => createQuery,
|
|
189
|
+
extractAuthWrappersTS: () => extractAuthWrappersTS,
|
|
190
|
+
extractDbCallsTS: () => extractDbCallsTS,
|
|
191
|
+
extractDeep: () => extractDeep,
|
|
192
|
+
initTreeSitter: () => initTreeSitter,
|
|
193
|
+
parseCodeTS: () => parseCodeTS,
|
|
194
|
+
parseFileTS: () => parseFileTS,
|
|
195
|
+
setExtractorConfig: () => setExtractorConfig
|
|
196
|
+
});
|
|
159
197
|
async function initTreeSitter() {
|
|
160
198
|
if (initialized) return;
|
|
161
199
|
if (initPromise) return initPromise;
|
|
@@ -179,17 +217,25 @@ function getQuery(name) {
|
|
|
179
217
|
ensureInit();
|
|
180
218
|
const cached = queryCache.get(name);
|
|
181
219
|
if (cached) return cached;
|
|
182
|
-
const scmPath = (0,
|
|
183
|
-
const scm = (0,
|
|
220
|
+
const scmPath = (0, import_node_path4.join)(queriesDir, `${name}.scm`);
|
|
221
|
+
const scm = (0, import_node_fs4.readFileSync)(scmPath, "utf-8");
|
|
184
222
|
const query = tsxLanguage.query(scm);
|
|
185
223
|
queryCache.set(name, query);
|
|
186
224
|
return query;
|
|
187
225
|
}
|
|
188
226
|
function parseSource(absPath) {
|
|
189
227
|
ensureInit();
|
|
190
|
-
const content = (0,
|
|
228
|
+
const content = (0, import_node_fs4.readFileSync)(absPath, "utf-8");
|
|
191
229
|
return parserInstance.parse(content);
|
|
192
230
|
}
|
|
231
|
+
function parseCodeTS(code) {
|
|
232
|
+
ensureInit();
|
|
233
|
+
return parserInstance.parse(code);
|
|
234
|
+
}
|
|
235
|
+
function createQuery(pattern) {
|
|
236
|
+
ensureInit();
|
|
237
|
+
return tsxLanguage.query(pattern);
|
|
238
|
+
}
|
|
193
239
|
function setExtractorConfig(config) {
|
|
194
240
|
extraDbIdentifiers = config.dbIdentifiers ?? [];
|
|
195
241
|
extraMutationMethods = config.mutationMethods ?? [];
|
|
@@ -644,17 +690,17 @@ function extractDeep(absPath) {
|
|
|
644
690
|
}
|
|
645
691
|
return { elements, stateVars, conditions, variables, responses, params };
|
|
646
692
|
}
|
|
647
|
-
var
|
|
693
|
+
var import_node_fs4, import_node_path4, tsxLanguage, parserInstance, initPromise, initialized, queriesDir, queryCache, PRISMA_MUTATION_METHODS_BUILTIN, DB_IDENTIFIERS_FALLBACK, extraDbIdentifiers, extraMutationMethods;
|
|
648
694
|
var init_ts_extractor = __esm({
|
|
649
695
|
"src/server/graph/core/ts-extractor.ts"() {
|
|
650
696
|
"use strict";
|
|
651
|
-
|
|
652
|
-
|
|
697
|
+
import_node_fs4 = require("node:fs");
|
|
698
|
+
import_node_path4 = require("node:path");
|
|
653
699
|
initialized = false;
|
|
654
700
|
queriesDir = (() => {
|
|
655
|
-
const srcPath = (0,
|
|
701
|
+
const srcPath = (0, import_node_path4.join)((0, import_node_path4.dirname)(__filename), "..", "queries");
|
|
656
702
|
if (require("fs").existsSync(srcPath)) return srcPath;
|
|
657
|
-
return (0,
|
|
703
|
+
return (0, import_node_path4.join)((0, import_node_path4.dirname)(__filename), "graph", "queries");
|
|
658
704
|
})();
|
|
659
705
|
queryCache = /* @__PURE__ */ new Map();
|
|
660
706
|
PRISMA_MUTATION_METHODS_BUILTIN = [
|
|
@@ -674,15 +720,15 @@ var init_ts_extractor = __esm({
|
|
|
674
720
|
}
|
|
675
721
|
});
|
|
676
722
|
|
|
677
|
-
// src/server/graph/parsers/
|
|
723
|
+
// src/server/graph/parsers/ts/typescript-project.ts
|
|
678
724
|
function walk(dir, exts) {
|
|
679
725
|
const results = [];
|
|
680
|
-
if (!(0,
|
|
681
|
-
for (const entry of (0,
|
|
682
|
-
const full = (0,
|
|
726
|
+
if (!(0, import_node_fs5.existsSync)(dir)) return results;
|
|
727
|
+
for (const entry of (0, import_node_fs5.readdirSync)(dir, { withFileTypes: true })) {
|
|
728
|
+
const full = (0, import_node_path5.join)(dir, entry.name);
|
|
683
729
|
if (entry.isDirectory()) {
|
|
684
730
|
results.push(...walk(full, exts));
|
|
685
|
-
} else if (exts.includes((0,
|
|
731
|
+
} else if (exts.includes((0, import_node_path5.extname)(entry.name))) {
|
|
686
732
|
results.push(full);
|
|
687
733
|
}
|
|
688
734
|
}
|
|
@@ -690,33 +736,33 @@ function walk(dir, exts) {
|
|
|
690
736
|
}
|
|
691
737
|
function walkWithIgnore(dir, exts, ignoreDirs) {
|
|
692
738
|
const results = [];
|
|
693
|
-
if (!(0,
|
|
694
|
-
for (const entry of (0,
|
|
739
|
+
if (!(0, import_node_fs5.existsSync)(dir)) return results;
|
|
740
|
+
for (const entry of (0, import_node_fs5.readdirSync)(dir, { withFileTypes: true })) {
|
|
695
741
|
if (entry.isDirectory()) {
|
|
696
742
|
if (ignoreDirs.has(entry.name)) continue;
|
|
697
|
-
results.push(...walkWithIgnore((0,
|
|
698
|
-
} else if (exts.includes((0,
|
|
699
|
-
results.push((0,
|
|
743
|
+
results.push(...walkWithIgnore((0, import_node_path5.join)(dir, entry.name), exts, ignoreDirs));
|
|
744
|
+
} else if (exts.includes((0, import_node_path5.extname)(entry.name))) {
|
|
745
|
+
results.push((0, import_node_path5.join)(dir, entry.name));
|
|
700
746
|
}
|
|
701
747
|
}
|
|
702
748
|
return results;
|
|
703
749
|
}
|
|
704
750
|
function toNodeId(srcDir, absPath) {
|
|
705
|
-
return (0,
|
|
751
|
+
return (0, import_node_path5.relative)(srcDir, absPath).replace(/\\/g, "/");
|
|
706
752
|
}
|
|
707
753
|
function resolveImport(srcDir, specifier) {
|
|
708
754
|
if (!specifier.startsWith("@/")) return null;
|
|
709
755
|
const rel = specifier.slice(2);
|
|
710
|
-
const base = (0,
|
|
711
|
-
for (const c of [base, base + ".ts", base + ".tsx", (0,
|
|
712
|
-
if ((0,
|
|
756
|
+
const base = (0, import_node_path5.join)(srcDir, rel);
|
|
757
|
+
for (const c of [base, base + ".ts", base + ".tsx", (0, import_node_path5.join)(base, "index.ts"), (0, import_node_path5.join)(base, "index.tsx")]) {
|
|
758
|
+
if ((0, import_node_fs5.existsSync)(c) && (0, import_node_fs5.statSync)(c).isFile()) return c;
|
|
713
759
|
}
|
|
714
760
|
return null;
|
|
715
761
|
}
|
|
716
762
|
function resolveRelativeImport(fromFile, specifier) {
|
|
717
|
-
const base = (0,
|
|
718
|
-
for (const c of [base, base + ".ts", base + ".tsx", (0,
|
|
719
|
-
if ((0,
|
|
763
|
+
const base = (0, import_node_path5.join)((0, import_node_path5.dirname)(fromFile), specifier);
|
|
764
|
+
for (const c of [base, base + ".ts", base + ".tsx", (0, import_node_path5.join)(base, "index.ts"), (0, import_node_path5.join)(base, "index.tsx")]) {
|
|
765
|
+
if ((0, import_node_fs5.existsSync)(c) && (0, import_node_fs5.statSync)(c).isFile()) return c;
|
|
720
766
|
}
|
|
721
767
|
return null;
|
|
722
768
|
}
|
|
@@ -737,7 +783,7 @@ function resolveBarrelMap(barrelAbsPath, parsedByPath, memo, visiting) {
|
|
|
737
783
|
const resolved = resolveRelativeImport(barrelAbsPath, re.from);
|
|
738
784
|
if (!resolved) continue;
|
|
739
785
|
if (re.isWildcard) {
|
|
740
|
-
const targetBn = (0,
|
|
786
|
+
const targetBn = (0, import_node_path5.basename)(resolved);
|
|
741
787
|
const targetIsBarrel = targetBn === "index.ts" || targetBn === "index.tsx";
|
|
742
788
|
if (targetIsBarrel) {
|
|
743
789
|
const nested = resolveBarrelMap(resolved, parsedByPath, memo, visiting);
|
|
@@ -764,12 +810,12 @@ function buildAllBarrelMaps(srcDir, parsedByPath) {
|
|
|
764
810
|
const barrels = /* @__PURE__ */ new Map();
|
|
765
811
|
const memo = /* @__PURE__ */ new Map();
|
|
766
812
|
for (const [absPath, parsed] of parsedByPath) {
|
|
767
|
-
const bn = (0,
|
|
813
|
+
const bn = (0, import_node_path5.basename)(absPath);
|
|
768
814
|
if (bn !== "index.ts" && bn !== "index.tsx") continue;
|
|
769
815
|
if (parsed.reExports.length === 0) continue;
|
|
770
816
|
const map = resolveBarrelMap(absPath, parsedByPath, memo, /* @__PURE__ */ new Set());
|
|
771
817
|
if (map.size > 0) {
|
|
772
|
-
const barrelId = (0,
|
|
818
|
+
const barrelId = (0, import_node_path5.relative)(srcDir, (0, import_node_path5.dirname)(absPath)).replace(/\\/g, "/");
|
|
773
819
|
barrels.set(barrelId, map);
|
|
774
820
|
}
|
|
775
821
|
}
|
|
@@ -790,7 +836,18 @@ function extractRoute(id) {
|
|
|
790
836
|
return route || "/";
|
|
791
837
|
}
|
|
792
838
|
function nameFromFilename(absPath) {
|
|
793
|
-
return (0,
|
|
839
|
+
return (0, import_node_path5.basename)(absPath, (0, import_node_path5.extname)(absPath)).replace(/[-_](\w)/g, (_, c) => c.toUpperCase()).replace(/^(\w)/, (_, c) => c.toUpperCase());
|
|
840
|
+
}
|
|
841
|
+
function filePathToApiRoute(apiDir, absPath) {
|
|
842
|
+
let route = "/" + (0, import_node_path5.relative)(apiDir, absPath).replace(/\\/g, "/").replace(/\/route\.tsx?$/, "");
|
|
843
|
+
route = route.replace(/\[([^\]]+)\]/g, ":$1");
|
|
844
|
+
route = route.replace(/\/+/g, "/");
|
|
845
|
+
if (route === "/") return "/api";
|
|
846
|
+
return "/api" + route;
|
|
847
|
+
}
|
|
848
|
+
function camelToPascal(s) {
|
|
849
|
+
if (!s) return s;
|
|
850
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
794
851
|
}
|
|
795
852
|
function resolveTemplateLiteralRoute(template, routeToNodeId) {
|
|
796
853
|
const parameterized = template.replace(/\$\{([^}]+)\}/g, (_, expr) => {
|
|
@@ -871,7 +928,7 @@ function matchRouteToPage(route, routeToNodeId) {
|
|
|
871
928
|
if (routeToNodeId.has(normalized)) return routeToNodeId.get(normalized);
|
|
872
929
|
return null;
|
|
873
930
|
}
|
|
874
|
-
function extractEdges(srcDir, absPath, sourceId, parsed, nodeIdSet,
|
|
931
|
+
function extractEdges(srcDir, absPath, sourceId, parsed, nodeIdSet, barrelMaps, routeToNodeId) {
|
|
875
932
|
const edges = [];
|
|
876
933
|
const flagged = [];
|
|
877
934
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -883,7 +940,7 @@ function extractEdges(srcDir, absPath, sourceId, parsed, nodeIdSet, nodeTypeMap,
|
|
|
883
940
|
if (label) edge.label = label;
|
|
884
941
|
edges.push(edge);
|
|
885
942
|
}
|
|
886
|
-
function edgeTypeFor(
|
|
943
|
+
function edgeTypeFor(isTypeOnlyImport, importedNames) {
|
|
887
944
|
if (isTypeOnlyImport) return "imports";
|
|
888
945
|
const anyRendered = importedNames.some((n) => parsed.jsxElements.has(n));
|
|
889
946
|
if (anyRendered) return "renders";
|
|
@@ -908,14 +965,14 @@ function extractEdges(srcDir, absPath, sourceId, parsed, nodeIdSet, nodeTypeMap,
|
|
|
908
965
|
}
|
|
909
966
|
for (const [targetId, targetNames] of byTarget) {
|
|
910
967
|
const allType = isTypeOnly || targetNames.every((n) => typeNames.has(n));
|
|
911
|
-
addEdge(targetId, edgeTypeFor(
|
|
968
|
+
addEdge(targetId, edgeTypeFor(allType, targetNames));
|
|
912
969
|
}
|
|
913
970
|
} else {
|
|
914
971
|
const resolved = resolveImport(srcDir, specifier);
|
|
915
972
|
if (resolved) {
|
|
916
973
|
const targetId = toNodeId(srcDir, resolved);
|
|
917
974
|
if (nodeIdSet.has(targetId) && !targetId.endsWith("/index.ts") && !targetId.endsWith("/index.tsx")) {
|
|
918
|
-
addEdge(targetId, edgeTypeFor(
|
|
975
|
+
addEdge(targetId, edgeTypeFor(isTypeOnly, names));
|
|
919
976
|
}
|
|
920
977
|
}
|
|
921
978
|
}
|
|
@@ -924,7 +981,7 @@ function extractEdges(srcDir, absPath, sourceId, parsed, nodeIdSet, nodeTypeMap,
|
|
|
924
981
|
if (resolved) {
|
|
925
982
|
const targetId = toNodeId(srcDir, resolved);
|
|
926
983
|
if (nodeIdSet.has(targetId) && !targetId.endsWith("/index.ts") && !targetId.endsWith("/index.tsx")) {
|
|
927
|
-
addEdge(targetId, edgeTypeFor(
|
|
984
|
+
addEdge(targetId, edgeTypeFor(isTypeOnly, names));
|
|
928
985
|
}
|
|
929
986
|
}
|
|
930
987
|
}
|
|
@@ -966,74 +1023,113 @@ function extractEdges(srcDir, absPath, sourceId, parsed, nodeIdSet, nodeTypeMap,
|
|
|
966
1023
|
}
|
|
967
1024
|
return { edges, flagged };
|
|
968
1025
|
}
|
|
1026
|
+
function hasNextConfig(rootDir) {
|
|
1027
|
+
return (0, import_node_fs5.existsSync)((0, import_node_path5.join)(rootDir, "next.config.ts")) || (0, import_node_fs5.existsSync)((0, import_node_path5.join)(rootDir, "next.config.js")) || (0, import_node_fs5.existsSync)((0, import_node_path5.join)(rootDir, "next.config.mjs"));
|
|
1028
|
+
}
|
|
969
1029
|
function detect(rootDir) {
|
|
970
|
-
|
|
1030
|
+
const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
|
|
1031
|
+
return paths !== null && hasNextConfig(rootDir);
|
|
971
1032
|
}
|
|
972
1033
|
function generate(rootDir) {
|
|
973
|
-
const
|
|
974
|
-
const
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
const
|
|
978
|
-
const
|
|
979
|
-
|
|
980
|
-
);
|
|
981
|
-
const
|
|
982
|
-
const configFiles = walk((0, import_node_path4.join)(srcDir, "config"), [".ts", ".tsx"]);
|
|
1034
|
+
const config = loadConfig(rootDir);
|
|
1035
|
+
const paths = resolveProjectPaths(rootDir, config);
|
|
1036
|
+
const srcDir = paths.srcDir;
|
|
1037
|
+
const apiDir = paths.apiDir;
|
|
1038
|
+
const appFiles = walk(paths.appDir, [".tsx", ".ts"]);
|
|
1039
|
+
const clientFiles = walk((0, import_node_path5.join)(srcDir, "client"), [".tsx", ".ts"]);
|
|
1040
|
+
const serverFiles = walk((0, import_node_path5.join)(srcDir, "server"), [".ts", ".tsx"]);
|
|
1041
|
+
const libFiles = walk((0, import_node_path5.join)(srcDir, "lib"), [".ts", ".tsx"]);
|
|
1042
|
+
const configFiles = walk((0, import_node_path5.join)(srcDir, "config"), [".ts", ".tsx"]);
|
|
983
1043
|
const allDiscovered = [...appFiles, ...clientFiles, ...serverFiles, ...libFiles, ...configFiles];
|
|
984
1044
|
const parsedByPath = /* @__PURE__ */ new Map();
|
|
985
1045
|
for (const absPath of allDiscovered) {
|
|
986
1046
|
parsedByPath.set(absPath, parseFileTS(absPath));
|
|
987
1047
|
}
|
|
988
1048
|
const barrelMaps = buildAllBarrelMaps(srcDir, parsedByPath);
|
|
989
|
-
const
|
|
990
|
-
const
|
|
1049
|
+
const uiNodes = [];
|
|
1050
|
+
const apiNodes = [];
|
|
991
1051
|
const nodeIdSet = /* @__PURE__ */ new Set();
|
|
992
|
-
const nodeTypeMap = /* @__PURE__ */ new Map();
|
|
993
1052
|
const routeToNodeId = /* @__PURE__ */ new Map();
|
|
1053
|
+
const fileSet = allDiscovered.filter((f) => !(0, import_node_path5.basename)(f).startsWith("index."));
|
|
994
1054
|
for (const absPath of fileSet) {
|
|
995
1055
|
const id = toNodeId(srcDir, absPath);
|
|
996
1056
|
const type = classifyType(absPath, id);
|
|
1057
|
+
if (type === "test" || type === "story") continue;
|
|
997
1058
|
const parsed = parsedByPath.get(absPath);
|
|
998
1059
|
const name = parsed.name || nameFromFilename(absPath);
|
|
999
|
-
const
|
|
1000
|
-
const deep = extractDeep(absPath);
|
|
1001
|
-
nodes.push({
|
|
1002
|
-
id,
|
|
1003
|
-
type,
|
|
1004
|
-
name,
|
|
1005
|
-
route,
|
|
1006
|
-
exports: parsed.exports,
|
|
1007
|
-
elements: deep.elements,
|
|
1008
|
-
stateVars: deep.stateVars,
|
|
1009
|
-
conditions: deep.conditions,
|
|
1010
|
-
variables: deep.variables
|
|
1011
|
-
});
|
|
1060
|
+
const layer = CLASSIFICATION_TO_LAYER[type] ?? "ui";
|
|
1012
1061
|
nodeIdSet.add(id);
|
|
1013
|
-
|
|
1014
|
-
|
|
1062
|
+
if (layer === "api") {
|
|
1063
|
+
const methods = [];
|
|
1064
|
+
for (const exp of parsed.exports) {
|
|
1065
|
+
if (HTTP_METHODS.has(exp)) methods.push(exp);
|
|
1066
|
+
}
|
|
1067
|
+
const dbCalls = extractDbCallsTS(absPath);
|
|
1068
|
+
const authWrappers = extractAuthWrappersTS(absPath);
|
|
1069
|
+
const deep = extractDeep(absPath);
|
|
1070
|
+
const routePath = (0, import_node_fs5.existsSync)(apiDir) ? filePathToApiRoute(apiDir, absPath) : `/api/${id.replace(/\/route\.tsx?$/, "")}`;
|
|
1071
|
+
const mutations = dbCalls.filter((c) => c.isMutation);
|
|
1072
|
+
const mutates = mutations.length > 0;
|
|
1073
|
+
const authStrategy = [...authWrappers];
|
|
1074
|
+
apiNodes.push({
|
|
1075
|
+
id,
|
|
1076
|
+
type: "endpoint",
|
|
1077
|
+
name: routePath,
|
|
1078
|
+
layer: "api",
|
|
1079
|
+
path: routePath,
|
|
1080
|
+
methods,
|
|
1081
|
+
handler: id,
|
|
1082
|
+
mutates,
|
|
1083
|
+
auth: authStrategy.length > 0 ? authStrategy : ["public"],
|
|
1084
|
+
db_models: [...new Set(dbCalls.map((c) => c.model))],
|
|
1085
|
+
db_operations: [...new Set(dbCalls.map((c) => `${c.model}.${c.method}`))],
|
|
1086
|
+
conditions: deep.conditions,
|
|
1087
|
+
variables: deep.variables,
|
|
1088
|
+
responses: deep.responses,
|
|
1089
|
+
params: deep.params,
|
|
1090
|
+
_dbCalls: dbCalls
|
|
1091
|
+
// temp: used for cross-ref building below
|
|
1092
|
+
});
|
|
1093
|
+
} else {
|
|
1094
|
+
const route = extractRoute(id);
|
|
1095
|
+
const deep = extractDeep(absPath);
|
|
1096
|
+
uiNodes.push({
|
|
1097
|
+
id,
|
|
1098
|
+
type,
|
|
1099
|
+
name,
|
|
1100
|
+
layer: "ui",
|
|
1101
|
+
route,
|
|
1102
|
+
exports: parsed.exports,
|
|
1103
|
+
elements: deep.elements,
|
|
1104
|
+
stateVars: deep.stateVars,
|
|
1105
|
+
conditions: deep.conditions,
|
|
1106
|
+
variables: deep.variables
|
|
1107
|
+
});
|
|
1108
|
+
if (route) routeToNodeId.set(route, id);
|
|
1109
|
+
}
|
|
1015
1110
|
}
|
|
1016
|
-
const
|
|
1017
|
-
const
|
|
1111
|
+
const uiEdges = [];
|
|
1112
|
+
const uiFlagged = [];
|
|
1018
1113
|
for (const absPath of fileSet) {
|
|
1019
|
-
const
|
|
1114
|
+
const id = toNodeId(srcDir, absPath);
|
|
1115
|
+
if (!nodeIdSet.has(id)) continue;
|
|
1020
1116
|
const parsed = parsedByPath.get(absPath);
|
|
1021
1117
|
const { edges, flagged } = extractEdges(
|
|
1022
1118
|
srcDir,
|
|
1023
1119
|
absPath,
|
|
1024
|
-
|
|
1120
|
+
id,
|
|
1025
1121
|
parsed,
|
|
1026
1122
|
nodeIdSet,
|
|
1027
|
-
nodeTypeMap,
|
|
1028
1123
|
barrelMaps,
|
|
1029
1124
|
routeToNodeId
|
|
1030
1125
|
);
|
|
1031
|
-
|
|
1032
|
-
|
|
1126
|
+
uiEdges.push(...edges);
|
|
1127
|
+
uiFlagged.push(...flagged);
|
|
1033
1128
|
}
|
|
1034
1129
|
const fetchCallEntries = [];
|
|
1035
1130
|
for (const absPath of fileSet) {
|
|
1036
1131
|
const sourceId = toNodeId(srcDir, absPath);
|
|
1132
|
+
if (!nodeIdSet.has(sourceId)) continue;
|
|
1037
1133
|
const parsed = parsedByPath.get(absPath);
|
|
1038
1134
|
if (parsed.fetchCalls.length === 0) continue;
|
|
1039
1135
|
fetchCallEntries.push({
|
|
@@ -1048,7 +1144,7 @@ function generate(rootDir) {
|
|
|
1048
1144
|
});
|
|
1049
1145
|
}
|
|
1050
1146
|
const externalScanned = new Set(allDiscovered.map((f) => f.replace(/\\/g, "/")));
|
|
1051
|
-
const
|
|
1147
|
+
const IGNORE_DIRS2 = /* @__PURE__ */ new Set([
|
|
1052
1148
|
"node_modules",
|
|
1053
1149
|
".next",
|
|
1054
1150
|
"dist",
|
|
@@ -1061,7 +1157,7 @@ function generate(rootDir) {
|
|
|
1061
1157
|
"out",
|
|
1062
1158
|
".vercel"
|
|
1063
1159
|
]);
|
|
1064
|
-
const externalCandidates = walkWithIgnore(rootDir, [".ts", ".tsx"],
|
|
1160
|
+
const externalCandidates = walkWithIgnore(rootDir, [".ts", ".tsx"], IGNORE_DIRS2);
|
|
1065
1161
|
for (const absPath of externalCandidates) {
|
|
1066
1162
|
const normalized = absPath.replace(/\\/g, "/");
|
|
1067
1163
|
if (externalScanned.has(normalized)) continue;
|
|
@@ -1071,11 +1167,11 @@ function generate(rootDir) {
|
|
|
1071
1167
|
} catch {
|
|
1072
1168
|
continue;
|
|
1073
1169
|
}
|
|
1074
|
-
const externalId = (0,
|
|
1170
|
+
const externalId = (0, import_node_path5.relative)(rootDir, absPath).replace(/\\/g, "/");
|
|
1075
1171
|
const edgesFromThis = [];
|
|
1076
1172
|
const seen = /* @__PURE__ */ new Set();
|
|
1077
1173
|
for (const imp of parsed.imports) {
|
|
1078
|
-
const { specifier,
|
|
1174
|
+
const { specifier, names } = imp;
|
|
1079
1175
|
let resolved = null;
|
|
1080
1176
|
if (specifier.startsWith("@/")) {
|
|
1081
1177
|
const relToSrc = specifier.slice(2);
|
|
@@ -1101,25 +1197,52 @@ function generate(rootDir) {
|
|
|
1101
1197
|
const targetId = toNodeId(srcDir, resolved);
|
|
1102
1198
|
if (!nodeIdSet.has(targetId)) continue;
|
|
1103
1199
|
if (targetId.endsWith("/index.ts") || targetId.endsWith("/index.tsx")) continue;
|
|
1104
|
-
const key = `${externalId}\u2192${targetId}
|
|
1200
|
+
const key = `${externalId}\u2192${targetId}`;
|
|
1105
1201
|
if (seen.has(key)) continue;
|
|
1106
1202
|
seen.add(key);
|
|
1107
1203
|
edgesFromThis.push({ source: externalId, target: targetId, type: "imports" });
|
|
1108
1204
|
}
|
|
1109
1205
|
if (edgesFromThis.length === 0) continue;
|
|
1110
|
-
|
|
1206
|
+
uiNodes.push({
|
|
1111
1207
|
id: externalId,
|
|
1112
1208
|
type: "external",
|
|
1113
1209
|
name: parsed.name || nameFromFilename(absPath),
|
|
1210
|
+
layer: "ui",
|
|
1114
1211
|
route: null,
|
|
1115
1212
|
exports: parsed.exports
|
|
1116
1213
|
});
|
|
1117
1214
|
nodeIdSet.add(externalId);
|
|
1118
|
-
|
|
1119
|
-
|
|
1215
|
+
uiEdges.push(...edgesFromThis);
|
|
1216
|
+
}
|
|
1217
|
+
const apiCrossRefs = [];
|
|
1218
|
+
for (const node of apiNodes) {
|
|
1219
|
+
const dbCalls = node._dbCalls;
|
|
1220
|
+
if (!dbCalls) continue;
|
|
1221
|
+
const seenModels = /* @__PURE__ */ new Set();
|
|
1222
|
+
for (const call of dbCalls) {
|
|
1223
|
+
if (seenModels.has(call.model)) continue;
|
|
1224
|
+
seenModels.add(call.model);
|
|
1225
|
+
apiCrossRefs.push({
|
|
1226
|
+
source: node.id,
|
|
1227
|
+
target: camelToPascal(call.model),
|
|
1228
|
+
type: call.isMutation ? "mutates" : "reads",
|
|
1229
|
+
layer: "db"
|
|
1230
|
+
});
|
|
1231
|
+
}
|
|
1232
|
+
delete node._dbCalls;
|
|
1233
|
+
}
|
|
1234
|
+
const apiNodeIds = new Set(apiNodes.map((n) => n.id));
|
|
1235
|
+
const apiEdges = [];
|
|
1236
|
+
const uiOnlyEdges = [];
|
|
1237
|
+
for (const edge of uiEdges) {
|
|
1238
|
+
if (apiNodeIds.has(edge.source)) {
|
|
1239
|
+
apiEdges.push(edge);
|
|
1240
|
+
} else {
|
|
1241
|
+
uiOnlyEdges.push(edge);
|
|
1242
|
+
}
|
|
1120
1243
|
}
|
|
1121
1244
|
const flaggedSet = /* @__PURE__ */ new Set();
|
|
1122
|
-
const dedupedFlagged =
|
|
1245
|
+
const dedupedFlagged = uiFlagged.filter((f) => {
|
|
1123
1246
|
const key = `${f.source}\u2192${f.target}\u2192${f.label}`;
|
|
1124
1247
|
if (flaggedSet.has(key)) return false;
|
|
1125
1248
|
flaggedSet.add(key);
|
|
@@ -1136,10 +1259,12 @@ function generate(rootDir) {
|
|
|
1136
1259
|
hook: 7,
|
|
1137
1260
|
lib: 8
|
|
1138
1261
|
};
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1262
|
+
uiNodes.sort((a, b) => (typePriority[a.type] ?? 99) - (typePriority[b.type] ?? 99) || a.id.localeCompare(b.id));
|
|
1263
|
+
uiOnlyEdges.sort((a, b) => a.source.localeCompare(b.source) || a.target.localeCompare(b.target));
|
|
1264
|
+
apiNodes.sort((a, b) => (a.path ?? "").localeCompare(b.path ?? ""));
|
|
1265
|
+
apiCrossRefs.sort((a, b) => a.source.localeCompare(b.source) || a.target.localeCompare(b.target));
|
|
1266
|
+
const byType = (t) => uiNodes.filter((n) => n.type === t).length;
|
|
1267
|
+
const uiStats = {
|
|
1143
1268
|
total_pages: byType("page"),
|
|
1144
1269
|
total_layouts: byType("layout"),
|
|
1145
1270
|
total_components: byType("component"),
|
|
@@ -1150,196 +1275,122 @@ function generate(rootDir) {
|
|
|
1150
1275
|
total_utils: byType("util"),
|
|
1151
1276
|
total_libs: byType("lib"),
|
|
1152
1277
|
total_external: byType("external"),
|
|
1153
|
-
total_edges:
|
|
1278
|
+
total_edges: uiOnlyEdges.length,
|
|
1154
1279
|
total_flagged: dedupedFlagged.length
|
|
1155
1280
|
};
|
|
1156
|
-
|
|
1281
|
+
const stripLayer = (nodes) => nodes.map(({ layer: _, ...rest }) => rest);
|
|
1282
|
+
const result = /* @__PURE__ */ new Map();
|
|
1283
|
+
result.set("ui", {
|
|
1157
1284
|
metadata: {
|
|
1158
1285
|
generated: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
|
|
1159
1286
|
scope: "main-app-only",
|
|
1160
1287
|
app_root: "src/",
|
|
1161
1288
|
layer: "ui",
|
|
1162
|
-
parser: "
|
|
1163
|
-
...
|
|
1289
|
+
parser: "typescript-project",
|
|
1290
|
+
...uiStats,
|
|
1164
1291
|
notes: "Auto-generated via TypeScript AST \u2014 edges derived from actual imports, renders from JSX usage, navigations from router/Link calls."
|
|
1165
1292
|
},
|
|
1166
|
-
nodes,
|
|
1167
|
-
edges:
|
|
1293
|
+
nodes: stripLayer(uiNodes),
|
|
1294
|
+
edges: uiOnlyEdges,
|
|
1168
1295
|
cross_refs: [],
|
|
1169
1296
|
contradictions: [],
|
|
1170
1297
|
warnings: [],
|
|
1171
1298
|
flagged_edges: dedupedFlagged,
|
|
1172
1299
|
patterns: {
|
|
1173
|
-
total_nodes:
|
|
1174
|
-
by_type:
|
|
1300
|
+
total_nodes: uiNodes.length,
|
|
1301
|
+
by_type: uiStats,
|
|
1175
1302
|
by_edge_type: {
|
|
1176
|
-
renders:
|
|
1177
|
-
imports:
|
|
1178
|
-
navigates:
|
|
1303
|
+
renders: uiOnlyEdges.filter((e) => e.type === "renders").length,
|
|
1304
|
+
imports: uiOnlyEdges.filter((e) => e.type === "imports").length,
|
|
1305
|
+
navigates: uiOnlyEdges.filter((e) => e.type === "navigates").length
|
|
1179
1306
|
},
|
|
1180
1307
|
fetch_calls: fetchCallEntries
|
|
1181
1308
|
}
|
|
1182
|
-
};
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
generate
|
|
1196
|
-
};
|
|
1197
|
-
}
|
|
1198
|
-
});
|
|
1199
|
-
|
|
1200
|
-
// src/server/graph/parsers/api/nextjs-routes.ts
|
|
1201
|
-
function walk2(dir) {
|
|
1202
|
-
const results = [];
|
|
1203
|
-
if (!(0, import_node_fs5.existsSync)(dir)) return results;
|
|
1204
|
-
for (const entry of (0, import_node_fs5.readdirSync)(dir, { withFileTypes: true })) {
|
|
1205
|
-
const full = (0, import_node_path5.join)(dir, entry.name);
|
|
1206
|
-
if (entry.isDirectory()) {
|
|
1207
|
-
results.push(...walk2(full));
|
|
1208
|
-
} else if (entry.name === "route.ts" || entry.name === "route.tsx") {
|
|
1209
|
-
results.push(full);
|
|
1210
|
-
}
|
|
1211
|
-
}
|
|
1212
|
-
return results;
|
|
1213
|
-
}
|
|
1214
|
-
function filePathToRoute(apiDir, absPath) {
|
|
1215
|
-
let route = "/" + (0, import_node_path5.relative)(apiDir, absPath).replace(/\\/g, "/").replace(/\/route\.tsx?$/, "");
|
|
1216
|
-
route = route.replace(/\[([^\]]+)\]/g, ":$1");
|
|
1217
|
-
route = route.replace(/\/+/g, "/");
|
|
1218
|
-
if (route === "/") return "/api";
|
|
1219
|
-
return "/api" + route;
|
|
1220
|
-
}
|
|
1221
|
-
function camelToPascal(s) {
|
|
1222
|
-
if (!s) return s;
|
|
1223
|
-
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
1224
|
-
}
|
|
1225
|
-
function detect2(rootDir) {
|
|
1226
|
-
return (0, import_node_fs5.existsSync)((0, import_node_path5.join)(rootDir, "src", "app", "api"));
|
|
1227
|
-
}
|
|
1228
|
-
function generate2(rootDir) {
|
|
1229
|
-
const apiDir = (0, import_node_path5.join)(rootDir, "src", "app", "api");
|
|
1230
|
-
const routeFiles = walk2(apiDir);
|
|
1231
|
-
const nodes = [];
|
|
1232
|
-
const edges = [];
|
|
1233
|
-
const crossRefs = [];
|
|
1234
|
-
const mutatorCount = {};
|
|
1235
|
-
const authUsage = {};
|
|
1236
|
-
let endpointsWithAuth = 0;
|
|
1237
|
-
let endpointsWithDbAccess = 0;
|
|
1238
|
-
for (const absPath of routeFiles) {
|
|
1239
|
-
const parsed = parseFileTS(absPath);
|
|
1240
|
-
const dbCalls = extractDbCallsTS(absPath);
|
|
1241
|
-
const authWrappers = extractAuthWrappersTS(absPath);
|
|
1242
|
-
const methods = [];
|
|
1243
|
-
for (const exp of parsed.exports) {
|
|
1244
|
-
if (HTTP_METHODS.has(exp)) methods.push(exp);
|
|
1245
|
-
}
|
|
1246
|
-
const routePath = filePathToRoute(apiDir, absPath);
|
|
1247
|
-
const relPath = (0, import_node_path5.relative)(rootDir, absPath).replace(/\\/g, "/");
|
|
1248
|
-
const mutations = dbCalls.filter((c) => c.isMutation);
|
|
1249
|
-
const reads = dbCalls.filter((c) => !c.isMutation);
|
|
1250
|
-
const mutates = mutations.length > 0;
|
|
1251
|
-
if (mutates) {
|
|
1252
|
-
for (const m of mutations) {
|
|
1253
|
-
mutatorCount[m.method] = (mutatorCount[m.method] ?? 0) + 1;
|
|
1309
|
+
});
|
|
1310
|
+
if (apiNodes.length > 0) {
|
|
1311
|
+
const mutatorNodes = apiNodes.filter((n) => n.mutates).length;
|
|
1312
|
+
const readOnlyNodes = apiNodes.filter((n) => !n.mutates).length;
|
|
1313
|
+
const authUsage = {};
|
|
1314
|
+
let endpointsWithAuth = 0;
|
|
1315
|
+
for (const n of apiNodes) {
|
|
1316
|
+
const auth = n.auth;
|
|
1317
|
+
if (auth.length > 0 && auth[0] !== "public") {
|
|
1318
|
+
endpointsWithAuth++;
|
|
1319
|
+
for (const w of auth) {
|
|
1320
|
+
authUsage[w] = (authUsage[w] ?? 0) + 1;
|
|
1321
|
+
}
|
|
1254
1322
|
}
|
|
1255
1323
|
}
|
|
1256
|
-
|
|
1257
|
-
const
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1324
|
+
const mutatorCount = {};
|
|
1325
|
+
for (const n of apiNodes) {
|
|
1326
|
+
const ops = n.db_operations;
|
|
1327
|
+
for (const op of ops) {
|
|
1328
|
+
const method = op.split(".")[1];
|
|
1329
|
+
if (method) mutatorCount[method] = (mutatorCount[method] ?? 0) + 1;
|
|
1330
|
+
}
|
|
1261
1331
|
}
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1332
|
+
result.set("api", {
|
|
1333
|
+
metadata: {
|
|
1334
|
+
generated: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
|
|
1335
|
+
scope: "main-app-only",
|
|
1336
|
+
stack: "nextjs-app-router",
|
|
1337
|
+
layer: "api",
|
|
1338
|
+
parser: "typescript-project",
|
|
1339
|
+
total_endpoints: apiNodes.length,
|
|
1340
|
+
total_methods: apiNodes.reduce((sum, n) => sum + n.methods.length, 0),
|
|
1341
|
+
endpoints_with_auth: endpointsWithAuth,
|
|
1342
|
+
endpoints_with_db_access: apiNodes.filter((n) => n.db_models.length > 0).length,
|
|
1343
|
+
mutator_endpoints: mutatorNodes,
|
|
1344
|
+
read_only_endpoints: readOnlyNodes
|
|
1345
|
+
},
|
|
1346
|
+
nodes: stripLayer(apiNodes),
|
|
1347
|
+
edges: apiEdges,
|
|
1348
|
+
cross_refs: apiCrossRefs,
|
|
1349
|
+
contradictions: [],
|
|
1350
|
+
warnings: [],
|
|
1351
|
+
flagged_edges: [],
|
|
1352
|
+
patterns: {
|
|
1353
|
+
total_endpoints: apiNodes.length,
|
|
1354
|
+
methods_breakdown: [...HTTP_METHODS].reduce((acc, m) => {
|
|
1355
|
+
acc[m] = apiNodes.filter((n) => n.methods.includes(m)).length;
|
|
1356
|
+
return acc;
|
|
1357
|
+
}, {}),
|
|
1358
|
+
auth_strategies: authUsage,
|
|
1359
|
+
mutation_operations: mutatorCount,
|
|
1360
|
+
mutator_vs_reader: { mutators: mutatorNodes, readers: readOnlyNodes }
|
|
1361
|
+
}
|
|
1281
1362
|
});
|
|
1282
|
-
const seenModels = /* @__PURE__ */ new Set();
|
|
1283
|
-
for (const call of dbCalls) {
|
|
1284
|
-
if (seenModels.has(call.model)) continue;
|
|
1285
|
-
seenModels.add(call.model);
|
|
1286
|
-
crossRefs.push({
|
|
1287
|
-
source: relPath,
|
|
1288
|
-
target: camelToPascal(call.model),
|
|
1289
|
-
type: call.isMutation ? "mutates" : "reads",
|
|
1290
|
-
layer: "db"
|
|
1291
|
-
});
|
|
1292
|
-
}
|
|
1293
1363
|
}
|
|
1294
|
-
|
|
1295
|
-
crossRefs.sort((a, b) => a.source.localeCompare(b.source) || a.target.localeCompare(b.target));
|
|
1296
|
-
const mutatorNodes = nodes.filter((n) => n.mutates).length;
|
|
1297
|
-
const readOnlyNodes = nodes.filter((n) => !n.mutates).length;
|
|
1298
|
-
return {
|
|
1299
|
-
metadata: {
|
|
1300
|
-
generated: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
|
|
1301
|
-
scope: "main-app-only",
|
|
1302
|
-
stack: "nextjs-app-router",
|
|
1303
|
-
layer: "api",
|
|
1304
|
-
parser: "nextjs-routes-ast",
|
|
1305
|
-
total_endpoints: nodes.length,
|
|
1306
|
-
total_methods: nodes.reduce((sum, n) => sum + n.methods.length, 0),
|
|
1307
|
-
endpoints_with_auth: endpointsWithAuth,
|
|
1308
|
-
endpoints_with_db_access: endpointsWithDbAccess,
|
|
1309
|
-
mutator_endpoints: mutatorNodes,
|
|
1310
|
-
read_only_endpoints: readOnlyNodes
|
|
1311
|
-
},
|
|
1312
|
-
nodes,
|
|
1313
|
-
edges,
|
|
1314
|
-
cross_refs: crossRefs,
|
|
1315
|
-
contradictions: [],
|
|
1316
|
-
warnings: [],
|
|
1317
|
-
flagged_edges: [],
|
|
1318
|
-
patterns: {
|
|
1319
|
-
total_endpoints: nodes.length,
|
|
1320
|
-
methods_breakdown: [...HTTP_METHODS].reduce((acc, m) => {
|
|
1321
|
-
acc[m] = nodes.filter((n) => n.methods.includes(m)).length;
|
|
1322
|
-
return acc;
|
|
1323
|
-
}, {}),
|
|
1324
|
-
auth_strategies: authUsage,
|
|
1325
|
-
mutation_operations: mutatorCount,
|
|
1326
|
-
mutator_vs_reader: { mutators: mutatorNodes, readers: readOnlyNodes }
|
|
1327
|
-
}
|
|
1328
|
-
};
|
|
1364
|
+
return result;
|
|
1329
1365
|
}
|
|
1330
|
-
var import_node_fs5, import_node_path5, HTTP_METHODS,
|
|
1331
|
-
var
|
|
1332
|
-
"src/server/graph/parsers/
|
|
1366
|
+
var import_node_fs5, import_node_path5, HTTP_METHODS, CLASSIFICATION_TO_LAYER, typescriptProjectParser;
|
|
1367
|
+
var init_typescript_project = __esm({
|
|
1368
|
+
"src/server/graph/parsers/ts/typescript-project.ts"() {
|
|
1333
1369
|
"use strict";
|
|
1334
1370
|
import_node_fs5 = require("node:fs");
|
|
1335
1371
|
import_node_path5 = require("node:path");
|
|
1372
|
+
init_config();
|
|
1373
|
+
init_resolve_paths();
|
|
1336
1374
|
init_ts_extractor();
|
|
1337
1375
|
HTTP_METHODS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"]);
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1376
|
+
CLASSIFICATION_TO_LAYER = {
|
|
1377
|
+
endpoint: "api",
|
|
1378
|
+
page: "ui",
|
|
1379
|
+
layout: "ui",
|
|
1380
|
+
component: "ui",
|
|
1381
|
+
ui: "ui",
|
|
1382
|
+
hook: "ui",
|
|
1383
|
+
context: "ui",
|
|
1384
|
+
config: "ui",
|
|
1385
|
+
lib: "ui",
|
|
1386
|
+
"mcp-tool": "ui",
|
|
1387
|
+
external: "ui"
|
|
1388
|
+
};
|
|
1389
|
+
typescriptProjectParser = {
|
|
1390
|
+
id: "typescript-project",
|
|
1391
|
+
layers: ["ui", "api"],
|
|
1392
|
+
detect,
|
|
1393
|
+
generate
|
|
1343
1394
|
};
|
|
1344
1395
|
}
|
|
1345
1396
|
});
|
|
@@ -1434,10 +1485,10 @@ function parseEnums(content) {
|
|
|
1434
1485
|
}
|
|
1435
1486
|
return nodes;
|
|
1436
1487
|
}
|
|
1437
|
-
function
|
|
1488
|
+
function detect2(rootDir) {
|
|
1438
1489
|
return (0, import_node_fs6.existsSync)((0, import_node_path6.join)(rootDir, "prisma", "schema.prisma"));
|
|
1439
1490
|
}
|
|
1440
|
-
function
|
|
1491
|
+
function generate2(rootDir) {
|
|
1441
1492
|
const schemaPath = (0, import_node_path6.join)(rootDir, "prisma", "schema.prisma");
|
|
1442
1493
|
const content = (0, import_node_fs6.readFileSync)(schemaPath, "utf-8");
|
|
1443
1494
|
const { nodes: modelNodes, relations } = parseModels(content);
|
|
@@ -1497,6 +1548,430 @@ var init_prisma_schema = __esm({
|
|
|
1497
1548
|
prismaSchemaParser = {
|
|
1498
1549
|
id: "prisma-schema",
|
|
1499
1550
|
layer: "db",
|
|
1551
|
+
detect: detect2,
|
|
1552
|
+
generate: generate2
|
|
1553
|
+
};
|
|
1554
|
+
}
|
|
1555
|
+
});
|
|
1556
|
+
|
|
1557
|
+
// src/server/graph/parsers/db/sql-migrations.ts
|
|
1558
|
+
function pgTypeToPrisma(pgType) {
|
|
1559
|
+
const upper = pgType.toUpperCase().trim();
|
|
1560
|
+
return PG_TO_PRISMA[upper] ?? upper;
|
|
1561
|
+
}
|
|
1562
|
+
function parseCreateTable(sql, state) {
|
|
1563
|
+
const re = /CREATE\s+TABLE\s+"(\w+)"\s*\(([\s\S]*?)\);/gi;
|
|
1564
|
+
let m;
|
|
1565
|
+
while ((m = re.exec(sql)) !== null) {
|
|
1566
|
+
const tableName = m[1];
|
|
1567
|
+
const body = m[2];
|
|
1568
|
+
const columns = /* @__PURE__ */ new Map();
|
|
1569
|
+
let primaryCol = null;
|
|
1570
|
+
for (const line of body.split("\n")) {
|
|
1571
|
+
const trimmed = line.trim().replace(/,\s*$/, "");
|
|
1572
|
+
if (!trimmed || trimmed.startsWith("--")) continue;
|
|
1573
|
+
const pkMatch = trimmed.match(/CONSTRAINT\s+"[^"]+"\s+PRIMARY\s+KEY\s*\("(\w+)"\)/i);
|
|
1574
|
+
if (pkMatch) {
|
|
1575
|
+
primaryCol = pkMatch[1];
|
|
1576
|
+
continue;
|
|
1577
|
+
}
|
|
1578
|
+
if (/^\s*CONSTRAINT\s/i.test(trimmed)) continue;
|
|
1579
|
+
const colMatch = trimmed.match(/^"(\w+)"\s+(.+)/);
|
|
1580
|
+
if (!colMatch) continue;
|
|
1581
|
+
const colName = colMatch[1];
|
|
1582
|
+
let rest = colMatch[2];
|
|
1583
|
+
const isNotNull = /\bNOT\s+NULL\b/i.test(rest);
|
|
1584
|
+
const defaultMatch = rest.match(/\bDEFAULT\s+(.+?)(?:\s*,?\s*$)/i);
|
|
1585
|
+
const defaultVal = defaultMatch ? defaultMatch[1].trim() : null;
|
|
1586
|
+
let colType = rest.replace(/\bNOT\s+NULL\b/gi, "").replace(/\bDEFAULT\s+.*/gi, "").trim().replace(/,\s*$/, "").trim();
|
|
1587
|
+
columns.set(colName, {
|
|
1588
|
+
name: colName,
|
|
1589
|
+
type: colType,
|
|
1590
|
+
nullable: !isNotNull,
|
|
1591
|
+
primary: false,
|
|
1592
|
+
unique: false,
|
|
1593
|
+
default: defaultVal
|
|
1594
|
+
});
|
|
1595
|
+
}
|
|
1596
|
+
if (primaryCol && columns.has(primaryCol)) {
|
|
1597
|
+
columns.get(primaryCol).primary = true;
|
|
1598
|
+
}
|
|
1599
|
+
state.tables.set(tableName, { name: tableName, columns });
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
function parseCreateEnum(sql, state) {
|
|
1603
|
+
const re = /CREATE\s+TYPE\s+"(\w+)"\s+AS\s+ENUM\s*\(([^)]+)\)/gi;
|
|
1604
|
+
let m;
|
|
1605
|
+
while ((m = re.exec(sql)) !== null) {
|
|
1606
|
+
const enumName = m[1];
|
|
1607
|
+
const valuesStr = m[2];
|
|
1608
|
+
const values = new Set(
|
|
1609
|
+
valuesStr.split(",").map((v) => v.trim().replace(/^'(.*)'$/, "$1")).filter(Boolean)
|
|
1610
|
+
);
|
|
1611
|
+
state.enums.set(enumName, { name: enumName, values });
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
function parseAlterTable(sql, state) {
|
|
1615
|
+
const addColRe = /ALTER\s+TABLE\s+"(\w+)"\s+ADD\s+COLUMN\s+"(\w+)"\s+(.+?);/gi;
|
|
1616
|
+
let m;
|
|
1617
|
+
while ((m = addColRe.exec(sql)) !== null) {
|
|
1618
|
+
const tableName = m[1];
|
|
1619
|
+
const colName = m[2];
|
|
1620
|
+
let rest = m[3];
|
|
1621
|
+
const table = state.tables.get(tableName);
|
|
1622
|
+
if (!table) continue;
|
|
1623
|
+
const isNotNull = /\bNOT\s+NULL\b/i.test(rest);
|
|
1624
|
+
const defaultMatch = rest.match(/\bDEFAULT\s+(.+?)$/i);
|
|
1625
|
+
const defaultVal = defaultMatch ? defaultMatch[1].trim() : null;
|
|
1626
|
+
let colType = rest.replace(/\bNOT\s+NULL\b/gi, "").replace(/\bDEFAULT\s+.*/gi, "").trim();
|
|
1627
|
+
table.columns.set(colName, {
|
|
1628
|
+
name: colName,
|
|
1629
|
+
type: colType,
|
|
1630
|
+
nullable: !isNotNull,
|
|
1631
|
+
primary: false,
|
|
1632
|
+
unique: false,
|
|
1633
|
+
default: defaultVal
|
|
1634
|
+
});
|
|
1635
|
+
}
|
|
1636
|
+
const dropColRe = /ALTER\s+TABLE\s+"(\w+)"\s+DROP\s+COLUMN\s+"(\w+)"/gi;
|
|
1637
|
+
while ((m = dropColRe.exec(sql)) !== null) {
|
|
1638
|
+
const table = state.tables.get(m[1]);
|
|
1639
|
+
if (table) table.columns.delete(m[2]);
|
|
1640
|
+
}
|
|
1641
|
+
const fkRe = /ALTER\s+TABLE\s+"(\w+)"\s+ADD\s+CONSTRAINT\s+"([^"]+)"\s+FOREIGN\s+KEY\s*\("(\w+)"\)\s+REFERENCES\s+"(\w+)"\("(\w+)"\)(?:\s+ON\s+DELETE\s+(\w+(?:\s+\w+)?))?/gi;
|
|
1642
|
+
while ((m = fkRe.exec(sql)) !== null) {
|
|
1643
|
+
state.fks.push({
|
|
1644
|
+
constraintName: m[2],
|
|
1645
|
+
sourceTable: m[1],
|
|
1646
|
+
sourceColumn: m[3],
|
|
1647
|
+
targetTable: m[4],
|
|
1648
|
+
targetColumn: m[5],
|
|
1649
|
+
onDelete: m[6] ?? null
|
|
1650
|
+
});
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
function parseAlterEnum(sql, state) {
|
|
1654
|
+
const re = /ALTER\s+TYPE\s+"(\w+)"\s+ADD\s+VALUE\s+'([^']+)'/gi;
|
|
1655
|
+
let m;
|
|
1656
|
+
while ((m = re.exec(sql)) !== null) {
|
|
1657
|
+
const en = state.enums.get(m[1]);
|
|
1658
|
+
if (en) en.values.add(m[2]);
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
function parseDropTable(sql, state) {
|
|
1662
|
+
const re = /DROP\s+TABLE\s+(?:IF\s+EXISTS\s+)?"(\w+)"/gi;
|
|
1663
|
+
let m;
|
|
1664
|
+
while ((m = re.exec(sql)) !== null) {
|
|
1665
|
+
state.tables.delete(m[1]);
|
|
1666
|
+
state.fks = state.fks.filter((fk) => fk.sourceTable !== m[1] && fk.targetTable !== m[1]);
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
function parseUniqueIndex(sql, state) {
|
|
1670
|
+
const re = /CREATE\s+UNIQUE\s+INDEX\s+"[^"]+"\s+ON\s+"(\w+)"\("(\w+)"\)/gi;
|
|
1671
|
+
let m;
|
|
1672
|
+
while ((m = re.exec(sql)) !== null) {
|
|
1673
|
+
const table = state.tables.get(m[1]);
|
|
1674
|
+
const col = table?.columns.get(m[2]);
|
|
1675
|
+
if (col) col.unique = true;
|
|
1676
|
+
if (!state.uniqueIndexes.has(m[1])) state.uniqueIndexes.set(m[1], /* @__PURE__ */ new Set());
|
|
1677
|
+
state.uniqueIndexes.get(m[1]).add(m[2]);
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
function parseMigrations(rootDir) {
|
|
1681
|
+
const migrationsDir = (0, import_node_path7.join)(rootDir, "prisma", "migrations");
|
|
1682
|
+
const state = {
|
|
1683
|
+
tables: /* @__PURE__ */ new Map(),
|
|
1684
|
+
enums: /* @__PURE__ */ new Map(),
|
|
1685
|
+
fks: [],
|
|
1686
|
+
uniqueIndexes: /* @__PURE__ */ new Map()
|
|
1687
|
+
};
|
|
1688
|
+
if (!(0, import_node_fs7.existsSync)(migrationsDir)) return state;
|
|
1689
|
+
const dirs = (0, import_node_fs7.readdirSync)(migrationsDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).sort();
|
|
1690
|
+
for (const dir of dirs) {
|
|
1691
|
+
const sqlPath = (0, import_node_path7.join)(migrationsDir, dir, "migration.sql");
|
|
1692
|
+
if (!(0, import_node_fs7.existsSync)(sqlPath)) continue;
|
|
1693
|
+
const sql = (0, import_node_fs7.readFileSync)(sqlPath, "utf-8");
|
|
1694
|
+
parseCreateEnum(sql, state);
|
|
1695
|
+
parseCreateTable(sql, state);
|
|
1696
|
+
parseAlterTable(sql, state);
|
|
1697
|
+
parseAlterEnum(sql, state);
|
|
1698
|
+
parseDropTable(sql, state);
|
|
1699
|
+
parseUniqueIndex(sql, state);
|
|
1700
|
+
}
|
|
1701
|
+
return state;
|
|
1702
|
+
}
|
|
1703
|
+
function loadPrismaState(rootDir) {
|
|
1704
|
+
const schemaPath = (0, import_node_path7.join)(rootDir, "prisma", "schema.prisma");
|
|
1705
|
+
if (!(0, import_node_fs7.existsSync)(schemaPath)) return null;
|
|
1706
|
+
const content = (0, import_node_fs7.readFileSync)(schemaPath, "utf-8");
|
|
1707
|
+
const tables = /* @__PURE__ */ new Map();
|
|
1708
|
+
const enums = /* @__PURE__ */ new Map();
|
|
1709
|
+
const relations = [];
|
|
1710
|
+
const modelRe = /model\s+(\w+)\s*\{([^}]+)\}/g;
|
|
1711
|
+
let m;
|
|
1712
|
+
while ((m = modelRe.exec(content)) !== null) {
|
|
1713
|
+
const modelName = m[1];
|
|
1714
|
+
const body = m[2];
|
|
1715
|
+
const cols = [];
|
|
1716
|
+
for (const line of body.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("//") && !l.startsWith("@@"))) {
|
|
1717
|
+
const fm = line.match(/^(\w+)\s+(\S+)(.*)/);
|
|
1718
|
+
if (!fm) continue;
|
|
1719
|
+
const fieldName = fm[1];
|
|
1720
|
+
const fieldType = fm[2];
|
|
1721
|
+
const rest = fm[3] ?? "";
|
|
1722
|
+
const baseType = fieldType.replace(/[?\[\]]/g, "");
|
|
1723
|
+
const isRelation = rest.includes("@relation");
|
|
1724
|
+
if (isRelation) {
|
|
1725
|
+
const relArgs = rest.match(/@relation\(([^)]*)\)/)?.[1] ?? "";
|
|
1726
|
+
const fieldsMatch = relArgs.match(/fields:\s*\[([^\]]+)\]/);
|
|
1727
|
+
if (fieldsMatch) {
|
|
1728
|
+
relations.push({ source: modelName, target: baseType, fk: fieldsMatch[1].trim() });
|
|
1729
|
+
}
|
|
1730
|
+
continue;
|
|
1731
|
+
}
|
|
1732
|
+
if (fieldType.endsWith("[]") || fieldType.endsWith("?") && content.includes(`model ${baseType}`)) {
|
|
1733
|
+
if (new RegExp(`model\\s+${baseType}\\s*\\{`).test(content)) continue;
|
|
1734
|
+
}
|
|
1735
|
+
cols.push({
|
|
1736
|
+
name: fieldName,
|
|
1737
|
+
type: fieldType.replace("?", ""),
|
|
1738
|
+
nullable: fieldType.endsWith("?") || fieldType.includes("?"),
|
|
1739
|
+
primary: rest.includes("@id"),
|
|
1740
|
+
unique: rest.includes("@unique")
|
|
1741
|
+
});
|
|
1742
|
+
}
|
|
1743
|
+
tables.set(modelName, { columns: cols });
|
|
1744
|
+
}
|
|
1745
|
+
const enumRe = /enum\s+(\w+)\s*\{([^}]+)\}/g;
|
|
1746
|
+
while ((m = enumRe.exec(content)) !== null) {
|
|
1747
|
+
const values = m[2].split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("//"));
|
|
1748
|
+
enums.set(m[1], values);
|
|
1749
|
+
}
|
|
1750
|
+
return { tables, enums, relations };
|
|
1751
|
+
}
|
|
1752
|
+
function verify(sqlState, prisma) {
|
|
1753
|
+
const contradictions = [];
|
|
1754
|
+
const flaggedEdges = [];
|
|
1755
|
+
for (const [name] of sqlState.tables) {
|
|
1756
|
+
if (!prisma.tables.has(name)) {
|
|
1757
|
+
contradictions.push({
|
|
1758
|
+
entity: name,
|
|
1759
|
+
source_a: "sql-migrations",
|
|
1760
|
+
source_b: "prisma-schema",
|
|
1761
|
+
detail: `Table "${name}" exists in migrations but not in schema.prisma`
|
|
1762
|
+
});
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
for (const [name] of prisma.tables) {
|
|
1766
|
+
if (!sqlState.tables.has(name)) {
|
|
1767
|
+
contradictions.push({
|
|
1768
|
+
entity: name,
|
|
1769
|
+
source_a: "prisma-schema",
|
|
1770
|
+
source_b: "sql-migrations",
|
|
1771
|
+
detail: `Table "${name}" in schema.prisma has no CREATE TABLE in migrations`
|
|
1772
|
+
});
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
for (const [tableName, prismaTable] of prisma.tables) {
|
|
1776
|
+
const sqlTable = sqlState.tables.get(tableName);
|
|
1777
|
+
if (!sqlTable) continue;
|
|
1778
|
+
for (const prismaCol of prismaTable.columns) {
|
|
1779
|
+
const sqlCol = sqlTable.columns.get(prismaCol.name);
|
|
1780
|
+
if (!sqlCol) {
|
|
1781
|
+
contradictions.push({
|
|
1782
|
+
entity: `${tableName}.${prismaCol.name}`,
|
|
1783
|
+
source_a: "prisma-schema",
|
|
1784
|
+
source_b: "sql-migrations",
|
|
1785
|
+
detail: `Column "${tableName}.${prismaCol.name}" in schema.prisma but not in migrations`
|
|
1786
|
+
});
|
|
1787
|
+
continue;
|
|
1788
|
+
}
|
|
1789
|
+
const mappedSqlType = pgTypeToPrisma(sqlCol.type);
|
|
1790
|
+
const prismaBaseType = prismaCol.type.replace(/[?\[\]]/g, "");
|
|
1791
|
+
if (mappedSqlType !== prismaBaseType && mappedSqlType !== prismaCol.type) {
|
|
1792
|
+
if (!sqlState.enums.has(prismaBaseType)) {
|
|
1793
|
+
contradictions.push({
|
|
1794
|
+
entity: `${tableName}.${prismaCol.name}`,
|
|
1795
|
+
source_a: "sql-migrations",
|
|
1796
|
+
source_b: "prisma-schema",
|
|
1797
|
+
detail: `Column "${tableName}.${prismaCol.name}": SQL type "${sqlCol.type}" (\u2192${mappedSqlType}) vs Prisma type "${prismaCol.type}"`
|
|
1798
|
+
});
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
if (sqlCol.nullable !== prismaCol.nullable) {
|
|
1802
|
+
contradictions.push({
|
|
1803
|
+
entity: `${tableName}.${prismaCol.name}`,
|
|
1804
|
+
source_a: "sql-migrations",
|
|
1805
|
+
source_b: "prisma-schema",
|
|
1806
|
+
detail: `Column "${tableName}.${prismaCol.name}": ${sqlCol.nullable ? "nullable" : "NOT NULL"} in SQL vs ${prismaCol.nullable ? "optional" : "required"} in Prisma`
|
|
1807
|
+
});
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
for (const [colName] of sqlTable.columns) {
|
|
1811
|
+
const inPrisma = prismaTable.columns.some((c) => c.name === colName);
|
|
1812
|
+
if (!inPrisma) {
|
|
1813
|
+
contradictions.push({
|
|
1814
|
+
entity: `${tableName}.${colName}`,
|
|
1815
|
+
source_a: "sql-migrations",
|
|
1816
|
+
source_b: "prisma-schema",
|
|
1817
|
+
detail: `Column "${tableName}.${colName}" in migrations but not in schema.prisma`
|
|
1818
|
+
});
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
for (const [name, sqlEnum] of sqlState.enums) {
|
|
1823
|
+
const prismaValues = prisma.enums.get(name);
|
|
1824
|
+
if (!prismaValues) {
|
|
1825
|
+
contradictions.push({
|
|
1826
|
+
entity: name,
|
|
1827
|
+
source_a: "sql-migrations",
|
|
1828
|
+
source_b: "prisma-schema",
|
|
1829
|
+
detail: `Enum "${name}" exists in migrations but not in schema.prisma`
|
|
1830
|
+
});
|
|
1831
|
+
continue;
|
|
1832
|
+
}
|
|
1833
|
+
const prismaSet = new Set(prismaValues);
|
|
1834
|
+
for (const val of sqlEnum.values) {
|
|
1835
|
+
if (!prismaSet.has(val)) {
|
|
1836
|
+
contradictions.push({
|
|
1837
|
+
entity: `${name}.${val}`,
|
|
1838
|
+
source_a: "sql-migrations",
|
|
1839
|
+
source_b: "prisma-schema",
|
|
1840
|
+
detail: `Enum "${name}": value "${val}" in migrations but not in schema.prisma`
|
|
1841
|
+
});
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
for (const val of prismaValues) {
|
|
1845
|
+
if (!sqlEnum.values.has(val)) {
|
|
1846
|
+
contradictions.push({
|
|
1847
|
+
entity: `${name}.${val}`,
|
|
1848
|
+
source_a: "prisma-schema",
|
|
1849
|
+
source_b: "sql-migrations",
|
|
1850
|
+
detail: `Enum "${name}": value "${val}" in schema.prisma but not in migrations`
|
|
1851
|
+
});
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
const prismaFkSet = new Set(prisma.relations.map((r) => `${r.source}|${r.target}|${r.fk}`));
|
|
1856
|
+
for (const fk of sqlState.fks) {
|
|
1857
|
+
const key = `${fk.sourceTable}|${fk.targetTable}|${fk.sourceColumn}`;
|
|
1858
|
+
if (!prismaFkSet.has(key)) {
|
|
1859
|
+
flaggedEdges.push({
|
|
1860
|
+
source: fk.sourceTable,
|
|
1861
|
+
target: fk.targetTable,
|
|
1862
|
+
type: "belongs_to",
|
|
1863
|
+
label: `FK "${fk.constraintName}" (${fk.sourceColumn}\u2192${fk.targetTable}) in migrations but no @relation in schema.prisma`,
|
|
1864
|
+
confidence: "high"
|
|
1865
|
+
});
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
return { contradictions, flaggedEdges };
|
|
1869
|
+
}
|
|
1870
|
+
function detect3(rootDir) {
|
|
1871
|
+
const migrationsDir = (0, import_node_path7.join)(rootDir, "prisma", "migrations");
|
|
1872
|
+
if (!(0, import_node_fs7.existsSync)(migrationsDir)) return false;
|
|
1873
|
+
return (0, import_node_fs7.readdirSync)(migrationsDir, { withFileTypes: true }).some((d) => d.isDirectory() && (0, import_node_fs7.existsSync)((0, import_node_path7.join)(migrationsDir, d.name, "migration.sql")));
|
|
1874
|
+
}
|
|
1875
|
+
function generate3(rootDir) {
|
|
1876
|
+
const sqlState = parseMigrations(rootDir);
|
|
1877
|
+
const prisma = loadPrismaState(rootDir);
|
|
1878
|
+
const prismaTableIds = prisma ? new Set(prisma.tables.keys()) : /* @__PURE__ */ new Set();
|
|
1879
|
+
const prismaEnumIds = prisma ? new Set(prisma.enums.keys()) : /* @__PURE__ */ new Set();
|
|
1880
|
+
const nodes = [];
|
|
1881
|
+
for (const [name, table] of sqlState.tables) {
|
|
1882
|
+
if (prismaTableIds.has(name)) continue;
|
|
1883
|
+
nodes.push({
|
|
1884
|
+
id: name,
|
|
1885
|
+
type: "table",
|
|
1886
|
+
name,
|
|
1887
|
+
source: "sql",
|
|
1888
|
+
columns: [...table.columns.values()].map((c) => ({
|
|
1889
|
+
name: c.name,
|
|
1890
|
+
type: c.type,
|
|
1891
|
+
primary: c.primary,
|
|
1892
|
+
unique: c.unique,
|
|
1893
|
+
nullable: c.nullable,
|
|
1894
|
+
default: c.default,
|
|
1895
|
+
isRelation: false,
|
|
1896
|
+
comment: null
|
|
1897
|
+
}))
|
|
1898
|
+
});
|
|
1899
|
+
}
|
|
1900
|
+
for (const [name, sqlEnum] of sqlState.enums) {
|
|
1901
|
+
if (prismaEnumIds.has(name)) continue;
|
|
1902
|
+
nodes.push({
|
|
1903
|
+
id: name,
|
|
1904
|
+
type: "enum",
|
|
1905
|
+
name,
|
|
1906
|
+
source: "sql",
|
|
1907
|
+
values: [...sqlEnum.values]
|
|
1908
|
+
});
|
|
1909
|
+
}
|
|
1910
|
+
const sqlOnlyTables = new Set(nodes.filter((n) => n.type === "table").map((n) => n.id));
|
|
1911
|
+
const edges = sqlState.fks.filter((fk) => sqlOnlyTables.has(fk.sourceTable)).map((fk) => ({
|
|
1912
|
+
source: fk.sourceTable,
|
|
1913
|
+
target: fk.targetTable,
|
|
1914
|
+
type: "belongs_to",
|
|
1915
|
+
fk: fk.sourceColumn,
|
|
1916
|
+
onDelete: fk.onDelete
|
|
1917
|
+
}));
|
|
1918
|
+
const { contradictions, flaggedEdges } = prisma ? verify(sqlState, prisma) : { contradictions: [], flaggedEdges: [] };
|
|
1919
|
+
return {
|
|
1920
|
+
metadata: {
|
|
1921
|
+
generated: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
|
|
1922
|
+
scope: "sql-migrations",
|
|
1923
|
+
source: "prisma/migrations/",
|
|
1924
|
+
layer: "db",
|
|
1925
|
+
sql_tables: sqlState.tables.size,
|
|
1926
|
+
sql_enums: sqlState.enums.size,
|
|
1927
|
+
sql_fks: sqlState.fks.length,
|
|
1928
|
+
additive_nodes: nodes.length,
|
|
1929
|
+
contradictions_found: contradictions.length,
|
|
1930
|
+
flagged_edges_found: flaggedEdges.length
|
|
1931
|
+
},
|
|
1932
|
+
nodes,
|
|
1933
|
+
edges,
|
|
1934
|
+
cross_refs: [],
|
|
1935
|
+
contradictions,
|
|
1936
|
+
warnings: [],
|
|
1937
|
+
flagged_edges: flaggedEdges
|
|
1938
|
+
};
|
|
1939
|
+
}
|
|
1940
|
+
var import_node_fs7, import_node_path7, PG_TO_PRISMA, sqlMigrationsParser;
|
|
1941
|
+
var init_sql_migrations = __esm({
|
|
1942
|
+
"src/server/graph/parsers/db/sql-migrations.ts"() {
|
|
1943
|
+
"use strict";
|
|
1944
|
+
import_node_fs7 = require("node:fs");
|
|
1945
|
+
import_node_path7 = require("node:path");
|
|
1946
|
+
PG_TO_PRISMA = {
|
|
1947
|
+
"TEXT": "String",
|
|
1948
|
+
"VARCHAR": "String",
|
|
1949
|
+
"CHAR": "String",
|
|
1950
|
+
"INTEGER": "Int",
|
|
1951
|
+
"INT": "Int",
|
|
1952
|
+
"SMALLINT": "Int",
|
|
1953
|
+
"BIGINT": "BigInt",
|
|
1954
|
+
"SERIAL": "Int",
|
|
1955
|
+
"BOOLEAN": "Boolean",
|
|
1956
|
+
"BOOL": "Boolean",
|
|
1957
|
+
"TIMESTAMP(3)": "DateTime",
|
|
1958
|
+
"TIMESTAMP": "DateTime",
|
|
1959
|
+
"TIMESTAMPTZ": "DateTime",
|
|
1960
|
+
"DATE": "DateTime",
|
|
1961
|
+
"DOUBLE PRECISION": "Float",
|
|
1962
|
+
"FLOAT": "Float",
|
|
1963
|
+
"REAL": "Float",
|
|
1964
|
+
"DECIMAL": "Decimal",
|
|
1965
|
+
"NUMERIC": "Decimal",
|
|
1966
|
+
"JSONB": "Json",
|
|
1967
|
+
"JSON": "Json",
|
|
1968
|
+
"BYTEA": "Bytes",
|
|
1969
|
+
"UUID": "String",
|
|
1970
|
+
"TEXT[]": "String[]"
|
|
1971
|
+
};
|
|
1972
|
+
sqlMigrationsParser = {
|
|
1973
|
+
id: "sql-migrations",
|
|
1974
|
+
layer: "db",
|
|
1500
1975
|
detect: detect3,
|
|
1501
1976
|
generate: generate3
|
|
1502
1977
|
};
|
|
@@ -1728,36 +2203,36 @@ var init_fetch_resolver = __esm({
|
|
|
1728
2203
|
});
|
|
1729
2204
|
|
|
1730
2205
|
// src/server/graph/parsers/crosslayer/api-annotations.ts
|
|
1731
|
-
function
|
|
1732
|
-
if (!(0,
|
|
2206
|
+
function walk2(dir, exts) {
|
|
2207
|
+
if (!(0, import_node_fs8.existsSync)(dir)) return [];
|
|
1733
2208
|
const results = [];
|
|
1734
|
-
for (const entry of (0,
|
|
2209
|
+
for (const entry of (0, import_node_fs8.readdirSync)(dir, { withFileTypes: true })) {
|
|
1735
2210
|
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
1736
|
-
const full = (0,
|
|
2211
|
+
const full = (0, import_node_path8.join)(dir, entry.name);
|
|
1737
2212
|
if (entry.isDirectory()) {
|
|
1738
|
-
results.push(...
|
|
1739
|
-
} else if (exts.includes((0,
|
|
2213
|
+
results.push(...walk2(full, exts));
|
|
2214
|
+
} else if (exts.includes((0, import_node_path8.extname)(entry.name))) {
|
|
1740
2215
|
results.push(full);
|
|
1741
2216
|
}
|
|
1742
2217
|
}
|
|
1743
2218
|
return results;
|
|
1744
2219
|
}
|
|
1745
2220
|
function toNodeId2(srcDir, absPath) {
|
|
1746
|
-
return (0,
|
|
2221
|
+
return (0, import_node_path8.relative)(srcDir, absPath).replace(/\\/g, "/");
|
|
1747
2222
|
}
|
|
1748
|
-
var
|
|
2223
|
+
var import_node_fs8, import_node_path8, API_ANNOTATION_RE, apiAnnotationsParser;
|
|
1749
2224
|
var init_api_annotations = __esm({
|
|
1750
2225
|
"src/server/graph/parsers/crosslayer/api-annotations.ts"() {
|
|
1751
2226
|
"use strict";
|
|
1752
|
-
|
|
1753
|
-
|
|
2227
|
+
import_node_fs8 = require("node:fs");
|
|
2228
|
+
import_node_path8 = require("node:path");
|
|
1754
2229
|
init_api_route_matching();
|
|
1755
2230
|
API_ANNOTATION_RE = /@api\s+(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+(\/\S+)/g;
|
|
1756
2231
|
apiAnnotationsParser = {
|
|
1757
2232
|
id: "api-annotations",
|
|
1758
2233
|
layer: "crosslayer",
|
|
1759
2234
|
detect(rootDir) {
|
|
1760
|
-
return (0,
|
|
2235
|
+
return (0, import_node_fs8.existsSync)((0, import_node_path8.join)(rootDir, "src"));
|
|
1761
2236
|
},
|
|
1762
2237
|
generate(rootDir, layerOutputs) {
|
|
1763
2238
|
const apiOutput = layerOutputs.get("api");
|
|
@@ -1768,13 +2243,13 @@ var init_api_annotations = __esm({
|
|
|
1768
2243
|
const uiNodeIds = new Set(uiOutput?.nodes.map((n) => n.id) ?? []);
|
|
1769
2244
|
const apiRoutes = loadApiRoutesFromOutput(apiOutput);
|
|
1770
2245
|
const apiPathMap = buildApiPathMap(apiRoutes);
|
|
1771
|
-
const srcDir = (0,
|
|
1772
|
-
const files =
|
|
2246
|
+
const srcDir = (0, import_node_path8.join)(rootDir, "src");
|
|
2247
|
+
const files = walk2(srcDir, [".ts", ".tsx"]);
|
|
1773
2248
|
const crossRefs = [];
|
|
1774
2249
|
const flaggedEdges = [];
|
|
1775
2250
|
const seen = /* @__PURE__ */ new Set();
|
|
1776
2251
|
for (const absPath of files) {
|
|
1777
|
-
const content = (0,
|
|
2252
|
+
const content = (0, import_node_fs8.readFileSync)(absPath, "utf-8");
|
|
1778
2253
|
const sourceId = toNodeId2(srcDir, absPath);
|
|
1779
2254
|
if (!uiNodeIds.has(sourceId)) continue;
|
|
1780
2255
|
let match;
|
|
@@ -1820,73 +2295,741 @@ var init_api_annotations = __esm({
|
|
|
1820
2295
|
});
|
|
1821
2296
|
|
|
1822
2297
|
// src/server/graph/parsers/crosslayer/url-literal-scanner.ts
|
|
1823
|
-
function
|
|
1824
|
-
if (!(0,
|
|
2298
|
+
function walk3(dir, exts) {
|
|
2299
|
+
if (!(0, import_node_fs9.existsSync)(dir)) return [];
|
|
1825
2300
|
const results = [];
|
|
1826
|
-
for (const entry of (0,
|
|
2301
|
+
for (const entry of (0, import_node_fs9.readdirSync)(dir, { withFileTypes: true })) {
|
|
1827
2302
|
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
1828
|
-
const full = (0,
|
|
2303
|
+
const full = (0, import_node_path9.join)(dir, entry.name);
|
|
1829
2304
|
if (entry.isDirectory()) {
|
|
1830
|
-
results.push(...
|
|
1831
|
-
} else if (exts.includes((0,
|
|
2305
|
+
results.push(...walk3(full, exts));
|
|
2306
|
+
} else if (exts.includes((0, import_node_path9.extname)(entry.name))) {
|
|
1832
2307
|
results.push(full);
|
|
1833
2308
|
}
|
|
1834
2309
|
}
|
|
1835
|
-
return results;
|
|
1836
|
-
}
|
|
1837
|
-
function toNodeId3(srcDir, absPath) {
|
|
1838
|
-
return (0,
|
|
2310
|
+
return results;
|
|
2311
|
+
}
|
|
2312
|
+
function toNodeId3(srcDir, absPath) {
|
|
2313
|
+
return (0, import_node_path9.relative)(srcDir, absPath).replace(/\\/g, "/");
|
|
2314
|
+
}
|
|
2315
|
+
var import_node_fs9, import_node_path9, URL_LITERAL_RE, urlLiteralScannerParser;
|
|
2316
|
+
var init_url_literal_scanner = __esm({
|
|
2317
|
+
"src/server/graph/parsers/crosslayer/url-literal-scanner.ts"() {
|
|
2318
|
+
"use strict";
|
|
2319
|
+
import_node_fs9 = require("node:fs");
|
|
2320
|
+
import_node_path9 = require("node:path");
|
|
2321
|
+
init_api_route_matching();
|
|
2322
|
+
init_config();
|
|
2323
|
+
init_resolve_paths();
|
|
2324
|
+
URL_LITERAL_RE = /['"`](\/api\/[^'"`\s]+?)['"`]/g;
|
|
2325
|
+
urlLiteralScannerParser = {
|
|
2326
|
+
id: "url-literal-scanner",
|
|
2327
|
+
layer: "crosslayer",
|
|
2328
|
+
detect(rootDir) {
|
|
2329
|
+
const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
|
|
2330
|
+
return paths !== null;
|
|
2331
|
+
},
|
|
2332
|
+
generate(rootDir, layerOutputs) {
|
|
2333
|
+
const apiOutput = layerOutputs.get("api");
|
|
2334
|
+
if (!apiOutput) {
|
|
2335
|
+
return { cross_refs: [], flagged_edges: [], warnings: [] };
|
|
2336
|
+
}
|
|
2337
|
+
const uiOutput = layerOutputs.get("ui");
|
|
2338
|
+
const uiNodeIds = new Set(uiOutput?.nodes.map((n) => n.id) ?? []);
|
|
2339
|
+
const apiRoutes = loadApiRoutesFromOutput(apiOutput);
|
|
2340
|
+
const apiPathMap = buildApiPathMap(apiRoutes);
|
|
2341
|
+
const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
|
|
2342
|
+
const srcDir = paths.srcDir;
|
|
2343
|
+
const clientDir = (0, import_node_path9.join)(srcDir, "client");
|
|
2344
|
+
const files = [
|
|
2345
|
+
...walk3(clientDir, [".ts", ".tsx"]),
|
|
2346
|
+
...walk3(paths.appDir, [".ts", ".tsx"])
|
|
2347
|
+
];
|
|
2348
|
+
const crossRefs = [];
|
|
2349
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2350
|
+
for (const absPath of files) {
|
|
2351
|
+
const sourceId = toNodeId3(srcDir, absPath);
|
|
2352
|
+
if (!uiNodeIds.has(sourceId)) continue;
|
|
2353
|
+
const content = (0, import_node_fs9.readFileSync)(absPath, "utf-8");
|
|
2354
|
+
let match;
|
|
2355
|
+
URL_LITERAL_RE.lastIndex = 0;
|
|
2356
|
+
while ((match = URL_LITERAL_RE.exec(content)) !== null) {
|
|
2357
|
+
const urlPath = match[1];
|
|
2358
|
+
const result = resolveUrlPath(urlPath, apiPathMap, apiRoutes);
|
|
2359
|
+
if (result.kind === "resolved" && result.nodeId) {
|
|
2360
|
+
const key = `${sourceId}|${result.nodeId}|references_api`;
|
|
2361
|
+
if (seen.has(key)) continue;
|
|
2362
|
+
seen.add(key);
|
|
2363
|
+
crossRefs.push({
|
|
2364
|
+
source: sourceId,
|
|
2365
|
+
target: result.nodeId,
|
|
2366
|
+
type: "references_api",
|
|
2367
|
+
layer: "api"
|
|
2368
|
+
});
|
|
2369
|
+
}
|
|
2370
|
+
}
|
|
2371
|
+
}
|
|
2372
|
+
return {
|
|
2373
|
+
cross_refs: crossRefs,
|
|
2374
|
+
flagged_edges: [],
|
|
2375
|
+
warnings: [],
|
|
2376
|
+
patterns: {
|
|
2377
|
+
url_literals_resolved: crossRefs.length
|
|
2378
|
+
}
|
|
2379
|
+
};
|
|
2380
|
+
}
|
|
2381
|
+
};
|
|
2382
|
+
}
|
|
2383
|
+
});
|
|
2384
|
+
|
|
2385
|
+
// src/server/graph/parsers/static/static-values.ts
|
|
2386
|
+
function tryLoadTreeSitter() {
|
|
2387
|
+
if (parseCode) return true;
|
|
2388
|
+
try {
|
|
2389
|
+
const extractor = (init_ts_extractor(), __toCommonJS(ts_extractor_exports));
|
|
2390
|
+
if (typeof extractor.parseCodeTS === "function") {
|
|
2391
|
+
parseCode = extractor.parseCodeTS;
|
|
2392
|
+
return true;
|
|
2393
|
+
}
|
|
2394
|
+
} catch {
|
|
2395
|
+
}
|
|
2396
|
+
return false;
|
|
2397
|
+
}
|
|
2398
|
+
function classifyScope(source, model) {
|
|
2399
|
+
if (source.includes("prisma/schema.prisma")) return "shared";
|
|
2400
|
+
if (source.includes("prisma/seed") && model) {
|
|
2401
|
+
if (SHARED_MODELS.has(model)) return "shared";
|
|
2402
|
+
if (DB_MODELS.has(model)) return "db";
|
|
2403
|
+
return "shared";
|
|
2404
|
+
}
|
|
2405
|
+
if (source.startsWith("src/client/") || source.startsWith("src/app/")) return "fe";
|
|
2406
|
+
if (source.startsWith("src/server/")) return "be";
|
|
2407
|
+
if (source.startsWith("src/config/")) return "be";
|
|
2408
|
+
if (source.startsWith("src/lib/")) return "shared";
|
|
2409
|
+
return "shared";
|
|
2410
|
+
}
|
|
2411
|
+
function extractEnumValues(rootDir) {
|
|
2412
|
+
const nodes = [];
|
|
2413
|
+
const edges = [];
|
|
2414
|
+
const schemaPaths = [
|
|
2415
|
+
(0, import_node_path10.join)(rootDir, "prisma", "schema.prisma"),
|
|
2416
|
+
(0, import_node_path10.join)(rootDir, "prisma", "schema")
|
|
2417
|
+
];
|
|
2418
|
+
let content = "";
|
|
2419
|
+
for (const p of schemaPaths) {
|
|
2420
|
+
if ((0, import_node_fs10.existsSync)(p)) {
|
|
2421
|
+
try {
|
|
2422
|
+
const stat = (0, import_node_fs10.statSync)(p);
|
|
2423
|
+
if (stat.isFile()) {
|
|
2424
|
+
content = (0, import_node_fs10.readFileSync)(p, "utf-8");
|
|
2425
|
+
} else if (stat.isDirectory()) {
|
|
2426
|
+
const files = (0, import_node_fs10.readdirSync)(p).filter((f) => f.endsWith(".prisma"));
|
|
2427
|
+
content = files.map((f) => (0, import_node_fs10.readFileSync)((0, import_node_path10.join)(p, f), "utf-8")).join("\n");
|
|
2428
|
+
}
|
|
2429
|
+
} catch {
|
|
2430
|
+
continue;
|
|
2431
|
+
}
|
|
2432
|
+
break;
|
|
2433
|
+
}
|
|
2434
|
+
}
|
|
2435
|
+
if (!content) return { nodes, edges };
|
|
2436
|
+
const enumRe = /enum\s+(\w+)\s*\{([^}]+)\}/g;
|
|
2437
|
+
let m;
|
|
2438
|
+
while ((m = enumRe.exec(content)) !== null) {
|
|
2439
|
+
const enumName = m[1];
|
|
2440
|
+
const body = m[2];
|
|
2441
|
+
const values = body.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("//"));
|
|
2442
|
+
const scope = classifyScope("prisma/schema.prisma");
|
|
2443
|
+
for (const val of values) {
|
|
2444
|
+
const nodeId = `${enumName}.${val}`;
|
|
2445
|
+
nodes.push({
|
|
2446
|
+
id: nodeId,
|
|
2447
|
+
type: "enum_value",
|
|
2448
|
+
name: val,
|
|
2449
|
+
parentEnum: enumName,
|
|
2450
|
+
value: val,
|
|
2451
|
+
scope,
|
|
2452
|
+
source: "prisma/schema.prisma"
|
|
2453
|
+
});
|
|
2454
|
+
edges.push({
|
|
2455
|
+
source: nodeId,
|
|
2456
|
+
target: `enum:${enumName}`,
|
|
2457
|
+
type: "member_of"
|
|
2458
|
+
});
|
|
2459
|
+
}
|
|
2460
|
+
nodes.push({
|
|
2461
|
+
id: `enum:${enumName}`,
|
|
2462
|
+
type: "enum_group",
|
|
2463
|
+
name: enumName,
|
|
2464
|
+
valueCount: values.length,
|
|
2465
|
+
scope,
|
|
2466
|
+
source: "prisma/schema.prisma"
|
|
2467
|
+
});
|
|
2468
|
+
}
|
|
2469
|
+
return { nodes, edges };
|
|
2470
|
+
}
|
|
2471
|
+
function extractPropsFromObjectNode(node) {
|
|
2472
|
+
const props = {};
|
|
2473
|
+
for (const child of node.namedChildren) {
|
|
2474
|
+
if (child.type !== "pair") continue;
|
|
2475
|
+
const keyNode = child.childForFieldName("key");
|
|
2476
|
+
const valNode = child.childForFieldName("value");
|
|
2477
|
+
if (!keyNode || !valNode) continue;
|
|
2478
|
+
const key = keyNode.type === "property_identifier" ? keyNode.text : keyNode.text.replace(/['"]/g, "");
|
|
2479
|
+
if (valNode.type === "string" || valNode.type === "template_string") {
|
|
2480
|
+
const fragment = valNode.namedChildren.find((c) => c.type === "string_fragment" || c.type === "template_substitution");
|
|
2481
|
+
props[key] = fragment ? fragment.text : valNode.text.replace(/^['"`]|['"`]$/g, "");
|
|
2482
|
+
} else if (valNode.type === "number") {
|
|
2483
|
+
props[key] = valNode.text;
|
|
2484
|
+
} else if (valNode.type === "true" || valNode.type === "false") {
|
|
2485
|
+
props[key] = valNode.text;
|
|
2486
|
+
}
|
|
2487
|
+
}
|
|
2488
|
+
return props;
|
|
2489
|
+
}
|
|
2490
|
+
function extractStringArrayFromNode(node) {
|
|
2491
|
+
const values = [];
|
|
2492
|
+
for (const child of node.namedChildren) {
|
|
2493
|
+
if (child.type === "string") {
|
|
2494
|
+
const frag = child.namedChildren.find((c) => c.type === "string_fragment");
|
|
2495
|
+
if (frag) values.push(frag.text);
|
|
2496
|
+
}
|
|
2497
|
+
}
|
|
2498
|
+
return values;
|
|
2499
|
+
}
|
|
2500
|
+
function findArrayDecl(root, varName) {
|
|
2501
|
+
function walk5(node) {
|
|
2502
|
+
if (node.type === "variable_declarator") {
|
|
2503
|
+
const nameNode = node.childForFieldName("name");
|
|
2504
|
+
const valueNode = node.childForFieldName("value");
|
|
2505
|
+
if (nameNode?.text === varName && valueNode?.type === "array") {
|
|
2506
|
+
return valueNode;
|
|
2507
|
+
}
|
|
2508
|
+
if (nameNode?.text === varName && valueNode?.type === "as_expression") {
|
|
2509
|
+
const inner = valueNode.namedChildren.find((c) => c.type === "array");
|
|
2510
|
+
if (inner) return inner;
|
|
2511
|
+
}
|
|
2512
|
+
}
|
|
2513
|
+
for (const child of node.namedChildren) {
|
|
2514
|
+
const found = walk5(child);
|
|
2515
|
+
if (found) return found;
|
|
2516
|
+
}
|
|
2517
|
+
return null;
|
|
2518
|
+
}
|
|
2519
|
+
return walk5(root);
|
|
2520
|
+
}
|
|
2521
|
+
function extractObjectPropsRegex(objStr) {
|
|
2522
|
+
const props = {};
|
|
2523
|
+
const propRe = /(\w+):\s*['"]([^'"]*)['"]/g;
|
|
2524
|
+
let m;
|
|
2525
|
+
while ((m = propRe.exec(objStr)) !== null) props[m[1]] = m[2];
|
|
2526
|
+
const numRe = /(\w+):\s*(-?\d+(?:\.\d+)?)\s*[,\n}]/g;
|
|
2527
|
+
while ((m = numRe.exec(objStr)) !== null) if (!props[m[1]]) props[m[1]] = m[2];
|
|
2528
|
+
return props;
|
|
2529
|
+
}
|
|
2530
|
+
function extractStringArrayRegex(arrStr) {
|
|
2531
|
+
return (arrStr.match(/'([^']+)'/g) ?? []).map((s) => s.replace(/'/g, ""));
|
|
2532
|
+
}
|
|
2533
|
+
function splitArrayObjectsRegex(arrayBody) {
|
|
2534
|
+
const objects = [];
|
|
2535
|
+
let depth = 0;
|
|
2536
|
+
let start = -1;
|
|
2537
|
+
for (let i = 0; i < arrayBody.length; i++) {
|
|
2538
|
+
if (arrayBody[i] === "{") {
|
|
2539
|
+
if (depth === 0) start = i;
|
|
2540
|
+
depth++;
|
|
2541
|
+
} else if (arrayBody[i] === "}") {
|
|
2542
|
+
depth--;
|
|
2543
|
+
if (depth === 0 && start >= 0) {
|
|
2544
|
+
objects.push(arrayBody.slice(start, i + 1));
|
|
2545
|
+
start = -1;
|
|
2546
|
+
}
|
|
2547
|
+
}
|
|
2548
|
+
}
|
|
2549
|
+
return objects;
|
|
2550
|
+
}
|
|
2551
|
+
function detectSeededArrays(content, sourceFile) {
|
|
2552
|
+
const results = [];
|
|
2553
|
+
const forOfRe = /for\s*\(\s*const\s+\w+\s+of\s+(\w+)\s*\)\s*\{/g;
|
|
2554
|
+
let fm;
|
|
2555
|
+
while ((fm = forOfRe.exec(content)) !== null) {
|
|
2556
|
+
const arrayName = fm[1];
|
|
2557
|
+
const lookahead = content.slice(fm.index + fm[0].length, fm.index + fm[0].length + 500);
|
|
2558
|
+
const prismaMatch = lookahead.match(/prisma\.(\w+)\.(create|upsert|update|createMany|findFirst)/);
|
|
2559
|
+
if (!prismaMatch) continue;
|
|
2560
|
+
results.push({ arrayName, prismaModel: prismaMatch[1], sourceFile });
|
|
2561
|
+
}
|
|
2562
|
+
return results;
|
|
2563
|
+
}
|
|
2564
|
+
function pickIdField(props) {
|
|
2565
|
+
for (const key of ["key", "slug", "id", "name", "templateId"]) {
|
|
2566
|
+
if (props[key]) return props[key];
|
|
2567
|
+
}
|
|
2568
|
+
return null;
|
|
2569
|
+
}
|
|
2570
|
+
function pickNameField(props) {
|
|
2571
|
+
for (const key of ["name", "slug", "key", "id"]) {
|
|
2572
|
+
if (props[key]) return props[key];
|
|
2573
|
+
}
|
|
2574
|
+
return null;
|
|
2575
|
+
}
|
|
2576
|
+
function modelToNodeType(model) {
|
|
2577
|
+
return `seed_${model.replace(/([A-Z])/g, "_$1").toLowerCase().replace(/^_/, "")}`;
|
|
2578
|
+
}
|
|
2579
|
+
function extractSeedData(rootDir) {
|
|
2580
|
+
const nodes = [];
|
|
2581
|
+
const edges = [];
|
|
2582
|
+
const seedFiles = [
|
|
2583
|
+
(0, import_node_path10.join)(rootDir, "prisma", "seed.ts"),
|
|
2584
|
+
(0, import_node_path10.join)(rootDir, "prisma", "seed.js"),
|
|
2585
|
+
(0, import_node_path10.join)(rootDir, "src", "server", "lib", "system-tags.ts")
|
|
2586
|
+
].filter(import_node_fs10.existsSync);
|
|
2587
|
+
const useTreeSitter = tryLoadTreeSitter();
|
|
2588
|
+
for (const filePath of seedFiles) {
|
|
2589
|
+
const content = (0, import_node_fs10.readFileSync)(filePath, "utf-8");
|
|
2590
|
+
const relPath = (0, import_node_path10.relative)(rootDir, filePath);
|
|
2591
|
+
const seeded = detectSeededArrays(content, relPath);
|
|
2592
|
+
let astRoot = null;
|
|
2593
|
+
if (useTreeSitter && parseCode) {
|
|
2594
|
+
try {
|
|
2595
|
+
const tree = parseCode(content);
|
|
2596
|
+
astRoot = tree.rootNode;
|
|
2597
|
+
} catch {
|
|
2598
|
+
}
|
|
2599
|
+
}
|
|
2600
|
+
for (const { arrayName, prismaModel, sourceFile } of seeded) {
|
|
2601
|
+
const nodeType = modelToNodeType(prismaModel);
|
|
2602
|
+
const scope = classifyScope(sourceFile, prismaModel);
|
|
2603
|
+
if (astRoot) {
|
|
2604
|
+
const arrayNode = findArrayDecl(astRoot, arrayName);
|
|
2605
|
+
if (!arrayNode) continue;
|
|
2606
|
+
for (const child of arrayNode.namedChildren) {
|
|
2607
|
+
if (child.type !== "object") continue;
|
|
2608
|
+
const props = extractPropsFromObjectNode(child);
|
|
2609
|
+
const idVal = pickIdField(props);
|
|
2610
|
+
if (!idVal) continue;
|
|
2611
|
+
const nameVal = pickNameField(props) ?? idVal;
|
|
2612
|
+
const nodeId = `seed:${prismaModel}:${idVal}`;
|
|
2613
|
+
const { scope: _seedScope, ...safeProps } = props;
|
|
2614
|
+
nodes.push({
|
|
2615
|
+
id: nodeId,
|
|
2616
|
+
type: nodeType,
|
|
2617
|
+
name: nameVal,
|
|
2618
|
+
value: idVal,
|
|
2619
|
+
model: prismaModel,
|
|
2620
|
+
...safeProps,
|
|
2621
|
+
seedScope: _seedScope ?? void 0,
|
|
2622
|
+
// preserve as seedScope
|
|
2623
|
+
scope,
|
|
2624
|
+
source: sourceFile
|
|
2625
|
+
});
|
|
2626
|
+
const permsArrayNode = child.namedChildren.filter((c) => c.type === "pair").find((c) => c.childForFieldName("key")?.text === "permissions");
|
|
2627
|
+
if (permsArrayNode) {
|
|
2628
|
+
const valNode = permsArrayNode.childForFieldName("value");
|
|
2629
|
+
if (valNode?.type === "array") {
|
|
2630
|
+
const permKeys = extractStringArrayFromNode(valNode);
|
|
2631
|
+
for (const pk of permKeys) {
|
|
2632
|
+
if (pk === "*") continue;
|
|
2633
|
+
edges.push({ source: nodeId, target: `seed:permission:${pk}`, type: "grants" });
|
|
2634
|
+
}
|
|
2635
|
+
}
|
|
2636
|
+
}
|
|
2637
|
+
}
|
|
2638
|
+
} else {
|
|
2639
|
+
const constRe = new RegExp(`const\\s+${arrayName}\\s*(?::[^=]+)?=\\s*\\[`, "g");
|
|
2640
|
+
const cm = constRe.exec(content);
|
|
2641
|
+
if (!cm) continue;
|
|
2642
|
+
const bracketStart = cm.index + cm[0].length - 1;
|
|
2643
|
+
let depth = 1, i = bracketStart + 1;
|
|
2644
|
+
while (i < content.length && depth > 0) {
|
|
2645
|
+
if (content[i] === "[") depth++;
|
|
2646
|
+
else if (content[i] === "]") depth--;
|
|
2647
|
+
i++;
|
|
2648
|
+
}
|
|
2649
|
+
if (depth !== 0) continue;
|
|
2650
|
+
const body = content.slice(bracketStart + 1, i - 1);
|
|
2651
|
+
const objects = splitArrayObjectsRegex(body);
|
|
2652
|
+
for (const objStr of objects) {
|
|
2653
|
+
const props = extractObjectPropsRegex(objStr);
|
|
2654
|
+
const idVal = pickIdField(props);
|
|
2655
|
+
if (!idVal) continue;
|
|
2656
|
+
const nameVal = pickNameField(props) ?? idVal;
|
|
2657
|
+
const nodeId = `seed:${prismaModel}:${idVal}`;
|
|
2658
|
+
const { scope: _rScope, ...rSafeProps } = props;
|
|
2659
|
+
nodes.push({
|
|
2660
|
+
id: nodeId,
|
|
2661
|
+
type: nodeType,
|
|
2662
|
+
name: nameVal,
|
|
2663
|
+
value: idVal,
|
|
2664
|
+
model: prismaModel,
|
|
2665
|
+
...rSafeProps,
|
|
2666
|
+
seedScope: _rScope ?? void 0,
|
|
2667
|
+
scope,
|
|
2668
|
+
source: sourceFile
|
|
2669
|
+
});
|
|
2670
|
+
const permArrayMatch = objStr.match(/permissions:\s*\[([^\]]*)\]/);
|
|
2671
|
+
if (permArrayMatch) {
|
|
2672
|
+
for (const pk of extractStringArrayRegex(permArrayMatch[1])) {
|
|
2673
|
+
if (pk === "*") continue;
|
|
2674
|
+
edges.push({ source: nodeId, target: `seed:permission:${pk}`, type: "grants" });
|
|
2675
|
+
}
|
|
2676
|
+
}
|
|
2677
|
+
}
|
|
2678
|
+
}
|
|
2679
|
+
}
|
|
2680
|
+
}
|
|
2681
|
+
return { nodes, edges };
|
|
2682
|
+
}
|
|
2683
|
+
function walkDir(dir, exts) {
|
|
2684
|
+
if (!(0, import_node_fs10.existsSync)(dir)) return [];
|
|
2685
|
+
const results = [];
|
|
2686
|
+
for (const entry of (0, import_node_fs10.readdirSync)(dir, { withFileTypes: true })) {
|
|
2687
|
+
if (entry.name === "node_modules" || entry.name === ".next" || entry.name === "dist") continue;
|
|
2688
|
+
const full = (0, import_node_path10.join)(dir, entry.name);
|
|
2689
|
+
if (entry.isDirectory()) results.push(...walkDir(full, exts));
|
|
2690
|
+
else if (exts.some((ext) => entry.name.endsWith(ext))) results.push(full);
|
|
2691
|
+
}
|
|
2692
|
+
return results;
|
|
2693
|
+
}
|
|
2694
|
+
function extractConstants(rootDir) {
|
|
2695
|
+
const nodes = [];
|
|
2696
|
+
const srcDir = (0, import_node_path10.join)(rootDir, "src");
|
|
2697
|
+
if (!(0, import_node_fs10.existsSync)(srcDir)) return { nodes };
|
|
2698
|
+
for (const filePath of walkDir(srcDir, [".ts", ".tsx"])) {
|
|
2699
|
+
const content = (0, import_node_fs10.readFileSync)(filePath, "utf-8");
|
|
2700
|
+
const relPath = (0, import_node_path10.relative)(rootDir, filePath);
|
|
2701
|
+
const constArrayRe = /export\s+const\s+([A-Z][A-Z_0-9]+)\s*(?::[^=]+)?\s*=\s*\[/g;
|
|
2702
|
+
let cm;
|
|
2703
|
+
while ((cm = constArrayRe.exec(content)) !== null) {
|
|
2704
|
+
const constName = cm[1];
|
|
2705
|
+
const bracketStart = cm.index + cm[0].length - 1;
|
|
2706
|
+
let depth = 1, i = bracketStart + 1;
|
|
2707
|
+
while (i < content.length && depth > 0) {
|
|
2708
|
+
if (content[i] === "[") depth++;
|
|
2709
|
+
else if (content[i] === "]") depth--;
|
|
2710
|
+
i++;
|
|
2711
|
+
}
|
|
2712
|
+
if (depth !== 0) continue;
|
|
2713
|
+
const body = content.slice(bracketStart + 1, i - 1);
|
|
2714
|
+
const stringValues = extractStringArrayRegex(body);
|
|
2715
|
+
const objectCount = splitArrayObjectsRegex(body).length;
|
|
2716
|
+
const valueCount = Math.max(stringValues.length, objectCount);
|
|
2717
|
+
if (valueCount < 2) continue;
|
|
2718
|
+
const scope = classifyScope(relPath);
|
|
2719
|
+
nodes.push({
|
|
2720
|
+
id: `const:${constName}`,
|
|
2721
|
+
type: "constant",
|
|
2722
|
+
name: constName,
|
|
2723
|
+
valueCount,
|
|
2724
|
+
values: stringValues.length > 0 && stringValues.length <= 30 ? stringValues : void 0,
|
|
2725
|
+
scope,
|
|
2726
|
+
source: relPath
|
|
2727
|
+
});
|
|
2728
|
+
}
|
|
2729
|
+
}
|
|
2730
|
+
return { nodes };
|
|
2731
|
+
}
|
|
2732
|
+
function detect4(rootDir) {
|
|
2733
|
+
return (0, import_node_fs10.existsSync)((0, import_node_path10.join)(rootDir, "prisma", "schema.prisma")) || (0, import_node_fs10.existsSync)((0, import_node_path10.join)(rootDir, "prisma", "seed.ts"));
|
|
2734
|
+
}
|
|
2735
|
+
function generate4(rootDir) {
|
|
2736
|
+
const enumResult = extractEnumValues(rootDir);
|
|
2737
|
+
const seedResult = extractSeedData(rootDir);
|
|
2738
|
+
const constResult = extractConstants(rootDir);
|
|
2739
|
+
const allNodes = [...enumResult.nodes, ...seedResult.nodes, ...constResult.nodes];
|
|
2740
|
+
const allEdges = [...enumResult.edges, ...seedResult.edges];
|
|
2741
|
+
const typeOrder = {
|
|
2742
|
+
enum_group: 0,
|
|
2743
|
+
enum_value: 1,
|
|
2744
|
+
seed_permission: 2,
|
|
2745
|
+
seed_role: 3,
|
|
2746
|
+
seed_tag: 4,
|
|
2747
|
+
seed_plan: 5,
|
|
2748
|
+
seed_provider: 6,
|
|
2749
|
+
constant: 7
|
|
2750
|
+
};
|
|
2751
|
+
allNodes.sort((a, b) => {
|
|
2752
|
+
const ta = typeOrder[a.type] ?? 8;
|
|
2753
|
+
const tb = typeOrder[b.type] ?? 8;
|
|
2754
|
+
if (ta !== tb) return ta - tb;
|
|
2755
|
+
return a.name.localeCompare(b.name);
|
|
2756
|
+
});
|
|
2757
|
+
const enumGroups = allNodes.filter((n) => n.type === "enum_group").length;
|
|
2758
|
+
const enumValues = allNodes.filter((n) => n.type === "enum_value").length;
|
|
2759
|
+
const seedNodes = allNodes.filter((n) => n.type.startsWith("seed_")).length;
|
|
2760
|
+
const constNodes = allNodes.filter((n) => n.type === "constant").length;
|
|
2761
|
+
const byScope = {};
|
|
2762
|
+
for (const n of allNodes) {
|
|
2763
|
+
const s = n.scope ?? "unknown";
|
|
2764
|
+
byScope[s] = (byScope[s] ?? 0) + 1;
|
|
2765
|
+
}
|
|
2766
|
+
return {
|
|
2767
|
+
metadata: {
|
|
2768
|
+
generated: (/* @__PURE__ */ new Date()).toISOString().slice(0, 10),
|
|
2769
|
+
scope: "static-values",
|
|
2770
|
+
layer: "static",
|
|
2771
|
+
enum_groups: enumGroups,
|
|
2772
|
+
enum_values: enumValues,
|
|
2773
|
+
seed_records: seedNodes,
|
|
2774
|
+
constants: constNodes,
|
|
2775
|
+
total_nodes: allNodes.length,
|
|
2776
|
+
total_edges: allEdges.length,
|
|
2777
|
+
parser: tryLoadTreeSitter() ? "tree-sitter" : "regex-fallback"
|
|
2778
|
+
},
|
|
2779
|
+
nodes: allNodes,
|
|
2780
|
+
edges: allEdges,
|
|
2781
|
+
cross_refs: [],
|
|
2782
|
+
contradictions: [],
|
|
2783
|
+
warnings: [],
|
|
2784
|
+
flagged_edges: [],
|
|
2785
|
+
patterns: {
|
|
2786
|
+
by_type: {
|
|
2787
|
+
enum_group: enumGroups,
|
|
2788
|
+
enum_value: enumValues,
|
|
2789
|
+
seed_permission: allNodes.filter((n) => n.type === "seed_permission").length,
|
|
2790
|
+
seed_role: allNodes.filter((n) => n.type === "seed_role").length,
|
|
2791
|
+
seed_tag: allNodes.filter((n) => n.type === "seed_tag").length,
|
|
2792
|
+
seed_plan: allNodes.filter((n) => n.type === "seed_plan").length,
|
|
2793
|
+
seed_provider: allNodes.filter((n) => n.type === "seed_provider").length,
|
|
2794
|
+
constant: constNodes
|
|
2795
|
+
},
|
|
2796
|
+
by_scope: byScope
|
|
2797
|
+
}
|
|
2798
|
+
};
|
|
2799
|
+
}
|
|
2800
|
+
var import_node_fs10, import_node_path10, parseCode, SHARED_MODELS, DB_MODELS, staticValuesParser;
|
|
2801
|
+
var init_static_values = __esm({
|
|
2802
|
+
"src/server/graph/parsers/static/static-values.ts"() {
|
|
2803
|
+
"use strict";
|
|
2804
|
+
import_node_fs10 = require("node:fs");
|
|
2805
|
+
import_node_path10 = require("node:path");
|
|
2806
|
+
parseCode = null;
|
|
2807
|
+
SHARED_MODELS = /* @__PURE__ */ new Set(["permission", "role", "tag"]);
|
|
2808
|
+
DB_MODELS = /* @__PURE__ */ new Set(["subscriptionPlan", "providerDefinition", "pipelineMasterTemplate"]);
|
|
2809
|
+
staticValuesParser = {
|
|
2810
|
+
id: "static-values",
|
|
2811
|
+
layer: "static",
|
|
2812
|
+
detect: detect4,
|
|
2813
|
+
generate: generate4
|
|
2814
|
+
};
|
|
2815
|
+
}
|
|
2816
|
+
});
|
|
2817
|
+
|
|
2818
|
+
// src/server/graph/parsers/crosslayer/static-ref-scanner.ts
|
|
2819
|
+
function walk4(dir, exts) {
|
|
2820
|
+
if (!(0, import_node_fs11.existsSync)(dir)) return [];
|
|
2821
|
+
const results = [];
|
|
2822
|
+
function recurse(d) {
|
|
2823
|
+
for (const entry of (0, import_node_fs11.readdirSync)(d, { withFileTypes: true })) {
|
|
2824
|
+
const full = (0, import_node_path11.join)(d, entry.name);
|
|
2825
|
+
if (entry.isDirectory()) {
|
|
2826
|
+
if (entry.name === "node_modules" || entry.name === ".next" || entry.name === "dist") continue;
|
|
2827
|
+
recurse(full);
|
|
2828
|
+
} else if (exts.some((ext) => entry.name.endsWith(ext))) {
|
|
2829
|
+
results.push(full);
|
|
2830
|
+
}
|
|
2831
|
+
}
|
|
2832
|
+
}
|
|
2833
|
+
recurse(dir);
|
|
2834
|
+
return results;
|
|
2835
|
+
}
|
|
2836
|
+
function isInCommentOrType(node) {
|
|
2837
|
+
let current = node.parent;
|
|
2838
|
+
while (current) {
|
|
2839
|
+
if (current.type === "comment" || current.type === "type_annotation" || current.type === "type_alias_declaration" || current.type === "interface_declaration" || current.type === "jsdoc") {
|
|
2840
|
+
return true;
|
|
2841
|
+
}
|
|
2842
|
+
current = current.parent;
|
|
2843
|
+
}
|
|
2844
|
+
return false;
|
|
2845
|
+
}
|
|
2846
|
+
function collectStaticRefs(root, valueLookup) {
|
|
2847
|
+
const refs = [];
|
|
2848
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2849
|
+
function visit(node) {
|
|
2850
|
+
if (node.type === "string_fragment") {
|
|
2851
|
+
const val = node.text;
|
|
2852
|
+
if (val.length >= MIN_VALUE_LENGTH && !SKIP_VALUES.has(val.toLowerCase())) {
|
|
2853
|
+
const targets = valueLookup.get(val);
|
|
2854
|
+
if (targets && !isInCommentOrType(node)) {
|
|
2855
|
+
for (const t of targets) {
|
|
2856
|
+
const key = t;
|
|
2857
|
+
if (!seen.has(key)) {
|
|
2858
|
+
seen.add(key);
|
|
2859
|
+
refs.push({ value: val, targetIds: [t] });
|
|
2860
|
+
}
|
|
2861
|
+
}
|
|
2862
|
+
}
|
|
2863
|
+
}
|
|
2864
|
+
}
|
|
2865
|
+
if (node.type === "member_expression") {
|
|
2866
|
+
const objNode = node.namedChildren.find((c) => c.type === "identifier");
|
|
2867
|
+
const propNode = node.namedChildren.find((c) => c.type === "property_identifier");
|
|
2868
|
+
if (objNode && propNode) {
|
|
2869
|
+
const combined = `${objNode.text}.${propNode.text}`;
|
|
2870
|
+
const targets = valueLookup.get(propNode.text);
|
|
2871
|
+
const directTarget = valueLookup.get(combined);
|
|
2872
|
+
if (directTarget && !isInCommentOrType(node)) {
|
|
2873
|
+
for (const t of directTarget) {
|
|
2874
|
+
if (!seen.has(t)) {
|
|
2875
|
+
seen.add(t);
|
|
2876
|
+
refs.push({ value: combined, targetIds: [t] });
|
|
2877
|
+
}
|
|
2878
|
+
}
|
|
2879
|
+
} else if (targets && !isInCommentOrType(node)) {
|
|
2880
|
+
for (const t of targets) {
|
|
2881
|
+
if (!seen.has(t)) {
|
|
2882
|
+
seen.add(t);
|
|
2883
|
+
refs.push({ value: propNode.text, targetIds: [t] });
|
|
2884
|
+
}
|
|
2885
|
+
}
|
|
2886
|
+
}
|
|
2887
|
+
}
|
|
2888
|
+
}
|
|
2889
|
+
for (const child of node.namedChildren) {
|
|
2890
|
+
visit(child);
|
|
2891
|
+
}
|
|
2892
|
+
}
|
|
2893
|
+
visit(root);
|
|
2894
|
+
return refs;
|
|
2895
|
+
}
|
|
2896
|
+
function collectStaticRefsRegex(content, valueLookup, allValues) {
|
|
2897
|
+
const refs = [];
|
|
2898
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2899
|
+
const escaped = allValues.map((v) => v.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
|
2900
|
+
const pattern = new RegExp(
|
|
2901
|
+
`(?:['"\`])(${escaped.join("|")})(?:['"\`])|\\b(\\w+)\\.(${escaped.join("|")})\\b`,
|
|
2902
|
+
"g"
|
|
2903
|
+
);
|
|
2904
|
+
let match;
|
|
2905
|
+
pattern.lastIndex = 0;
|
|
2906
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
2907
|
+
const val = match[1] ?? match[3];
|
|
2908
|
+
if (!val) continue;
|
|
2909
|
+
const targets = valueLookup.get(val);
|
|
2910
|
+
if (!targets) continue;
|
|
2911
|
+
for (const t of targets) {
|
|
2912
|
+
if (!seen.has(t)) {
|
|
2913
|
+
seen.add(t);
|
|
2914
|
+
refs.push({ value: val, targetIds: [t] });
|
|
2915
|
+
}
|
|
2916
|
+
}
|
|
2917
|
+
}
|
|
2918
|
+
return refs;
|
|
1839
2919
|
}
|
|
1840
|
-
var
|
|
1841
|
-
var
|
|
1842
|
-
"src/server/graph/parsers/crosslayer/
|
|
2920
|
+
var import_node_fs11, import_node_path11, MIN_VALUE_LENGTH, SKIP_VALUES, staticRefScannerParser;
|
|
2921
|
+
var init_static_ref_scanner = __esm({
|
|
2922
|
+
"src/server/graph/parsers/crosslayer/static-ref-scanner.ts"() {
|
|
1843
2923
|
"use strict";
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
2924
|
+
import_node_fs11 = require("node:fs");
|
|
2925
|
+
import_node_path11 = require("node:path");
|
|
2926
|
+
init_config();
|
|
2927
|
+
init_resolve_paths();
|
|
2928
|
+
MIN_VALUE_LENGTH = 4;
|
|
2929
|
+
SKIP_VALUES = /* @__PURE__ */ new Set([
|
|
2930
|
+
"true",
|
|
2931
|
+
"false",
|
|
2932
|
+
"null",
|
|
2933
|
+
"undefined",
|
|
2934
|
+
"none",
|
|
2935
|
+
"default",
|
|
2936
|
+
"name",
|
|
2937
|
+
"type",
|
|
2938
|
+
"data",
|
|
2939
|
+
"text",
|
|
2940
|
+
"info",
|
|
2941
|
+
"error",
|
|
2942
|
+
"open",
|
|
2943
|
+
"read",
|
|
2944
|
+
"user",
|
|
2945
|
+
"test",
|
|
2946
|
+
"json",
|
|
2947
|
+
"form"
|
|
2948
|
+
]);
|
|
2949
|
+
staticRefScannerParser = {
|
|
2950
|
+
id: "static-ref-scanner",
|
|
1850
2951
|
layer: "crosslayer",
|
|
1851
2952
|
detect(rootDir) {
|
|
1852
|
-
|
|
2953
|
+
const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
|
|
2954
|
+
return paths !== null;
|
|
1853
2955
|
},
|
|
1854
2956
|
generate(rootDir, layerOutputs) {
|
|
1855
|
-
const
|
|
1856
|
-
if (!
|
|
2957
|
+
const staticOutput = layerOutputs.get("static");
|
|
2958
|
+
if (!staticOutput || staticOutput.nodes.length === 0) {
|
|
1857
2959
|
return { cross_refs: [], flagged_edges: [], warnings: [] };
|
|
1858
2960
|
}
|
|
1859
|
-
const
|
|
1860
|
-
const
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
2961
|
+
const valueLookup = /* @__PURE__ */ new Map();
|
|
2962
|
+
for (const node of staticOutput.nodes) {
|
|
2963
|
+
const type = node.type;
|
|
2964
|
+
let valueStr = null;
|
|
2965
|
+
if (type === "enum_value") {
|
|
2966
|
+
valueStr = node.value;
|
|
2967
|
+
const fullId = node.id;
|
|
2968
|
+
if (!valueLookup.has(fullId)) valueLookup.set(fullId, []);
|
|
2969
|
+
valueLookup.get(fullId).push(node.id);
|
|
2970
|
+
} else if (type.startsWith("seed_")) {
|
|
2971
|
+
valueStr = node.value;
|
|
2972
|
+
}
|
|
2973
|
+
if (!valueStr || valueStr.length < MIN_VALUE_LENGTH || SKIP_VALUES.has(valueStr.toLowerCase())) continue;
|
|
2974
|
+
if (!valueLookup.has(valueStr)) valueLookup.set(valueStr, []);
|
|
2975
|
+
valueLookup.get(valueStr).push(node.id);
|
|
2976
|
+
}
|
|
2977
|
+
const allValues = [...valueLookup.keys()].sort((a, b) => b.length - a.length);
|
|
2978
|
+
if (allValues.length === 0) {
|
|
2979
|
+
return { cross_refs: [], flagged_edges: [], warnings: [] };
|
|
2980
|
+
}
|
|
2981
|
+
const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
|
|
2982
|
+
if (!paths) return { cross_refs: [], flagged_edges: [], warnings: [] };
|
|
2983
|
+
const srcDir = paths.srcDir;
|
|
1866
2984
|
const files = [
|
|
1867
|
-
...walk4(
|
|
1868
|
-
...walk4(appDir, [".ts", ".tsx"])
|
|
2985
|
+
...walk4((0, import_node_path11.join)(srcDir, "client"), [".ts", ".tsx"]),
|
|
2986
|
+
...walk4(paths.appDir, [".ts", ".tsx"]),
|
|
2987
|
+
...walk4((0, import_node_path11.join)(srcDir, "server"), [".ts", ".tsx"]),
|
|
2988
|
+
...walk4((0, import_node_path11.join)(srcDir, "lib"), [".ts", ".tsx"]),
|
|
2989
|
+
...walk4((0, import_node_path11.join)(srcDir, "config"), [".ts", ".tsx"])
|
|
1869
2990
|
];
|
|
2991
|
+
const uiOutput = layerOutputs.get("ui");
|
|
2992
|
+
const apiOutput = layerOutputs.get("api");
|
|
2993
|
+
const uiNodeIds = new Set(uiOutput?.nodes.map((n) => n.id) ?? []);
|
|
2994
|
+
const apiNodeIds = new Set(apiOutput?.nodes.map((n) => n.id) ?? []);
|
|
2995
|
+
let parseCode2 = null;
|
|
2996
|
+
try {
|
|
2997
|
+
const extractor = (init_ts_extractor(), __toCommonJS(ts_extractor_exports));
|
|
2998
|
+
if (typeof extractor.parseCodeTS === "function") {
|
|
2999
|
+
parseCode2 = extractor.parseCodeTS;
|
|
3000
|
+
}
|
|
3001
|
+
} catch {
|
|
3002
|
+
}
|
|
1870
3003
|
const crossRefs = [];
|
|
1871
3004
|
const seen = /* @__PURE__ */ new Set();
|
|
3005
|
+
let filesScanned = 0;
|
|
1872
3006
|
for (const absPath of files) {
|
|
1873
|
-
const sourceId =
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
3007
|
+
const sourceId = (0, import_node_path11.relative)(srcDir, absPath).replace(/\\/g, "/");
|
|
3008
|
+
const sourceLayer = uiNodeIds.has(sourceId) ? "ui" : apiNodeIds.has(sourceId) ? "api" : null;
|
|
3009
|
+
if (!sourceLayer) continue;
|
|
3010
|
+
const content = (0, import_node_fs11.readFileSync)(absPath, "utf-8");
|
|
3011
|
+
filesScanned++;
|
|
3012
|
+
let fileRefs;
|
|
3013
|
+
if (parseCode2) {
|
|
3014
|
+
try {
|
|
3015
|
+
const tree = parseCode2(content);
|
|
3016
|
+
fileRefs = collectStaticRefs(tree.rootNode, valueLookup);
|
|
3017
|
+
} catch {
|
|
3018
|
+
fileRefs = collectStaticRefsRegex(content, valueLookup, allValues);
|
|
3019
|
+
}
|
|
3020
|
+
} else {
|
|
3021
|
+
fileRefs = collectStaticRefsRegex(content, valueLookup, allValues);
|
|
3022
|
+
}
|
|
3023
|
+
for (const ref of fileRefs) {
|
|
3024
|
+
for (const targetId of ref.targetIds) {
|
|
3025
|
+
const key = `${sourceId}|${targetId}`;
|
|
1883
3026
|
if (seen.has(key)) continue;
|
|
1884
3027
|
seen.add(key);
|
|
1885
3028
|
crossRefs.push({
|
|
1886
3029
|
source: sourceId,
|
|
1887
|
-
target:
|
|
1888
|
-
type: "
|
|
1889
|
-
layer: "
|
|
3030
|
+
target: targetId,
|
|
3031
|
+
type: "references_static",
|
|
3032
|
+
layer: "static"
|
|
1890
3033
|
});
|
|
1891
3034
|
}
|
|
1892
3035
|
}
|
|
@@ -1896,7 +3039,10 @@ var init_url_literal_scanner = __esm({
|
|
|
1896
3039
|
flagged_edges: [],
|
|
1897
3040
|
warnings: [],
|
|
1898
3041
|
patterns: {
|
|
1899
|
-
|
|
3042
|
+
files_scanned: filesScanned,
|
|
3043
|
+
static_values_tracked: valueLookup.size,
|
|
3044
|
+
references_found: crossRefs.length,
|
|
3045
|
+
parser: parseCode2 ? "tree-sitter" : "regex-fallback"
|
|
1900
3046
|
}
|
|
1901
3047
|
};
|
|
1902
3048
|
}
|
|
@@ -1905,14 +3051,19 @@ var init_url_literal_scanner = __esm({
|
|
|
1905
3051
|
});
|
|
1906
3052
|
|
|
1907
3053
|
// src/server/graph/core/parser-registry.ts
|
|
3054
|
+
function isMultiLayerParser(p) {
|
|
3055
|
+
return "layers" in p && Array.isArray(p.layers);
|
|
3056
|
+
}
|
|
1908
3057
|
function registerBuiltins(registry, disabled) {
|
|
1909
3058
|
const builtins = [
|
|
1910
|
-
|
|
1911
|
-
nextjsRoutesParser,
|
|
3059
|
+
typescriptProjectParser,
|
|
1912
3060
|
prismaSchemaParser,
|
|
3061
|
+
sqlMigrationsParser,
|
|
3062
|
+
staticValuesParser,
|
|
1913
3063
|
fetchResolverParser,
|
|
1914
3064
|
apiAnnotationsParser,
|
|
1915
|
-
urlLiteralScannerParser
|
|
3065
|
+
urlLiteralScannerParser,
|
|
3066
|
+
staticRefScannerParser
|
|
1916
3067
|
];
|
|
1917
3068
|
for (const parser of builtins) {
|
|
1918
3069
|
if (disabled.has(parser.id)) continue;
|
|
@@ -1922,11 +3073,11 @@ function registerBuiltins(registry, disabled) {
|
|
|
1922
3073
|
function loadCustomParsers(registry, config, rootDir, disabled) {
|
|
1923
3074
|
for (const entry of config.parsers?.custom ?? []) {
|
|
1924
3075
|
try {
|
|
1925
|
-
const absPath = (0,
|
|
3076
|
+
const absPath = (0, import_node_path12.resolve)(rootDir, entry.path);
|
|
1926
3077
|
const mod = require(absPath);
|
|
1927
3078
|
const parser = "default" in mod ? mod.default : mod;
|
|
1928
3079
|
if (disabled.has(parser.id)) continue;
|
|
1929
|
-
if (parser.layer !== entry.layer) {
|
|
3080
|
+
if (!isMultiLayerParser(parser) && parser.layer !== entry.layer) {
|
|
1930
3081
|
process.stderr.write(
|
|
1931
3082
|
`[launch-chart] custom parser "${parser.id}" declares layer "${parser.layer}" but config says "${entry.layer}" \u2014 using parser's layer
|
|
1932
3083
|
`
|
|
@@ -1946,20 +3097,23 @@ function createRegistry(config, rootDir) {
|
|
|
1946
3097
|
loadCustomParsers(registry, config, rootDir, disabled);
|
|
1947
3098
|
return registry;
|
|
1948
3099
|
}
|
|
1949
|
-
var
|
|
3100
|
+
var import_node_path12, ParserRegistry;
|
|
1950
3101
|
var init_parser_registry = __esm({
|
|
1951
3102
|
"src/server/graph/core/parser-registry.ts"() {
|
|
1952
3103
|
"use strict";
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
init_nextjs_routes();
|
|
3104
|
+
import_node_path12 = require("node:path");
|
|
3105
|
+
init_typescript_project();
|
|
1956
3106
|
init_prisma_schema();
|
|
3107
|
+
init_sql_migrations();
|
|
1957
3108
|
init_fetch_resolver();
|
|
1958
3109
|
init_api_annotations();
|
|
1959
3110
|
init_url_literal_scanner();
|
|
3111
|
+
init_static_values();
|
|
3112
|
+
init_static_ref_scanner();
|
|
1960
3113
|
ParserRegistry = class {
|
|
1961
3114
|
constructor() {
|
|
1962
|
-
this.
|
|
3115
|
+
this.singleLayerParsers = /* @__PURE__ */ new Map();
|
|
3116
|
+
this.multiLayerParsers = [];
|
|
1963
3117
|
this.ids = /* @__PURE__ */ new Set();
|
|
1964
3118
|
}
|
|
1965
3119
|
register(parser) {
|
|
@@ -1967,19 +3121,44 @@ var init_parser_registry = __esm({
|
|
|
1967
3121
|
throw new Error(`Duplicate parser id: ${parser.id}`);
|
|
1968
3122
|
}
|
|
1969
3123
|
this.ids.add(parser.id);
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
3124
|
+
if (isMultiLayerParser(parser)) {
|
|
3125
|
+
this.multiLayerParsers.push(parser);
|
|
3126
|
+
} else {
|
|
3127
|
+
const list = this.singleLayerParsers.get(parser.layer) ?? [];
|
|
3128
|
+
list.push(parser);
|
|
3129
|
+
this.singleLayerParsers.set(parser.layer, list);
|
|
3130
|
+
}
|
|
1973
3131
|
}
|
|
3132
|
+
/** Get single-layer parsers for a specific layer. */
|
|
1974
3133
|
getParsers(layer) {
|
|
1975
|
-
return this.
|
|
3134
|
+
return this.singleLayerParsers.get(layer) ?? [];
|
|
3135
|
+
}
|
|
3136
|
+
/** Get multi-layer parsers that can produce output for the given layer. */
|
|
3137
|
+
getMultiLayerParsersFor(layer) {
|
|
3138
|
+
return this.multiLayerParsers.filter((p) => p.layers.includes(layer));
|
|
3139
|
+
}
|
|
3140
|
+
/** Get all multi-layer parsers. */
|
|
3141
|
+
getMultiLayerParsers() {
|
|
3142
|
+
return [...this.multiLayerParsers];
|
|
1976
3143
|
}
|
|
1977
3144
|
getCrossLayerParsers() {
|
|
1978
|
-
return this.
|
|
3145
|
+
return this.singleLayerParsers.get("crosslayer") ?? [];
|
|
3146
|
+
}
|
|
3147
|
+
/** All layers that registered parsers can produce (single + multi). */
|
|
3148
|
+
getAvailableLayers() {
|
|
3149
|
+
const layers = /* @__PURE__ */ new Set();
|
|
3150
|
+
for (const key of this.singleLayerParsers.keys()) {
|
|
3151
|
+
if (key !== "crosslayer") layers.add(key);
|
|
3152
|
+
}
|
|
3153
|
+
for (const mp of this.multiLayerParsers) {
|
|
3154
|
+
for (const l of mp.layers) layers.add(l);
|
|
3155
|
+
}
|
|
3156
|
+
return [...layers];
|
|
1979
3157
|
}
|
|
1980
3158
|
getAll() {
|
|
1981
3159
|
const all = [];
|
|
1982
|
-
for (const list of this.
|
|
3160
|
+
for (const list of this.singleLayerParsers.values()) all.push(...list);
|
|
3161
|
+
all.push(...this.multiLayerParsers);
|
|
1983
3162
|
return all;
|
|
1984
3163
|
}
|
|
1985
3164
|
};
|
|
@@ -2116,10 +3295,10 @@ var init_merge = __esm({
|
|
|
2116
3295
|
|
|
2117
3296
|
// src/server/graph/core/graph-builder.ts
|
|
2118
3297
|
function readGraphFromDisk(rootDir, layer) {
|
|
2119
|
-
const filePath = (0,
|
|
2120
|
-
if (!(0,
|
|
3298
|
+
const filePath = (0, import_node_path13.join)(rootDir, ".launchsecure", "graphs", `${layer}.json`);
|
|
3299
|
+
if (!(0, import_node_fs12.existsSync)(filePath)) return null;
|
|
2121
3300
|
try {
|
|
2122
|
-
return JSON.parse((0,
|
|
3301
|
+
return JSON.parse((0, import_node_fs12.readFileSync)(filePath, "utf-8"));
|
|
2123
3302
|
} catch {
|
|
2124
3303
|
return null;
|
|
2125
3304
|
}
|
|
@@ -2127,18 +3306,24 @@ function readGraphFromDisk(rootDir, layer) {
|
|
|
2127
3306
|
function generateLayer(rootDir, layer) {
|
|
2128
3307
|
const config = loadConfig(rootDir);
|
|
2129
3308
|
const registry = createRegistry(config, rootDir);
|
|
2130
|
-
const parsers = registry.getParsers(layer);
|
|
2131
3309
|
const outputs = [];
|
|
2132
|
-
for (const parser of
|
|
3310
|
+
for (const parser of registry.getParsers(layer)) {
|
|
2133
3311
|
if (!parser.detect(rootDir)) continue;
|
|
2134
3312
|
outputs.push(parser.generate(rootDir));
|
|
2135
3313
|
}
|
|
3314
|
+
for (const mp of registry.getMultiLayerParsersFor(layer)) {
|
|
3315
|
+
if (!mp.detect(rootDir)) continue;
|
|
3316
|
+
const multiOutput = mp.generate(rootDir);
|
|
3317
|
+
const layerOutput = multiOutput.get(layer);
|
|
3318
|
+
if (layerOutput) outputs.push(layerOutput);
|
|
3319
|
+
}
|
|
2136
3320
|
if (outputs.length === 0) return null;
|
|
2137
3321
|
let merged = outputs.length === 1 ? outputs[0] : mergeGraphOutputs(outputs, layer);
|
|
2138
3322
|
if (layer === "ui") {
|
|
2139
3323
|
const layerOutputs = /* @__PURE__ */ new Map();
|
|
2140
3324
|
layerOutputs.set("ui", merged);
|
|
2141
|
-
for (const otherLayer of
|
|
3325
|
+
for (const otherLayer of registry.getAvailableLayers()) {
|
|
3326
|
+
if (otherLayer === "ui") continue;
|
|
2142
3327
|
const existing = readGraphFromDisk(rootDir, otherLayer);
|
|
2143
3328
|
if (existing) layerOutputs.set(otherLayer, existing);
|
|
2144
3329
|
}
|
|
@@ -2159,16 +3344,29 @@ function generateLayer(rootDir, layer) {
|
|
|
2159
3344
|
function generateAll(rootDir) {
|
|
2160
3345
|
const config = loadConfig(rootDir);
|
|
2161
3346
|
const registry = createRegistry(config, rootDir);
|
|
2162
|
-
const
|
|
3347
|
+
const allLayers = registry.getAvailableLayers();
|
|
3348
|
+
const layerOrder = [
|
|
3349
|
+
...allLayers.filter((l) => l !== "ui"),
|
|
3350
|
+
...allLayers.filter((l) => l === "ui")
|
|
3351
|
+
];
|
|
2163
3352
|
const layerOutputs = /* @__PURE__ */ new Map();
|
|
2164
3353
|
const results = [];
|
|
3354
|
+
const multiLayerResults = /* @__PURE__ */ new Map();
|
|
2165
3355
|
for (const layer of layerOrder) {
|
|
2166
|
-
const parsers = registry.getParsers(layer);
|
|
2167
3356
|
const outputs = [];
|
|
2168
|
-
for (const parser of
|
|
3357
|
+
for (const parser of registry.getParsers(layer)) {
|
|
2169
3358
|
if (!parser.detect(rootDir)) continue;
|
|
2170
3359
|
outputs.push(parser.generate(rootDir));
|
|
2171
3360
|
}
|
|
3361
|
+
for (const mp of registry.getMultiLayerParsersFor(layer)) {
|
|
3362
|
+
if (!mp.detect(rootDir)) continue;
|
|
3363
|
+
if (!multiLayerResults.has(mp.id)) {
|
|
3364
|
+
multiLayerResults.set(mp.id, mp.generate(rootDir));
|
|
3365
|
+
}
|
|
3366
|
+
const cached = multiLayerResults.get(mp.id);
|
|
3367
|
+
const layerOutput = cached.get(layer);
|
|
3368
|
+
if (layerOutput) outputs.push(layerOutput);
|
|
3369
|
+
}
|
|
2172
3370
|
if (outputs.length === 0) continue;
|
|
2173
3371
|
const merged = outputs.length === 1 ? outputs[0] : mergeGraphOutputs(outputs, layer);
|
|
2174
3372
|
layerOutputs.set(layer, merged);
|
|
@@ -2194,14 +3392,16 @@ function generateAll(rootDir) {
|
|
|
2194
3392
|
}
|
|
2195
3393
|
}
|
|
2196
3394
|
const byLayer = new Map(results.map((r) => [r.layer, r]));
|
|
2197
|
-
|
|
3395
|
+
const wellKnownOrder = ["ui", "api", "db"];
|
|
3396
|
+
const extras = [...byLayer.keys()].filter((l) => !wellKnownOrder.includes(l)).sort();
|
|
3397
|
+
return [...wellKnownOrder, ...extras].map((l) => byLayer.get(l)).filter((r) => !!r);
|
|
2198
3398
|
}
|
|
2199
|
-
var
|
|
3399
|
+
var import_node_fs12, import_node_path13;
|
|
2200
3400
|
var init_graph_builder = __esm({
|
|
2201
3401
|
"src/server/graph/core/graph-builder.ts"() {
|
|
2202
3402
|
"use strict";
|
|
2203
|
-
|
|
2204
|
-
|
|
3403
|
+
import_node_fs12 = require("node:fs");
|
|
3404
|
+
import_node_path13 = require("node:path");
|
|
2205
3405
|
init_config();
|
|
2206
3406
|
init_parser_registry();
|
|
2207
3407
|
init_merge();
|
|
@@ -2240,18 +3440,18 @@ function detectConventionDirs(rootDir, extraConventionDirs = []) {
|
|
|
2240
3440
|
const conventionDirs = [...CONVENTION_DIRS_BUILTIN, ...extraConventionDirs];
|
|
2241
3441
|
const searchDirs = [
|
|
2242
3442
|
rootDir,
|
|
2243
|
-
(0,
|
|
2244
|
-
(0,
|
|
2245
|
-
(0,
|
|
3443
|
+
(0, import_node_path14.join)(rootDir, "src"),
|
|
3444
|
+
(0, import_node_path14.join)(rootDir, "app"),
|
|
3445
|
+
(0, import_node_path14.join)(rootDir, "lib")
|
|
2246
3446
|
];
|
|
2247
3447
|
for (const base of searchDirs) {
|
|
2248
3448
|
for (const convention of conventionDirs) {
|
|
2249
|
-
const dir = (0,
|
|
2250
|
-
if (!(0,
|
|
3449
|
+
const dir = (0, import_node_path14.join)(base, convention);
|
|
3450
|
+
if (!(0, import_node_fs13.existsSync)(dir)) continue;
|
|
2251
3451
|
try {
|
|
2252
|
-
const stat = (0,
|
|
3452
|
+
const stat = (0, import_node_fs13.statSync)(dir);
|
|
2253
3453
|
if (!stat.isDirectory()) continue;
|
|
2254
|
-
const entries = (0,
|
|
3454
|
+
const entries = (0, import_node_fs13.readdirSync)(dir, { withFileTypes: true }).filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => e.name);
|
|
2255
3455
|
if (entries.length > 0) {
|
|
2256
3456
|
const relPath = dir.replace(rootDir + "/", "").replace(rootDir + "\\", "");
|
|
2257
3457
|
result.set(relPath, entries);
|
|
@@ -2323,12 +3523,12 @@ function extractModuleFromPath(id, extraTrivial, extraSkipSegments) {
|
|
|
2323
3523
|
}
|
|
2324
3524
|
return "root";
|
|
2325
3525
|
}
|
|
2326
|
-
var
|
|
3526
|
+
var import_node_fs13, import_node_path14, CONVENTION_DIRS_BUILTIN, GENERIC_ROLE_NAMES_BUILTIN, SKIP_SEGMENTS_BUILTIN, TRIVIAL_GROUPS, cachedRootDir, cachedConventionDirs, moduleTagger;
|
|
2327
3527
|
var init_module_tagger = __esm({
|
|
2328
3528
|
"src/server/graph/taggers/module-tagger.ts"() {
|
|
2329
3529
|
"use strict";
|
|
2330
|
-
|
|
2331
|
-
|
|
3530
|
+
import_node_fs13 = require("node:fs");
|
|
3531
|
+
import_node_path14 = require("node:path");
|
|
2332
3532
|
CONVENTION_DIRS_BUILTIN = ["features", "modules", "domains", "areas"];
|
|
2333
3533
|
GENERIC_ROLE_NAMES_BUILTIN = /* @__PURE__ */ new Set([
|
|
2334
3534
|
// JS/TS
|
|
@@ -2540,7 +3740,7 @@ function loadCustomTaggers(registry, config, rootDir, disabled) {
|
|
|
2540
3740
|
for (const entry of config.taggers?.custom ?? []) {
|
|
2541
3741
|
if (disabled.has(entry.id)) continue;
|
|
2542
3742
|
try {
|
|
2543
|
-
const absPath = (0,
|
|
3743
|
+
const absPath = (0, import_node_path15.resolve)(rootDir, entry.path);
|
|
2544
3744
|
const mod = require(absPath);
|
|
2545
3745
|
const tagger = "default" in mod ? mod.default : mod;
|
|
2546
3746
|
const override = config.taggers?.trackUntagged?.[tagger.id];
|
|
@@ -2561,11 +3761,11 @@ function createTaggerRegistry(config, rootDir) {
|
|
|
2561
3761
|
loadCustomTaggers(registry, config, rootDir, disabled);
|
|
2562
3762
|
return registry;
|
|
2563
3763
|
}
|
|
2564
|
-
var
|
|
3764
|
+
var import_node_path15, TaggerRegistry, BUILTIN_TAGGERS;
|
|
2565
3765
|
var init_tagger_registry = __esm({
|
|
2566
3766
|
"src/server/graph/core/tagger-registry.ts"() {
|
|
2567
3767
|
"use strict";
|
|
2568
|
-
|
|
3768
|
+
import_node_path15 = require("node:path");
|
|
2569
3769
|
init_module_tagger();
|
|
2570
3770
|
init_screen_tagger();
|
|
2571
3771
|
TaggerRegistry = class {
|
|
@@ -2593,18 +3793,18 @@ var init_tagger_registry = __esm({
|
|
|
2593
3793
|
|
|
2594
3794
|
// src/server/graph/core/tag-store.ts
|
|
2595
3795
|
function tagsFilePath(rootDir) {
|
|
2596
|
-
return (0,
|
|
3796
|
+
return (0, import_node_path16.join)(rootDir, GRAPHS_DIR, TAGS_FILENAME);
|
|
2597
3797
|
}
|
|
2598
3798
|
function readTagStore(rootDir) {
|
|
2599
3799
|
const filePath = tagsFilePath(rootDir);
|
|
2600
|
-
if (!(0,
|
|
2601
|
-
const stat = (0,
|
|
3800
|
+
if (!(0, import_node_fs14.existsSync)(filePath)) return {};
|
|
3801
|
+
const stat = (0, import_node_fs14.statSync)(filePath);
|
|
2602
3802
|
const cached = tagCache.get(filePath);
|
|
2603
3803
|
if (cached && cached.mtimeMs === stat.mtimeMs) {
|
|
2604
3804
|
return cached.store;
|
|
2605
3805
|
}
|
|
2606
3806
|
try {
|
|
2607
|
-
const content = (0,
|
|
3807
|
+
const content = (0, import_node_fs14.readFileSync)(filePath, "utf-8");
|
|
2608
3808
|
const store = JSON.parse(content);
|
|
2609
3809
|
tagCache.set(filePath, { mtimeMs: stat.mtimeMs, store });
|
|
2610
3810
|
return store;
|
|
@@ -2614,15 +3814,15 @@ function readTagStore(rootDir) {
|
|
|
2614
3814
|
}
|
|
2615
3815
|
function writeTagStore(rootDir, store) {
|
|
2616
3816
|
const filePath = tagsFilePath(rootDir);
|
|
2617
|
-
const dir = (0,
|
|
2618
|
-
(0,
|
|
3817
|
+
const dir = (0, import_node_path16.dirname)(filePath);
|
|
3818
|
+
(0, import_node_fs14.mkdirSync)(dir, { recursive: true });
|
|
2619
3819
|
const cleaned = {};
|
|
2620
3820
|
for (const [nodeId, tags] of Object.entries(store)) {
|
|
2621
3821
|
if (Object.keys(tags).length > 0) {
|
|
2622
3822
|
cleaned[nodeId] = tags;
|
|
2623
3823
|
}
|
|
2624
3824
|
}
|
|
2625
|
-
(0,
|
|
3825
|
+
(0, import_node_fs14.writeFileSync)(filePath, JSON.stringify(cleaned, null, 2) + "\n", "utf-8");
|
|
2626
3826
|
tagCache.delete(filePath);
|
|
2627
3827
|
}
|
|
2628
3828
|
function setTag(rootDir, nodeId, key, value) {
|
|
@@ -2640,12 +3840,12 @@ function removeTag(rootDir, nodeId, key) {
|
|
|
2640
3840
|
}
|
|
2641
3841
|
writeTagStore(rootDir, store);
|
|
2642
3842
|
}
|
|
2643
|
-
var
|
|
3843
|
+
var import_node_fs14, import_node_path16, TAGS_FILENAME, GRAPHS_DIR, tagCache;
|
|
2644
3844
|
var init_tag_store = __esm({
|
|
2645
3845
|
"src/server/graph/core/tag-store.ts"() {
|
|
2646
3846
|
"use strict";
|
|
2647
|
-
|
|
2648
|
-
|
|
3847
|
+
import_node_fs14 = require("node:fs");
|
|
3848
|
+
import_node_path16 = require("node:path");
|
|
2649
3849
|
TAGS_FILENAME = "tags.json";
|
|
2650
3850
|
GRAPHS_DIR = ".launchsecure/graphs";
|
|
2651
3851
|
tagCache = /* @__PURE__ */ new Map();
|
|
@@ -2653,18 +3853,23 @@ var init_tag_store = __esm({
|
|
|
2653
3853
|
});
|
|
2654
3854
|
|
|
2655
3855
|
// src/server/graph/index.ts
|
|
3856
|
+
function getAvailableLayers(rootDir) {
|
|
3857
|
+
const dir = (0, import_node_path17.join)(rootDir, GRAPHS_DIR2);
|
|
3858
|
+
if (!(0, import_node_fs15.existsSync)(dir)) return [];
|
|
3859
|
+
return (0, import_node_fs15.readdirSync)(dir).filter((f) => f.endsWith(".json") && f !== "tags.json").map((f) => f.replace(".json", ""));
|
|
3860
|
+
}
|
|
2656
3861
|
function graphsDir(rootDir) {
|
|
2657
|
-
return (0,
|
|
3862
|
+
return (0, import_node_path17.join)(rootDir, GRAPHS_DIR2);
|
|
2658
3863
|
}
|
|
2659
3864
|
function graphFilePath(rootDir, layer) {
|
|
2660
|
-
return (0,
|
|
3865
|
+
return (0, import_node_path17.join)(graphsDir(rootDir), `${layer}.json`);
|
|
2661
3866
|
}
|
|
2662
3867
|
function tagsFilePath2(rootDir) {
|
|
2663
|
-
return (0,
|
|
3868
|
+
return (0, import_node_path17.join)(graphsDir(rootDir), "tags.json");
|
|
2664
3869
|
}
|
|
2665
3870
|
function getMtimeMs(filePath) {
|
|
2666
|
-
if (!(0,
|
|
2667
|
-
return (0,
|
|
3871
|
+
if (!(0, import_node_fs15.existsSync)(filePath)) return 0;
|
|
3872
|
+
return (0, import_node_fs15.statSync)(filePath).mtimeMs;
|
|
2668
3873
|
}
|
|
2669
3874
|
function invalidateCache(filePath) {
|
|
2670
3875
|
graphCache.delete(filePath);
|
|
@@ -2703,20 +3908,20 @@ function applyTags(graph, layer, rootDir) {
|
|
|
2703
3908
|
}
|
|
2704
3909
|
function readGraphRaw(rootDir, layer) {
|
|
2705
3910
|
const filePath = graphFilePath(rootDir, layer);
|
|
2706
|
-
if (!(0,
|
|
2707
|
-
const stat = (0,
|
|
3911
|
+
if (!(0, import_node_fs15.existsSync)(filePath)) return null;
|
|
3912
|
+
const stat = (0, import_node_fs15.statSync)(filePath);
|
|
2708
3913
|
const cached = graphCache.get(filePath);
|
|
2709
3914
|
if (cached && cached.mtimeMs === stat.mtimeMs) {
|
|
2710
3915
|
return cached.graph;
|
|
2711
3916
|
}
|
|
2712
|
-
const content = (0,
|
|
3917
|
+
const content = (0, import_node_fs15.readFileSync)(filePath, "utf-8");
|
|
2713
3918
|
const graph = JSON.parse(content);
|
|
2714
3919
|
graphCache.set(filePath, { mtimeMs: stat.mtimeMs, graph });
|
|
2715
3920
|
return graph;
|
|
2716
3921
|
}
|
|
2717
3922
|
function readGraph(rootDir, layer) {
|
|
2718
3923
|
const rawFilePath = graphFilePath(rootDir, layer);
|
|
2719
|
-
if (!(0,
|
|
3924
|
+
if (!(0, import_node_fs15.existsSync)(rawFilePath)) return null;
|
|
2720
3925
|
const rawMtime = getMtimeMs(rawFilePath);
|
|
2721
3926
|
const tagsMtime = getMtimeMs(tagsFilePath2(rootDir));
|
|
2722
3927
|
const cacheKey = `${rootDir}:${layer}`;
|
|
@@ -2732,7 +3937,7 @@ function readGraph(rootDir, layer) {
|
|
|
2732
3937
|
}
|
|
2733
3938
|
function readAllGraphs(rootDir) {
|
|
2734
3939
|
const result = {};
|
|
2735
|
-
for (const layer of
|
|
3940
|
+
for (const layer of getAvailableLayers(rootDir)) {
|
|
2736
3941
|
const graph = readGraph(rootDir, layer);
|
|
2737
3942
|
if (graph) result[layer] = graph;
|
|
2738
3943
|
}
|
|
@@ -2746,22 +3951,22 @@ async function generateGraph(rootDir, layer) {
|
|
|
2746
3951
|
mutationMethods: config.parsers?.patterns?.mutationMethods
|
|
2747
3952
|
});
|
|
2748
3953
|
const dir = graphsDir(rootDir);
|
|
2749
|
-
(0,
|
|
3954
|
+
(0, import_node_fs15.mkdirSync)(dir, { recursive: true });
|
|
2750
3955
|
const results = layer ? [generateLayer(rootDir, layer)].filter((r) => r !== null) : generateAll(rootDir);
|
|
2751
3956
|
for (const result of results) {
|
|
2752
3957
|
const filePath = graphFilePath(rootDir, result.layer);
|
|
2753
|
-
(0,
|
|
3958
|
+
(0, import_node_fs15.writeFileSync)(filePath, JSON.stringify(result.output, null, 2) + "\n", "utf-8");
|
|
2754
3959
|
invalidateCache(filePath);
|
|
2755
3960
|
invalidateTaggedCache(rootDir, result.layer);
|
|
2756
3961
|
}
|
|
2757
3962
|
return results;
|
|
2758
3963
|
}
|
|
2759
|
-
var
|
|
3964
|
+
var import_node_fs15, import_node_path17, GRAPHS_DIR2, graphCache, taggedCache;
|
|
2760
3965
|
var init_graph = __esm({
|
|
2761
3966
|
"src/server/graph/index.ts"() {
|
|
2762
3967
|
"use strict";
|
|
2763
|
-
|
|
2764
|
-
|
|
3968
|
+
import_node_fs15 = require("node:fs");
|
|
3969
|
+
import_node_path17 = require("node:path");
|
|
2765
3970
|
init_graph_builder();
|
|
2766
3971
|
init_config();
|
|
2767
3972
|
init_tagger_registry();
|
|
@@ -2769,12 +3974,294 @@ var init_graph = __esm({
|
|
|
2769
3974
|
init_ts_extractor();
|
|
2770
3975
|
init_tag_store();
|
|
2771
3976
|
GRAPHS_DIR2 = ".launchsecure/graphs";
|
|
2772
|
-
LAYERS = ["ui", "api", "db"];
|
|
2773
3977
|
graphCache = /* @__PURE__ */ new Map();
|
|
2774
3978
|
taggedCache = /* @__PURE__ */ new Map();
|
|
2775
3979
|
}
|
|
2776
3980
|
});
|
|
2777
3981
|
|
|
3982
|
+
// src/server/graph/core/audit-core.ts
|
|
3983
|
+
function readGraphFile(rootDir, layer) {
|
|
3984
|
+
const filePath = (0, import_node_path18.join)(rootDir, ".launchsecure", "graphs", `${layer}.json`);
|
|
3985
|
+
if (!(0, import_node_fs16.existsSync)(filePath)) return null;
|
|
3986
|
+
try {
|
|
3987
|
+
return JSON.parse((0, import_node_fs16.readFileSync)(filePath, "utf-8"));
|
|
3988
|
+
} catch {
|
|
3989
|
+
return null;
|
|
3990
|
+
}
|
|
3991
|
+
}
|
|
3992
|
+
function checkSchemaDrift(rootDir) {
|
|
3993
|
+
const findings = [];
|
|
3994
|
+
const db = readGraphFile(rootDir, "db");
|
|
3995
|
+
if (!db) {
|
|
3996
|
+
findings.push({ id: "no-db-graph", severity: "info", category: "schema_drift", title: "No DB graph", detail: "Run generate_graph first to populate the DB layer." });
|
|
3997
|
+
return buildReport("db", "schema_drift", findings);
|
|
3998
|
+
}
|
|
3999
|
+
for (const c of db.contradictions ?? []) {
|
|
4000
|
+
const isTableLevel = c.detail.includes("Table ") && (c.detail.includes("has no CREATE TABLE") || c.detail.includes("not in schema.prisma"));
|
|
4001
|
+
findings.push({
|
|
4002
|
+
id: `drift:${c.entity}`,
|
|
4003
|
+
severity: isTableLevel ? "error" : "warning",
|
|
4004
|
+
category: "schema_drift",
|
|
4005
|
+
title: c.entity,
|
|
4006
|
+
detail: c.detail
|
|
4007
|
+
});
|
|
4008
|
+
}
|
|
4009
|
+
return buildReport("db", "schema_drift", findings);
|
|
4010
|
+
}
|
|
4011
|
+
function checkOrphanFks(rootDir) {
|
|
4012
|
+
const findings = [];
|
|
4013
|
+
const db = readGraphFile(rootDir, "db");
|
|
4014
|
+
if (!db) return buildReport("db", "orphan_fks", findings);
|
|
4015
|
+
for (const f of db.flagged_edges ?? []) {
|
|
4016
|
+
findings.push({
|
|
4017
|
+
id: `fk:${f.source}->${f.target}`,
|
|
4018
|
+
severity: "warning",
|
|
4019
|
+
category: "orphan_fks",
|
|
4020
|
+
title: `${f.source} \u2192 ${f.target}`,
|
|
4021
|
+
detail: f.label
|
|
4022
|
+
});
|
|
4023
|
+
}
|
|
4024
|
+
return buildReport("db", "orphan_fks", findings);
|
|
4025
|
+
}
|
|
4026
|
+
function checkUnprotectedRoutes(rootDir) {
|
|
4027
|
+
const findings = [];
|
|
4028
|
+
const api = readGraphFile(rootDir, "api");
|
|
4029
|
+
const staticGraph = readGraphFile(rootDir, "static");
|
|
4030
|
+
if (!api) return buildReport("api", "unprotected_routes", findings);
|
|
4031
|
+
const routePermsPath = (0, import_node_path18.join)(rootDir, "src", "config", "route-permissions.ts");
|
|
4032
|
+
let routePermsContent = "";
|
|
4033
|
+
if ((0, import_node_fs16.existsSync)(routePermsPath)) {
|
|
4034
|
+
routePermsContent = (0, import_node_fs16.readFileSync)(routePermsPath, "utf-8");
|
|
4035
|
+
}
|
|
4036
|
+
const registeredRoutes = /* @__PURE__ */ new Set();
|
|
4037
|
+
const routeEntryRe = /path:\s*'([^']+)'/g;
|
|
4038
|
+
let rm;
|
|
4039
|
+
while ((rm = routeEntryRe.exec(routePermsContent)) !== null) {
|
|
4040
|
+
registeredRoutes.add(rm[1].replace(/:(\w+)/g, "[$1]"));
|
|
4041
|
+
}
|
|
4042
|
+
for (const node of api.nodes) {
|
|
4043
|
+
if (node.type !== "endpoint") continue;
|
|
4044
|
+
const route = node.route ?? "";
|
|
4045
|
+
if (!route) continue;
|
|
4046
|
+
const normalized = "/api" + (route.startsWith("/") ? route : "/" + route);
|
|
4047
|
+
const isRegistered = registeredRoutes.has(normalized) || [...registeredRoutes].some((r) => routeMatchesPattern(normalized, r));
|
|
4048
|
+
if (!isRegistered) {
|
|
4049
|
+
const methods = node.methods ?? [];
|
|
4050
|
+
findings.push({
|
|
4051
|
+
id: `unprotected:${node.id}`,
|
|
4052
|
+
severity: "warning",
|
|
4053
|
+
category: "unprotected_routes",
|
|
4054
|
+
title: `${methods.join(",")} ${route}`,
|
|
4055
|
+
detail: `API endpoint has no entry in ROUTE_PERMISSIONS. Methods: ${methods.join(", ")}`,
|
|
4056
|
+
file: node.id
|
|
4057
|
+
});
|
|
4058
|
+
}
|
|
4059
|
+
}
|
|
4060
|
+
return buildReport("api", "unprotected_routes", findings);
|
|
4061
|
+
}
|
|
4062
|
+
function routeMatchesPattern(route, pattern) {
|
|
4063
|
+
const routeParts = route.split("/");
|
|
4064
|
+
const patternParts = pattern.split("/");
|
|
4065
|
+
if (routeParts.length !== patternParts.length) return false;
|
|
4066
|
+
for (let i = 0; i < routeParts.length; i++) {
|
|
4067
|
+
const rp = routeParts[i];
|
|
4068
|
+
const pp = patternParts[i];
|
|
4069
|
+
if (rp === pp) continue;
|
|
4070
|
+
if (pp.startsWith("[") || pp.startsWith(":")) continue;
|
|
4071
|
+
if (rp.startsWith("[") || rp.startsWith(":")) continue;
|
|
4072
|
+
return false;
|
|
4073
|
+
}
|
|
4074
|
+
return true;
|
|
4075
|
+
}
|
|
4076
|
+
function checkDeadScreens(rootDir) {
|
|
4077
|
+
const findings = [];
|
|
4078
|
+
const ui = readGraphFile(rootDir, "ui");
|
|
4079
|
+
if (!ui) return buildReport("ui", "dead_screens", findings);
|
|
4080
|
+
const pages = ui.nodes.filter((n) => n.type === "page" || n.type === "layout");
|
|
4081
|
+
const navTargets = /* @__PURE__ */ new Set();
|
|
4082
|
+
for (const e of ui.edges) {
|
|
4083
|
+
if (e.type === "navigates" || e.type === "renders" || e.type === "imports") {
|
|
4084
|
+
navTargets.add(e.target);
|
|
4085
|
+
}
|
|
4086
|
+
}
|
|
4087
|
+
for (const cr of ui.cross_refs ?? []) {
|
|
4088
|
+
navTargets.add(cr.target);
|
|
4089
|
+
}
|
|
4090
|
+
for (const page of pages) {
|
|
4091
|
+
if (page.id.endsWith("layout.tsx") && page.id.split("/").length <= 2) continue;
|
|
4092
|
+
if (["error.tsx", "loading.tsx", "not-found.tsx", "template.tsx"].some((s) => page.id.endsWith(s))) continue;
|
|
4093
|
+
if (!navTargets.has(page.id)) {
|
|
4094
|
+
findings.push({
|
|
4095
|
+
id: `dead:${page.id}`,
|
|
4096
|
+
severity: "info",
|
|
4097
|
+
category: "dead_screens",
|
|
4098
|
+
title: page.name,
|
|
4099
|
+
detail: `Page "${page.id}" has no incoming navigation, render, or import edges.`,
|
|
4100
|
+
file: page.id
|
|
4101
|
+
});
|
|
4102
|
+
}
|
|
4103
|
+
}
|
|
4104
|
+
return buildReport("ui", "dead_screens", findings);
|
|
4105
|
+
}
|
|
4106
|
+
function checkUnenforcedPermissions(rootDir) {
|
|
4107
|
+
const findings = [];
|
|
4108
|
+
const staticGraph = readGraphFile(rootDir, "static");
|
|
4109
|
+
if (!staticGraph) return buildReport("static", "unenforced_permissions", findings);
|
|
4110
|
+
const permissions = staticGraph.nodes.filter((n) => n.type === "seed_permission").map((n) => ({ id: n.id, key: n.value, name: n.name }));
|
|
4111
|
+
const routePermsPath = (0, import_node_path18.join)(rootDir, "src", "config", "route-permissions.ts");
|
|
4112
|
+
let routePermsContent = "";
|
|
4113
|
+
if ((0, import_node_fs16.existsSync)(routePermsPath)) {
|
|
4114
|
+
routePermsContent = (0, import_node_fs16.readFileSync)(routePermsPath, "utf-8");
|
|
4115
|
+
}
|
|
4116
|
+
for (const perm of permissions) {
|
|
4117
|
+
const regex = new RegExp(`permission:\\s*['"]${perm.key}['"]`);
|
|
4118
|
+
if (!regex.test(routePermsContent)) {
|
|
4119
|
+
findings.push({
|
|
4120
|
+
id: `unenforced:${perm.key}`,
|
|
4121
|
+
severity: "warning",
|
|
4122
|
+
category: "unenforced_permissions",
|
|
4123
|
+
title: `${perm.name} (${perm.key})`,
|
|
4124
|
+
detail: `Permission "${perm.key}" exists in seed data but has no entry in ROUTE_PERMISSIONS \u2014 no API route requires it.`
|
|
4125
|
+
});
|
|
4126
|
+
}
|
|
4127
|
+
}
|
|
4128
|
+
return buildReport("static", "unenforced_permissions", findings);
|
|
4129
|
+
}
|
|
4130
|
+
function checkHardcodedValues(rootDir) {
|
|
4131
|
+
const findings = [];
|
|
4132
|
+
const staticGraph = readGraphFile(rootDir, "static");
|
|
4133
|
+
if (!staticGraph) return buildReport("static", "hardcoded_values", findings);
|
|
4134
|
+
const knownValues = /* @__PURE__ */ new Set();
|
|
4135
|
+
for (const n of staticGraph.nodes) {
|
|
4136
|
+
if (n.type === "enum_value") knownValues.add(n.value);
|
|
4137
|
+
}
|
|
4138
|
+
const api = readGraphFile(rootDir, "api");
|
|
4139
|
+
if (!api) return buildReport("static", "hardcoded_values", findings);
|
|
4140
|
+
const allCapsRe = /['"]([A-Z][A-Z_]{2,})['"]/g;
|
|
4141
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4142
|
+
for (const node of api.nodes) {
|
|
4143
|
+
if (node.type !== "endpoint") continue;
|
|
4144
|
+
const filePath = (0, import_node_path18.join)(rootDir, "src", node.id);
|
|
4145
|
+
if (!(0, import_node_fs16.existsSync)(filePath)) continue;
|
|
4146
|
+
const content = (0, import_node_fs16.readFileSync)(filePath, "utf-8");
|
|
4147
|
+
let m;
|
|
4148
|
+
allCapsRe.lastIndex = 0;
|
|
4149
|
+
while ((m = allCapsRe.exec(content)) !== null) {
|
|
4150
|
+
const val = m[1];
|
|
4151
|
+
if (knownValues.has(val)) continue;
|
|
4152
|
+
if (["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS", "UTF", "NULL", "TRUE", "FALSE", "JSON", "HTML", "CSS", "ENV"].includes(val)) continue;
|
|
4153
|
+
const key = `${node.id}:${val}`;
|
|
4154
|
+
if (seen.has(key)) continue;
|
|
4155
|
+
seen.add(key);
|
|
4156
|
+
findings.push({
|
|
4157
|
+
id: `hardcoded:${key}`,
|
|
4158
|
+
severity: "info",
|
|
4159
|
+
category: "hardcoded_values",
|
|
4160
|
+
title: `"${val}" in ${node.id}`,
|
|
4161
|
+
detail: `ALL_CAPS string literal "${val}" appears in API code but is not in the enum/seed inventory. May be an unregistered constant.`,
|
|
4162
|
+
file: node.id
|
|
4163
|
+
});
|
|
4164
|
+
}
|
|
4165
|
+
}
|
|
4166
|
+
return buildReport("static", "hardcoded_values", findings);
|
|
4167
|
+
}
|
|
4168
|
+
function buildReport(layer, check, findings) {
|
|
4169
|
+
return {
|
|
4170
|
+
layer,
|
|
4171
|
+
check,
|
|
4172
|
+
findings,
|
|
4173
|
+
summary: {
|
|
4174
|
+
errors: findings.filter((f) => f.severity === "error").length,
|
|
4175
|
+
warnings: findings.filter((f) => f.severity === "warning").length,
|
|
4176
|
+
info: findings.filter((f) => f.severity === "info").length
|
|
4177
|
+
},
|
|
4178
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
4179
|
+
};
|
|
4180
|
+
}
|
|
4181
|
+
function getAvailableChecks() {
|
|
4182
|
+
const result = {};
|
|
4183
|
+
for (const [layer, checks] of Object.entries(CHECKS)) {
|
|
4184
|
+
result[layer] = Object.keys(checks);
|
|
4185
|
+
}
|
|
4186
|
+
return result;
|
|
4187
|
+
}
|
|
4188
|
+
function runAudit(rootDir, layer, check) {
|
|
4189
|
+
if (layer === "all") {
|
|
4190
|
+
const reports = [];
|
|
4191
|
+
for (const [l, checks] of Object.entries(CHECKS)) {
|
|
4192
|
+
for (const fn of Object.values(checks)) {
|
|
4193
|
+
reports.push(fn(rootDir));
|
|
4194
|
+
}
|
|
4195
|
+
}
|
|
4196
|
+
return reports;
|
|
4197
|
+
}
|
|
4198
|
+
const layerChecks = CHECKS[layer];
|
|
4199
|
+
if (!layerChecks) {
|
|
4200
|
+
return [{
|
|
4201
|
+
layer,
|
|
4202
|
+
check: "unknown",
|
|
4203
|
+
findings: [{ id: "invalid-layer", severity: "error", category: "system", title: "Invalid layer", detail: `Layer "${layer}" has no audit checks. Available: ${Object.keys(CHECKS).join(", ")}` }],
|
|
4204
|
+
summary: { errors: 1, warnings: 0, info: 0 },
|
|
4205
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
4206
|
+
}];
|
|
4207
|
+
}
|
|
4208
|
+
if (check) {
|
|
4209
|
+
const fn = layerChecks[check];
|
|
4210
|
+
if (!fn) {
|
|
4211
|
+
return [{
|
|
4212
|
+
layer,
|
|
4213
|
+
check,
|
|
4214
|
+
findings: [{ id: "invalid-check", severity: "error", category: "system", title: "Invalid check", detail: `Check "${check}" not found for layer "${layer}". Available: ${Object.keys(layerChecks).join(", ")}` }],
|
|
4215
|
+
summary: { errors: 1, warnings: 0, info: 0 },
|
|
4216
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
4217
|
+
}];
|
|
4218
|
+
}
|
|
4219
|
+
return [fn(rootDir)];
|
|
4220
|
+
}
|
|
4221
|
+
return Object.values(layerChecks).map((fn) => fn(rootDir));
|
|
4222
|
+
}
|
|
4223
|
+
function formatAsPrompt(reports) {
|
|
4224
|
+
const lines = [];
|
|
4225
|
+
for (const report of reports) {
|
|
4226
|
+
if (report.findings.length === 0) continue;
|
|
4227
|
+
lines.push(`## ${report.layer.toUpperCase()} \u2014 ${report.check} (${report.findings.length} findings)`);
|
|
4228
|
+
lines.push("");
|
|
4229
|
+
for (const f of report.findings) {
|
|
4230
|
+
const tag = f.severity === "error" ? "ERROR" : f.severity === "warning" ? "WARNING" : "INFO";
|
|
4231
|
+
const filePart = f.file ? ` (${f.file}${f.line ? `:${f.line}` : ""})` : "";
|
|
4232
|
+
lines.push(`- [${tag}] ${f.title}${filePart}`);
|
|
4233
|
+
lines.push(` ${f.detail}`);
|
|
4234
|
+
}
|
|
4235
|
+
lines.push("");
|
|
4236
|
+
}
|
|
4237
|
+
if (lines.length === 0) return "No audit findings.";
|
|
4238
|
+
return lines.join("\n");
|
|
4239
|
+
}
|
|
4240
|
+
var import_node_fs16, import_node_path18, CHECKS;
|
|
4241
|
+
var init_audit_core = __esm({
|
|
4242
|
+
"src/server/graph/core/audit-core.ts"() {
|
|
4243
|
+
"use strict";
|
|
4244
|
+
import_node_fs16 = require("node:fs");
|
|
4245
|
+
import_node_path18 = require("node:path");
|
|
4246
|
+
CHECKS = {
|
|
4247
|
+
db: {
|
|
4248
|
+
schema_drift: checkSchemaDrift,
|
|
4249
|
+
orphan_fks: checkOrphanFks
|
|
4250
|
+
},
|
|
4251
|
+
api: {
|
|
4252
|
+
unprotected_routes: checkUnprotectedRoutes
|
|
4253
|
+
},
|
|
4254
|
+
ui: {
|
|
4255
|
+
dead_screens: checkDeadScreens
|
|
4256
|
+
},
|
|
4257
|
+
static: {
|
|
4258
|
+
unenforced_permissions: checkUnenforcedPermissions,
|
|
4259
|
+
hardcoded_values: checkHardcodedValues
|
|
4260
|
+
}
|
|
4261
|
+
};
|
|
4262
|
+
}
|
|
4263
|
+
});
|
|
4264
|
+
|
|
2778
4265
|
// src/server/chart-serve.ts
|
|
2779
4266
|
var chart_serve_exports = {};
|
|
2780
4267
|
__export(chart_serve_exports, {
|
|
@@ -2787,31 +4274,39 @@ function randomPort() {
|
|
|
2787
4274
|
function findProjectRoot(startDir) {
|
|
2788
4275
|
let dir = startDir;
|
|
2789
4276
|
for (let i = 0; i < 8; i++) {
|
|
2790
|
-
const graphsDir2 =
|
|
2791
|
-
if (
|
|
2792
|
-
const parent =
|
|
4277
|
+
const graphsDir2 = import_node_path19.default.join(dir, ".launchsecure", "graphs");
|
|
4278
|
+
if (import_node_fs17.default.existsSync(import_node_path19.default.join(graphsDir2, "ui.json")) || import_node_fs17.default.existsSync(import_node_path19.default.join(graphsDir2, "api.json")) || import_node_fs17.default.existsSync(import_node_path19.default.join(graphsDir2, "db.json"))) return dir;
|
|
4279
|
+
const parent = import_node_path19.default.dirname(dir);
|
|
2793
4280
|
if (parent === dir) break;
|
|
2794
4281
|
dir = parent;
|
|
2795
4282
|
}
|
|
2796
4283
|
dir = startDir;
|
|
2797
4284
|
for (let i = 0; i < 8; i++) {
|
|
2798
|
-
if (
|
|
2799
|
-
const parent =
|
|
4285
|
+
if (import_node_fs17.default.existsSync(import_node_path19.default.join(dir, ".git"))) return dir;
|
|
4286
|
+
const parent = import_node_path19.default.dirname(dir);
|
|
2800
4287
|
if (parent === dir) break;
|
|
2801
4288
|
dir = parent;
|
|
2802
4289
|
}
|
|
2803
4290
|
return startDir;
|
|
2804
4291
|
}
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
if (!
|
|
2808
|
-
|
|
2809
|
-
|
|
4292
|
+
function resolveRequestRoot(url, monorepoRoot, projects) {
|
|
4293
|
+
const projectParam = url.searchParams.get("project");
|
|
4294
|
+
if (!projectParam || projects.length === 0) return monorepoRoot;
|
|
4295
|
+
const resolved = import_node_path19.default.resolve(monorepoRoot, projectParam);
|
|
4296
|
+
if (!resolved.startsWith(monorepoRoot)) {
|
|
4297
|
+
throw new Error("Project path outside monorepo root");
|
|
4298
|
+
}
|
|
4299
|
+
return resolved;
|
|
4300
|
+
}
|
|
4301
|
+
async function buildMergedGraph(root) {
|
|
4302
|
+
let graphs = readAllGraphs(root);
|
|
4303
|
+
if (Object.keys(graphs).length === 0) {
|
|
4304
|
+
await generateGraph(root);
|
|
4305
|
+
graphs = readAllGraphs(root);
|
|
2810
4306
|
}
|
|
2811
4307
|
const nodes = [];
|
|
2812
4308
|
const rawLinks = [];
|
|
2813
|
-
const
|
|
2814
|
-
for (const layer of LAYERS2) {
|
|
4309
|
+
for (const layer of Object.keys(graphs)) {
|
|
2815
4310
|
const g = graphs[layer];
|
|
2816
4311
|
if (!g) continue;
|
|
2817
4312
|
for (const n of g.nodes) {
|
|
@@ -2848,16 +4343,16 @@ async function buildMergedGraph(projectRoot) {
|
|
|
2848
4343
|
};
|
|
2849
4344
|
}
|
|
2850
4345
|
function serveStatic(res, filePath) {
|
|
2851
|
-
if (!
|
|
2852
|
-
const ext =
|
|
4346
|
+
if (!import_node_fs17.default.existsSync(filePath) || !import_node_fs17.default.statSync(filePath).isFile()) return false;
|
|
4347
|
+
const ext = import_node_path19.default.extname(filePath).toLowerCase();
|
|
2853
4348
|
const mime = MIME_TYPES[ext] ?? "application/octet-stream";
|
|
2854
4349
|
res.writeHead(200, { "Content-Type": mime, "Cache-Control": "no-cache" });
|
|
2855
|
-
|
|
4350
|
+
import_node_fs17.default.createReadStream(filePath).pipe(res);
|
|
2856
4351
|
return true;
|
|
2857
4352
|
}
|
|
2858
4353
|
function serveIndex(res, clientDir) {
|
|
2859
|
-
const indexPath =
|
|
2860
|
-
if (!
|
|
4354
|
+
const indexPath = import_node_path19.default.join(clientDir, "index.html");
|
|
4355
|
+
if (!import_node_fs17.default.existsSync(indexPath)) {
|
|
2861
4356
|
res.writeHead(500, { "Content-Type": "text/plain" });
|
|
2862
4357
|
res.end(`LaunchChart client bundle not found at ${clientDir}. Run 'npm run build:chart-client'.`);
|
|
2863
4358
|
return;
|
|
@@ -2909,19 +4404,40 @@ async function startChartServer(opts = {}) {
|
|
|
2909
4404
|
}
|
|
2910
4405
|
return { port: existing.port, url: existing.url };
|
|
2911
4406
|
}
|
|
2912
|
-
const clientDir = opts.clientDir ??
|
|
4407
|
+
const clientDir = opts.clientDir ?? import_node_path19.default.join(__dirname, "..", "chart-client");
|
|
4408
|
+
const rootConfig = loadConfig(projectRoot);
|
|
4409
|
+
const projects = rootConfig.projects ?? [];
|
|
2913
4410
|
const server = import_node_http.default.createServer((req, res) => {
|
|
2914
4411
|
try {
|
|
2915
4412
|
const url2 = new URL(req.url ?? "/", `http://${req.headers.host}`);
|
|
4413
|
+
let reqRoot;
|
|
4414
|
+
try {
|
|
4415
|
+
reqRoot = resolveRequestRoot(url2, projectRoot, projects);
|
|
4416
|
+
} catch (err2) {
|
|
4417
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
4418
|
+
res.end(JSON.stringify({ ok: false, error: String(err2) }));
|
|
4419
|
+
return;
|
|
4420
|
+
}
|
|
4421
|
+
if (req.method === "GET" && url2.pathname === "/api/projects") {
|
|
4422
|
+
const projectList = projects.length > 0 ? projects.map((p) => {
|
|
4423
|
+
const absRoot = import_node_path19.default.resolve(projectRoot, p.root);
|
|
4424
|
+
const hasGraphs = import_node_fs17.default.existsSync(import_node_path19.default.join(absRoot, ".launchsecure", "graphs"));
|
|
4425
|
+
const hasNextConfig2 = import_node_fs17.default.existsSync(import_node_path19.default.join(absRoot, "next.config.ts")) || import_node_fs17.default.existsSync(import_node_path19.default.join(absRoot, "next.config.js")) || import_node_fs17.default.existsSync(import_node_path19.default.join(absRoot, "next.config.mjs"));
|
|
4426
|
+
return { name: p.name, root: p.root, hasGraphs, hasNextConfig: hasNextConfig2 };
|
|
4427
|
+
}) : [{ name: import_node_path19.default.basename(projectRoot), root: ".", hasGraphs: import_node_fs17.default.existsSync(import_node_path19.default.join(projectRoot, ".launchsecure", "graphs")), hasNextConfig: true }];
|
|
4428
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
4429
|
+
res.end(JSON.stringify({ projects: projectList, monorepoRoot: projectRoot }));
|
|
4430
|
+
return;
|
|
4431
|
+
}
|
|
2916
4432
|
if (req.method === "GET" && url2.pathname === "/api/project-graph") {
|
|
2917
4433
|
const regenerate = url2.searchParams.get("regenerate") === "1";
|
|
2918
4434
|
(async () => {
|
|
2919
|
-
if (regenerate) await generateGraph(
|
|
2920
|
-
const merged = await buildMergedGraph(
|
|
4435
|
+
if (regenerate) await generateGraph(reqRoot);
|
|
4436
|
+
const merged = await buildMergedGraph(reqRoot);
|
|
2921
4437
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2922
4438
|
res.end(JSON.stringify({
|
|
2923
4439
|
...merged,
|
|
2924
|
-
debug: { cwd, projectRoot }
|
|
4440
|
+
debug: { cwd, projectRoot: reqRoot }
|
|
2925
4441
|
}));
|
|
2926
4442
|
})().catch((e) => {
|
|
2927
4443
|
res.writeHead(500);
|
|
@@ -2930,15 +4446,15 @@ async function startChartServer(opts = {}) {
|
|
|
2930
4446
|
return;
|
|
2931
4447
|
}
|
|
2932
4448
|
if (req.method === "GET" && url2.pathname === "/api/raw-graphs") {
|
|
2933
|
-
const graphs = readAllGraphs(
|
|
4449
|
+
const graphs = readAllGraphs(reqRoot);
|
|
2934
4450
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2935
4451
|
res.end(JSON.stringify({ ui: graphs.ui ?? null, api: graphs.api ?? null, db: graphs.db ?? null }));
|
|
2936
4452
|
return;
|
|
2937
4453
|
}
|
|
2938
4454
|
if (req.method === "POST" && url2.pathname === "/api/generate-graph") {
|
|
2939
4455
|
(async () => {
|
|
2940
|
-
await generateGraph(
|
|
2941
|
-
const graphs = readAllGraphs(
|
|
4456
|
+
await generateGraph(reqRoot);
|
|
4457
|
+
const graphs = readAllGraphs(reqRoot);
|
|
2942
4458
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2943
4459
|
res.end(JSON.stringify({
|
|
2944
4460
|
ok: true,
|
|
@@ -2954,41 +4470,43 @@ async function startChartServer(opts = {}) {
|
|
|
2954
4470
|
}
|
|
2955
4471
|
if (req.method === "GET" && url2.pathname === "/api/file-content") {
|
|
2956
4472
|
const relPath = url2.searchParams.get("path");
|
|
2957
|
-
if (!relPath || relPath.includes("..") ||
|
|
4473
|
+
if (!relPath || relPath.includes("..") || import_node_path19.default.isAbsolute(relPath)) {
|
|
2958
4474
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
2959
4475
|
res.end(JSON.stringify({ error: "Invalid path" }));
|
|
2960
4476
|
return;
|
|
2961
4477
|
}
|
|
2962
|
-
const filePath =
|
|
2963
|
-
if (!filePath.startsWith(
|
|
4478
|
+
const filePath = import_node_path19.default.join(reqRoot, relPath);
|
|
4479
|
+
if (!filePath.startsWith(reqRoot) || !import_node_fs17.default.existsSync(filePath) || !import_node_fs17.default.statSync(filePath).isFile()) {
|
|
2964
4480
|
res.writeHead(404, { "Content-Type": "application/json" });
|
|
2965
4481
|
res.end(JSON.stringify({ error: "File not found" }));
|
|
2966
4482
|
return;
|
|
2967
4483
|
}
|
|
2968
|
-
const ext =
|
|
4484
|
+
const ext = import_node_path19.default.extname(filePath).toLowerCase();
|
|
2969
4485
|
const langMap = { ".ts": "typescript", ".tsx": "tsx", ".js": "javascript", ".jsx": "jsx", ".prisma": "prisma", ".json": "json", ".css": "css" };
|
|
2970
|
-
const content =
|
|
4486
|
+
const content = import_node_fs17.default.readFileSync(filePath, "utf-8");
|
|
2971
4487
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2972
4488
|
res.end(JSON.stringify({ content, language: langMap[ext] ?? "text", path: relPath }));
|
|
2973
4489
|
return;
|
|
2974
4490
|
}
|
|
2975
4491
|
if (req.method === "GET" && url2.pathname === "/api/health") {
|
|
2976
4492
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2977
|
-
res.end(JSON.stringify({ ok: true, projectRoot }));
|
|
4493
|
+
res.end(JSON.stringify({ ok: true, projectRoot: reqRoot }));
|
|
2978
4494
|
return;
|
|
2979
4495
|
}
|
|
2980
4496
|
if (req.method === "GET" && url2.pathname === "/api/parser-config") {
|
|
2981
|
-
const config2 = loadConfig(
|
|
2982
|
-
const
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
4497
|
+
const config2 = loadConfig(reqRoot);
|
|
4498
|
+
const registry = createRegistry(config2, reqRoot);
|
|
4499
|
+
const detection = [];
|
|
4500
|
+
for (const parser of registry.getAll()) {
|
|
4501
|
+
if ("layers" in parser && Array.isArray(parser.layers)) {
|
|
4502
|
+
const mp = parser;
|
|
4503
|
+
detection.push({ id: mp.id, layers: mp.layers, detected: mp.detect(reqRoot) });
|
|
4504
|
+
} else if ("layer" in parser && parser.layer !== "crosslayer") {
|
|
4505
|
+
const sp = parser;
|
|
4506
|
+
detection.push({ id: sp.id, layers: [sp.layer], detected: sp.detect(reqRoot) });
|
|
4507
|
+
}
|
|
4508
|
+
}
|
|
4509
|
+
const crosslayerParsers = registry.getCrossLayerParsers().map((p) => ({ id: p.id }));
|
|
2992
4510
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
2993
4511
|
res.end(JSON.stringify({ config: config2, detection, crosslayerParsers }));
|
|
2994
4512
|
return;
|
|
@@ -3001,8 +4519,8 @@ async function startChartServer(opts = {}) {
|
|
|
3001
4519
|
req.on("end", () => {
|
|
3002
4520
|
try {
|
|
3003
4521
|
const newConfig = JSON.parse(body);
|
|
3004
|
-
const configPath =
|
|
3005
|
-
|
|
4522
|
+
const configPath = import_node_path19.default.join(reqRoot, ".launchchart.json");
|
|
4523
|
+
import_node_fs17.default.writeFileSync(configPath, JSON.stringify(newConfig, null, 2) + "\n", "utf-8");
|
|
3006
4524
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
3007
4525
|
res.end(JSON.stringify({ ok: true }));
|
|
3008
4526
|
} catch (err2) {
|
|
@@ -3013,7 +4531,7 @@ async function startChartServer(opts = {}) {
|
|
|
3013
4531
|
return;
|
|
3014
4532
|
}
|
|
3015
4533
|
if (req.method === "GET" && url2.pathname === "/api/tagger-config") {
|
|
3016
|
-
const config2 = loadConfig(
|
|
4534
|
+
const config2 = loadConfig(reqRoot);
|
|
3017
4535
|
const builtinTaggers = [
|
|
3018
4536
|
{ id: "module", tagKey: "module", trackUntagged: config2.taggers?.trackUntagged?.module ?? true },
|
|
3019
4537
|
{ id: "screen", tagKey: "screen", trackUntagged: config2.taggers?.trackUntagged?.screen ?? true }
|
|
@@ -3033,10 +4551,10 @@ async function startChartServer(opts = {}) {
|
|
|
3033
4551
|
req.on("end", () => {
|
|
3034
4552
|
try {
|
|
3035
4553
|
const taggerConfig = JSON.parse(body);
|
|
3036
|
-
const config2 = loadConfig(
|
|
4554
|
+
const config2 = loadConfig(reqRoot);
|
|
3037
4555
|
config2.taggers = taggerConfig;
|
|
3038
|
-
const configPath =
|
|
3039
|
-
|
|
4556
|
+
const configPath = import_node_path19.default.join(reqRoot, ".launchchart.json");
|
|
4557
|
+
import_node_fs17.default.writeFileSync(configPath, JSON.stringify(config2, null, 2) + "\n", "utf-8");
|
|
3040
4558
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
3041
4559
|
res.end(JSON.stringify({ ok: true }));
|
|
3042
4560
|
} catch (err2) {
|
|
@@ -3047,7 +4565,7 @@ async function startChartServer(opts = {}) {
|
|
|
3047
4565
|
return;
|
|
3048
4566
|
}
|
|
3049
4567
|
if (req.method === "GET" && url2.pathname === "/api/tags") {
|
|
3050
|
-
const store = readTagStore(
|
|
4568
|
+
const store = readTagStore(reqRoot);
|
|
3051
4569
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
3052
4570
|
res.end(JSON.stringify(store));
|
|
3053
4571
|
return;
|
|
@@ -3065,7 +4583,7 @@ async function startChartServer(opts = {}) {
|
|
|
3065
4583
|
res.end(JSON.stringify({ ok: false, error: "nodeId, key, and value are required" }));
|
|
3066
4584
|
return;
|
|
3067
4585
|
}
|
|
3068
|
-
setTag(
|
|
4586
|
+
setTag(reqRoot, nodeId, key, value);
|
|
3069
4587
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
3070
4588
|
res.end(JSON.stringify({ ok: true }));
|
|
3071
4589
|
} catch (err2) {
|
|
@@ -3088,7 +4606,76 @@ async function startChartServer(opts = {}) {
|
|
|
3088
4606
|
res.end(JSON.stringify({ ok: false, error: "nodeId and key are required" }));
|
|
3089
4607
|
return;
|
|
3090
4608
|
}
|
|
3091
|
-
removeTag(
|
|
4609
|
+
removeTag(reqRoot, nodeId, key);
|
|
4610
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
4611
|
+
res.end(JSON.stringify({ ok: true }));
|
|
4612
|
+
} catch (err2) {
|
|
4613
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
4614
|
+
res.end(JSON.stringify({ ok: false, error: String(err2) }));
|
|
4615
|
+
}
|
|
4616
|
+
});
|
|
4617
|
+
return;
|
|
4618
|
+
}
|
|
4619
|
+
if (req.method === "GET" && url2.pathname === "/api/detected-paths") {
|
|
4620
|
+
const config2 = loadConfig(reqRoot);
|
|
4621
|
+
const paths = resolveProjectPaths(reqRoot, config2);
|
|
4622
|
+
const isOverride = !!config2.paths?.appDir;
|
|
4623
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
4624
|
+
res.end(JSON.stringify({
|
|
4625
|
+
projectRoot: reqRoot,
|
|
4626
|
+
detected: paths ? {
|
|
4627
|
+
srcDir: import_node_path19.default.relative(reqRoot, paths.srcDir) || ".",
|
|
4628
|
+
appDir: import_node_path19.default.relative(reqRoot, paths.appDir),
|
|
4629
|
+
apiDir: import_node_path19.default.relative(reqRoot, paths.apiDir)
|
|
4630
|
+
} : null,
|
|
4631
|
+
isOverride
|
|
4632
|
+
}));
|
|
4633
|
+
return;
|
|
4634
|
+
}
|
|
4635
|
+
if (req.method === "GET" && url2.pathname === "/api/browse-dir") {
|
|
4636
|
+
const browsePath = url2.searchParams.get("path") || projectRoot;
|
|
4637
|
+
const abs = import_node_path19.default.resolve(browsePath);
|
|
4638
|
+
const twoUp = import_node_path19.default.resolve(projectRoot, "..", "..");
|
|
4639
|
+
if (!abs.startsWith(twoUp)) {
|
|
4640
|
+
res.writeHead(403, { "Content-Type": "application/json" });
|
|
4641
|
+
res.end(JSON.stringify({ ok: false, error: "Path outside allowed range" }));
|
|
4642
|
+
return;
|
|
4643
|
+
}
|
|
4644
|
+
try {
|
|
4645
|
+
const entries = import_node_fs17.default.readdirSync(abs, { withFileTypes: true });
|
|
4646
|
+
const dirs = entries.filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules" && e.name !== "dist" && e.name !== ".next").map((e) => e.name).sort();
|
|
4647
|
+
const parent = abs !== twoUp ? import_node_path19.default.dirname(abs) : null;
|
|
4648
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
4649
|
+
res.end(JSON.stringify({ current: abs, parent, dirs, relative: import_node_path19.default.relative(projectRoot, abs) || "." }));
|
|
4650
|
+
} catch (err2) {
|
|
4651
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
4652
|
+
res.end(JSON.stringify({ ok: false, error: String(err2) }));
|
|
4653
|
+
}
|
|
4654
|
+
return;
|
|
4655
|
+
}
|
|
4656
|
+
if (req.method === "GET" && url2.pathname === "/api/audit") {
|
|
4657
|
+
const layer = url2.searchParams.get("layer") ?? "all";
|
|
4658
|
+
const check = url2.searchParams.get("check") ?? void 0;
|
|
4659
|
+
const reports = runAudit(reqRoot, layer, check);
|
|
4660
|
+
const prompt = formatAsPrompt(reports);
|
|
4661
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
4662
|
+
res.end(JSON.stringify({ reports, prompt, checks: getAvailableChecks() }));
|
|
4663
|
+
return;
|
|
4664
|
+
}
|
|
4665
|
+
if (req.method === "POST" && url2.pathname === "/api/projects") {
|
|
4666
|
+
let body = "";
|
|
4667
|
+
req.on("data", (chunk) => {
|
|
4668
|
+
body += chunk.toString();
|
|
4669
|
+
});
|
|
4670
|
+
req.on("end", () => {
|
|
4671
|
+
try {
|
|
4672
|
+
const { projects: newProjects } = JSON.parse(body);
|
|
4673
|
+
const config2 = loadConfig(projectRoot);
|
|
4674
|
+
config2.projects = newProjects.length > 0 ? newProjects : void 0;
|
|
4675
|
+
const configPath = import_node_path19.default.join(projectRoot, ".launchchart.json");
|
|
4676
|
+
import_node_fs17.default.writeFileSync(configPath, JSON.stringify(config2, null, 2) + "\n", "utf-8");
|
|
4677
|
+
projects.length = 0;
|
|
4678
|
+
if (config2.projects) projects.push(...config2.projects);
|
|
3092
4679
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
3093
4680
|
res.end(JSON.stringify({ ok: true }));
|
|
3094
4681
|
} catch (err2) {
|
|
@@ -3099,7 +4686,7 @@ async function startChartServer(opts = {}) {
|
|
|
3099
4686
|
return;
|
|
3100
4687
|
}
|
|
3101
4688
|
if (url2.pathname !== "/") {
|
|
3102
|
-
const staticPath =
|
|
4689
|
+
const staticPath = import_node_path19.default.join(clientDir, url2.pathname);
|
|
3103
4690
|
if (serveStatic(res, staticPath)) return;
|
|
3104
4691
|
}
|
|
3105
4692
|
serveIndex(res, clientDir);
|
|
@@ -3137,6 +4724,10 @@ async function startChartServer(opts = {}) {
|
|
|
3137
4724
|
`);
|
|
3138
4725
|
process.stderr.write(`[launch-chart] project root: ${projectRoot}
|
|
3139
4726
|
`);
|
|
4727
|
+
if (projects.length > 0) {
|
|
4728
|
+
process.stderr.write(`[launch-chart] multi-project mode: ${projects.length} projects
|
|
4729
|
+
`);
|
|
4730
|
+
}
|
|
3140
4731
|
}
|
|
3141
4732
|
return { port, url };
|
|
3142
4733
|
}
|
|
@@ -3155,19 +4746,19 @@ function runServeCli(argv) {
|
|
|
3155
4746
|
process.exit(1);
|
|
3156
4747
|
});
|
|
3157
4748
|
}
|
|
3158
|
-
var import_node_http,
|
|
4749
|
+
var import_node_http, import_node_fs17, import_node_path19, MAX_PORT_SCAN, MIME_TYPES;
|
|
3159
4750
|
var init_chart_serve = __esm({
|
|
3160
4751
|
"src/server/chart-serve.ts"() {
|
|
3161
4752
|
"use strict";
|
|
3162
4753
|
import_node_http = __toESM(require("node:http"));
|
|
3163
|
-
|
|
3164
|
-
|
|
4754
|
+
import_node_fs17 = __toESM(require("node:fs"));
|
|
4755
|
+
import_node_path19 = __toESM(require("node:path"));
|
|
3165
4756
|
init_graph();
|
|
3166
4757
|
init_lockfile();
|
|
3167
4758
|
init_config();
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
4759
|
+
init_parser_registry();
|
|
4760
|
+
init_resolve_paths();
|
|
4761
|
+
init_audit_core();
|
|
3171
4762
|
MAX_PORT_SCAN = 3;
|
|
3172
4763
|
MIME_TYPES = {
|
|
3173
4764
|
".html": "text/html; charset=utf-8",
|
|
@@ -3183,6 +4774,179 @@ var init_chart_serve = __esm({
|
|
|
3183
4774
|
}
|
|
3184
4775
|
});
|
|
3185
4776
|
|
|
4777
|
+
// src/server/graph/core/language-detection.ts
|
|
4778
|
+
function walkForExtensions(dir, extCounts, depth = 0) {
|
|
4779
|
+
if (depth > 10) return;
|
|
4780
|
+
if (!(0, import_node_fs18.existsSync)(dir)) return;
|
|
4781
|
+
let entries;
|
|
4782
|
+
try {
|
|
4783
|
+
entries = (0, import_node_fs18.readdirSync)(dir, { withFileTypes: true });
|
|
4784
|
+
} catch {
|
|
4785
|
+
return;
|
|
4786
|
+
}
|
|
4787
|
+
for (const entry of entries) {
|
|
4788
|
+
if (entry.name.startsWith(".") && entry.isDirectory()) continue;
|
|
4789
|
+
if (entry.isDirectory()) {
|
|
4790
|
+
if (IGNORE_DIRS.has(entry.name)) continue;
|
|
4791
|
+
walkForExtensions((0, import_node_path20.join)(dir, entry.name), extCounts, depth + 1);
|
|
4792
|
+
} else {
|
|
4793
|
+
const ext = (0, import_node_path20.extname)(entry.name).toLowerCase();
|
|
4794
|
+
if (ext && EXTENSION_TO_LANGUAGE[ext]) {
|
|
4795
|
+
extCounts.set(ext, (extCounts.get(ext) ?? 0) + 1);
|
|
4796
|
+
}
|
|
4797
|
+
}
|
|
4798
|
+
}
|
|
4799
|
+
}
|
|
4800
|
+
function detectLanguages(rootDir, supportedLanguages) {
|
|
4801
|
+
const extCounts = /* @__PURE__ */ new Map();
|
|
4802
|
+
walkForExtensions(rootDir, extCounts);
|
|
4803
|
+
const langData = /* @__PURE__ */ new Map();
|
|
4804
|
+
for (const [ext, count] of extCounts) {
|
|
4805
|
+
const lang = EXTENSION_TO_LANGUAGE[ext];
|
|
4806
|
+
if (!lang) continue;
|
|
4807
|
+
if (!langData.has(lang)) {
|
|
4808
|
+
langData.set(lang, { extensions: /* @__PURE__ */ new Set(), fileCount: 0 });
|
|
4809
|
+
}
|
|
4810
|
+
const data = langData.get(lang);
|
|
4811
|
+
data.extensions.add(ext);
|
|
4812
|
+
data.fileCount += count;
|
|
4813
|
+
}
|
|
4814
|
+
const results = [];
|
|
4815
|
+
for (const [lang, data] of langData) {
|
|
4816
|
+
if (AUXILIARY_LANGUAGES.has(lang)) continue;
|
|
4817
|
+
const parserIds = supportedLanguages.get(lang) ?? [];
|
|
4818
|
+
results.push({
|
|
4819
|
+
id: lang,
|
|
4820
|
+
extensions: [...data.extensions].sort(),
|
|
4821
|
+
fileCount: data.fileCount,
|
|
4822
|
+
supported: parserIds.length > 0,
|
|
4823
|
+
parserIds
|
|
4824
|
+
});
|
|
4825
|
+
}
|
|
4826
|
+
results.sort((a, b) => {
|
|
4827
|
+
if (a.supported !== b.supported) return a.supported ? -1 : 1;
|
|
4828
|
+
return b.fileCount - a.fileCount;
|
|
4829
|
+
});
|
|
4830
|
+
return results;
|
|
4831
|
+
}
|
|
4832
|
+
var import_node_fs18, import_node_path20, EXTENSION_TO_LANGUAGE, IGNORE_DIRS, AUXILIARY_LANGUAGES;
|
|
4833
|
+
var init_language_detection = __esm({
|
|
4834
|
+
"src/server/graph/core/language-detection.ts"() {
|
|
4835
|
+
"use strict";
|
|
4836
|
+
import_node_fs18 = require("node:fs");
|
|
4837
|
+
import_node_path20 = require("node:path");
|
|
4838
|
+
EXTENSION_TO_LANGUAGE = {
|
|
4839
|
+
// Web / Frontend
|
|
4840
|
+
".ts": "typescript",
|
|
4841
|
+
".tsx": "typescript",
|
|
4842
|
+
".js": "javascript",
|
|
4843
|
+
".jsx": "javascript",
|
|
4844
|
+
".mjs": "javascript",
|
|
4845
|
+
".cjs": "javascript",
|
|
4846
|
+
".vue": "vue",
|
|
4847
|
+
".svelte": "svelte",
|
|
4848
|
+
".html": "html",
|
|
4849
|
+
".css": "css",
|
|
4850
|
+
".scss": "scss",
|
|
4851
|
+
".less": "less",
|
|
4852
|
+
// Backend
|
|
4853
|
+
".py": "python",
|
|
4854
|
+
".go": "go",
|
|
4855
|
+
".rs": "rust",
|
|
4856
|
+
".rb": "ruby",
|
|
4857
|
+
".java": "java",
|
|
4858
|
+
".kt": "kotlin",
|
|
4859
|
+
".kts": "kotlin",
|
|
4860
|
+
".scala": "scala",
|
|
4861
|
+
".cs": "csharp",
|
|
4862
|
+
".fs": "fsharp",
|
|
4863
|
+
".php": "php",
|
|
4864
|
+
".ex": "elixir",
|
|
4865
|
+
".exs": "elixir",
|
|
4866
|
+
".erl": "erlang",
|
|
4867
|
+
".hs": "haskell",
|
|
4868
|
+
".lua": "lua",
|
|
4869
|
+
".pl": "perl",
|
|
4870
|
+
".r": "r",
|
|
4871
|
+
".R": "r",
|
|
4872
|
+
".jl": "julia",
|
|
4873
|
+
".dart": "dart",
|
|
4874
|
+
".clj": "clojure",
|
|
4875
|
+
".cljs": "clojure",
|
|
4876
|
+
// Systems
|
|
4877
|
+
".c": "c",
|
|
4878
|
+
".h": "c",
|
|
4879
|
+
".cpp": "cpp",
|
|
4880
|
+
".cc": "cpp",
|
|
4881
|
+
".cxx": "cpp",
|
|
4882
|
+
".hpp": "cpp",
|
|
4883
|
+
".swift": "swift",
|
|
4884
|
+
".m": "objective-c",
|
|
4885
|
+
".mm": "objective-c",
|
|
4886
|
+
".zig": "zig",
|
|
4887
|
+
".nim": "nim",
|
|
4888
|
+
// Shell / Config
|
|
4889
|
+
".sh": "shell",
|
|
4890
|
+
".bash": "shell",
|
|
4891
|
+
".zsh": "shell",
|
|
4892
|
+
".fish": "shell",
|
|
4893
|
+
// Data / Schema
|
|
4894
|
+
".prisma": "prisma",
|
|
4895
|
+
".sql": "sql",
|
|
4896
|
+
".graphql": "graphql",
|
|
4897
|
+
".gql": "graphql",
|
|
4898
|
+
".proto": "protobuf",
|
|
4899
|
+
// Config
|
|
4900
|
+
".json": "json",
|
|
4901
|
+
".yaml": "yaml",
|
|
4902
|
+
".yml": "yaml",
|
|
4903
|
+
".toml": "toml",
|
|
4904
|
+
".xml": "xml",
|
|
4905
|
+
// Docs
|
|
4906
|
+
".md": "markdown",
|
|
4907
|
+
".mdx": "mdx",
|
|
4908
|
+
".rst": "restructuredtext"
|
|
4909
|
+
};
|
|
4910
|
+
IGNORE_DIRS = /* @__PURE__ */ new Set([
|
|
4911
|
+
"node_modules",
|
|
4912
|
+
".next",
|
|
4913
|
+
"dist",
|
|
4914
|
+
".launchsecure",
|
|
4915
|
+
".git",
|
|
4916
|
+
"coverage",
|
|
4917
|
+
".turbo",
|
|
4918
|
+
"build",
|
|
4919
|
+
"out",
|
|
4920
|
+
".vercel",
|
|
4921
|
+
".cache",
|
|
4922
|
+
"__pycache__",
|
|
4923
|
+
".tox",
|
|
4924
|
+
"venv",
|
|
4925
|
+
".venv",
|
|
4926
|
+
"target",
|
|
4927
|
+
"vendor",
|
|
4928
|
+
"Pods",
|
|
4929
|
+
".gradle",
|
|
4930
|
+
".idea",
|
|
4931
|
+
".vscode"
|
|
4932
|
+
]);
|
|
4933
|
+
AUXILIARY_LANGUAGES = /* @__PURE__ */ new Set([
|
|
4934
|
+
"json",
|
|
4935
|
+
"yaml",
|
|
4936
|
+
"toml",
|
|
4937
|
+
"xml",
|
|
4938
|
+
"markdown",
|
|
4939
|
+
"mdx",
|
|
4940
|
+
"restructuredtext",
|
|
4941
|
+
"html",
|
|
4942
|
+
"css",
|
|
4943
|
+
"scss",
|
|
4944
|
+
"less",
|
|
4945
|
+
"shell"
|
|
4946
|
+
]);
|
|
4947
|
+
}
|
|
4948
|
+
});
|
|
4949
|
+
|
|
3186
4950
|
// src/server/graph-mcp.ts
|
|
3187
4951
|
var graph_mcp_exports = {};
|
|
3188
4952
|
__export(graph_mcp_exports, {
|
|
@@ -3269,8 +5033,8 @@ function neighborhood(graph, centerId, hops, layer, minimal) {
|
|
|
3269
5033
|
const dstIn = visited.has(e.target) || next.has(e.target);
|
|
3270
5034
|
if (srcIn && dstIn) projectedEdges++;
|
|
3271
5035
|
}
|
|
3272
|
-
const perNode = minimal ? EST_CHARS_PER_NODE_MIN[layer] : EST_CHARS_PER_NODE_FULL[layer];
|
|
3273
|
-
const projectedChars = projectedVisited * perNode + projectedEdges * EST_CHARS_PER_EDGE[layer];
|
|
5036
|
+
const perNode = minimal ? EST_CHARS_PER_NODE_MIN[layer] ?? DEFAULT_EST_NODE_MIN : EST_CHARS_PER_NODE_FULL[layer] ?? DEFAULT_EST_NODE_FULL;
|
|
5037
|
+
const projectedChars = projectedVisited * perNode + projectedEdges * (EST_CHARS_PER_EDGE[layer] ?? DEFAULT_EST_EDGE);
|
|
3274
5038
|
if (projectedChars > NEIGHBORHOOD_BUDGET_CHARS) {
|
|
3275
5039
|
budgetExceeded = true;
|
|
3276
5040
|
break;
|
|
@@ -3318,9 +5082,6 @@ function err(text) {
|
|
|
3318
5082
|
async function handleGenerateGraph(args) {
|
|
3319
5083
|
const rootDir = process.cwd();
|
|
3320
5084
|
const layer = args.layer;
|
|
3321
|
-
if (layer && !["ui", "api", "db"].includes(layer)) {
|
|
3322
|
-
return err(`Invalid layer "${layer}". Must be one of: ui, api, db`);
|
|
3323
|
-
}
|
|
3324
5085
|
const results = await generateGraph(rootDir, layer);
|
|
3325
5086
|
if (results.length === 0) {
|
|
3326
5087
|
return err(
|
|
@@ -3357,8 +5118,11 @@ function runReadGraphQueryRaw(rootDir, args) {
|
|
|
3357
5118
|
const offset = args.offset ?? 0;
|
|
3358
5119
|
const limit = args.limit;
|
|
3359
5120
|
const hasFilter = !!(search || type || module_ || nodeId || tagKey && tagValue);
|
|
3360
|
-
if (layer
|
|
3361
|
-
|
|
5121
|
+
if (layer) {
|
|
5122
|
+
const available = getAvailableLayers(rootDir);
|
|
5123
|
+
if (available.length > 0 && !available.includes(layer)) {
|
|
5124
|
+
return { error: `No graph found for layer "${layer}". Available: ${available.join(", ")}. Run generate_graph first.` };
|
|
5125
|
+
}
|
|
3362
5126
|
}
|
|
3363
5127
|
if (!layer) {
|
|
3364
5128
|
const graphs = readAllGraphs(rootDir);
|
|
@@ -3512,9 +5276,12 @@ function handleReadGraph(args) {
|
|
|
3512
5276
|
return okJson(result);
|
|
3513
5277
|
}
|
|
3514
5278
|
function nodeToFilePath(rootDir, layer, nodeId) {
|
|
3515
|
-
if (layer === "ui") return (0,
|
|
3516
|
-
if (layer === "
|
|
3517
|
-
|
|
5279
|
+
if (layer === "ui" || layer === "api") return (0, import_node_path21.join)(rootDir, "src", nodeId);
|
|
5280
|
+
if (layer === "db") return (0, import_node_path21.join)(rootDir, "prisma", "schema.prisma");
|
|
5281
|
+
const withSrc = (0, import_node_path21.join)(rootDir, "src", nodeId);
|
|
5282
|
+
if ((0, import_node_fs19.existsSync)(withSrc)) return withSrc;
|
|
5283
|
+
const direct = (0, import_node_path21.join)(rootDir, nodeId);
|
|
5284
|
+
if ((0, import_node_fs19.existsSync)(direct)) return direct;
|
|
3518
5285
|
return null;
|
|
3519
5286
|
}
|
|
3520
5287
|
function handleInspectNode(args) {
|
|
@@ -3603,7 +5370,10 @@ function handleGrepNodes(args) {
|
|
|
3603
5370
|
const layer = args.layer;
|
|
3604
5371
|
if (!pattern) return err("pattern is required");
|
|
3605
5372
|
if (!layer) return err("layer is required (ui, api, or db)");
|
|
3606
|
-
|
|
5373
|
+
const grepAvailable = getAvailableLayers(rootDir);
|
|
5374
|
+
if (grepAvailable.length > 0 && !grepAvailable.includes(layer)) {
|
|
5375
|
+
return err(`No graph found for layer "${layer}". Available: ${grepAvailable.join(", ")}. Run generate_graph first.`);
|
|
5376
|
+
}
|
|
3607
5377
|
const hasFilter = !!(args.search || args.type || args.module || args.node_id);
|
|
3608
5378
|
if (!hasFilter) {
|
|
3609
5379
|
return err(
|
|
@@ -3654,11 +5424,11 @@ function handleGrepNodes(args) {
|
|
|
3654
5424
|
let filesSearched = 0;
|
|
3655
5425
|
let truncated = false;
|
|
3656
5426
|
for (const [filePath, nodeId] of filePaths) {
|
|
3657
|
-
if (!(0,
|
|
5427
|
+
if (!(0, import_node_fs19.existsSync)(filePath)) continue;
|
|
3658
5428
|
filesSearched++;
|
|
3659
5429
|
let content;
|
|
3660
5430
|
try {
|
|
3661
|
-
content = (0,
|
|
5431
|
+
content = (0, import_node_fs19.readFileSync)(filePath, "utf-8");
|
|
3662
5432
|
} catch {
|
|
3663
5433
|
continue;
|
|
3664
5434
|
}
|
|
@@ -3723,11 +5493,11 @@ function handleStartChartServer(args) {
|
|
|
3723
5493
|
});
|
|
3724
5494
|
}
|
|
3725
5495
|
const entryPath = process.argv[1];
|
|
3726
|
-
const logDir = (0,
|
|
3727
|
-
(0,
|
|
3728
|
-
const logPath = (0,
|
|
3729
|
-
const out = (0,
|
|
3730
|
-
const err2 = (0,
|
|
5496
|
+
const logDir = (0, import_node_path21.join)((0, import_node_os2.homedir)(), ".launchsecure");
|
|
5497
|
+
(0, import_node_fs19.mkdirSync)(logDir, { recursive: true });
|
|
5498
|
+
const logPath = (0, import_node_path21.join)(logDir, "launch-chart.log");
|
|
5499
|
+
const out = (0, import_node_fs19.openSync)(logPath, "a");
|
|
5500
|
+
const err2 = (0, import_node_fs19.openSync)(logPath, "a");
|
|
3731
5501
|
const portArgs = args.port ? ["--port", String(args.port)] : [];
|
|
3732
5502
|
const child = (0, import_node_child_process2.spawn)(process.execPath, [entryPath, "serve", ...portArgs], {
|
|
3733
5503
|
detached: true,
|
|
@@ -3790,14 +5560,55 @@ function handleRemoveTag(args) {
|
|
|
3790
5560
|
removeTag(rootDir, nodeId, key);
|
|
3791
5561
|
return okJson({ ok: true, node_id: nodeId, removed_key: key });
|
|
3792
5562
|
}
|
|
5563
|
+
function handleAuditLayer(args) {
|
|
5564
|
+
const rootDir = process.cwd();
|
|
5565
|
+
const layer = args.layer;
|
|
5566
|
+
const check = args.check;
|
|
5567
|
+
if (!layer) return err("layer is required");
|
|
5568
|
+
const reports = runAudit(rootDir, layer, check);
|
|
5569
|
+
const totalFindings = reports.reduce((acc, r) => acc + r.findings.length, 0);
|
|
5570
|
+
const totalErrors = reports.reduce((acc, r) => acc + r.summary.errors, 0);
|
|
5571
|
+
const totalWarnings = reports.reduce((acc, r) => acc + r.summary.warnings, 0);
|
|
5572
|
+
const totalInfo = reports.reduce((acc, r) => acc + r.summary.info, 0);
|
|
5573
|
+
const lines = [];
|
|
5574
|
+
lines.push(`Audit: ${layer}${check ? ` / ${check}` : ""} \u2014 ${totalFindings} findings (${totalErrors} errors, ${totalWarnings} warnings, ${totalInfo} info)`);
|
|
5575
|
+
lines.push("");
|
|
5576
|
+
for (const report of reports) {
|
|
5577
|
+
if (report.findings.length === 0) {
|
|
5578
|
+
lines.push(`\u2713 ${report.check}: no issues found`);
|
|
5579
|
+
continue;
|
|
5580
|
+
}
|
|
5581
|
+
lines.push(`\u2500\u2500 ${report.check} (${report.findings.length}) \u2500\u2500`);
|
|
5582
|
+
for (const f of report.findings) {
|
|
5583
|
+
const tag = f.severity === "error" ? "\u2717" : f.severity === "warning" ? "!" : "\xB7";
|
|
5584
|
+
const filePart = f.file ? ` [${f.file}]` : "";
|
|
5585
|
+
lines.push(` ${tag} ${f.title}${filePart}`);
|
|
5586
|
+
lines.push(` ${f.detail}`);
|
|
5587
|
+
}
|
|
5588
|
+
lines.push("");
|
|
5589
|
+
}
|
|
5590
|
+
return okText(lines.join("\n"));
|
|
5591
|
+
}
|
|
3793
5592
|
function handleDetectProjectStack() {
|
|
3794
5593
|
const rootDir = process.cwd();
|
|
3795
|
-
const parsers = [
|
|
3796
|
-
{ id: "react-nextjs", layer: "ui", detected: reactNextjsParser.detect(rootDir) },
|
|
3797
|
-
{ id: "nextjs-routes", layer: "api", detected: nextjsRoutesParser.detect(rootDir) },
|
|
3798
|
-
{ id: "prisma-schema", layer: "db", detected: prismaSchemaParser.detect(rootDir) }
|
|
3799
|
-
];
|
|
3800
5594
|
const config = loadConfig(rootDir);
|
|
5595
|
+
const registry = createRegistry(config, rootDir);
|
|
5596
|
+
const parserResults = [];
|
|
5597
|
+
for (const parser of registry.getAll()) {
|
|
5598
|
+
if ("layers" in parser && Array.isArray(parser.layers)) {
|
|
5599
|
+
const mp = parser;
|
|
5600
|
+
parserResults.push({ id: mp.id, layers: mp.layers, detected: mp.detect(rootDir) });
|
|
5601
|
+
} else if ("layer" in parser && parser.layer !== "crosslayer") {
|
|
5602
|
+
const sp = parser;
|
|
5603
|
+
parserResults.push({ id: sp.id, layers: [sp.layer], detected: sp.detect(rootDir) });
|
|
5604
|
+
}
|
|
5605
|
+
}
|
|
5606
|
+
const availableLayers = /* @__PURE__ */ new Set();
|
|
5607
|
+
for (const p of parserResults) {
|
|
5608
|
+
if (p.detected) {
|
|
5609
|
+
for (const l of p.layers) availableLayers.add(l);
|
|
5610
|
+
}
|
|
5611
|
+
}
|
|
3801
5612
|
let stats = { calls_api: 0, references_api: 0, out_of_pattern: 0, annotations: 0 };
|
|
3802
5613
|
const uiGraph = readGraph(rootDir, "ui");
|
|
3803
5614
|
if (uiGraph) {
|
|
@@ -3809,20 +5620,20 @@ function handleDetectProjectStack() {
|
|
|
3809
5620
|
if (f.type === "out_of_pattern") stats.out_of_pattern++;
|
|
3810
5621
|
}
|
|
3811
5622
|
}
|
|
3812
|
-
const srcDir = (0,
|
|
3813
|
-
if ((0,
|
|
5623
|
+
const srcDir = (0, import_node_path21.join)(rootDir, "src");
|
|
5624
|
+
if ((0, import_node_fs19.existsSync)(srcDir)) {
|
|
3814
5625
|
const scanDir = (dir) => {
|
|
3815
|
-
if (!(0,
|
|
3816
|
-
for (const entry of (0,
|
|
5626
|
+
if (!(0, import_node_fs19.existsSync)(dir)) return;
|
|
5627
|
+
for (const entry of (0, import_node_fs19.readdirSync)(dir, { withFileTypes: true })) {
|
|
3817
5628
|
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
3818
|
-
const full = (0,
|
|
5629
|
+
const full = (0, import_node_path21.join)(dir, entry.name);
|
|
3819
5630
|
if (entry.isDirectory()) {
|
|
3820
5631
|
scanDir(full);
|
|
3821
5632
|
continue;
|
|
3822
5633
|
}
|
|
3823
|
-
if (![".ts", ".tsx"].includes((0,
|
|
5634
|
+
if (![".ts", ".tsx"].includes((0, import_node_path21.extname)(entry.name))) continue;
|
|
3824
5635
|
try {
|
|
3825
|
-
const content = (0,
|
|
5636
|
+
const content = (0, import_node_fs19.readFileSync)(full, "utf-8");
|
|
3826
5637
|
const matches = content.match(/@api\s+(GET|POST|PUT|DELETE|PATCH)\s+\/\S+/g);
|
|
3827
5638
|
if (matches) stats.annotations += matches.length;
|
|
3828
5639
|
} catch {
|
|
@@ -3837,8 +5648,16 @@ function handleDetectProjectStack() {
|
|
|
3837
5648
|
} else if (stats.calls_api === 0 && stats.references_api > 0) {
|
|
3838
5649
|
recommendedPrimary = "url-literal-scanner";
|
|
3839
5650
|
}
|
|
5651
|
+
const supportedLanguages = /* @__PURE__ */ new Map();
|
|
5652
|
+
supportedLanguages.set("typescript", parserResults.filter((p) => p.detected && p.layers.some((l) => l === "ui" || l === "api")).map((p) => p.id));
|
|
5653
|
+
supportedLanguages.set("prisma", parserResults.filter((p) => p.detected && p.layers.includes("db")).map((p) => p.id));
|
|
5654
|
+
const languages = detectLanguages(rootDir, supportedLanguages);
|
|
5655
|
+
const unsupported = languages.filter((l) => !l.supported);
|
|
5656
|
+
const unsupportedHint = unsupported.length > 0 ? unsupported.map((l) => `${l.id} (${l.fileCount} files)`).join(", ") + " \u2014 detected but not yet supported" : null;
|
|
3840
5657
|
return okJson({
|
|
3841
|
-
|
|
5658
|
+
languages,
|
|
5659
|
+
parsers: parserResults,
|
|
5660
|
+
available_layers: [...availableLayers],
|
|
3842
5661
|
crosslayer_parsers: [
|
|
3843
5662
|
{ id: "fetch-resolver", description: "Detects direct fetch()/api.get() calls with inline URLs" },
|
|
3844
5663
|
{ id: "api-annotations", description: "Scans for @api METHOD /path annotations in JSDoc/comments" },
|
|
@@ -3846,6 +5665,7 @@ function handleDetectProjectStack() {
|
|
|
3846
5665
|
],
|
|
3847
5666
|
stats,
|
|
3848
5667
|
recommended_primary: recommendedPrimary,
|
|
5668
|
+
...unsupportedHint ? { unsupported_hint: unsupportedHint } : {},
|
|
3849
5669
|
current_config: Object.keys(config).length > 0 ? config : null,
|
|
3850
5670
|
config_path: ".launchchart.json"
|
|
3851
5671
|
});
|
|
@@ -3921,6 +5741,10 @@ async function handleMessage(msg) {
|
|
|
3921
5741
|
respond(id ?? null, handleRemoveTag(args));
|
|
3922
5742
|
return;
|
|
3923
5743
|
}
|
|
5744
|
+
if (toolName === "audit_layer") {
|
|
5745
|
+
respond(id ?? null, handleAuditLayer(args));
|
|
5746
|
+
return;
|
|
5747
|
+
}
|
|
3924
5748
|
respondError(id ?? null, -32601, `Unknown tool: ${toolName}`);
|
|
3925
5749
|
return;
|
|
3926
5750
|
}
|
|
@@ -3956,20 +5780,20 @@ function startGraphMcpServer() {
|
|
|
3956
5780
|
process.stderr.write(`[launchsecure-graph] MCP server started (cwd: ${process.cwd()})
|
|
3957
5781
|
`);
|
|
3958
5782
|
}
|
|
3959
|
-
var
|
|
5783
|
+
var import_node_fs19, import_node_path21, import_node_child_process2, import_node_os2, SERVER_INFO, TOOLS, COMPACT_SCHEMA, COMPACT_NODE_KNOWN_KEYS, DEEP_FIELDS, EST_CHARS_PER_NODE_FULL, EST_CHARS_PER_NODE_MIN, EST_CHARS_PER_EDGE, DEFAULT_EST_NODE_FULL, DEFAULT_EST_NODE_MIN, DEFAULT_EST_EDGE, NEIGHBORHOOD_BUDGET_CHARS, BATCH_BUDGET_CHARS;
|
|
3960
5784
|
var init_graph_mcp = __esm({
|
|
3961
5785
|
"src/server/graph-mcp.ts"() {
|
|
3962
5786
|
"use strict";
|
|
3963
|
-
|
|
3964
|
-
|
|
5787
|
+
import_node_fs19 = require("node:fs");
|
|
5788
|
+
import_node_path21 = require("node:path");
|
|
3965
5789
|
import_node_child_process2 = require("node:child_process");
|
|
3966
5790
|
import_node_os2 = require("node:os");
|
|
3967
5791
|
init_graph();
|
|
3968
5792
|
init_lockfile();
|
|
3969
5793
|
init_config();
|
|
3970
|
-
|
|
3971
|
-
|
|
3972
|
-
|
|
5794
|
+
init_parser_registry();
|
|
5795
|
+
init_language_detection();
|
|
5796
|
+
init_audit_core();
|
|
3973
5797
|
SERVER_INFO = {
|
|
3974
5798
|
name: "launchsecure-graph",
|
|
3975
5799
|
version: "0.0.1"
|
|
@@ -3977,14 +5801,13 @@ var init_graph_mcp = __esm({
|
|
|
3977
5801
|
TOOLS = [
|
|
3978
5802
|
{
|
|
3979
5803
|
name: "generate_graph",
|
|
3980
|
-
description: "Regenerate the structural project graph by scanning source code in the current working directory.
|
|
5804
|
+
description: "Regenerate the structural project graph by scanning source code in the current working directory. Auto-detects project languages and frameworks, then parses into layers (e.g. ui, api, db) based on content classification. Writes JSON to .launchsecure/graphs/{layer}.json and returns node/edge counts. Run this when project structure has changed (new files, moved components, schema updates). Fast: typically <1s. After generation, use read_graph with filters to query.",
|
|
3981
5805
|
inputSchema: {
|
|
3982
5806
|
type: "object",
|
|
3983
5807
|
properties: {
|
|
3984
5808
|
layer: {
|
|
3985
5809
|
type: "string",
|
|
3986
|
-
|
|
3987
|
-
description: "Specific layer to regenerate. Omit to regenerate all detectable layers."
|
|
5810
|
+
description: "Specific layer to regenerate (e.g. 'ui', 'api', 'db'). Omit to regenerate all detectable layers. Run detect_project_stack to see available layers."
|
|
3988
5811
|
}
|
|
3989
5812
|
}
|
|
3990
5813
|
}
|
|
@@ -3997,8 +5820,7 @@ var init_graph_mcp = __esm({
|
|
|
3997
5820
|
properties: {
|
|
3998
5821
|
layer: {
|
|
3999
5822
|
type: "string",
|
|
4000
|
-
|
|
4001
|
-
description: "Graph layer to query: ui, api, or db. Required if any filter is provided."
|
|
5823
|
+
description: "Graph layer to query (e.g. 'ui', 'api', 'db'). Required if any filter is provided. Run detect_project_stack to see available layers."
|
|
4002
5824
|
},
|
|
4003
5825
|
search: {
|
|
4004
5826
|
type: "string",
|
|
@@ -4050,7 +5872,7 @@ var init_graph_mcp = __esm({
|
|
|
4050
5872
|
items: {
|
|
4051
5873
|
type: "object",
|
|
4052
5874
|
properties: {
|
|
4053
|
-
layer: { type: "string"
|
|
5875
|
+
layer: { type: "string" },
|
|
4054
5876
|
search: { type: "string" },
|
|
4055
5877
|
type: { type: "string" },
|
|
4056
5878
|
module: { type: "string" },
|
|
@@ -4092,8 +5914,7 @@ Returns: { pattern, filter, files_searched, total_matches, matches: [{file, line
|
|
|
4092
5914
|
properties: {
|
|
4093
5915
|
layer: {
|
|
4094
5916
|
type: "string",
|
|
4095
|
-
|
|
4096
|
-
description: "Graph layer to scope files (required)."
|
|
5917
|
+
description: "Graph layer to scope files (required, e.g. 'ui', 'api', 'db')."
|
|
4097
5918
|
},
|
|
4098
5919
|
pattern: {
|
|
4099
5920
|
type: "string",
|
|
@@ -4126,8 +5947,7 @@ Returns deep fields only \u2014 not structural metadata (use read_graph for that
|
|
|
4126
5947
|
properties: {
|
|
4127
5948
|
layer: {
|
|
4128
5949
|
type: "string",
|
|
4129
|
-
|
|
4130
|
-
description: "Graph layer (required)."
|
|
5950
|
+
description: "Graph layer (required, e.g. 'ui', 'api', 'db')."
|
|
4131
5951
|
},
|
|
4132
5952
|
node_id: {
|
|
4133
5953
|
type: "string",
|
|
@@ -4187,7 +6007,7 @@ Use this when the user asks "is the chart running", "show me the project graph U
|
|
|
4187
6007
|
},
|
|
4188
6008
|
{
|
|
4189
6009
|
name: "detect_project_stack",
|
|
4190
|
-
description: "Detect project frameworks, available parsers, and recommend parser configuration. Scans the project to identify
|
|
6010
|
+
description: "Detect project languages, frameworks, available parsers, and recommend parser configuration. Scans the project to identify all languages present (TypeScript, Python, Go, etc.) and reports which are supported by registered parsers vs detected-but-unsupported. Also detects frameworks (Next.js, Prisma, React, etc.), provides cross-layer detection stats (fetch calls, @api annotations, URL literals), and returns available graph layers. \n\nUse this when setting up launch-chart for a new project, reviewing parser configuration, or checking what languages are in the project.",
|
|
4191
6011
|
inputSchema: {
|
|
4192
6012
|
type: "object",
|
|
4193
6013
|
properties: {}
|
|
@@ -4232,6 +6052,24 @@ Use this when the user asks "is the chart running", "show me the project graph U
|
|
|
4232
6052
|
},
|
|
4233
6053
|
required: ["node_id", "key"]
|
|
4234
6054
|
}
|
|
6055
|
+
},
|
|
6056
|
+
{
|
|
6057
|
+
name: "audit_layer",
|
|
6058
|
+
description: "Run audit checks on a graph layer. Surfaces schema drift, unprotected routes, hardcoded values, dead screens, and other discrepancies. Returns findings with severity and file locations.\n\nAvailable layers and checks:\n- db: schema_drift (Prisma vs SQL migration mismatches), orphan_fks (FKs without @relation)\n- api: unprotected_routes (endpoints missing from ROUTE_PERMISSIONS)\n- ui: dead_screens (pages with no incoming navigation)\n- static: unenforced_permissions (seed permissions with no route enforcement), hardcoded_values (ALL_CAPS strings not in inventory)\n- all: run every check across all layers",
|
|
6059
|
+
inputSchema: {
|
|
6060
|
+
type: "object",
|
|
6061
|
+
properties: {
|
|
6062
|
+
layer: {
|
|
6063
|
+
type: "string",
|
|
6064
|
+
description: "Layer to audit: 'db', 'api', 'ui', 'static', or 'all'."
|
|
6065
|
+
},
|
|
6066
|
+
check: {
|
|
6067
|
+
type: "string",
|
|
6068
|
+
description: "Specific check to run (e.g. 'schema_drift', 'unprotected_routes'). Omit to run all checks for the layer."
|
|
6069
|
+
}
|
|
6070
|
+
},
|
|
6071
|
+
required: ["layer"]
|
|
6072
|
+
}
|
|
4235
6073
|
}
|
|
4236
6074
|
];
|
|
4237
6075
|
COMPACT_SCHEMA = {
|
|
@@ -4288,6 +6126,9 @@ Use this when the user asks "is the chart running", "show me the project graph U
|
|
|
4288
6126
|
api: 65,
|
|
4289
6127
|
db: 65
|
|
4290
6128
|
};
|
|
6129
|
+
DEFAULT_EST_NODE_FULL = 300;
|
|
6130
|
+
DEFAULT_EST_NODE_MIN = 150;
|
|
6131
|
+
DEFAULT_EST_EDGE = 65;
|
|
4291
6132
|
NEIGHBORHOOD_BUDGET_CHARS = 55e3;
|
|
4292
6133
|
BATCH_BUDGET_CHARS = 6e4;
|
|
4293
6134
|
}
|
|
@@ -4295,10 +6136,10 @@ Use this when the user asks "is the chart running", "show me the project graph U
|
|
|
4295
6136
|
|
|
4296
6137
|
// src/server/graph-mcp-entry.ts
|
|
4297
6138
|
var import_node_child_process3 = require("node:child_process");
|
|
4298
|
-
var
|
|
4299
|
-
var
|
|
6139
|
+
var import_node_fs20 = require("node:fs");
|
|
6140
|
+
var import_node_path22 = __toESM(require("node:path"));
|
|
4300
6141
|
var import_node_os3 = require("node:os");
|
|
4301
|
-
var
|
|
6142
|
+
var import_node_fs21 = require("node:fs");
|
|
4302
6143
|
init_lockfile();
|
|
4303
6144
|
function logStderr(msg) {
|
|
4304
6145
|
process.stderr.write(`[launch-chart] ${msg}
|
|
@@ -4314,11 +6155,11 @@ function maybeAutoServe() {
|
|
|
4314
6155
|
return;
|
|
4315
6156
|
}
|
|
4316
6157
|
try {
|
|
4317
|
-
const logDir =
|
|
4318
|
-
(0,
|
|
4319
|
-
const logPath =
|
|
4320
|
-
const out = (0,
|
|
4321
|
-
const err2 = (0,
|
|
6158
|
+
const logDir = import_node_path22.default.join((0, import_node_os3.homedir)(), ".launchsecure");
|
|
6159
|
+
(0, import_node_fs21.mkdirSync)(logDir, { recursive: true });
|
|
6160
|
+
const logPath = import_node_path22.default.join(logDir, "launch-chart.log");
|
|
6161
|
+
const out = (0, import_node_fs20.openSync)(logPath, "a");
|
|
6162
|
+
const err2 = (0, import_node_fs20.openSync)(logPath, "a");
|
|
4322
6163
|
const entryPath = process.argv[1];
|
|
4323
6164
|
const child = (0, import_node_child_process3.spawn)(process.execPath, [entryPath, "serve"], {
|
|
4324
6165
|
detached: true,
|