@launchsecure/launch-kit 0.0.21 → 0.0.23

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,9 +1410,17 @@ 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
- if (route) routeToNodeId.set(route, id);
1417
+ if (route) {
1418
+ routeToNodeId.set(route, id);
1419
+ const trimmed = route.replace(/\/\*[^/]+\?$/, "");
1420
+ if (trimmed && trimmed !== route && !routeToNodeId.has(trimmed)) {
1421
+ routeToNodeId.set(trimmed, id);
1422
+ }
1423
+ }
1313
1424
  }
1314
1425
  }
1315
1426
  const uiEdges = [];
@@ -1331,6 +1442,29 @@ function generate(rootDir) {
1331
1442
  uiEdges.push(...edges);
1332
1443
  uiFlagged.push(...flagged);
1333
1444
  }
1445
+ const layoutsById = /* @__PURE__ */ new Set();
1446
+ for (const n of uiNodes) {
1447
+ if (n.type === "layout") layoutsById.add(n.id);
1448
+ }
1449
+ function findClosestLayout(pageId) {
1450
+ let dir = pageId.replace(/\/page\.tsx$/, "");
1451
+ while (dir.length > 0) {
1452
+ const candidate = `${dir}/layout.tsx`;
1453
+ if (layoutsById.has(candidate)) return candidate;
1454
+ const slash = dir.lastIndexOf("/");
1455
+ if (slash < 0) break;
1456
+ dir = dir.slice(0, slash);
1457
+ }
1458
+ if (layoutsById.has("app/layout.tsx")) return "app/layout.tsx";
1459
+ return null;
1460
+ }
1461
+ for (const n of uiNodes) {
1462
+ if (n.type !== "page") continue;
1463
+ const layoutId = findClosestLayout(n.id);
1464
+ if (layoutId && layoutId !== n.id) {
1465
+ uiEdges.push({ source: layoutId, target: n.id, type: "wraps" });
1466
+ }
1467
+ }
1334
1468
  const fetchCallEntries = [];
