@launchsecure/launch-kit 0.0.21 → 0.0.22

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.
@@ -631,6 +631,12 @@ function parseFileTS(absPath) {
631
631
  if (linkTemplate) {
632
632
  navigations.push({ kind: "link-href", target: linkTemplate, isTemplate: true });
633
633
  }
634
+ if (caps["nav.redirect.literal"]) {
635
+ navigations.push({ kind: "router-replace", target: caps["nav.redirect.literal"], isTemplate: false });
636
+ }
637
+ if (caps["nav.redirect.template"]) {
638
+ navigations.push({ kind: "router-replace", target: caps["nav.redirect.template"], isTemplate: true });
639
+ }
634
640
  if (caps["nav.window.literal"]) {
635
641
  navigations.push({ kind: "window-location", target: caps["nav.window.literal"], isTemplate: false });
636
642
  }
@@ -709,15 +715,25 @@ function extractDbCallsTS(absPath) {
709
715
  const seen = /* @__PURE__ */ new Set();
710
716
  for (const m of matches) {
711
717
  const caps = captureMap(m);
718
+ const sbTable = caps["sb.table"];
719
+ const sbMethod = caps["sb.method"];
720
+ if (sbTable && sbMethod) {
721
+ const key2 = `sql:${sbTable}.${sbMethod}`;
722
+ if (seen.has(key2)) continue;
723
+ seen.add(key2);
724
+ const isMutation = SUPABASE_MUTATION_METHODS_BUILTIN.has(sbMethod) || extraMutationMethods.includes(sbMethod);
725
+ calls.push({ model: sbTable, method: sbMethod, isMutation, kind: "sql" });
726
+ continue;
727
+ }
712
728
  const identifier = caps["db.identifier"];
713
729
  const model = caps["db.model"];
714
730
  const method = caps["db.method"];
715
731
  if (!identifier || !model || !method) continue;
716
732
  if (!dbIdentifiers.has(identifier)) continue;
717
- const key = `${model}.${method}`;
733
+ const key = `orm:${model}.${method}`;
718
734
  if (seen.has(key)) continue;
719
735
  seen.add(key);
720
- calls.push({ model, method, isMutation: getMutationMethods().has(method) });
736
+ calls.push({ model, method, isMutation: getMutationMethods().has(method), kind: "orm" });
721
737
  }
722
738
  return calls;
723
739
  }
@@ -731,6 +747,9 @@ function classifyFile(absPath) {
731
747
  const captures = classifyQuery.captures(root);
732
748
  const capNames = new Set(captures.map((c) => c.name));
733
749
  if (capNames.has("http_export") || capNames.has("http_export_fn")) return "endpoint";
750
+ if (capNames.has("use_server_directive") && fileName !== "page.tsx" && fileName !== "layout.tsx") {
751
+ return "server-action";
752
+ }
734
753
  if (fileName === "page.tsx" && capNames.has("has_jsx")) return "page";
735
754
  if (fileName === "layout.tsx" && capNames.has("has_jsx")) return "layout";
736
755
  if (capNames.has("has_create_context") || capNames.has("has_create_context_bare")) return "context";
@@ -751,6 +770,36 @@ function extractAuthWrappersTS(absPath) {
751
770
  wrappers.add(caps["wrapper.fn_name"]);
752
771
  }
753
772
  }
773
+ const inlineHelpers = /* @__PURE__ */ new Set();
774
+ for (const stmt of childrenOfType(root, "import_statement")) {
775
+ const sourceNode = childOfType(stmt, "string");
776
+ const frag = sourceNode ? childOfType(sourceNode, "string_fragment") : void 0;
777
+ if (!frag) continue;
778
+ const provider = INLINE_AUTH_IMPORTS.find((p) => p.module.test(frag.text));
779
+ if (!provider) continue;
780
+ const clause = childOfType(stmt, "import_clause");
781
+ if (!clause) continue;
782
+ const named = childOfType(clause, "named_imports");
783
+ if (!named) continue;
784
+ for (const specNode of childrenOfType(named, "import_specifier")) {
785
+ const ids = childrenOfType(specNode, "identifier");
786
+ const importedName = ids[0]?.text;
787
+ const localName = ids[ids.length - 1]?.text;
788
+ if (importedName && provider.helpers.includes(importedName) && localName) {
789
+ inlineHelpers.add(localName);
790
+ }
791
+ }
792
+ }
793
+ if (inlineHelpers.size > 0) {
794
+ const text = root.text;
795
+ for (const name of inlineHelpers) {
796
+ const re = new RegExp(`\\b${name}\\s*\\(`);
797
+ if (re.test(text)) {
798
+ wrappers.add("inline");
799
+ break;
800
+ }
801
+ }
802
+ }
754
803
  return wrappers;
755
804
  }
756
805
  function trunc(s, max = 120) {
@@ -909,7 +958,7 @@ function extractDeep(absPath) {
909
958
  }
910
959
  return { elements, stateVars, conditions, variables, responses, params };
911
960
  }
