@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.
@@ -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 barrelId = (0, import_node_path2.relative)(srcDir, (0, import_node_path2.dirname)(absPath)).replace(/\\/g, "/");
6639
- const map = /* @__PURE__ */ new Map();
6640
- for (const re of parsed.reExports) {
6641
- if (!re.from.startsWith(".")) continue;
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) => f.endsWith("/page.tsx") || f.endsWith("/layout.tsx")
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 HTTP_METHODS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"]);
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 (HTTP_METHODS.has(exp)) methods.push(exp);
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: [...HTTP_METHODS].reduce((acc, m) => {
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 = ["ui", "api", "db"];
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
- return results;
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, import_node_path6.join)(rootDir, "src", nodeId);
7904
- if (layer === "api") return (0, import_node_path6.join)(rootDir, nodeId);
7905
- if (layer === "db") return (0, import_node_path6.join)(rootDir, "prisma", "schema.prisma");
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, import_node_fs6.existsSync)(filePath)) continue;
8405
+ if (!(0, import_node_fs7.existsSync)(filePath)) continue;
7966
8406
  filesSearched++;
7967
8407
  let content;
7968
8408
  try {
7969
- content = (0, import_node_fs6.readFileSync)(filePath, "utf-8");
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
  }