1335
1469
  for (const absPath of fileSet) {
1336
1470
  const sourceId = toNodeId(srcDir, rootDir, absPath);
@@ -1412,17 +1546,37 @@ function generate(rootDir) {
1412
1546
  if (!dbCalls) continue;
1413
1547
  const seenModels = /* @__PURE__ */ new Set();
1414
1548
  for (const call of dbCalls) {
1415
- if (seenModels.has(call.model)) continue;
1416
- seenModels.add(call.model);
1549
+ const target = call.kind === "sql" ? call.model : camelToPascal(call.model);
1550
+ if (seenModels.has(target)) continue;
1551
+ seenModels.add(target);
1417
1552
  apiCrossRefs.push({
1418
1553
  source: node.id,
1419
- target: camelToPascal(call.model),
1554
+ target,
1420
1555
  type: call.isMutation ? "mutates" : "reads",
1421
1556
  layer: "db"
1422
1557
  });
1423
1558
  }
1424
1559
  delete node._dbCalls;
1425
1560
  }
1561
+ const uiCrossRefs = [];
1562
+ for (const node of uiNodes) {
1563
+ const dbCalls = node._dbCalls;
1564
+ if (!dbCalls) continue;
1565
+ const seenModels = /* @__PURE__ */ new Set();
1566
+ for (const call of dbCalls) {
1567
+ const target = call.kind === "sql" ? call.model : camelToPascal(call.model);
1568
+ if (seenModels.has(target)) continue;
1569
+ seenModels.add(target);
1570
+ uiCrossRefs.push({
1571
+ source: node.id,
1572
+ target,
1573
+ type: call.isMutation ? "mutates" : "reads",
1574
+ layer: "db"
1575
+ });
1576
+ }
1577
+ delete node._dbCalls;
1578
+ }
1579
+ uiCrossRefs.sort((a, b) => a.source.localeCompare(b.source) || a.target.localeCompare(b.target));
1426
1580
  const apiNodeIds = new Set(apiNodes.map((n) => n.id));
1427
1581
  const apiEdges = [];
1428
1582
  const uiOnlyEdges = [];
@@ -1484,7 +1638,7 @@ function generate(rootDir) {
1484
1638
  },
1485
1639
  nodes: stripLayer(uiNodes),
1486
1640
  edges: uiOnlyEdges,
1487
- cross_refs: [],
1641
+ cross_refs: uiCrossRefs,
1488
1642
  contradictions: [],
1489
1643
  warnings: [],
1490
1644
  flagged_edges: dedupedFlagged,
@@ -1568,6 +1722,7 @@ var init_typescript_project = __esm({
1568
1722
  HTTP_METHODS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"]);
1569
1723
  CLASSIFICATION_TO_LAYER = {
1570
1724
  endpoint: "api",
1725
+ "server-action": "api",
1571
1726
  page: "ui",
1572
1727
  layout: "ui",
1573
1728
  component: "ui",
@@ -2348,6 +2503,10 @@ function buildApiPathMap(routes) {
2348
2503
  const map = /* @__PURE__ */ new Map();
2349
2504
  for (const r of routes) {
2350
2505
  if (!map.has(r.path)) map.set(r.path, r.nodeId);
2506
+ const trimmed = r.path.replace(/\/\*[^/]+\?$/, "");
2507
+ if (trimmed && trimmed !== r.path && !map.has(trimmed)) {
2508
+ map.set(trimmed, r.nodeId);
2509
+ }
2351
2510
  }
2352
2511
  return map;
2353
2512
  }
@@ -2370,26 +2529,54 @@ function normalizeFetchUrl(raw) {
2370
2529
  return { path: s || "/", hadInterpolation };
2371
2530
  }
2372
2531
  function scoreApiRouteMatch(candidate, known) {
2373
- if (candidate.length !== known.length) return -1;
2374
2532
  let score = 0;
2375
- for (let i = 0; i < candidate.length; i++) {
2533
+ let i = 0, j = 0;
2534
+ while (i < candidate.length && j < known.length) {
2376
2535
  const a = candidate[i];
2377
- const b = known[i];
2536
+ const b = known[j];
2537
+ if (b.startsWith("*") && b.endsWith("?")) {
2538
+ score += 1;
2539
+ return score;
2540
+ }
2541
+ if (b.startsWith("*")) {
2542
+ const remaining = candidate.length - i;
2543
+ if (remaining < 1) return -1;
2544
+ score += 1 + remaining;
2545
+ return score;
2546
+ }
2378
2547
  if (a === b) {
2379
2548
  score += 3;
2549
+ i++;
2550
+ j++;
2380
2551
  continue;
2381
2552
  }
2382
2553
  if (a.startsWith(":") && b.startsWith(":")) {
2383
2554
  score += 2;
2555
+ i++;
2556
+ j++;
2384
2557
  continue;
2385
2558
  }
2386
2559
  if (a.startsWith(":") || b.startsWith(":")) {
2387
2560
  score += 1;
2561
+ i++;
2562
+ j++;
2388
2563
  continue;
2389
2564
  }
2390
2565
  return -1;
2391
2566
  }
2392
- return score;
2567
+ if (i === candidate.length) {
2568
+ while (j < known.length) {
2569
+ const b = known[j];
2570
+ if (b.startsWith("*") && b.endsWith("?")) {
2571
+ score += 1;
2572
+ j++;
2573
+ continue;
2574
+ }
2575
+ return -1;
2576
+ }
2577
+ return score;
2578
+ }
2579
+ return -1;
2393
2580
  }
2394
2581
  function resolveFetchCall(call, apiPathMap, apiRoutes) {
2395
2582
  const raw = call.url;
@@ -2556,22 +2743,12 @@ var init_fetch_resolver = __esm({
2556
2743
  });
2557
2744
 
2558
2745
  // 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
- }
2746
+ function toNodeId2(srcDir, rootDir, absPath) {
2747
+ const relFromSrc = (0, import_node_path8.relative)(srcDir, absPath).replace(/\\/g, "/");
2748
+ if (relFromSrc.startsWith("..")) {
2749
+ return (0, import_node_path8.relative)(rootDir, absPath).replace(/\\/g, "/");
2570
2750
  }
2571
- return results;
2572
- }
2573
- function toNodeId2(srcDir, absPath) {
2574
- return (0, import_node_path8.relative)(srcDir, absPath).replace(/\\/g, "/");
2751
+ return relFromSrc;
2575
2752
  }
2576
2753
  var import_node_fs9, import_node_path8, API_ANNOTATION_RE, apiAnnotationsParser;
2577
2754
  var init_api_annotations = __esm({
@@ -2580,31 +2757,53 @@ var init_api_annotations = __esm({
2580
2757
  import_node_fs9 = require("node:fs");
2581
2758
  import_node_path8 = require("node:path");
2582
2759
  init_api_route_matching();
2760
+ init_config();
2761
+ init_resolve_paths();
2762
+ init_walk();
2583
2763
  API_ANNOTATION_RE = /@api\s+(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+(\/\S+)/g;
2584
2764
  apiAnnotationsParser = {
2585
2765
  id: "api-annotations",
2586
2766
  layer: "crosslayer",
2587
2767
  concern: "api-binding",
2588
2768
  detect(rootDir) {
2589
- return (0, import_node_fs9.existsSync)((0, import_node_path8.join)(rootDir, "src"));
2769
+ return resolveProjectPaths(rootDir, loadConfig(rootDir)) !== null;
2590
2770
  },
2591
2771
  generate(rootDir, layerOutputs) {
2592
2772
  const apiOutput = layerOutputs.get("api");
2593
2773
  if (!apiOutput) {
2594
2774
  return { cross_refs: [], flagged_edges: [], warnings: [] };
2595
2775
  }
2776
+ const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
2777
+ if (!paths) {
2778
+ return { cross_refs: [], flagged_edges: [], warnings: [] };
2779
+ }
2596
2780
  const uiOutput = layerOutputs.get("ui");
2597
2781
  const uiNodeIds = new Set(uiOutput?.nodes.map((n) => n.id) ?? []);
2598
2782
  const apiRoutes = loadApiRoutesFromOutput(apiOutput);
2599
2783
  const apiPathMap = buildApiPathMap(apiRoutes);
2600
- const srcDir = (0, import_node_path8.join)(rootDir, "src");
2601
- const files = walk2(srcDir, [".ts", ".tsx"]);
2784
+ const srcDir = paths.srcDir;
2785
+ const seen = /* @__PURE__ */ new Set();
2786
+ const files = [];
2787
+ for (const root of paths.srcRoots) {
2788
+ for (const f of walk(root, [".ts", ".tsx"])) {
2789
+ if (!seen.has(f)) {
2790
+ seen.add(f);
2791
+ files.push(f);
2792
+ }
2793
+ }
2794
+ }
2795
+ for (const conv of paths.conventionFiles) {
2796
+ if (!seen.has(conv)) {
2797
+ seen.add(conv);
2798
+ files.push(conv);
2799
+ }
2800
+ }
2602
2801
  const crossRefs = [];
2603
2802
  const flaggedEdges = [];
2604
- const seen = /* @__PURE__ */ new Set();
2803
+ const seenEdge = /* @__PURE__ */ new Set();
2605
2804
  for (const absPath of files) {
2606
2805
  const content = (0, import_node_fs9.readFileSync)(absPath, "utf-8");
2607
- const sourceId = toNodeId2(srcDir, absPath);
2806
+ const sourceId = toNodeId2(srcDir, rootDir, absPath);
2608
2807
  if (!uiNodeIds.has(sourceId)) continue;
2609
2808
  let match;
2610
2809
  API_ANNOTATION_RE.lastIndex = 0;
@@ -2614,8 +2813,8 @@ var init_api_annotations = __esm({
2614
2813
  const result = resolveUrlPath(urlPath, apiPathMap, apiRoutes);
2615
2814
  if (result.kind === "resolved" && result.nodeId) {
2616
2815
  const key = `${sourceId}|${result.nodeId}|calls_api`;
2617
- if (seen.has(key)) continue;
2618
- seen.add(key);
2816
+ if (seenEdge.has(key)) continue;
2817
+ seenEdge.add(key);
2619
2818
  crossRefs.push({
2620
2819
  source: sourceId,
2621
2820
  target: result.nodeId,
@@ -2649,22 +2848,12 @@ var init_api_annotations = __esm({
2649
2848
  });
2650
2849
 
2651
2850
  // 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
- }
2851
+ function toNodeId3(srcDir, rootDir, absPath) {
2852
+ const relFromSrc = (0, import_node_path9.relative)(srcDir, absPath).replace(/\\/g, "/");
2853
+ if (relFromSrc.startsWith("..")) {
2854
+ return (0, import_node_path9.relative)(rootDir, absPath).replace(/\\/g, "/");
2663
2855
  }
2664
- return results;
2665
- }
2666
- function toNodeId3(srcDir, absPath) {
2667
- return (0, import_node_path9.relative)(srcDir, absPath).replace(/\\/g, "/");
2856
+ return relFromSrc;
2668
2857
  }
2669
2858
  var import_node_fs10, import_node_path9, URL_LITERAL_RE, urlLiteralScannerParser;
2670
2859
  var init_url_literal_scanner = __esm({
@@ -2675,7 +2864,8 @@ var init_url_literal_scanner = __esm({
2675
2864
  init_api_route_matching();
2676
2865
  init_config();
2677
2866
  init_resolve_paths();
2678
- URL_LITERAL_RE = /['"`](\/api\/[^'"`\s]+?)['"`]/g;
2867
+ init_walk();
2868
+ URL_LITERAL_RE = /['"`](\/[a-zA-Z][^'"`\s]*?)['"`]/g;
2679
2869
  urlLiteralScannerParser = {
2680
2870
  id: "url-literal-scanner",
2681
2871
  layer: "crosslayer",
@@ -2695,15 +2885,26 @@ var init_url_literal_scanner = __esm({
2695
2885
  const apiPathMap = buildApiPathMap(apiRoutes);
2696
2886
  const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
2697
2887
  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
- ];
2888
+ const seenFile = /* @__PURE__ */ new Set();
2889
+ const files = [];
2890
+ for (const root of paths.srcRoots) {
2891
+ for (const f of walk(root, [".ts", ".tsx"])) {
2892
+ if (!seenFile.has(f)) {
2893
+ seenFile.add(f);
2894
+ files.push(f);
2895
+ }
2896
+ }
2897
+ }
2898
+ for (const conv of paths.conventionFiles) {
2899
+ if (!seenFile.has(conv)) {
2900
+ seenFile.add(conv);
2901
+ files.push(conv);
2902
+ }
2903
+ }
2703
2904
  const crossRefs = [];
2704
2905
  const seen = /* @__PURE__ */ new Set();
2705
2906
  for (const absPath of files) {
2706
- const sourceId = toNodeId3(srcDir, absPath);
2907
+ const sourceId = toNodeId3(srcDir, rootDir, absPath);
2707
2908
  if (!uiNodeIds.has(sourceId)) continue;
2708
2909
  const content = (0, import_node_fs10.readFileSync)(absPath, "utf-8");
2709
2910
  let match;
@@ -2766,10 +2967,15 @@ function classifyScope(source, model) {
2766
2967
  function extractEnumValues(rootDir) {
2767
2968
  const nodes = [];
2768
2969
  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
- ];
2970
+ const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
2971
+ const schemaPaths = [];
2972
+ if (paths?.dbConfig.kind === "prisma" && paths.dbConfig.schemaPath) {
2973
+ schemaPaths.push(paths.dbConfig.schemaPath);
2974
+ schemaPaths.push((0, import_node_path10.join)((0, import_node_path10.dirname)(paths.dbConfig.schemaPath), "schema"));
2975
+ } else {
2976
+ schemaPaths.push((0, import_node_path10.join)(rootDir, "prisma", "schema.prisma"));
2977
+ schemaPaths.push((0, import_node_path10.join)(rootDir, "prisma", "schema"));
2978
+ }
2773
2979
  let content = "";
2774
2980
  for (const p of schemaPaths) {
2775
2981
  if ((0, import_node_fs11.existsSync)(p)) {
@@ -2853,7 +3059,7 @@ function extractStringArrayFromNode(node) {
2853
3059
  return values;
2854
3060
  }
2855
3061
  function findArrayDecl(root, varName) {
2856
- function walk4(node) {
3062
+ function walk2(node) {
2857
3063
  if (node.type === "variable_declarator") {
2858
3064
  const nameNode = node.childForFieldName("name");
2859
3065
  const valueNode = node.childForFieldName("value");
@@ -2866,12 +3072,12 @@ function findArrayDecl(root, varName) {
2866
3072
  }
2867
3073
  }
2868
3074
  for (const child of node.namedChildren) {
2869
- const found = walk4(child);
3075
+ const found = walk2(child);
2870
3076
  if (found) return found;
2871
3077
  }
2872
3078
  return null;
2873
3079
  }
2874
- return walk4(root);
3080
+ return walk2(root);
2875
3081
  }
2876
3082
  function extractObjectPropsRegex(objStr) {
2877
3083
  const props = {};
@@ -2934,11 +3140,26 @@ function modelToNodeType(model) {
2934
3140
  function extractSeedData(rootDir) {
2935
3141
  const nodes = [];
2936
3142
  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);
3143
+ const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
3144
+ const candidates = [];
3145
+ if (paths?.dbDir) {
3146
+ candidates.push((0, import_node_path10.join)(paths.dbDir, "seed.ts"));
3147
+ candidates.push((0, import_node_path10.join)(paths.dbDir, "seed.js"));
3148
+ } else {
3149
+ candidates.push((0, import_node_path10.join)(rootDir, "prisma", "seed.ts"));
3150
+ candidates.push((0, import_node_path10.join)(rootDir, "prisma", "seed.js"));
3151
+ }
3152
+ const baseRoots = paths?.srcRoots ?? [(0, import_node_path10.join)(rootDir, "src")];
3153
+ for (const root of baseRoots) {
3154
+ candidates.push((0, import_node_path10.join)(root, "server", "lib", "system-tags.ts"));
3155
+ }
3156
+ const seedFiles = candidates.filter((p) => {
3157
+ try {
3158
+ return (0, import_node_fs11.existsSync)(p) && (0, import_node_fs11.statSync)(p).isFile();
3159
+ } catch {
3160
+ return false;
3161
+ }
3162
+ });
2942
3163
  const useTreeSitter = tryLoadTreeSitter();
2943
3164
  for (const filePath of seedFiles) {
2944
3165
  const content = (0, import_node_fs11.readFileSync)(filePath, "utf-8");
@@ -3048,9 +3269,20 @@ function walkDir(dir, exts) {
3048
3269
  }
3049
3270
  function extractConstants(rootDir) {
3050
3271
  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"])) {
3272
+ const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
3273
+ const roots = paths?.srcRoots ?? [(0, import_node_path10.join)(rootDir, "src")];
3274
+ const seenFile = /* @__PURE__ */ new Set();
3275
+ const allFiles = [];
3276
+ for (const root of roots) {
3277
+ for (const f of walkDir(root, [".ts", ".tsx"])) {
3278
+ if (!seenFile.has(f)) {
3279
+ seenFile.add(f);
3280
+ allFiles.push(f);
3281
+ }
3282
+ }
3283
+ }
3284
+ if (allFiles.length === 0) return { nodes };
3285
+ for (const filePath of allFiles) {
3054
3286
  const content = (0, import_node_fs11.readFileSync)(filePath, "utf-8");
3055
3287
  const relPath = (0, import_node_path10.relative)(rootDir, filePath);
3056
3288
  const constArrayRe = /export\s+const\s+([A-Z][A-Z_0-9]+)\s*(?::[^=]+)?\s*=\s*\[/g;
@@ -3085,6 +3317,13 @@ function extractConstants(rootDir) {
3085
3317
  return { nodes };
3086
3318
  }
3087
3319
  function detect4(rootDir) {
3320
+ const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
3321
+ if (paths?.dbConfig.kind === "prisma" && paths.dbConfig.schemaPath && (0, import_node_fs11.existsSync)(paths.dbConfig.schemaPath)) {
3322
+ return true;
3323
+ }
3324
+ if (paths?.dbDir) {
3325
+ 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;
3326
+ }
3088
3327
  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
3328
  }
3090
3329
  function generate4(rootDir) {
@@ -3158,6 +3397,8 @@ var init_static_values = __esm({
3158
3397
  "use strict";
3159
3398
  import_node_fs11 = require("node:fs");
3160
3399
  import_node_path10 = require("node:path");
3400
+ init_config();
3401
+ init_resolve_paths();
3161
3402
  parseCode = null;
3162
3403
  SHARED_MODELS = /* @__PURE__ */ new Set(["permission", "role", "tag"]);
3163
3404
  DB_MODELS = /* @__PURE__ */ new Set(["subscriptionPlan", "providerDefinition", "pipelineMasterTemplate"]);
@@ -3321,13 +3562,22 @@ var init_static_ref_scanner = __esm({
3321
3562
  const paths = resolveProjectPaths(rootDir, loadConfig(rootDir));
3322
3563
  if (!paths) return { cross_refs: [], flagged_edges: [], warnings: [] };
3323
3564
  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
- ];
3565
+ const seenFile = /* @__PURE__ */ new Set();
3566
+ const files = [];
3567
+ for (const root of paths.srcRoots) {
3568
+ for (const f of walkWithIgnore(root, [".ts", ".tsx"])) {
3569
+ if (!seenFile.has(f)) {
3570
+ seenFile.add(f);
3571
+ files.push(f);
3572
+ }
3573
+ }
3574
+ }
3575
+ for (const conv of paths.conventionFiles) {
3576
+ if (!seenFile.has(conv)) {
3577
+ seenFile.add(conv);
3578
+ files.push(conv);
3579
+ }
3580
+ }
3331
3581
  const uiOutput = layerOutputs.get("ui");
3332
3582
  const apiOutput = layerOutputs.get("api");
3333
3583
  const uiNodeIds = new Set(uiOutput?.nodes.map((n) => n.id) ?? []);
@@ -3344,7 +3594,8 @@ var init_static_ref_scanner = __esm({
3344
3594
  const seen = /* @__PURE__ */ new Set();
3345
3595
  let filesScanned = 0;
3346
3596
  for (const absPath of files) {
3347
- const sourceId = (0, import_node_path11.relative)(srcDir, absPath).replace(/\\/g, "/");
3597
+ const relFromSrc = (0, import_node_path11.relative)(srcDir, absPath).replace(/\\/g, "/");
3598
+ const sourceId = relFromSrc.startsWith("..") ? (0, import_node_path11.relative)(rootDir, absPath).replace(/\\/g, "/") : relFromSrc;
3348
3599
  const sourceLayer = uiNodeIds.has(sourceId) ? "ui" : apiNodeIds.has(sourceId) ? "api" : null;
3349
3600
  if (!sourceLayer) continue;
3350
3601
  const content = (0, import_node_fs12.readFileSync)(absPath, "utf-8");
@@ -3816,13 +4067,17 @@ function isTrivialGroup(name, extraTrivial) {
3816
4067
  function normalizeGroupName(name) {
3817
4068
  return name.toLowerCase().replace(/-pages?$/, "").replace(/-layout$/, "").replace(/-wrapper$/, "");
3818
4069
  }
3819
- function extractModuleFromPath(id, extraTrivial, extraSkipSegments) {
4070
+ function extractModuleFromPath(id, extraTrivial, extraSkipSegments, extraGenericRoles) {
3820
4071
  const segments = id.split("/");
3821
4072
  const routeGroups = extractRouteGroups(id);
3822
4073
  const skipSegments = new Set(SKIP_SEGMENTS_BUILTIN);
4074
+ for (const r of GENERIC_ROLE_NAMES_BUILTIN) skipSegments.add(r);
3823
4075
  if (extraSkipSegments) {
3824
4076
  for (const s of extraSkipSegments) skipSegments.add(s);
3825
4077
  }
4078
+ if (extraGenericRoles) {
4079
+ for (const r of extraGenericRoles) skipSegments.add(r);
4080
+ }
3826
4081
  const moduleGroups = routeGroups.filter((g) => !isTrivialGroup(g, extraTrivial)).map(normalizeGroupName);
3827
4082
  if (moduleGroups.length > 0) {
3828
4083
  return moduleGroups[moduleGroups.length - 1];
@@ -3888,6 +4143,9 @@ var init_module_tagger = __esm({
3888
4143
  "__tests__",
3889
4144
  "spec",
3890
4145
  "specs",
4146
+ "scripts",
4147
+ "bin",
4148
+ "tools",
3891
4149
  // Go
3892
4150
  "cmd",
3893
4151
  "pkg",
@@ -4398,7 +4656,7 @@ function checkDeadScreens(rootDir) {
4398
4656
  const pages = ui.nodes.filter((n) => n.type === "page" || n.type === "layout");
4399
4657
  const navTargets = /* @__PURE__ */ new Set();
4400
4658
  for (const e of ui.edges) {
4401
- if (e.type === "navigates" || e.type === "renders" || e.type === "imports") {
4659
+ if (e.type === "navigates" || e.type === "renders" || e.type === "imports" || e.type === "wraps") {
4402
4660
  navTargets.add(e.target);
4403
4661
  }
4404
4662
  }
@@ -4408,13 +4666,15 @@ function checkDeadScreens(rootDir) {
4408
4666
  for (const page of pages) {
4409
4667
  if (page.id.endsWith("layout.tsx") && page.id.split("/").length <= 2) continue;
4410
4668
  if (["error.tsx", "loading.tsx", "not-found.tsx", "template.tsx"].some((s) => page.id.endsWith(s))) continue;
4669
+ const route = page.route;
4670
+ if (route === "/" || route === "") continue;
4411
4671
  if (!navTargets.has(page.id)) {
4412
4672
  findings.push({
4413
4673
  id: `dead:${page.id}`,
4414
4674
  severity: "info",
4415
4675
  category: "dead_screens",
4416
4676
  title: page.name,
4417
- detail: `Page "${page.id}" has no incoming navigation, render, or import edges.`,
4677
+ detail: `Page "${page.id}" has no incoming navigation, render, import, or layout-wrap edges.`,
4418
4678
  file: page.id
4419
4679
  });
4420
4680
  }
@@ -6108,7 +6368,7 @@ function handleDetectProjectStack() {
6108
6368
  const descriptions = {
6109
6369
  "fetch-resolver": "Detects direct fetch()/api.get() calls with inline URLs",
6110
6370
  "api-annotations": "Scans for @api METHOD /path annotations in JSDoc/comments",
6111
- "url-literal-scanner": "Finds /api/... string literals as fallback detection",
6371
+ "url-literal-scanner": "Finds path-like string literals (any /\u2026) and resolves them to API routes",
6112
6372
  "static-ref-scanner": "Finds references to static values (enums, permissions, roles)"
6113
6373
  };
6114
6374
  const grouped = {};