912
- var import_node_fs5, import_node_path5, tsxLanguage, parserInstance, initPromise, initialized, queriesDir, queryCache, PRISMA_MUTATION_METHODS_BUILTIN, DB_IDENTIFIERS_FALLBACK, extraDbIdentifiers, extraMutationMethods;
961
+ var import_node_fs5, import_node_path5, tsxLanguage, parserInstance, initPromise, initialized, queriesDir, queryCache, PRISMA_MUTATION_METHODS_BUILTIN, SUPABASE_MUTATION_METHODS_BUILTIN, DB_IDENTIFIERS_FALLBACK, extraDbIdentifiers, extraMutationMethods, INLINE_AUTH_IMPORTS;
913
962
  var init_ts_extractor = __esm({
914
963
  "src/server/graph/core/ts-extractor.ts"() {
915
964
  "use strict";
@@ -933,9 +982,21 @@ var init_ts_extractor = __esm({
933
982
  "delete",
934
983
  "deleteMany"
935
984
  ];
985
+ SUPABASE_MUTATION_METHODS_BUILTIN = /* @__PURE__ */ new Set([
986
+ "insert",
987
+ "update",
988
+ "delete",
989
+ "upsert"
990
+ ]);
936
991
  DB_IDENTIFIERS_FALLBACK = ["db", "prisma"];
937
992
  extraDbIdentifiers = [];
938
993
  extraMutationMethods = [];
994
+ INLINE_AUTH_IMPORTS = [
995
+ { module: /^@clerk\/nextjs(\/server)?$/, helpers: ["auth", "currentUser"] },
996
+ { module: /^next-auth(\/.+)?$/, helpers: ["auth", "getServerSession"] },
997
+ { module: /^@auth\//, helpers: ["auth"] },
998
+ { module: /^@supabase\/auth-helpers/, helpers: ["createServerClient"] }
999
+ ];
939
1000
  }
940
1001
  });
941
1002
 
@@ -1027,6 +1088,8 @@ function extractRoute(id) {
1027
1088
  if (!id.endsWith("/page.tsx")) return null;
1028
1089
  let route = id.replace(/^app\//, "/").replace(/\/page\.tsx$/, "");
1029
1090
  route = route.replace(/\/\([^)]+\)/g, "");
1091
+ route = route.replace(/\[\[\.\.\.([^\]]+)\]\]/g, "*$1?");
1092
+ route = route.replace(/\[\.\.\.([^\]]+)\]/g, "*$1");
1030
1093
  route = route.replace(/\[([^\]]+)\]/g, ":$1");
1031
1094
  route = route.replace(/\/+/g, "/");
1032
1095
  if (!route.startsWith("/")) route = "/" + route;
@@ -1038,6 +1101,7 @@ function nameFromFilename(absPath) {
1038
1101
  function filePathToAppRoute(appDir, absPath) {
1039
1102
  let route = ("/" + (0, import_node_path6.relative)(appDir, absPath).replace(/\\/g, "/")).replace(/\/route\.tsx?$/, "");
1040
1103
  route = route.replace(/\/\([^)]+\)/g, "");
1104
+ route = route.replace(/\[\[\.\.\.([^\]]+)\]\]/g, "*$1?");
1041
1105
  route = route.replace(/\[\.\.\.([^\]]+)\]/g, "*$1");
1042
1106
  route = route.replace(/\[([^\]]+)\]/g, ":$1");
1043
1107
  route = route.replace(/\/+/g, "/");
