@launchsecure/launch-kit 0.0.3 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chart-client/assets/index-BPR5akxH.js +323 -0
- package/dist/chart-client/assets/index-DhNl1aFF.css +1 -0
- package/dist/chart-client/index.html +21 -0
- package/dist/client/assets/{index-CcHIoRl6.js → index-D9e81rsq.js} +68 -63
- package/dist/client/assets/index-nR-HgoHH.css +32 -0
- package/dist/client/index.html +2 -2
- package/dist/server/chart-serve.js +1764 -0
- package/dist/server/cli.js +481 -20
- package/dist/server/graph-mcp-entry.js +1145 -289
- package/package.json +7 -3
- package/dist/client/assets/index-C8GAsRGO.css +0 -32
package/dist/server/cli.js
CHANGED
|
@@ -6341,6 +6341,7 @@ function getTs() {
|
|
|
6341
6341
|
}
|
|
6342
6342
|
return tsModule;
|
|
6343
6343
|
}
|
|
6344
|
+
var HTTP_METHODS = /* @__PURE__ */ new Set(["get", "post", "put", "patch", "delete", "head", "options"]);
|
|
6344
6345
|
function parseFile(absPath) {
|
|
6345
6346
|
const ts = getTs();
|
|
6346
6347
|
const content = (0, import_node_fs.readFileSync)(absPath, "utf-8");
|
|
@@ -6363,6 +6364,8 @@ function parseFile(absPath) {
|
|
|
6363
6364
|
const reExports = [];
|
|
6364
6365
|
const jsxElements = /* @__PURE__ */ new Set();
|
|
6365
6366
|
const navigations = [];
|
|
6367
|
+
const fetchCalls = [];
|
|
6368
|
+
const includeConcat = process.env.LAUNCH_CHART_INCLUDE_CONCAT_FETCHES === "1";
|
|
6366
6369
|
function addExport(name2, kind) {
|
|
6367
6370
|
if (!exportsSet.has(name2)) {
|
|
6368
6371
|
exportsSet.add(name2);
|
|
@@ -6385,6 +6388,33 @@ function parseFile(absPath) {
|
|
|
6385
6388
|
}
|
|
6386
6389
|
return null;
|
|
6387
6390
|
}
|
|
6391
|
+
function looksLikeUrl(s) {
|
|
6392
|
+
return s.startsWith("/") || /^(https?:)?\/\//i.test(s);
|
|
6393
|
+
}
|
|
6394
|
+
function templateStartsWithSlash(expr) {
|
|
6395
|
+
const head = expr.head.text;
|
|
6396
|
+
return head.startsWith("/");
|
|
6397
|
+
}
|
|
6398
|
+
function extractUrlFromFetchArg(arg) {
|
|
6399
|
+
if (ts.isStringLiteral(arg) || ts.isNoSubstitutionTemplateLiteral(arg)) {
|
|
6400
|
+
if (!looksLikeUrl(arg.text)) return null;
|
|
6401
|
+
return { url: arg.text, isTemplate: false };
|
|
6402
|
+
}
|
|
6403
|
+
if (ts.isTemplateExpression(arg)) {
|
|
6404
|
+
if (!templateStartsWithSlash(arg)) return null;
|
|
6405
|
+
return { url: arg.getText(sourceFile), isTemplate: true };
|
|
6406
|
+
}
|
|
6407
|
+
if (includeConcat && ts.isBinaryExpression(arg) && arg.operatorToken.kind === ts.SyntaxKind.PlusToken) {
|
|
6408
|
+
let leftmost = arg;
|
|
6409
|
+
while (ts.isBinaryExpression(leftmost) && leftmost.operatorToken.kind === ts.SyntaxKind.PlusToken) {
|
|
6410
|
+
leftmost = leftmost.left;
|
|
6411
|
+
}
|
|
6412
|
+
if ((ts.isStringLiteral(leftmost) || ts.isNoSubstitutionTemplateLiteral(leftmost)) && leftmost.text.startsWith("/")) {
|
|
6413
|
+
return { url: arg.getText(sourceFile), isTemplate: false, isConcat: true };
|
|
6414
|
+
}
|
|
6415
|
+
}
|
|
6416
|
+
return null;
|
|
6417
|
+
}
|
|
6388
6418
|
function visit(node) {
|
|
6389
6419
|
if (ts.isImportDeclaration(node)) {
|
|
6390
6420
|
const moduleSpec = node.moduleSpecifier;
|
|
@@ -6408,6 +6438,8 @@ function parseFile(absPath) {
|
|
|
6408
6438
|
}
|
|
6409
6439
|
if (names.length > 0 || isTypeOnly) {
|
|
6410
6440
|
imports.push({ names, specifier, isTypeOnly, typeNames });
|
|
6441
|
+
} else if (!clause) {
|
|
6442
|
+
imports.push({ names: [], specifier, isTypeOnly: false, typeNames: /* @__PURE__ */ new Set() });
|
|
6411
6443
|
}
|
|
6412
6444
|
}
|
|
6413
6445
|
}
|
|
@@ -6421,6 +6453,19 @@ function parseFile(absPath) {
|
|
|
6421
6453
|
reExports.push({ name: exportedName, from: fromSpec });
|
|
6422
6454
|
}
|
|
6423
6455
|
}
|
|
6456
|
+
} else if (!node.exportClause && fromSpec) {
|
|
6457
|
+
reExports.push({ name: "*", from: fromSpec, isWildcard: true });
|
|
6458
|
+
}
|
|
6459
|
+
}
|
|
6460
|
+
if (ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.ImportKeyword) {
|
|
6461
|
+
const arg = node.arguments[0];
|
|
6462
|
+
if (arg && ts.isStringLiteral(arg)) {
|
|
6463
|
+
imports.push({
|
|
6464
|
+
names: [],
|
|
6465
|
+
specifier: arg.text,
|
|
6466
|
+
isTypeOnly: false,
|
|
6467
|
+
typeNames: /* @__PURE__ */ new Set()
|
|
6468
|
+
});
|
|
6424
6469
|
}
|
|
6425
6470
|
}
|
|
6426
6471
|
if (ts.isExportAssignment(node) && !node.isExportEquals) {
|
|
@@ -6482,6 +6527,36 @@ function parseFile(absPath) {
|
|
|
6482
6527
|
}
|
|
6483
6528
|
}
|
|
6484
6529
|
}
|
|
6530
|
+
if (ts.isCallExpression(node) && node.arguments.length > 0) {
|
|
6531
|
+
const expr = node.expression;
|
|
6532
|
+
const firstArg = node.arguments[0];
|
|
6533
|
+
if (ts.isIdentifier(expr) && expr.text === "fetch") {
|
|
6534
|
+
const extracted = extractUrlFromFetchArg(firstArg);
|
|
6535
|
+
if (extracted) {
|
|
6536
|
+
fetchCalls.push({
|
|
6537
|
+
url: extracted.url,
|
|
6538
|
+
isTemplate: extracted.isTemplate,
|
|
6539
|
+
...extracted.isConcat ? { isConcat: true } : {},
|
|
6540
|
+
kind: "fetch"
|
|
6541
|
+
});
|
|
6542
|
+
}
|
|
6543
|
+
}
|
|
6544
|
+
if (ts.isPropertyAccessExpression(expr) && ts.isIdentifier(expr.name)) {
|
|
6545
|
+
const methodName = expr.name.text;
|
|
6546
|
+
if (HTTP_METHODS.has(methodName)) {
|
|
6547
|
+
const extracted = extractUrlFromFetchArg(firstArg);
|
|
6548
|
+
if (extracted) {
|
|
6549
|
+
fetchCalls.push({
|
|
6550
|
+
method: methodName.toUpperCase(),
|
|
6551
|
+
url: extracted.url,
|
|
6552
|
+
isTemplate: extracted.isTemplate,
|
|
6553
|
+
...extracted.isConcat ? { isConcat: true } : {},
|
|
6554
|
+
kind: "client-method"
|
|
6555
|
+
});
|
|
6556
|
+
}
|
|
6557
|
+
}
|
|
6558
|
+
}
|
|
6559
|
+
}
|
|
6485
6560
|
if (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) {
|
|
6486
6561
|
const tagName = node.tagName;
|
|
6487
6562
|
if (ts.isIdentifier(tagName) && tagName.text === "Link") {
|
|
@@ -6531,7 +6606,8 @@ function parseFile(absPath) {
|
|
|
6531
6606
|
imports,
|
|
6532
6607
|
reExports,
|
|
6533
6608
|
jsxElements,
|
|
6534
|
-
navigations
|
|
6609
|
+
navigations,
|
|
6610
|
+
fetchCalls
|
|
6535
6611
|
};
|
|
6536
6612
|
}
|
|
6537
6613
|
var MUTATION_METHODS = /* @__PURE__ */ new Set([
|
|
@@ -6610,6 +6686,19 @@ function walk(dir, exts) {
|
|
|
6610
6686
|
}
|
|
6611
6687
|
return results;
|
|
6612
6688
|
}
|
|
6689
|
+
function walkWithIgnore(dir, exts, ignoreDirs) {
|
|
6690
|
+
const results = [];
|
|
6691
|
+
if (!(0, import_node_fs2.existsSync)(dir)) return results;
|
|
6692
|
+
for (const entry of (0, import_node_fs2.readdirSync)(dir, { withFileTypes: true })) {
|
|
6693
|
+
if (entry.isDirectory()) {
|
|
6694
|
+
if (ignoreDirs.has(entry.name)) continue;
|
|
6695
|
+
results.push(...walkWithIgnore((0, import_node_path2.join)(dir, entry.name), exts, ignoreDirs));
|
|
6696
|
+
} else if (exts.includes((0, import_node_path2.extname)(entry.name))) {
|
|
6697
|
+
results.push((0, import_node_path2.join)(dir, entry.name));
|
|
6698
|
+
}
|
|
6699
|
+
}
|
|
6700
|
+
return results;
|
|
6701
|
+
}
|
|
6613
6702
|
function toNodeId(srcDir, absPath) {
|
|
6614
6703
|
return (0, import_node_path2.relative)(srcDir, absPath).replace(/\\/g, "/");
|
|
6615
6704
|
}
|
|
@@ -6629,20 +6718,58 @@ function resolveRelativeImport(fromFile, specifier) {
|
|
|
6629
6718
|
}
|
|
6630
6719
|
return null;
|
|
6631
6720
|
}
|
|
6721
|
+
function resolveBarrelMap(barrelAbsPath, parsedByPath, memo, visiting) {
|
|
6722
|
+
const cached = memo.get(barrelAbsPath);
|
|
6723
|
+
if (cached) return cached;
|
|
6724
|
+
if (visiting.has(barrelAbsPath)) return /* @__PURE__ */ new Map();
|
|
6725
|
+
visiting.add(barrelAbsPath);
|
|
6726
|
+
const parsed = parsedByPath.get(barrelAbsPath);
|
|
6727
|
+
const map = /* @__PURE__ */ new Map();
|
|
6728
|
+
if (!parsed) {
|
|
6729
|
+
visiting.delete(barrelAbsPath);
|
|
6730
|
+
memo.set(barrelAbsPath, map);
|
|
6731
|
+
return map;
|
|
6732
|
+
}
|
|
6733
|
+
for (const re of parsed.reExports) {
|
|
6734
|
+
if (!re.from.startsWith(".")) continue;
|
|
6735
|
+
const resolved = resolveRelativeImport(barrelAbsPath, re.from);
|
|
6736
|
+
if (!resolved) continue;
|
|
6737
|
+
if (re.isWildcard) {
|
|
6738
|
+
const targetBn = (0, import_node_path2.basename)(resolved);
|
|
6739
|
+
const targetIsBarrel = targetBn === "index.ts" || targetBn === "index.tsx";
|
|
6740
|
+
if (targetIsBarrel) {
|
|
6741
|
+
const nested = resolveBarrelMap(resolved, parsedByPath, memo, visiting);
|
|
6742
|
+
for (const [name, target] of nested) {
|
|
6743
|
+
if (!map.has(name)) map.set(name, target);
|
|
6744
|
+
}
|
|
6745
|
+
} else {
|
|
6746
|
+
const targetParsed = parsedByPath.get(resolved);
|
|
6747
|
+
if (targetParsed) {
|
|
6748
|
+
for (const exp of targetParsed.exports) {
|
|
6749
|
+
if (!map.has(exp)) map.set(exp, resolved);
|
|
6750
|
+
}
|
|
6751
|
+
}
|
|
6752
|
+
}
|
|
6753
|
+
} else {
|
|
6754
|
+
if (!map.has(re.name)) map.set(re.name, resolved);
|
|
6755
|
+
}
|
|
6756
|
+
}
|
|
6757
|
+
visiting.delete(barrelAbsPath);
|
|
6758
|
+
memo.set(barrelAbsPath, map);
|
|
6759
|
+
return map;
|
|
6760
|
+
}
|
|
6632
6761
|
function buildAllBarrelMaps(srcDir, parsedByPath) {
|
|
6633
6762
|
const barrels = /* @__PURE__ */ new Map();
|
|
6763
|
+
const memo = /* @__PURE__ */ new Map();
|
|
6634
6764
|
for (const [absPath, parsed] of parsedByPath) {
|
|
6635
6765
|
const bn = (0, import_node_path2.basename)(absPath);
|
|
6636
6766
|
if (bn !== "index.ts" && bn !== "index.tsx") continue;
|
|
6637
6767
|
if (parsed.reExports.length === 0) continue;
|
|
6638
|
-
const
|
|
6639
|
-
|
|
6640
|
-
|
|
6641
|
-
|
|
6642
|
-
const resolved = resolveRelativeImport(absPath, re.from);
|
|
6643
|
-
if (resolved) map.set(re.name, resolved);
|
|
6768
|
+
const map = resolveBarrelMap(absPath, parsedByPath, memo, /* @__PURE__ */ new Set());
|
|
6769
|
+
if (map.size > 0) {
|
|
6770
|
+
const barrelId = (0, import_node_path2.relative)(srcDir, (0, import_node_path2.dirname)(absPath)).replace(/\\/g, "/");
|
|
6771
|
+
barrels.set(barrelId, map);
|
|
6644
6772
|
}
|
|
6645
|
-
if (map.size > 0) barrels.set(barrelId, map);
|
|
6646
6773
|
}
|
|
6647
6774
|
return barrels;
|
|
6648
6775
|
}
|
|
@@ -6780,6 +6907,105 @@ function matchRouteToPage(route, routeToNodeId) {
|
|
|
6780
6907
|
if (routeToNodeId.has(normalized)) return routeToNodeId.get(normalized);
|
|
6781
6908
|
return null;
|
|
6782
6909
|
}
|
|
6910
|
+
function loadApiRoutes(rootDir) {
|
|
6911
|
+
const apiJsonPath = (0, import_node_path2.join)(rootDir, ".launchsecure", "graphs", "api.json");
|
|
6912
|
+
if (!(0, import_node_fs2.existsSync)(apiJsonPath)) return [];
|
|
6913
|
+
try {
|
|
6914
|
+
const parsed = JSON.parse((0, import_node_fs2.readFileSync)(apiJsonPath, "utf-8"));
|
|
6915
|
+
const routes = [];
|
|
6916
|
+
for (const n of parsed.nodes ?? []) {
|
|
6917
|
+
const path9 = n.path;
|
|
6918
|
+
if (!path9 || typeof path9 !== "string") continue;
|
|
6919
|
+
routes.push({
|
|
6920
|
+
path: path9,
|
|
6921
|
+
nodeId: n.id,
|
|
6922
|
+
segments: path9.split("/").filter(Boolean)
|
|
6923
|
+
});
|
|
6924
|
+
}
|
|
6925
|
+
return routes;
|
|
6926
|
+
} catch {
|
|
6927
|
+
return [];
|
|
6928
|
+
}
|
|
6929
|
+
}
|
|
6930
|
+
function buildApiPathMap(routes) {
|
|
6931
|
+
const map = /* @__PURE__ */ new Map();
|
|
6932
|
+
for (const r of routes) {
|
|
6933
|
+
if (!map.has(r.path)) map.set(r.path, r.nodeId);
|
|
6934
|
+
}
|
|
6935
|
+
return map;
|
|
6936
|
+
}
|
|
6937
|
+
function normalizeFetchUrl(raw) {
|
|
6938
|
+
let s = raw.replace(/^`|`$/g, "");
|
|
6939
|
+
const qIdx = s.indexOf("?");
|
|
6940
|
+
if (qIdx >= 0) s = s.slice(0, qIdx);
|
|
6941
|
+
const hIdx = s.indexOf("#");
|
|
6942
|
+
if (hIdx >= 0) s = s.slice(0, hIdx);
|
|
6943
|
+
let hadInterpolation = false;
|
|
6944
|
+
s = s.replace(/\$\{([^}]+)\}/g, (_, expr) => {
|
|
6945
|
+
hadInterpolation = true;
|
|
6946
|
+
const cleaned = expr.trim();
|
|
6947
|
+
const last = cleaned.split(".").pop() ?? cleaned;
|
|
6948
|
+
const name = last.replace(/[^\w]/g, "") || "param";
|
|
6949
|
+
return ":" + name;
|
|
6950
|
+
});
|
|
6951
|
+
s = s.replace(/\/+/g, "/");
|
|
6952
|
+
if (s.length > 1 && s.endsWith("/")) s = s.slice(0, -1);
|
|
6953
|
+
return { path: s || "/", hadInterpolation };
|
|
6954
|
+
}
|
|
6955
|
+
function scoreApiRouteMatch(candidate, known) {
|
|
6956
|
+
if (candidate.length !== known.length) return -1;
|
|
6957
|
+
let score = 0;
|
|
6958
|
+
for (let i = 0; i < candidate.length; i++) {
|
|
6959
|
+
const a = candidate[i];
|
|
6960
|
+
const b = known[i];
|
|
6961
|
+
if (a === b) {
|
|
6962
|
+
score += 3;
|
|
6963
|
+
continue;
|
|
6964
|
+
}
|
|
6965
|
+
if (a.startsWith(":") && b.startsWith(":")) {
|
|
6966
|
+
score += 2;
|
|
6967
|
+
continue;
|
|
6968
|
+
}
|
|
6969
|
+
if (a.startsWith(":") || b.startsWith(":")) {
|
|
6970
|
+
score += 1;
|
|
6971
|
+
continue;
|
|
6972
|
+
}
|
|
6973
|
+
return -1;
|
|
6974
|
+
}
|
|
6975
|
+
return score;
|
|
6976
|
+
}
|
|
6977
|
+
function resolveFetchCall(call, apiPathMap, apiRoutes) {
|
|
6978
|
+
const raw = call.url;
|
|
6979
|
+
if (/^(https?:)?\/\//i.test(raw)) {
|
|
6980
|
+
return { kind: "external", normalizedUrl: raw };
|
|
6981
|
+
}
|
|
6982
|
+
if (call.isConcat) {
|
|
6983
|
+
return { kind: "dynamic", normalizedUrl: raw };
|
|
6984
|
+
}
|
|
6985
|
+
const { path: path9, hadInterpolation } = normalizeFetchUrl(raw);
|
|
6986
|
+
if (!path9.startsWith("/")) {
|
|
6987
|
+
return { kind: "unresolved", normalizedUrl: path9 };
|
|
6988
|
+
}
|
|
6989
|
+
const segs = path9.split("/").filter(Boolean);
|
|
6990
|
+
if (hadInterpolation && segs.length > 0 && segs[0].startsWith(":")) {
|
|
6991
|
+
return { kind: "dynamic", normalizedUrl: path9 };
|
|
6992
|
+
}
|
|
6993
|
+
const exact = apiPathMap.get(path9);
|
|
6994
|
+
if (exact) return { kind: "resolved", nodeId: exact, normalizedUrl: path9 };
|
|
6995
|
+
let bestScore = -1;
|
|
6996
|
+
let bestId = null;
|
|
6997
|
+
for (const r of apiRoutes) {
|
|
6998
|
+
const score = scoreApiRouteMatch(segs, r.segments);
|
|
6999
|
+
if (score > bestScore) {
|
|
7000
|
+
bestScore = score;
|
|
7001
|
+
bestId = r.nodeId;
|
|
7002
|
+
}
|
|
7003
|
+
}
|
|
7004
|
+
if (bestId && bestScore > 0) {
|
|
7005
|
+
return { kind: "resolved", nodeId: bestId, normalizedUrl: path9 };
|
|
7006
|
+
}
|
|
7007
|
+
return { kind: "unresolved", normalizedUrl: path9 };
|
|
7008
|
+
}
|
|
6783
7009
|
function extractEdges(srcDir, absPath, sourceId, parsed, nodeIdSet, nodeTypeMap, barrelMaps, routeToNodeId) {
|
|
6784
7010
|
const edges = [];
|
|
6785
7011
|
const flagged = [];
|
|
@@ -6884,7 +7110,7 @@ function detect(rootDir) {
|
|
|
6884
7110
|
function generate(rootDir) {
|
|
6885
7111
|
const srcDir = (0, import_node_path2.join)(rootDir, "src");
|
|
6886
7112
|
const appFiles = walk((0, import_node_path2.join)(srcDir, "app"), [".tsx", ".ts"]).filter(
|
|
6887
|
-
(f) =>
|
|
7113
|
+
(f) => (0, import_node_path2.basename)(f) !== "route.ts" && (0, import_node_path2.basename)(f) !== "route.tsx"
|
|
6888
7114
|
);
|
|
6889
7115
|
const clientFiles = walk((0, import_node_path2.join)(srcDir, "client"), [".tsx", ".ts"]);
|
|
6890
7116
|
const serverFiles = walk((0, import_node_path2.join)(srcDir, "server"), [".ts", ".tsx"]).filter(
|
|
@@ -6917,6 +7143,7 @@ function generate(rootDir) {
|
|
|
6917
7143
|
}
|
|
6918
7144
|
const allEdges = [];
|
|
6919
7145
|
const allFlagged = [];
|
|
7146
|
+
const crossRefs = [];
|
|
6920
7147
|
for (const absPath of fileSet) {
|
|
6921
7148
|
const sourceId = toNodeId(srcDir, absPath);
|
|
6922
7149
|
const parsed = parsedByPath.get(absPath);
|
|
@@ -6933,6 +7160,139 @@ function generate(rootDir) {
|
|
|
6933
7160
|
allEdges.push(...edges);
|
|
6934
7161
|
allFlagged.push(...flagged);
|
|
6935
7162
|
}
|
|
7163
|
+
const apiRoutes = loadApiRoutes(rootDir);
|
|
7164
|
+
const apiPathMap = buildApiPathMap(apiRoutes);
|
|
7165
|
+
const includeExternalFetches = process.env.LAUNCH_CHART_INCLUDE_EXTERNAL_FETCHES === "1";
|
|
7166
|
+
const fetchSeen = /* @__PURE__ */ new Set();
|
|
7167
|
+
let fetchResolvedCount = 0;
|
|
7168
|
+
let fetchDynamicCount = 0;
|
|
7169
|
+
let fetchUnresolvedCount = 0;
|
|
7170
|
+
let fetchExternalCount = 0;
|
|
7171
|
+
for (const absPath of fileSet) {
|
|
7172
|
+
const sourceId = toNodeId(srcDir, absPath);
|
|
7173
|
+
const parsed = parsedByPath.get(absPath);
|
|
7174
|
+
if (parsed.fetchCalls.length === 0) continue;
|
|
7175
|
+
for (const call of parsed.fetchCalls) {
|
|
7176
|
+
const result = resolveFetchCall(call, apiPathMap, apiRoutes);
|
|
7177
|
+
const methodTag = call.method ?? (call.kind === "fetch" ? "GET?" : "?");
|
|
7178
|
+
if (result.kind === "resolved" && result.nodeId) {
|
|
7179
|
+
const key = `${sourceId}\u2192${result.nodeId}\u2192calls_api`;
|
|
7180
|
+
if (fetchSeen.has(key)) continue;
|
|
7181
|
+
fetchSeen.add(key);
|
|
7182
|
+
crossRefs.push({
|
|
7183
|
+
source: sourceId,
|
|
7184
|
+
target: result.nodeId,
|
|
7185
|
+
type: "calls_api",
|
|
7186
|
+
layer: "api"
|
|
7187
|
+
});
|
|
7188
|
+
fetchResolvedCount++;
|
|
7189
|
+
continue;
|
|
7190
|
+
}
|
|
7191
|
+
if (result.kind === "dynamic") {
|
|
7192
|
+
fetchDynamicCount++;
|
|
7193
|
+
allFlagged.push({
|
|
7194
|
+
source: sourceId,
|
|
7195
|
+
target: "DYNAMIC",
|
|
7196
|
+
type: "calls_api",
|
|
7197
|
+
label: call.isConcat ? `${methodTag} fetch with concat: ${call.url}` : `${methodTag} fetch with template: ${call.url}`,
|
|
7198
|
+
confidence: call.isConcat ? "low" : "medium"
|
|
7199
|
+
});
|
|
7200
|
+
continue;
|
|
7201
|
+
}
|
|
7202
|
+
if (result.kind === "external") {
|
|
7203
|
+
fetchExternalCount++;
|
|
7204
|
+
if (!includeExternalFetches) continue;
|
|
7205
|
+
allFlagged.push({
|
|
7206
|
+
source: sourceId,
|
|
7207
|
+
target: "EXTERNAL",
|
|
7208
|
+
type: "calls_external",
|
|
7209
|
+
label: `${methodTag} external fetch: ${call.url}`,
|
|
7210
|
+
confidence: "high"
|
|
7211
|
+
});
|
|
7212
|
+
continue;
|
|
7213
|
+
}
|
|
7214
|
+
fetchUnresolvedCount++;
|
|
7215
|
+
allFlagged.push({
|
|
7216
|
+
source: sourceId,
|
|
7217
|
+
target: "UNRESOLVED",
|
|
7218
|
+
type: "calls_api",
|
|
7219
|
+
label: `${methodTag} fetch to unknown path: ${result.normalizedUrl}`,
|
|
7220
|
+
confidence: "medium"
|
|
7221
|
+
});
|
|
7222
|
+
}
|
|
7223
|
+
}
|
|
7224
|
+
const externalScanned = new Set(allDiscovered.map((f) => f.replace(/\\/g, "/")));
|
|
7225
|
+
const IGNORE_DIRS = /* @__PURE__ */ new Set([
|
|
7226
|
+
"node_modules",
|
|
7227
|
+
".next",
|
|
7228
|
+
"dist",
|
|
7229
|
+
".launchsecure",
|
|
7230
|
+
".git",
|
|
7231
|
+
"src",
|
|
7232
|
+
"coverage",
|
|
7233
|
+
".turbo",
|
|
7234
|
+
"build",
|
|
7235
|
+
"out",
|
|
7236
|
+
".vercel"
|
|
7237
|
+
]);
|
|
7238
|
+
const externalCandidates = walkWithIgnore(rootDir, [".ts", ".tsx"], IGNORE_DIRS);
|
|
7239
|
+
for (const absPath of externalCandidates) {
|
|
7240
|
+
const normalized = absPath.replace(/\\/g, "/");
|
|
7241
|
+
if (externalScanned.has(normalized)) continue;
|
|
7242
|
+
let parsed;
|
|
7243
|
+
try {
|
|
7244
|
+
parsed = parseFile(absPath);
|
|
7245
|
+
} catch {
|
|
7246
|
+
continue;
|
|
7247
|
+
}
|
|
7248
|
+
const externalId = (0, import_node_path2.relative)(rootDir, absPath).replace(/\\/g, "/");
|
|
7249
|
+
const edgesFromThis = [];
|
|
7250
|
+
const seen = /* @__PURE__ */ new Set();
|
|
7251
|
+
for (const imp of parsed.imports) {
|
|
7252
|
+
const { specifier, isTypeOnly, names } = imp;
|
|
7253
|
+
let resolved = null;
|
|
7254
|
+
if (specifier.startsWith("@/")) {
|
|
7255
|
+
const relToSrc = specifier.slice(2);
|
|
7256
|
+
const barrelMap = barrelMaps.get(relToSrc);
|
|
7257
|
+
if (barrelMap && names.length > 0) {
|
|
7258
|
+
for (const name of names) {
|
|
7259
|
+
const targetAbs = barrelMap.get(name);
|
|
7260
|
+
if (!targetAbs) continue;
|
|
7261
|
+
const targetId2 = toNodeId(srcDir, targetAbs);
|
|
7262
|
+
if (!nodeIdSet.has(targetId2)) continue;
|
|
7263
|
+
const key2 = `${externalId}\u2192${targetId2}`;
|
|
7264
|
+
if (seen.has(key2)) continue;
|
|
7265
|
+
seen.add(key2);
|
|
7266
|
+
edgesFromThis.push({ source: externalId, target: targetId2, type: "imports" });
|
|
7267
|
+
}
|
|
7268
|
+
continue;
|
|
7269
|
+
}
|
|
7270
|
+
resolved = resolveImport(srcDir, specifier);
|
|
7271
|
+
} else if (specifier.startsWith(".")) {
|
|
7272
|
+
resolved = resolveRelativeImport(absPath, specifier);
|
|
7273
|
+
}
|
|
7274
|
+
if (!resolved) continue;
|
|
7275
|
+
const targetId = toNodeId(srcDir, resolved);
|
|
7276
|
+
if (!nodeIdSet.has(targetId)) continue;
|
|
7277
|
+
if (targetId.endsWith("/index.ts") || targetId.endsWith("/index.tsx")) continue;
|
|
7278
|
+
const key = `${externalId}\u2192${targetId}\u2192${isTypeOnly ? "type" : "value"}`;
|
|
7279
|
+
if (seen.has(key)) continue;
|
|
7280
|
+
seen.add(key);
|
|
7281
|
+
edgesFromThis.push({ source: externalId, target: targetId, type: "imports" });
|
|
7282
|
+
}
|
|
7283
|
+
if (edgesFromThis.length === 0) continue;
|
|
7284
|
+
nodes.push({
|
|
7285
|
+
id: externalId,
|
|
7286
|
+
type: "external",
|
|
7287
|
+
name: parsed.name || nameFromFilename(absPath),
|
|
7288
|
+
route: null,
|
|
7289
|
+
module: "external",
|
|
7290
|
+
exports: parsed.exports
|
|
7291
|
+
});
|
|
7292
|
+
nodeIdSet.add(externalId);
|
|
7293
|
+
nodeTypeMap.set(externalId, "external");
|
|
7294
|
+
allEdges.push(...edgesFromThis);
|
|
7295
|
+
}
|
|
6936
7296
|
const flaggedSet = /* @__PURE__ */ new Set();
|
|
6937
7297
|
const dedupedFlagged = allFlagged.filter((f) => {
|
|
6938
7298
|
const key = `${f.source}\u2192${f.target}\u2192${f.label}`;
|
|
@@ -6964,6 +7324,7 @@ function generate(rootDir) {
|
|
|
6964
7324
|
total_configs: byType("config"),
|
|
6965
7325
|
total_utils: byType("util"),
|
|
6966
7326
|
total_libs: byType("lib"),
|
|
7327
|
+
total_external: byType("external"),
|
|
6967
7328
|
total_edges: allEdges.length,
|
|
6968
7329
|
total_flagged: dedupedFlagged.length
|
|
6969
7330
|
};
|
|
@@ -6975,11 +7336,20 @@ function generate(rootDir) {
|
|
|
6975
7336
|
layer: "ui",
|
|
6976
7337
|
parser: "react-nextjs-ast",
|
|
6977
7338
|
...stats,
|
|
7339
|
+
api_call_detection: {
|
|
7340
|
+
includeExternalFetches,
|
|
7341
|
+
includeConcatFetches: process.env.LAUNCH_CHART_INCLUDE_CONCAT_FETCHES === "1",
|
|
7342
|
+
apiRoutesLoaded: apiRoutes.length,
|
|
7343
|
+
resolved: fetchResolvedCount,
|
|
7344
|
+
dynamic: fetchDynamicCount,
|
|
7345
|
+
unresolved: fetchUnresolvedCount,
|
|
7346
|
+
external: fetchExternalCount
|
|
7347
|
+
},
|
|
6978
7348
|
notes: "Auto-generated via TypeScript AST \u2014 edges derived from actual imports, renders from JSX usage, navigations from router/Link calls."
|
|
6979
7349
|
},
|
|
6980
7350
|
nodes,
|
|
6981
7351
|
edges: allEdges,
|
|
6982
|
-
cross_refs:
|
|
7352
|
+
cross_refs: crossRefs,
|
|
6983
7353
|
contradictions: [],
|
|
6984
7354
|
warnings: [],
|
|
6985
7355
|
flagged_edges: dedupedFlagged,
|
|
@@ -7004,7 +7374,7 @@ var reactNextjsParser = {
|
|
|
7004
7374
|
// src/server/graph/parsers/api/nextjs-routes.ts
|
|
7005
7375
|
var import_node_fs3 = require("node:fs");
|
|
7006
7376
|
var import_node_path3 = require("node:path");
|
|
7007
|
-
var
|
|
7377
|
+
var HTTP_METHODS2 = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"]);
|
|
7008
7378
|
function walk2(dir) {
|
|
7009
7379
|
const results = [];
|
|
7010
7380
|
if (!(0, import_node_fs3.existsSync)(dir)) return results;
|
|
@@ -7048,7 +7418,7 @@ function generate2(rootDir) {
|
|
|
7048
7418
|
const authWrappers = extractAuthWrappers(absPath);
|
|
7049
7419
|
const methods = [];
|
|
7050
7420
|
for (const exp of parsed.exports) {
|
|
7051
|
-
if (
|
|
7421
|
+
if (HTTP_METHODS2.has(exp)) methods.push(exp);
|
|
7052
7422
|
}
|
|
7053
7423
|
const routePath = filePathToRoute(apiDir, absPath);
|
|
7054
7424
|
const relPath = (0, import_node_path3.relative)(rootDir, absPath).replace(/\\/g, "/");
|
|
@@ -7118,7 +7488,7 @@ function generate2(rootDir) {
|
|
|
7118
7488
|
flagged_edges: [],
|
|
7119
7489
|
patterns: {
|
|
7120
7490
|
total_endpoints: nodes.length,
|
|
7121
|
-
methods_breakdown: [...
|
|
7491
|
+
methods_breakdown: [...HTTP_METHODS2].reduce((acc, m) => {
|
|
7122
7492
|
acc[m] = nodes.filter((n) => n.methods.includes(m)).length;
|
|
7123
7493
|
return acc;
|
|
7124
7494
|
}, {}),
|
|
@@ -7310,13 +7680,14 @@ function generateLayer(rootDir, layer) {
|
|
|
7310
7680
|
};
|
|
7311
7681
|
}
|
|
7312
7682
|
function generateAll(rootDir) {
|
|
7313
|
-
const layers = ["
|
|
7683
|
+
const layers = ["api", "db", "ui"];
|
|
7314
7684
|
const results = [];
|
|
7315
7685
|
for (const layer of layers) {
|
|
7316
7686
|
const result = generateLayer(rootDir, layer);
|
|
7317
7687
|
if (result) results.push(result);
|
|
7318
7688
|
}
|
|
7319
|
-
|
|
7689
|
+
const byLayer = new Map(results.map((r) => [r.layer, r]));
|
|
7690
|
+
return ["ui", "api", "db"].map((l) => byLayer.get(l)).filter((r) => !!r);
|
|
7320
7691
|
}
|
|
7321
7692
|
|
|
7322
7693
|
// src/server/graph/index.ts
|
|
@@ -7421,8 +7792,69 @@ function handleGraphCommand(subcommand, args) {
|
|
|
7421
7792
|
}
|
|
7422
7793
|
|
|
7423
7794
|
// src/server/graph-mcp.ts
|
|
7795
|
+
var import_node_fs7 = require("node:fs");
|
|
7796
|
+
var import_node_path7 = require("node:path");
|
|
7797
|
+
|
|
7798
|
+
// src/server/lockfile.ts
|
|
7799
|
+
var import_node_child_process = require("node:child_process");
|
|
7424
7800
|
var import_node_fs6 = require("node:fs");
|
|
7801
|
+
var import_node_os = require("node:os");
|
|
7425
7802
|
var import_node_path6 = require("node:path");
|
|
7803
|
+
function lockDir() {
|
|
7804
|
+
return (0, import_node_path6.join)((0, import_node_os.homedir)(), ".launchsecure");
|
|
7805
|
+
}
|
|
7806
|
+
function lockPath() {
|
|
7807
|
+
return (0, import_node_path6.join)(lockDir(), "launch-chart.lock");
|
|
7808
|
+
}
|
|
7809
|
+
function readLock() {
|
|
7810
|
+
const p = lockPath();
|
|
7811
|
+
if (!(0, import_node_fs6.existsSync)(p)) return null;
|
|
7812
|
+
try {
|
|
7813
|
+
const data = JSON.parse((0, import_node_fs6.readFileSync)(p, "utf-8"));
|
|
7814
|
+
if (typeof data.pid !== "number" || typeof data.port !== "number") return null;
|
|
7815
|
+
return data;
|
|
7816
|
+
} catch {
|
|
7817
|
+
return null;
|
|
7818
|
+
}
|
|
7819
|
+
}
|
|
7820
|
+
function isPidAlive(pid) {
|
|
7821
|
+
try {
|
|
7822
|
+
process.kill(pid, 0);
|
|
7823
|
+
return true;
|
|
7824
|
+
} catch {
|
|
7825
|
+
return false;
|
|
7826
|
+
}
|
|
7827
|
+
}
|
|
7828
|
+
function getListenerPid(port) {
|
|
7829
|
+
try {
|
|
7830
|
+
const out = (0, import_node_child_process.execFileSync)("lsof", ["-nP", "-iTCP:" + port, "-sTCP:LISTEN", "-t"], {
|
|
7831
|
+
encoding: "utf-8",
|
|
7832
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
7833
|
+
timeout: 500
|
|
7834
|
+
}).trim();
|
|
7835
|
+
if (!out) return null;
|
|
7836
|
+
const pid = parseInt(out.split("\n")[0], 10);
|
|
7837
|
+
return Number.isFinite(pid) ? pid : null;
|
|
7838
|
+
} catch {
|
|
7839
|
+
return null;
|
|
7840
|
+
}
|
|
7841
|
+
}
|
|
7842
|
+
function getLiveLock() {
|
|
7843
|
+
const lock = readLock();
|
|
7844
|
+
if (!lock) return null;
|
|
7845
|
+
const listenerPid = getListenerPid(lock.port);
|
|
7846
|
+
const live = listenerPid !== null ? listenerPid === lock.pid : isPidAlive(lock.pid);
|
|
7847
|
+
if (!live) {
|
|
7848
|
+
try {
|
|
7849
|
+
(0, import_node_fs6.unlinkSync)(lockPath());
|
|
7850
|
+
} catch {
|
|
7851
|
+
}
|
|
7852
|
+
return null;
|
|
7853
|
+
}
|
|
7854
|
+
return lock;
|
|
7855
|
+
}
|
|
7856
|
+
|
|
7857
|
+
// src/server/graph-mcp.ts
|
|
7426
7858
|
var SERVER_INFO = {
|
|
7427
7859
|
name: "launchsecure-graph",
|
|
7428
7860
|
version: "0.0.1"
|
|
@@ -7548,6 +7980,14 @@ Returns: { pattern, filter, files_searched, total_matches, matches: [{file, line
|
|
|
7548
7980
|
},
|
|
7549
7981
|
required: ["layer", "pattern"]
|
|
7550
7982
|
}
|
|
7983
|
+
},
|
|
7984
|
+
{
|
|
7985
|
+
name: "get_graph_ui_url",
|
|
7986
|
+
description: 'Return the URL of a running launch-chart UI server if one exists. The UI is a visual, interactive view of the merged UI+API+DB project graph served by `launch-chart serve` (or auto-started via LAUNCH_CHART_AUTOSERVE=1). \n\nReturns: { running: boolean, url?: string, port?: number, pid?: number, startedAt?: string, cwd?: string }. If running is false, no server is currently live \u2014 suggest the user run `launch-chart serve` to start one. \n\nUse this when the user asks "open the graph", "show me the project graph UI", "where\'s the chart", etc.',
|
|
7987
|
+
inputSchema: {
|
|
7988
|
+
type: "object",
|
|
7989
|
+
properties: {}
|
|
7990
|
+
}
|
|
7551
7991
|
}
|
|
7552
7992
|
];
|
|
7553
7993
|
function matchesSearch(node, query) {
|
|
@@ -7900,9 +8340,9 @@ function handleReadGraph(args) {
|
|
|
7900
8340
|
return okJson(result);
|
|
7901
8341
|
}
|
|
7902
8342
|
function nodeToFilePath(rootDir, layer, nodeId) {
|
|
7903
|
-
if (layer === "ui") return (0,
|
|
7904
|
-
if (layer === "api") return (0,
|
|
7905
|
-
if (layer === "db") return (0,
|
|
8343
|
+
if (layer === "ui") return (0, import_node_path7.join)(rootDir, "src", nodeId);
|
|
8344
|
+
if (layer === "api") return (0, import_node_path7.join)(rootDir, nodeId);
|
|
8345
|
+
if (layer === "db") return (0, import_node_path7.join)(rootDir, "prisma", "schema.prisma");
|
|
7906
8346
|
return null;
|
|
7907
8347
|
}
|
|
7908
8348
|
function handleGrepNodes(args) {
|
|
@@ -7962,11 +8402,11 @@ function handleGrepNodes(args) {
|
|
|
7962
8402
|
let filesSearched = 0;
|
|
7963
8403
|
let truncated = false;
|
|
7964
8404
|
for (const [filePath, nodeId] of filePaths) {
|
|
7965
|
-
if (!(0,
|
|
8405
|
+
if (!(0, import_node_fs7.existsSync)(filePath)) continue;
|
|
7966
8406
|
filesSearched++;
|
|
7967
8407
|
let content;
|
|
7968
8408
|
try {
|
|
7969
|
-
content = (0,
|
|
8409
|
+
content = (0, import_node_fs7.readFileSync)(filePath, "utf-8");
|
|
7970
8410
|
} catch {
|
|
7971
8411
|
continue;
|
|
7972
8412
|
}
|
|
@@ -8003,6 +8443,23 @@ function handleGrepNodes(args) {
|
|
|
8003
8443
|
truncated
|
|
8004
8444
|
});
|
|
8005
8445
|
}
|
|
8446
|
+
function handleGetGraphUiUrl() {
|
|
8447
|
+
const lock = getLiveLock();
|
|
8448
|
+
if (!lock) {
|
|
8449
|
+
return okJson({
|
|
8450
|
+
running: false,
|
|
8451
|
+
hint: "No launch-chart UI server is currently running. Start one with `launch-chart serve`, or set LAUNCH_CHART_AUTOSERVE=1 in your MCP config to auto-start it alongside the MCP server."
|
|
8452
|
+
});
|
|
8453
|
+
}
|
|
8454
|
+
return okJson({
|
|
8455
|
+
running: true,
|
|
8456
|
+
url: lock.url,
|
|
8457
|
+
port: lock.port,
|
|
8458
|
+
pid: lock.pid,
|
|
8459
|
+
cwd: lock.cwd,
|
|
8460
|
+
startedAt: lock.startedAt
|
|
8461
|
+
});
|
|
8462
|
+
}
|
|
8006
8463
|
function send(msg) {
|
|
8007
8464
|
process.stdout.write(JSON.stringify(msg) + "\n");
|
|
8008
8465
|
}
|
|
@@ -8046,6 +8503,10 @@ function handleMessage(msg) {
|
|
|
8046
8503
|
respond(id ?? null, handleGrepNodes(args));
|
|
8047
8504
|
return;
|
|
8048
8505
|
}
|
|
8506
|
+
if (toolName === "get_graph_ui_url") {
|
|
8507
|
+
respond(id ?? null, handleGetGraphUiUrl());
|
|
8508
|
+
return;
|
|
8509
|
+
}
|
|
8049
8510
|
respondError(id ?? null, -32601, `Unknown tool: ${toolName}`);
|
|
8050
8511
|
return;
|
|
8051
8512
|
}
|