@decantr/cli 3.0.2 → 3.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import "./chunk-GN63PIKK.js";
2
+ import "./chunk-ZBGL7RID.js";
3
3
  import "./chunk-SIDKK73N.js";
4
- import "./chunk-B2PJDAMS.js";
5
- import "./chunk-4NDOHCYY.js";
6
- import "./chunk-7Z74ETDR.js";
4
+ import "./chunk-EL4KSW7E.js";
5
+ import "./chunk-XUHPMETF.js";
6
+ import "./chunk-XINHWP4T.js";
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  createProjectHealthReport,
3
3
  listWorkspaceAppCandidates
4
- } from "./chunk-4NDOHCYY.js";
4
+ } from "./chunk-XUHPMETF.js";
5
5
 
6
6
  // src/commands/workspace.ts
7
7
  import { execFileSync } from "child_process";
@@ -456,6 +456,8 @@ function walkSvelteKitRoutes(dir, baseDir, segments) {
456
456
  return routes;
457
457
  }
458
458
  var ROUTER_FILE_EXTENSIONS = /* @__PURE__ */ new Set([".tsx", ".ts", ".jsx", ".js"]);
459
+ var ROUTE_VARIABLE_NAMES = "(?:path|pathname|route|currentPath|currentRoute|locationPath|activePath)";
460
+ var ROUTE_ASSET_EXTENSION_RE = /\.(?:avif|bmp|css|gif|ico|jpeg|jpg|js|json|map|mp4|pdf|png|svg|webp|woff2?)$/i;
459
461
  function collectRouteCandidateFiles(dir, files, depth = 0) {
460
462
  if (depth > 5) return;
461
463
  let entries;
@@ -496,30 +498,72 @@ function scanReactRouter(projectRoot) {
496
498
  continue;
497
499
  }
498
500
  const isReactRouterFile = content.includes("react-router-dom") || content.includes("react-router") || content.includes("<Routes") || content.includes("createBrowserRouter") || content.includes("createHashRouter") || content.includes("RouterProvider") || content.includes("HashRouter") || content.includes("BrowserRouter");
499
- if (!isReactRouterFile) continue;
500
501
  const relativePath = relative2(projectRoot, absolutePath);
501
502
  const pathMatches = /* @__PURE__ */ new Set();
502
- for (const match of content.matchAll(/<Route\b[^>]*\bpath=["'`]([^"'`]+)["'`]/g)) {
503
- pathMatches.add(match[1]);
503
+ if (isReactRouterFile) {
504
+ for (const match of content.matchAll(/<Route\b[^>]*\bpath=["'`]([^"'`]+)["'`]/g)) {
505
+ pathMatches.add(match[1]);
506
+ }
507
+ for (const match of content.matchAll(/\bpath\s*:\s*["'`]([^"'`]+)["'`]/g)) {
508
+ pathMatches.add(match[1]);
509
+ }
504
510
  }
505
- for (const match of content.matchAll(/\bpath\s*:\s*["'`]([^"'`]+)["'`]/g)) {
506
- pathMatches.add(match[1]);
511
+ for (const route of detectPathnameBranchRoutes(content)) {
512
+ pathMatches.add(route);
507
513
  }
514
+ if (!isReactRouterFile && pathMatches.size === 0) continue;
508
515
  if (pathMatches.size === 0 && (content.includes("<Routes") || content.includes("RouterProvider"))) {
509
516
  pathMatches.add("/");
510
517
  }
511
518
  for (const path of pathMatches) {
512
- if (!routeMap.has(path)) {
513
- routeMap.set(path, {
514
- path,
515
- file: relativePath,
516
- hasLayout: false
517
- });
518
- }
519
+ const routePath = normalizeDetectedRouteLiteral(path);
520
+ if (!routePath || routeMap.has(routePath)) continue;
521
+ routeMap.set(routePath, {
522
+ path: routePath,
523
+ file: relativePath,
524
+ hasLayout: false
525
+ });
519
526
  }
520
527
  }
521
528
  return [...routeMap.values()];
522
529
  }
530
+ function normalizeDetectedRouteLiteral(value) {
531
+ const cleaned = value.trim().split(/[?#]/)[0];
532
+ if (!cleaned || cleaned === "/") return "/";
533
+ if (cleaned === "*" || cleaned === "**" || cleaned.startsWith("#")) return null;
534
+ if (!cleaned.startsWith("/") || cleaned.startsWith("//")) return null;
535
+ if (ROUTE_ASSET_EXTENSION_RE.test(cleaned)) return null;
536
+ return cleaned.replace(/\/+$/g, "") || "/";
537
+ }
538
+ function collectRouteLiterals(pattern, content, routes) {
539
+ let count = 0;
540
+ for (const match of content.matchAll(pattern)) {
541
+ const route = normalizeDetectedRouteLiteral(match[1] ?? "");
542
+ if (!route) continue;
543
+ routes.add(route);
544
+ count += 1;
545
+ }
546
+ return count;
547
+ }
548
+ function detectPathnameBranchRoutes(content) {
549
+ const routes = /* @__PURE__ */ new Set();
550
+ const comparison = new RegExp(
551
+ `\\b${ROUTE_VARIABLE_NAMES}\\b\\s*(?:===|!==|==|!=)\\s*["'\`](\\/[^"'\`]+)["'\`]`,
552
+ "g"
553
+ );
554
+ const reversedComparison = new RegExp(
555
+ `["'\`](\\/[^"'\`]+)["'\`]\\s*(?:===|!==|==|!=)\\s*\\b${ROUTE_VARIABLE_NAMES}\\b`,
556
+ "g"
557
+ );
558
+ const strongMatches = collectRouteLiterals(comparison, content, routes) + collectRouteLiterals(reversedComparison, content, routes) + collectRouteLiterals(/\bcase\s+["'`](\/[^"'`]+)["'`]\s*:/g, content, routes);
559
+ const hasPathnameSignal = /\b(?:window\.|document\.)?location\.pathname\b|\bpathname\b/.test(
560
+ content
561
+ );
562
+ if (strongMatches > 0 || hasPathnameSignal) {
563
+ collectRouteLiterals(/\b(?:href|to)\s*=\s*["'`](\/[^"'`]+)["'`]/g, content, routes);
564
+ }
565
+ return [...routes];
566
+ }
523
567
  function hasReactRouterDependency(projectRoot) {
524
568
  return hasDependency(projectRoot, ["react-router", "react-router-dom"]);
525
569
  }
@@ -693,8 +737,8 @@ function extractCSSVariables(content) {
693
737
  const colors = {};
694
738
  const variables = [];
695
739
  const varRegex = /--([\w-]+)\s*:\s*([^;]+)/g;
696
- let match;
697
- while ((match = varRegex.exec(content)) !== null) {
740
+ let match = varRegex.exec(content);
741
+ while (match !== null) {
698
742
  const name = match[1];
699
743
  const value = match[2].trim();
700
744
  variables.push(`--${name}`);
@@ -714,6 +758,24 @@ function extractCSSVariables(content) {
714
758
  if (value.startsWith("#") || value.startsWith("rgb") || value.startsWith("hsl") || colorPatterns.some((p) => name.includes(p))) {
715
759
  colors[name] = value;
716
760
  }
761
+ match = varRegex.exec(content);
762
+ }
763
+ return { colors, variables };
764
+ }
765
+ function extractTailwindThemeTokens(content) {
766
+ const colors = {};
767
+ const variables = [];
768
+ for (const match of content.matchAll(
769
+ /\b([a-zA-Z][\w-]*)\s*:\s*['"`](#[0-9a-fA-F]{3,8}|rgba?\([^)]+\)|hsla?\([^)]+\)|oklch\([^)]+\)|var\(--[^)]+\))['"`]/g
770
+ )) {
771
+ const name = match[1];
772
+ const value = match[2];
773
+ if (/color|bg|background|surface|text|foreground|border|primary|secondary|accent|muted|danger|error|success|warning/i.test(
774
+ name
775
+ ) || /^#|^(?:rgba?|hsla?|oklch)\(/i.test(value) || /^var\(--/i.test(value)) {
776
+ colors[name] = value;
777
+ variables.push(`theme.colors.${name}`);
778
+ }
717
779
  }
718
780
  return { colors, variables };
719
781
  }
@@ -875,6 +937,16 @@ function scanStyling(projectRoot) {
875
937
  colors = { ...colors, ...extracted.colors };
876
938
  cssVariables.push(...extracted.variables);
877
939
  }
940
+ for (const cfg of TAILWIND_CONFIGS) {
941
+ const configPath = join3(projectRoot, cfg);
942
+ if (!existsSync3(configPath)) continue;
943
+ try {
944
+ const extracted = extractTailwindThemeTokens(readFileSync3(configPath, "utf-8"));
945
+ colors = { ...colors, ...extracted.colors };
946
+ cssVariables.push(...extracted.variables);
947
+ } catch {
948
+ }
949
+ }
878
950
  cssVariables = [...new Set(cssVariables)];
879
951
  const darkMode = detectDarkMode(projectRoot, cssContents);
880
952
  if (approach === "unknown" && cssContents.length > 0) {
@@ -1091,9 +1163,37 @@ function routePathname(route) {
1091
1163
  function declaredRouteObserved(route, observedRoutes) {
1092
1164
  return observedRoutes.has(route) || route.includes("?") && observedRoutes.has(routePathname(route));
1093
1165
  }
1166
+ function sectionForRoute(essence, route) {
1167
+ const mappedSectionId = essence.blueprint.routes?.[route]?.section;
1168
+ const sections = essence.blueprint.sections ?? [];
1169
+ if (mappedSectionId) {
1170
+ return sections.find((section) => section.id === mappedSectionId) ?? null;
1171
+ }
1172
+ return sections.find(
1173
+ (section) => section.pages?.some((page) => page.route === route || page.route === routePathname(route))
1174
+ ) ?? null;
1175
+ }
1176
+ function readObservedRouteSource(projectRoot, file) {
1177
+ if (!file || file === ".") return "";
1178
+ try {
1179
+ return readFileSync5(join5(projectRoot, file), "utf-8");
1180
+ } catch {
1181
+ return "";
1182
+ }
1183
+ }
1184
+ function routeLooksPublicFullBleed(path, file, code) {
1185
+ return /\b(?:pricing|plans|marketing|landing|public|hero|full-bleed|fullbleed|marketing-bleed)\b/i.test(
1186
+ `${path} ${file} ${code}`
1187
+ );
1188
+ }
1189
+ function routeLooksAppShell(file, code) {
1190
+ return /\b(?:sidebar|side-nav|sidenav|app-frame|dashboard-shell|app-shell)\b|<\s*(?:Sidebar|AppShell|DashboardShell)\b/i.test(
1191
+ `${file} ${code}`
1192
+ );
1193
+ }
1094
1194
  function hasDoctrineEffect(essence, key) {
1095
1195
  const effects = essence.dna.constraints?.effects;
1096
- return Boolean(effects && effects[key]);
1196
+ return Boolean(effects?.[key]);
1097
1197
  }
1098
1198
  function hasActionableDoctrineSource(doctrine, area) {
1099
1199
  return doctrine.sources.some(
@@ -1168,6 +1268,32 @@ function scanBrownfieldIssues(projectRoot, essence) {
1168
1268
  suggestion: "Confirm whether these are generated/dynamic routes or stale contract entries."
1169
1269
  });
1170
1270
  }
1271
+ const shellDriftRoutes = [];
1272
+ for (const route of routes.routes.slice(0, 80)) {
1273
+ const section = sectionForRoute(essence, route.path);
1274
+ const shell = section?.shell ?? "";
1275
+ if (!shell) continue;
1276
+ const source = readObservedRouteSource(projectRoot, route.file);
1277
+ const shellHasSidebar = /\bsidebar\b/i.test(shell);
1278
+ const shellIsPublicLight = /^(?:main-only|main-footer|topnav-main|topnav-main-footer)$/i.test(
1279
+ shell
1280
+ );
1281
+ if (shellHasSidebar && routeLooksPublicFullBleed(route.path, route.file, source)) {
1282
+ shellDriftRoutes.push(`${route.path} (${shell} vs public/full-bleed source)`);
1283
+ } else if (shellIsPublicLight && routeLooksAppShell(route.file, source)) {
1284
+ shellDriftRoutes.push(`${route.path} (${shell} vs app-shell source)`);
1285
+ }
1286
+ }
1287
+ if (shellDriftRoutes.length > 0) {
1288
+ issues.push({
1289
+ type: "warning",
1290
+ rule: "brownfield-shell-drift",
1291
+ message: `Observed route shell signals disagree with the Decantr section shell: ${routeLabel(
1292
+ shellDriftRoutes
1293
+ )}.`,
1294
+ suggestion: "Regenerate and merge a brownfield proposal, or update the affected section shell after reviewing the route source."
1295
+ });
1296
+ }
1171
1297
  const adoptionMode = projectJson.initialized?.adoptionMode;
1172
1298
  const themeId = essence.dna.theme.id;
1173
1299
  if (adoptionMode === "contract-only" && themeId === "luminarum" && (styling.approach !== "unknown" || styling.cssVariables.length > 0)) {