@@ -1086,25 +1150,52 @@ function resolveTemplateLiteralRoute(template, routeToNodeId) {
1086
1150
  function routeMatchScore(candidate, known) {
1087
1151
  const segsA = candidate.split("/");
1088
1152
  const segsB = known.split("/");
1089
- if (segsA.length !== segsB.length) return -1;
1090
1153
  let score = 0;
1091
- for (let i = 0; i < segsA.length; i++) {
1092
- const a = segsA[i], b = segsB[i];
1154
+ let i = 0, j = 0;
1155
+ while (i < segsA.length && j < segsB.length) {
1156
+ const a = segsA[i], b = segsB[j];
1157
+ if (b.startsWith("*") && b.endsWith("?")) {
1158
+ score += 1;
1159
+ return score;
1160
+ }
1161
+ if (b.startsWith("*")) {
1162
+ const remaining = segsA.length - i;
1163
+ if (remaining < 1) return -1;
1164
+ score += 1 + remaining;
1165
+ return score;
1166
+ }
1093
1167
  if (a === b) {
1094
1168
  score += 3;
1169
+ i++;
1170
+ j++;
1095
1171
  continue;
1096
1172
  }
1097
1173
  if (a.startsWith(":") && b.startsWith(":")) {
1098
1174
  score += 2;
1175
+ i++;
1176
+ j++;
1099
1177
  continue;
1100
1178
  }
1101
1179
  if (a.startsWith(":") || b.startsWith(":")) {
1102
- score += 0;
1180
+ i++;
1181
+ j++;
1103
1182
  continue;
1104
1183
  }
1105
1184
  return -1;
1106
1185
  }
1107
- return score;
1186
+ if (i === segsA.length) {
1187
+ while (j < segsB.length) {
1188
+ const b = segsB[j];
1189
+ if (b.startsWith("*") && b.endsWith("?")) {
1190
+ score += 1;
1191
+ j++;
1192
+ continue;
1193
+ }
1194
+ return -1;
1195
+ }
1196
+ return score;
1197
+ }
1198
+ return -1;
1108
1199
  }
1109
1200
  function templateToRoute(template) {
1110
1201
  return template.replace(/\$\{([^}]+)\}/g, (_, expr) => {
@@ -1139,7 +1230,7 @@ function extractEdges(srcDir, rootDir, absPath, sourceId, parsed, nodeIdSet, bar
1139
1230
  edges.push(edge);
1140
1231
  }
1141
1232
  function edgeTypeFor(isTypeOnlyImport, importedNames) {
1142
- if (isTypeOnlyImport) return "imports";
1233
+ if (isTypeOnlyImport) return "imports_type";
1143
1234
  const anyRendered = importedNames.some((n) => parsed.jsxElements.has(n));
1144
1235
  if (anyRendered) return "renders";
1145
1236
  return "imports";
@@ -1170,7 +1261,8 @@ function extractEdges(srcDir, rootDir, absPath, sourceId, parsed, nodeIdSet, bar
1170
1261
  if (resolved) {
1171
1262
  const targetId = toNodeId(srcDir, rootDir, resolved);
1172
1263
  if (nodeIdSet.has(targetId) && !targetId.endsWith("/index.ts") && !targetId.endsWith("/index.tsx")) {
1173
- addEdge(targetId, edgeTypeFor(isTypeOnly, names));
1264
+ const allType = isTypeOnly || names.length > 0 && names.every((n) => typeNames.has(n));
1265
+ addEdge(targetId, edgeTypeFor(allType, names));
1174
1266
  }
1175
1267
  }
1176
1268
  }
@@ -1179,7 +1271,8 @@ function extractEdges(srcDir, rootDir, absPath, sourceId, parsed, nodeIdSet, bar
1179
1271
  if (resolved) {
1180
1272
  const targetId = toNodeId(srcDir, rootDir, resolved);
1181
1273
  if (nodeIdSet.has(targetId) && !targetId.endsWith("/index.ts") && !targetId.endsWith("/index.tsx")) {
1182
- addEdge(targetId, edgeTypeFor(isTypeOnly, names));
1274
+ const allType = isTypeOnly || names.length > 0 && names.every((n) => typeNames.has(n));
1275
+ addEdge(targetId, edgeTypeFor(allType, names));
1183
1276
  }
1184
1277
  }
1185
1278
  }
@@ -1264,26 +1357,34 @@ function generate(rootDir) {
1264
1357
  const layer = CLASSIFICATION_TO_LAYER[type] ?? "ui";
1265
1358
  nodeIdSet.add(id);
1266
1359
  if (layer === "api") {
1267
- const methods = [];
1268
- for (const exp of parsed.exports) {
1269
- if (HTTP_METHODS.has(exp)) methods.push(exp);
1270
- }
1271
1360
  const dbCalls = extractDbCallsTS(absPath);
1272
1361
  const authWrappers = extractAuthWrappersTS(absPath);
1273
1362
  const deep = extractDeep(absPath);
1274
- const routePath = filePathToAppRoute(paths.appDir, absPath);
1275
1363
  const mutations = dbCalls.filter((c) => c.isMutation);
1276
1364
  const mutates = mutations.length > 0;
1277
1365
  const authStrategy = [...authWrappers];
1366
+ const isServerAction = type === "server-action";
1367
+ const methods = [];
1368
+ if (!isServerAction) {
1369
+ for (const exp of parsed.exports) {
1370
+ if (HTTP_METHODS.has(exp)) methods.push(exp);
1371
+ }
1372
+ }
1373
+ const routePath = isServerAction ? null : filePathToAppRoute(paths.appDir, absPath);
1374
+ const actions = isServerAction ? parsed.exports.filter((e) => !HTTP_METHODS.has(e)) : [];
1278
1375
  apiNodes.push({
1279
1376
  id,
1280
- type: "endpoint",
1281
- name: routePath,
1377
+ type: isServerAction ? "server-action" : "endpoint",
1378
+ // For HTTP routes: name = URL path. For Server Actions: name = file id +
1379
+ // exported action names — the callable surface, not a URL.
1380
+ name: isServerAction ? actions.length > 0 ? `${id} (${actions.join(", ")})` : id : routePath,
1282
1381
  layer: "api",
1283
1382
  path: routePath,
1383
+ // null for Server Actions
1284
1384
  methods,
1285
1385
  handler: id,
1286
1386
  mutates,
1387
+ ...isServerAction ? { actions } : {},
1287
1388
  auth: authStrategy.length > 0 ? authStrategy : ["public"],
1288
1389
  db_models: [...new Set(dbCalls.map((c) => c.model))],
1289
1390
  db_operations: [...new Set(dbCalls.map((c) => `${c.model}.${c.method}`))],
@@ -1297,6 +1398,8 @@ function generate(rootDir) {
1297
1398
  } else {
1298
1399
  const route = extractRoute(id);
1299
1400
  const deep = extractDeep(absPath);
1401
+ const dbCalls = extractDbCallsTS(absPath);
1402
+ const authWrappers = type === "page" || type === "layout" ? [...extractAuthWrappersTS(absPath)] : [];
1300
1403
  uiNodes.push({
1301
1404
  id,
1302
1405
  type,
@@ -1307,7 +1410,9 @@ function generate(rootDir) {
1307
1410
  elements: deep.elements,
1308
1411
  stateVars: deep.stateVars,
1309
1412
  conditions: deep.conditions,
1310
- variables: deep.variables
1413
+ variables: deep.variables,
1414
+ ...authWrappers.length > 0 ? { auth: authWrappers } : {},
1415
+ ...dbCalls.length > 0 ? { _dbCalls: dbCalls } : {}
1311
1416
  });
1312
1417
  if (route) routeToNodeId.set(route, id);
1313
1418
  }
@@ -1331,6 +1436,29 @@ function generate(rootDir) {
1331
1436
  uiEdges.push(...edges);
1332
1437
  uiFlagged.push(...flagged);
1333
1438
  }
1439
+ const layoutsById = /* @__PURE__ */ new Set();
1440
+ for (const n of uiNodes) {
1441
+ if (n.type === "layout") layoutsById.add(n.id);
1442
+ }
1443
+ function findClosestLayout(pageId) {
1444
+ let dir = pageId.replace(/\/page\.tsx$/, "");
1445
+ while (dir.length > 0) {
1446
+ const candidate = `${dir}/layout.tsx`;
1447
+ if (layoutsById.has(candidate)) return candidate;
1448
+ const slash = dir.lastIndexOf("/");
1449
+ if (slash < 0) break;
1450
+ dir = dir.slice(0, slash);
1451
+ }
1452
+ if (layoutsById.has("app/layout.tsx")) return "app/layout.tsx";
1453
+ return null;
1454
+ }
1455
+ for (const n of uiNodes) {
1456
+ if (n.type !== "page") continue;
1457
+ const layoutId = findClosestLayout(n.id);
1458
+ if (layoutId && layoutId !== n.id) {
1459
+ uiEdges.push({ source: layoutId, target: n.id, type: "wraps" });
1460
+ }
1461
+ }
1334
1462
  const fetchCallEntries = [];
1335
1463
  for (const absPath of fileSet) {
1336
1464
  const sourceId = toNodeId(srcDir, rootDir, absPath);
@@ -1412,17 +1540,37 @@ function generate(rootDir) {
1412
1540
  if (!dbCalls) continue;
1413
1541
  const seenModels = /* @__PURE__ */ new Set();
1414
1542
  for (const call of dbCalls) {
1415
- if (seenModels.has(call.model)) continue;
1416
- seenModels.add(call.model);
1543
+ const target = call.kind === "sql" ? call.model : camelToPascal(call.model);
1544
+ if (seenModels.has(target)) continue;
1545
+ seenModels.add(target);
1417
1546
  apiCrossRefs.push({
1418
1547
  source: node.id,
1419
- target: camelToPascal(call.model),
1548
+ target,
1549
+ type: call.isMutation ? "mutates" : "reads",
1550
+ layer: "db"
1551
+ });
1552
+ }
1553
+ delete node._dbCalls;
1554
+ }
1555
+ const uiCrossRefs = [];
1556
+ for (const node of uiNodes) {
1557
+ const dbCalls = node._dbCalls;
1558
+ if (!dbCalls) continue;
1559
+ const seenModels = /* @__PURE__ */ new Set();
1560
+ for (const call of dbCalls) {
1561
+ const target = call.kind === "sql" ? call.model : camelToPascal(call.model);
1562
+ if (seenModels.has(target)) continue;
1563
+ seenModels.add(target);
1564
+ uiCrossRefs.push({
1565
+ source: node.id,
1566
+ target,
1420
1567
  type: call.isMutation ? "mutates" : "reads",
1421
1568
  layer: "db"
1422
1569
  });
1423
1570
  }
1424
1571
  delete node._dbCalls;
1425
1572
  }
1573
+ uiCrossRefs.sort((a, b) => a.source.localeCompare(b.source) || a.target.localeCompare(b.target));
1426
1574
  const apiNodeIds = new Set(apiNodes.map((n) => n.id));
1427
1575
  const apiEdges = [];
1428
1576
  const uiOnlyEdges = [];
@@ -1484,7 +1632,7 @@ function generate(rootDir) {
1484
1632
  },
1485
1633
  nodes: stripLayer(uiNodes),
1486
1634
  edges: uiOnlyEdges,
1487
- cross_refs: [],
1635
+ cross_refs: uiCrossRefs,
1488
1636
  contradictions: [],
1489
1637
  warnings: [],
1490
1638
  flagged_edges: dedupedFlagged,
@@ -1568,6 +1716,7 @@ var init_typescript_project = __esm({
1568
1716
  HTTP_METHODS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"]);
1569
1717
  CLASSIFICATION_TO_LAYER = {
1570
1718
  endpoint: "api",
1719
+ "server-action": "api",
1571
1720
  page: "ui",
1572
1721
  layout: "ui",
1573
1722
  component: "ui",
@@ -2370,26 +2519,54 @@ function normalizeFetchUrl(raw) {
2370
2519
  return { path: s || "/", hadInterpolation };
2371
2520
  }
2372
2521
  function scoreApiRouteMatch(candidate, known) {
2373
- if (candidate.length !== known.length) return -1;
2374
2522
  let score = 0;
2375
- for (let i = 0; i < candidate.length; i++) {
2523
+ let i = 0, j = 0;
2524
+ while (i < candidate.length && j < known.length) {
2376
2525
  const a = candidate[i];
2377
- const b = known[i];
2526
+ const b = known[j];
2527
+ if (b.startsWith("*") && b.endsWith("?")) {
2528
+ score += 1;
2529
+ return score;
2530
+ }
2531
+ if (b.startsWith("*")) {
2532
+ const remaining = candidate.length - i;
2533
+ if (remaining < 1) return -1;
2534
+ score += 1 + remaining;
2535
+ return score;
2536
+ }
2378
2537
  if (a === b) {
2379
2538
  score += 3;
2539
+ i++;
2540
+ j++;
2380
2541
  continue;
2381
2542
  }
2382
2543
  if (a.startsWith(":") && b.startsWith(":")) {
2383
2544
  score += 2;
2545
+ i++;
2546
+ j++;
2384
2547
  continue;
2385
2548
  }
2386
2549
  if (a.startsWith(":") || b.startsWith(":")) {
2387
2550
  score += 1;
2551
+ i++;
2552
+ j++;
2388
2553
  continue;
2389
2554
  }
2390
2555
  return -1;
2391
2556
  }
2392
- return score;
2557
+ if (i === candidate.length) {
2558
+ while (j < known.length) {
2559
+ const b = known[j];
2560
+ if (b.startsWith("*") && b.endsWith("?")) {
2561
+ score += 1;
2562
+ j++;
2563
+ continue;
2564
+ }
2565
+ return -1;
2566
+ }
2567
+ return score;
2568
+ }
2569
+ return -1;
2393
2570
  }
2394
2571
  function resolveFetchCall(call, apiPathMap, apiRoutes) {
2395
2572
  const raw = call.url;
@@ -2556,22 +2733,12 @@ var init_fetch_resolver = __esm({
2556
2733
  });
2557
2734
 
2558
2735
  // src/server/graph/parsers/crosslayer/api-annotations.ts
2559
- function walk2(dir, exts) {
2560
- if (!(0, import_node_fs9.existsSync)(dir)) return [];
2561
- const results = [];
2562
- for (const entry of (0, import_node_fs9.readdirSync)(dir, { withFileTypes: true })) {
2563
- if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
2564
- const full = (0, import_node_path8.join)(dir, entry.name);
2565
- if (entry.isDirectory()) {
2566
- results.push(...walk2(full, exts));
2567
- } else if (exts.includes((0, import_node_path8.extname)(entry.name))) {
2568
- results.push(full);
2569
- }
2736
+ function toNodeId2(srcDir, rootDir, absPath) {
2737
+ const relFromSrc = (0, import_node_path8.relative)(srcDir, absPath).replace(/\\/g, "/");
2738
+ if (relFromSrc.startsWith("..")) {
2739
+ return (0, import_node_path8.relative)(rootDir, absPath).replace(/\\/g, "/");
2570
2740
  }
2571
- return results;
2572
- }
2573
- function toNodeId2(srcDir, absPath) {
2574
- return (0, import_node_path8.relative)(srcDir, absPath).replace(/\\/g, "/");
2741
+ return relFromSrc;
2575
2742
  }
2576
2743
  var import_node_fs9, import_node_path8, API_ANNOTATION_RE, apiAnnotationsParser;
2577
2744
  var init_api_annotations = __esm({
@@ -2580,31 +2747,53 @@ var init_api_annotations = __esm({
2580
2747
  import_node_fs9 = require("node:fs");
2581
2748
  import_node_path8 = require("node:path");
2582
2749
  init_api_route_matching();
2750
+ init_config();
2751
+ init_resolve_paths();
2752
+ init_walk();
2583
2753
  API_ANNOTATION_RE = /@api\s+(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+(\/\S+)/g;
2584
2754
  apiAnnotationsParser = {
2585
2755
  id: "api-annotations",
2586
2756
  layer: "crosslayer",
2587
2757
  concern: "api-binding",
2588
2758
  detect(rootDir) {
2589
- return (0, import_node_fs9.existsSync)((0, import_node_path8.join)(rootDir, "src"));
2759
+ return resolveProjectPaths(rootDir, loadConfig(rootDir)) !== null;
2590
2760
  },
2591
2761
  generate(rootDir, layerOutputs) {
2592
2762
  const apiOutput = layerOutputs.get("api");
2593
2763
  if (!apiOutput) {
2594
2764
  return { cross_refs: [], flagged_edges: [], warnings: [] };
2595
2765
  }
2766
+ const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
2767
+ if (!paths) {
2768
+ return { cross_refs: [], flagged_edges: [], warnings: [] };
2769
+ }
2596
2770
  const uiOutput = layerOutputs.get("ui");
2597
2771
  const uiNodeIds = new Set(uiOutput?.nodes.map((n) => n.id) ?? []);
2598
2772
  const apiRoutes = loadApiRoutesFromOutput(apiOutput);
2599
2773
  const apiPathMap = buildApiPathMap(apiRoutes);
2600
- const srcDir = (0, import_node_path8.join)(rootDir, "src");
2601
- const files = walk2(srcDir, [".ts", ".tsx"]);
2774
+ const srcDir = paths.srcDir;
2775
+ const seen = /* @__PURE__ */ new Set();
2776
+ const files = [];
2777
+ for (const root of paths.srcRoots) {
2778
+ for (const f of walk(root, [".ts", ".tsx"])) {
2779
+ if (!seen.has(f)) {
2780
+ seen.add(f);
2781
+ files.push(f);
2782
+ }
2783
+ }
2784
+ }
2785
+ for (const conv of paths.conventionFiles) {
2786
+ if (!seen.has(conv)) {
2787
+ seen.add(conv);
2788
+ files.push(conv);
2789
+ }
2790
+ }
2602
2791
  const crossRefs = [];
2603
2792
  const flaggedEdges = [];
2604
- const seen = /* @__PURE__ */ new Set();
2793
+ const seenEdge = /* @__PURE__ */ new Set();
2605
2794
  for (const absPath of files) {
2606
2795
  const content = (0, import_node_fs9.readFileSync)(absPath, "utf-8");
2607
- const sourceId = toNodeId2(srcDir, absPath);
2796
+ const sourceId = toNodeId2(srcDir, rootDir, absPath);
2608
2797
  if (!uiNodeIds.has(sourceId)) continue;
2609
2798
  let match;
2610
2799
  API_ANNOTATION_RE.lastIndex = 0;
@@ -2614,8 +2803,8 @@ var init_api_annotations = __esm({
2614
2803
  const result = resolveUrlPath(urlPath, apiPathMap, apiRoutes);
2615
2804
  if (result.kind === "resolved" && result.nodeId) {
2616
2805
  const key = `${sourceId}|${result.nodeId}|calls_api`;
2617
- if (seen.has(key)) continue;
2618
- seen.add(key);
2806
+ if (seenEdge.has(key)) continue;
2807
+ seenEdge.add(key);
2619
2808
  crossRefs.push({
2620
2809
  source: sourceId,
2621
2810
  target: result.nodeId,
@@ -2649,22 +2838,12 @@ var init_api_annotations = __esm({
2649
2838
  });
2650
2839
 
2651
2840
  // src/server/graph/parsers/crosslayer/url-literal-scanner.ts
2652
- function walk3(dir, exts) {
2653
- if (!(0, import_node_fs10.existsSync)(dir)) return [];
2654
- const results = [];
2655
- for (const entry of (0, import_node_fs10.readdirSync)(dir, { withFileTypes: true })) {
2656
- if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
2657
- const full = (0, import_node_path9.join)(dir, entry.name);
2658
- if (entry.isDirectory()) {
2659
- results.push(...walk3(full, exts));
2660
- } else if (exts.includes((0, import_node_path9.extname)(entry.name))) {
2661
- results.push(full);
2662
- }
2841
+ function toNodeId3(srcDir, rootDir, absPath) {
2842
+ const relFromSrc = (0, import_node_path9.relative)(srcDir, absPath).replace(/\\/g, "/");
2843
+ if (relFromSrc.startsWith("..")) {
2844
+ return (0, import_node_path9.relative)(rootDir, absPath).replace(/\\/g, "/");
2663
2845
  }
2664
- return results;
2665
- }
2666
- function toNodeId3(srcDir, absPath) {
2667
- return (0, import_node_path9.relative)(srcDir, absPath).replace(/\\/g, "/");
2846
+ return relFromSrc;
2668
2847
  }
2669
2848
  var import_node_fs10, import_node_path9, URL_LITERAL_RE, urlLiteralScannerParser;
2670
2849
  var init_url_literal_scanner = __esm({
@@ -2675,7 +2854,8 @@ var init_url_literal_scanner = __esm({
2675
2854
  init_api_route_matching();
2676
2855
  init_config();
2677
2856
  init_resolve_paths();
2678
- URL_LITERAL_RE = /['"`](\/api\/[^'"`\s]+?)['"`]/g;
2857
+ init_walk();
2858
+ URL_LITERAL_RE = /['"`](\/[a-zA-Z][^'"`\s]*?)['"`]/g;
2679
2859
  urlLiteralScannerParser = {
2680
2860
  id: "url-literal-scanner",
2681
2861
  layer: "crosslayer",
@@ -2695,15 +2875,26 @@ var init_url_literal_scanner = __esm({
2695
2875
  const apiPathMap = buildApiPathMap(apiRoutes);
2696
2876
  const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
2697
2877
  const srcDir = paths.srcDir;
2698
- const clientDir = (0, import_node_path9.join)(srcDir, "client");
2699
- const files = [
2700
- ...walk3(clientDir, [".ts", ".tsx"]),
2701
- ...walk3(paths.appDir, [".ts", ".tsx"])
2702
- ];
2878
+ const seenFile = /* @__PURE__ */ new Set();
2879
+ const files = [];
2880
+ for (const root of paths.srcRoots) {
2881
+ for (const f of walk(root, [".ts", ".tsx"])) {
2882
+ if (!seenFile.has(f)) {
2883
+ seenFile.add(f);
2884
+ files.push(f);
2885
+ }
2886
+ }
2887
+ }
2888
+ for (const conv of paths.conventionFiles) {
2889
+ if (!seenFile.has(conv)) {
2890
+ seenFile.add(conv);
2891
+ files.push(conv);
2892
+ }
2893
+ }
2703
2894
  const crossRefs = [];
2704
2895
  const seen = /* @__PURE__ */ new Set();
2705
2896
  for (const absPath of files) {
2706
- const sourceId = toNodeId3(srcDir, absPath);
2897
+ const sourceId = toNodeId3(srcDir, rootDir, absPath);
2707
2898
  if (!uiNodeIds.has(sourceId)) continue;
2708
2899
  const content = (0, import_node_fs10.readFileSync)(absPath, "utf-8");
2709
2900
  let match;
@@ -2766,10 +2957,15 @@ function classifyScope(source, model) {
2766
2957
  function extractEnumValues(rootDir) {
2767
2958
  const nodes = [];
2768
2959
  const edges = [];
2769
- const schemaPaths = [
2770
- (0, import_node_path10.join)(rootDir, "prisma", "schema.prisma"),
2771
- (0, import_node_path10.join)(rootDir, "prisma", "schema")
2772
- ];
2960
+ const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
2961
+ const schemaPaths = [];
2962
+ if (paths?.dbConfig.kind === "prisma" && paths.dbConfig.schemaPath) {
2963
+ schemaPaths.push(paths.dbConfig.schemaPath);
2964
+ schemaPaths.push((0, import_node_path10.join)((0, import_node_path10.dirname)(paths.dbConfig.schemaPath), "schema"));
2965
+ } else {
2966
+ schemaPaths.push((0, import_node_path10.join)(rootDir, "prisma", "schema.prisma"));
2967
+ schemaPaths.push((0, import_node_path10.join)(rootDir, "prisma", "schema"));
2968
+ }
2773
2969
  let content = "";
2774
2970
  for (const p of schemaPaths) {
2775
2971
  if ((0, import_node_fs11.existsSync)(p)) {
@@ -2853,7 +3049,7 @@ function extractStringArrayFromNode(node) {
2853
3049
  return values;
2854
3050
  }
2855
3051
  function findArrayDecl(root, varName) {
2856
- function walk4(node) {
3052
+ function walk2(node) {
2857
3053
  if (node.type === "variable_declarator") {
2858
3054
  const nameNode = node.childForFieldName("name");
2859
3055
  const valueNode = node.childForFieldName("value");
@@ -2866,12 +3062,12 @@ function findArrayDecl(root, varName) {
2866
3062
  }
2867
3063
  }
2868
3064
  for (const child of node.namedChildren) {
2869
- const found = walk4(child);
3065
+ const found = walk2(child);
2870
3066
  if (found) return found;
2871
3067
  }
2872
3068
  return null;
2873
3069
  }
2874
- return walk4(root);
3070
+ return walk2(root);
2875
3071
  }
2876
3072
  function extractObjectPropsRegex(objStr) {
2877
3073
  const props = {};
@@ -2934,11 +3130,26 @@ function modelToNodeType(model) {
2934
3130
  function extractSeedData(rootDir) {
2935
3131
  const nodes = [];
2936
3132
  const edges = [];
2937
- const seedFiles = [
2938
- (0, import_node_path10.join)(rootDir, "prisma", "seed.ts"),
2939
- (0, import_node_path10.join)(rootDir, "prisma", "seed.js"),
2940
- (0, import_node_path10.join)(rootDir, "src", "server", "lib", "system-tags.ts")
2941
- ].filter(import_node_fs11.existsSync);
3133
+ const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
3134
+ const candidates = [];
3135
+ if (paths?.dbDir) {
3136
+ candidates.push((0, import_node_path10.join)(paths.dbDir, "seed.ts"));
3137
+ candidates.push((0, import_node_path10.join)(paths.dbDir, "seed.js"));
3138
+ } else {
3139
+ candidates.push((0, import_node_path10.join)(rootDir, "prisma", "seed.ts"));
3140
+ candidates.push((0, import_node_path10.join)(rootDir, "prisma", "seed.js"));
3141
+ }
3142
+ const baseRoots = paths?.srcRoots ?? [(0, import_node_path10.join)(rootDir, "src")];
3143
+ for (const root of baseRoots) {
3144
+ candidates.push((0, import_node_path10.join)(root, "server", "lib", "system-tags.ts"));
3145
+ }
3146
+ const seedFiles = candidates.filter((p) => {
3147
+ try {
3148
+ return (0, import_node_fs11.existsSync)(p) && (0, import_node_fs11.statSync)(p).isFile();
3149
+ } catch {
3150
+ return false;
3151
+ }
3152
+ });
2942
3153
  const useTreeSitter = tryLoadTreeSitter();
2943
3154
  for (const filePath of seedFiles) {
2944
3155
  const content = (0, import_node_fs11.readFileSync)(filePath, "utf-8");
@@ -3048,9 +3259,20 @@ function walkDir(dir, exts) {
3048
3259
  }
3049
3260
  function extractConstants(rootDir) {
3050
3261
  const nodes = [];
3051
- const srcDir = (0, import_node_path10.join)(rootDir, "src");
3052
- if (!(0, import_node_fs11.existsSync)(srcDir)) return { nodes };
3053
- for (const filePath of walkDir(srcDir, [".ts", ".tsx"])) {
3262
+ const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
3263
+ const roots = paths?.srcRoots ?? [(0, import_node_path10.join)(rootDir, "src")];
3264
+ const seenFile = /* @__PURE__ */ new Set();
3265
+ const allFiles = [];
3266
+ for (const root of roots) {
3267
+ for (const f of walkDir(root, [".ts", ".tsx"])) {
3268
+ if (!seenFile.has(f)) {
3269
+ seenFile.add(f);
3270
+ allFiles.push(f);
3271
+ }
3272
+ }
3273
+ }
3274
+ if (allFiles.length === 0) return { nodes };
3275
+ for (const filePath of allFiles) {
3054
3276
  const content = (0, import_node_fs11.readFileSync)(filePath, "utf-8");
3055
3277
  const relPath = (0, import_node_path10.relative)(rootDir, filePath);
3056
3278
  const constArrayRe = /export\s+const\s+([A-Z][A-Z_0-9]+)\s*(?::[^=]+)?\s*=\s*\[/g;
@@ -3085,6 +3307,13 @@ function extractConstants(rootDir) {
3085
3307
  return { nodes };
3086
3308
  }
3087
3309
  function detect4(rootDir) {
3310
+ const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
3311
+ if (paths?.dbConfig.kind === "prisma" && paths.dbConfig.schemaPath && (0, import_node_fs11.existsSync)(paths.dbConfig.schemaPath)) {
3312
+ return true;
3313
+ }
3314
+ if (paths?.dbDir) {
3315
+ if ((0, import_node_fs11.existsSync)((0, import_node_path10.join)(paths.dbDir, "seed.ts")) || (0, import_node_fs11.existsSync)((0, import_node_path10.join)(paths.dbDir, "seed.js"))) return true;
3316
+ }
3088
3317
  return (0, import_node_fs11.existsSync)((0, import_node_path10.join)(rootDir, "prisma", "schema.prisma")) || (0, import_node_fs11.existsSync)((0, import_node_path10.join)(rootDir, "prisma", "seed.ts"));
3089
3318
  }
3090
3319
  function generate4(rootDir) {
@@ -3158,6 +3387,8 @@ var init_static_values = __esm({
3158
3387
  "use strict";
3159
3388
  import_node_fs11 = require("node:fs");
3160
3389
  import_node_path10 = require("node:path");
3390
+ init_config();
3391
+ init_resolve_paths();
3161
3392
  parseCode = null;
3162
3393
  SHARED_MODELS = /* @__PURE__ */ new Set(["permission", "role", "tag"]);
3163
3394
  DB_MODELS = /* @__PURE__ */ new Set(["subscriptionPlan", "providerDefinition", "pipelineMasterTemplate"]);
@@ -3321,13 +3552,22 @@ var init_static_ref_scanner = __esm({
3321
3552
  const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
3322
3553
  if (!paths) return { cross_refs: [], flagged_edges: [], warnings: [] };
3323
3554
  const srcDir = paths.srcDir;
3324
- const files = [
3325
- ...walkWithIgnore((0, import_node_path11.join)(srcDir, "client"), [".ts", ".tsx"]),
3326
- ...walkWithIgnore(paths.appDir, [".ts", ".tsx"]),
3327
- ...walkWithIgnore((0, import_node_path11.join)(srcDir, "server"), [".ts", ".tsx"]),
3328
- ...walkWithIgnore((0, import_node_path11.join)(srcDir, "lib"), [".ts", ".tsx"]),
3329
- ...walkWithIgnore((0, import_node_path11.join)(srcDir, "config"), [".ts", ".tsx"])
3330
- ];
3555
+ const seenFile = /* @__PURE__ */ new Set();
3556
+ const files = [];
3557
+ for (const root of paths.srcRoots) {
3558
+ for (const f of walkWithIgnore(root, [".ts", ".tsx"])) {
3559
+ if (!seenFile.has(f)) {
3560
+ seenFile.add(f);
3561
+ files.push(f);
3562
+ }
3563
+ }
3564
+ }
3565
+ for (const conv of paths.conventionFiles) {
3566
+ if (!seenFile.has(conv)) {
3567
+ seenFile.add(conv);
3568
+ files.push(conv);
3569
+ }
3570
+ }
3331
3571
  const uiOutput = layerOutputs.get("ui");
3332
3572
  const apiOutput = layerOutputs.get("api");
3333
3573
  const uiNodeIds = new Set(uiOutput?.nodes.map((n) => n.id) ?? []);
@@ -3344,7 +3584,8 @@ var init_static_ref_scanner = __esm({
3344
3584
  const seen = /* @__PURE__ */ new Set();
3345
3585
  let filesScanned = 0;
3346
3586
  for (const absPath of files) {
3347
- const sourceId = (0, import_node_path11.relative)(srcDir, absPath).replace(/\\/g, "/");
3587
+ const relFromSrc = (0, import_node_path11.relative)(srcDir, absPath).replace(/\\/g, "/");
3588
+ const sourceId = relFromSrc.startsWith("..") ? (0, import_node_path11.relative)(rootDir, absPath).replace(/\\/g, "/") : relFromSrc;
3348
3589
  const sourceLayer = uiNodeIds.has(sourceId) ? "ui" : apiNodeIds.has(sourceId) ? "api" : null;
3349
3590
  if (!sourceLayer) continue;
3350
3591
  const content = (0, import_node_fs12.readFileSync)(absPath, "utf-8");
@@ -3816,13 +4057,17 @@ function isTrivialGroup(name, extraTrivial) {
3816
4057
  function normalizeGroupName(name) {
3817
4058
  return name.toLowerCase().replace(/-pages?$/, "").replace(/-layout$/, "").replace(/-wrapper$/, "");
3818
4059
  }
3819
- function extractModuleFromPath(id, extraTrivial, extraSkipSegments) {
4060
+ function extractModuleFromPath(id, extraTrivial, extraSkipSegments, extraGenericRoles) {
3820
4061
  const segments = id.split("/");
3821
4062
  const routeGroups = extractRouteGroups(id);
3822
4063
  const skipSegments = new Set(SKIP_SEGMENTS_BUILTIN);
4064
+ for (const r of GENERIC_ROLE_NAMES_BUILTIN) skipSegments.add(r);
3823
4065
  if (extraSkipSegments) {
3824
4066
  for (const s of extraSkipSegments) skipSegments.add(s);
3825
4067
  }
4068
+ if (extraGenericRoles) {
4069
+ for (const r of extraGenericRoles) skipSegments.add(r);
4070
+ }
3826
4071
  const moduleGroups = routeGroups.filter((g) => !isTrivialGroup(g, extraTrivial)).map(normalizeGroupName);
3827
4072
  if (moduleGroups.length > 0) {
3828
4073
  return moduleGroups[moduleGroups.length - 1];
@@ -6108,7 +6353,7 @@ function handleDetectProjectStack() {
6108
6353
  const descriptions = {
6109
6354
  "fetch-resolver": "Detects direct fetch()/api.get() calls with inline URLs",
6110
6355
  "api-annotations": "Scans for @api METHOD /path annotations in JSDoc/comments",
6111
- "url-literal-scanner": "Finds /api/... string literals as fallback detection",
6356
+ "url-literal-scanner": "Finds path-like string literals (any /\u2026) and resolves them to API routes",
6112
6357
  "static-ref-scanner": "Finds references to static values (enums, permissions, roles)"
6113
6358
  };
6114
6359
  const grouped = {};