@decantr/cli 2.10.0 → 2.12.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/README.md +6 -4
- package/dist/bin.js +4 -4
- package/dist/{chunk-4SZ4SEKT.js → chunk-CAKQWTZS.js} +67 -29
- package/dist/{chunk-QDMXZSVK.js → chunk-F6SWDGKO.js} +1 -1
- package/dist/{chunk-WPP3JEMC.js → chunk-PHRVC575.js} +6 -5
- package/dist/{chunk-HQUTNZKF.js → chunk-SWPAAAO4.js} +1043 -447
- package/dist/{heal-2UCSPRTK.js → heal-WMSAX4LU.js} +1 -1
- package/dist/{health-VAYRQVTF.js → health-DBCUR6VQ.js} +2 -2
- package/dist/index.js +4 -4
- package/dist/{studio-WPETOZRW.js → studio-L6JJQYCS.js} +3 -3
- package/dist/{workspace-PBO3BJRL.js → workspace-BCA5RCHN.js} +3 -3
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -20,7 +20,7 @@ npx @decantr/cli new my-app --blueprint=esports-hq
|
|
|
20
20
|
Use `decantr setup` when you are unsure which path applies. It detects whether the repo is empty, already attached, or a Brownfield app and recommends the right entry path.
|
|
21
21
|
Use `decantr new` for a greenfield workspace in a fresh directory. With a blueprint/archetype it uses the runnable adapter and Decantr CSS; without registry content it creates a contract-only workspace unless you explicitly pass `--adoption=decantr-css`.
|
|
22
22
|
Use `decantr adopt` when you already have an app and want Decantr governance without adopting a blueprint. Brownfield attach is proposal-driven: Decantr inventories the app, writes an observed essence proposal, hydrates hosted execution packs when online, and only applies the contract when you explicitly accept or merge it.
|
|
23
|
-
Use `decantr doctor` when the next step is unclear, `decantr task` before asking an LLM to modify a route, `decantr verify` after the edit, and `decantr ci` in required automation. Use `decantr codify --from-audit` when you want project-owned UI patterns
|
|
23
|
+
Use `decantr doctor` when the next step is unclear, `decantr task` before asking an LLM to modify a route, `decantr verify` after the edit, and `decantr ci` in required automation. Use `decantr codify --from-audit --style-bridge` when you want project-owned UI patterns, local rules, and token/class bridge mappings such as button/card/shell/theme standards to appear in future task context and verification. Once accepted, that local law is the first Hybrid lane: the app still owns source and styling, but Decantr treats accepted local patterns, rules, and style bridge mappings as project authority.
|
|
24
24
|
In monorepos, app-scoped commands accept `--project <app-path>`. `setup` shows attach guidance before adoption and the day-two loop after adoption. Hosted pack hydration also follows the essence path: `decantr registry compile-packs apps/web/decantr.essence.json --write-context` writes into `apps/web/.decantr/context`. In contract-only/offline Brownfield, deferred hosted packs are optional context unless a present manifest references missing files.
|
|
25
25
|
Use `decantr init`, `decantr analyze`, `decantr check`, and `decantr health` as advanced primitives when you need direct control over one step.
|
|
26
26
|
|
|
@@ -45,7 +45,7 @@ decantr adopt --yes
|
|
|
45
45
|
decantr doctor
|
|
46
46
|
decantr ci --fail-on error
|
|
47
47
|
decantr ci init
|
|
48
|
-
decantr codify --from-audit
|
|
48
|
+
decantr codify --from-audit --style-bridge
|
|
49
49
|
decantr codify --accept
|
|
50
50
|
decantr task /feed "add saved recipe actions"
|
|
51
51
|
decantr verify --brownfield --local-patterns
|
|
@@ -75,12 +75,13 @@ pnpm exec decantr setup
|
|
|
75
75
|
pnpm exec decantr workspace list
|
|
76
76
|
pnpm exec decantr adopt --project apps/web --yes
|
|
77
77
|
pnpm exec decantr doctor --project apps/web
|
|
78
|
+
pnpm exec decantr codify --from-audit --style-bridge --project apps/web
|
|
78
79
|
pnpm exec decantr ci init --project apps/web
|
|
79
80
|
```
|
|
80
81
|
|
|
81
82
|
Assistant rule integration is preview-first: `--assistant-bridge=preview` writes `.decantr/context/assistant-bridge.md`, `decantr rules preview` prints the bridge, and `--assistant-bridge=apply` or `decantr rules apply` mutates supported rule files with idempotent marked blocks.
|
|
82
83
|
|
|
83
|
-
Brownfield analysis also writes `.decantr/doctrine-map.json`, a ranked source-precedence map across security/data, architecture, design-system, workflow/CI, feature/business, assistant-specific, stale, and unsafe-to-cite evidence. It also writes `.decantr/brownfield-intelligence.json`, `.decantr/theme-inventory.json`, and `.decantr/enrichment-backlog.md`. The proposal groups routes into observed semantic domains such as auth, RBAC, billing, reporting, facilities, settings, and public surfaces across Next App/Pages Router, React Router, Angular Router, SvelteKit, Vue Router, and Nuxt file routes. Existing styling systems such as Tailwind, Bootstrap, MUI, Chakra, plain CSS, and Decantr CSS are observed as evidence instead of replaced. Theme variants are observed in the theme inventory without changing Essence V4. `decantr codify --from-audit` proposes `.decantr/local-patterns.proposal.json` and `.decantr/rules.proposal.json` with Hybrid authority guidance
|
|
84
|
+
Brownfield analysis also writes `.decantr/doctrine-map.json`, a ranked source-precedence map across security/data, architecture, design-system, workflow/CI, feature/business, assistant-specific, stale, and unsafe-to-cite evidence. It also writes `.decantr/brownfield-intelligence.json`, `.decantr/theme-inventory.json`, and `.decantr/enrichment-backlog.md`. The proposal groups routes into observed semantic domains such as auth, RBAC, billing, reporting, facilities, settings, and public surfaces across Next App/Pages Router, React Router, Angular Router, SvelteKit, Vue Router, and Nuxt file routes. Existing styling systems such as Tailwind, Bootstrap, MUI, Chakra, plain CSS, and Decantr CSS are observed as evidence instead of replaced. Theme variants are observed in the theme inventory without changing Essence V4. `decantr codify --from-audit` proposes `.decantr/local-patterns.proposal.json` and `.decantr/rules.proposal.json` with Hybrid authority guidance and source-derived button/card class hints; `decantr codify --style-bridge` proposes `.decantr/style-bridge.proposal.json`, mapping Decantr intent to project-owned tokens/classes without requiring `@decantr/css`. After review, `decantr codify --accept` promotes whichever proposals exist. `decantr doctor` reports whether the app is contract-only, Hybrid local law, style bridge, Decantr CSS, or Hybrid composition. `decantr task` prints that authority block and warns before mixing runtimes or adding Decantr CSS to a non-Decantr-CSS app. `decantr suggest --from-code` surfaces accepted local patterns and style bridge mappings from the app root or selected `--project`, and `decantr ci` prints accepted local-rule findings plus style bridge status in text, markdown, and JSON reports. `decantr verify --brownfield --local-patterns` uses the Brownfield guard layer plus accepted local law to flag actionable missing doctrine coverage, unsafe context, missing assistant bridges, style drift, raw local-rule violations, and unsafe defaults without treating current database migrations as stale docs.
|
|
84
85
|
|
|
85
86
|
## What It Does
|
|
86
87
|
|
|
@@ -111,12 +112,13 @@ decantr adopt --yes
|
|
|
111
112
|
decantr adopt --project apps/web --yes
|
|
112
113
|
decantr doctor
|
|
113
114
|
decantr doctor --project apps/web
|
|
114
|
-
decantr codify --from-audit
|
|
115
|
+
decantr codify --from-audit --style-bridge
|
|
115
116
|
decantr codify --accept
|
|
116
117
|
decantr task /feed "add saved recipe actions"
|
|
117
118
|
decantr verify --brownfield --local-patterns
|
|
118
119
|
decantr verify --base-url http://localhost:3000 --evidence
|
|
119
120
|
decantr ci --project apps/web
|
|
121
|
+
decantr ci --project apps/web --fail-on warn
|
|
120
122
|
decantr ci --workspace --changed --since origin/main
|
|
121
123
|
decantr ci init --project apps/web
|
|
122
124
|
decantr init --existing --blueprint=esports-hq
|
package/dist/bin.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import "./chunk-
|
|
2
|
+
import "./chunk-SWPAAAO4.js";
|
|
3
3
|
import "./chunk-IQZCIMQJ.js";
|
|
4
|
-
import "./chunk-
|
|
5
|
-
import "./chunk-
|
|
6
|
-
import "./chunk-
|
|
4
|
+
import "./chunk-F6SWDGKO.js";
|
|
5
|
+
import "./chunk-PHRVC575.js";
|
|
6
|
+
import "./chunk-CAKQWTZS.js";
|
|
@@ -660,8 +660,8 @@ function scanRoutes(projectRoot) {
|
|
|
660
660
|
}
|
|
661
661
|
|
|
662
662
|
// src/analyzers/styling.ts
|
|
663
|
-
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
|
|
664
|
-
import { join as join3 } from "path";
|
|
663
|
+
import { existsSync as existsSync3, readdirSync as readdirSync3, readFileSync as readFileSync3, statSync as statSync3 } from "fs";
|
|
664
|
+
import { join as join3, relative as relative3 } from "path";
|
|
665
665
|
var TAILWIND_CONFIGS = [
|
|
666
666
|
"tailwind.config.js",
|
|
667
667
|
"tailwind.config.ts",
|
|
@@ -674,8 +674,10 @@ var GLOBALS_CSS_PATHS = [
|
|
|
674
674
|
"src/styles/global.css",
|
|
675
675
|
"src/styles/globals.css",
|
|
676
676
|
"src/styles/main.css",
|
|
677
|
+
"src/styles/themes.css",
|
|
677
678
|
"src/styles.css",
|
|
678
679
|
"styles/globals.css",
|
|
680
|
+
"styles/themes.css",
|
|
679
681
|
"styles.css",
|
|
680
682
|
"assets/css/main.css",
|
|
681
683
|
"src/index.css",
|
|
@@ -716,8 +718,15 @@ function extractCSSVariables(content) {
|
|
|
716
718
|
return { colors, variables };
|
|
717
719
|
}
|
|
718
720
|
function detectDarkMode(projectRoot, cssContents) {
|
|
721
|
+
const darkSelectors = [
|
|
722
|
+
/\.dark\b/i,
|
|
723
|
+
/html\.dark\b/i,
|
|
724
|
+
/\[data-theme\s*=\s*["']dark["']\]/i,
|
|
725
|
+
/prefers-color-scheme\s*:\s*dark/i,
|
|
726
|
+
/color-scheme\s*:\s*dark/i
|
|
727
|
+
];
|
|
719
728
|
for (const cssContent of cssContents) {
|
|
720
|
-
if (
|
|
729
|
+
if (darkSelectors.some((selector) => selector.test(cssContent))) {
|
|
721
730
|
return true;
|
|
722
731
|
}
|
|
723
732
|
}
|
|
@@ -763,6 +772,46 @@ function detectDarkMode(projectRoot, cssContents) {
|
|
|
763
772
|
}
|
|
764
773
|
return false;
|
|
765
774
|
}
|
|
775
|
+
function collectCssFiles(projectRoot) {
|
|
776
|
+
const seen = /* @__PURE__ */ new Set();
|
|
777
|
+
const ordered = [];
|
|
778
|
+
const add = (path) => {
|
|
779
|
+
if (seen.has(path)) return;
|
|
780
|
+
if (!existsSync3(join3(projectRoot, path))) return;
|
|
781
|
+
seen.add(path);
|
|
782
|
+
ordered.push(path);
|
|
783
|
+
};
|
|
784
|
+
for (const rel of GLOBALS_CSS_PATHS) add(rel);
|
|
785
|
+
for (const rel of DECANTR_STYLE_PATHS) add(rel);
|
|
786
|
+
const roots = ["src/styles", "styles", "assets/css", "src/app", "app"];
|
|
787
|
+
const visit = (dir) => {
|
|
788
|
+
const absolute = join3(projectRoot, dir);
|
|
789
|
+
if (!existsSync3(absolute)) return;
|
|
790
|
+
let entries;
|
|
791
|
+
try {
|
|
792
|
+
entries = readdirSync3(absolute);
|
|
793
|
+
} catch {
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
for (const entry of entries) {
|
|
797
|
+
const child = join3(absolute, entry);
|
|
798
|
+
let stat;
|
|
799
|
+
try {
|
|
800
|
+
stat = statSync3(child);
|
|
801
|
+
} catch {
|
|
802
|
+
continue;
|
|
803
|
+
}
|
|
804
|
+
if (stat.isDirectory()) {
|
|
805
|
+
if (entry === "node_modules" || entry === ".next" || entry === "dist") continue;
|
|
806
|
+
visit(relative3(projectRoot, child).replace(/\\/g, "/"));
|
|
807
|
+
} else if (stat.isFile() && /\.css$/i.test(entry)) {
|
|
808
|
+
add(relative3(projectRoot, child).replace(/\\/g, "/"));
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
};
|
|
812
|
+
for (const root of roots) visit(root);
|
|
813
|
+
return ordered.slice(0, 80);
|
|
814
|
+
}
|
|
766
815
|
function scanStyling(projectRoot) {
|
|
767
816
|
let approach = "unknown";
|
|
768
817
|
let configFile;
|
|
@@ -811,23 +860,12 @@ function scanStyling(projectRoot) {
|
|
|
811
860
|
configFile = decantrStyleFiles.join(" + ");
|
|
812
861
|
}
|
|
813
862
|
const cssContents = [];
|
|
814
|
-
|
|
863
|
+
const cssFiles = collectCssFiles(projectRoot);
|
|
864
|
+
for (const rel of cssFiles) {
|
|
815
865
|
const fullPath = join3(projectRoot, rel);
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
} catch {
|
|
820
|
-
}
|
|
821
|
-
}
|
|
822
|
-
}
|
|
823
|
-
for (const rel of DECANTR_STYLE_PATHS) {
|
|
824
|
-
if (GLOBALS_CSS_PATHS.includes(rel)) continue;
|
|
825
|
-
const fullPath = join3(projectRoot, rel);
|
|
826
|
-
if (existsSync3(fullPath)) {
|
|
827
|
-
try {
|
|
828
|
-
cssContents.push(readFileSync3(fullPath, "utf-8"));
|
|
829
|
-
} catch {
|
|
830
|
-
}
|
|
866
|
+
try {
|
|
867
|
+
cssContents.push(readFileSync3(fullPath, "utf-8"));
|
|
868
|
+
} catch {
|
|
831
869
|
}
|
|
832
870
|
}
|
|
833
871
|
let colors = {};
|
|
@@ -841,7 +879,7 @@ function scanStyling(projectRoot) {
|
|
|
841
879
|
const darkMode = detectDarkMode(projectRoot, cssContents);
|
|
842
880
|
if (approach === "unknown" && cssContents.length > 0) {
|
|
843
881
|
approach = "css";
|
|
844
|
-
configFile =
|
|
882
|
+
configFile = cssFiles[0];
|
|
845
883
|
}
|
|
846
884
|
return {
|
|
847
885
|
approach,
|
|
@@ -1206,11 +1244,11 @@ function scanBrownfieldIssues(projectRoot, essence) {
|
|
|
1206
1244
|
}
|
|
1207
1245
|
|
|
1208
1246
|
// src/guard-context.ts
|
|
1209
|
-
import { existsSync as existsSync7, readdirSync as
|
|
1247
|
+
import { existsSync as existsSync7, readdirSync as readdirSync5, readFileSync as readFileSync7 } from "fs";
|
|
1210
1248
|
import { join as join7 } from "path";
|
|
1211
1249
|
|
|
1212
1250
|
// src/bundled-content.ts
|
|
1213
|
-
import { existsSync as existsSync6, readdirSync as
|
|
1251
|
+
import { existsSync as existsSync6, readdirSync as readdirSync4, readFileSync as readFileSync6 } from "fs";
|
|
1214
1252
|
import { dirname, join as join6 } from "path";
|
|
1215
1253
|
import { fileURLToPath } from "url";
|
|
1216
1254
|
function bundledDirCandidates(contentType) {
|
|
@@ -1244,7 +1282,7 @@ function loadBundledContentList(contentType) {
|
|
|
1244
1282
|
for (const dir of bundledDirCandidates(contentType)) {
|
|
1245
1283
|
if (!existsSync6(dir)) continue;
|
|
1246
1284
|
try {
|
|
1247
|
-
for (const file of
|
|
1285
|
+
for (const file of readdirSync4(dir).filter((name) => name.endsWith(".json"))) {
|
|
1248
1286
|
const id = file.replace(/\.json$/, "");
|
|
1249
1287
|
if (seen.has(id)) continue;
|
|
1250
1288
|
const path = join6(dir, file);
|
|
@@ -1262,7 +1300,7 @@ function loadBundledContentList(contentType) {
|
|
|
1262
1300
|
function loadJsonEntries(dir) {
|
|
1263
1301
|
if (!existsSync7(dir)) return [];
|
|
1264
1302
|
try {
|
|
1265
|
-
return
|
|
1303
|
+
return readdirSync5(dir).filter((file) => file.endsWith(".json") && file !== "index.json").map((file) => JSON.parse(readFileSync7(join7(dir, file), "utf-8")));
|
|
1266
1304
|
} catch {
|
|
1267
1305
|
return [];
|
|
1268
1306
|
}
|
|
@@ -1307,8 +1345,8 @@ function buildGuardRegistryContext(projectRoot = process.cwd()) {
|
|
|
1307
1345
|
}
|
|
1308
1346
|
|
|
1309
1347
|
// src/lib/scan-interactions.ts
|
|
1310
|
-
import { existsSync as existsSync8, readdirSync as
|
|
1311
|
-
import { extname as extname2, join as join8, relative as
|
|
1348
|
+
import { existsSync as existsSync8, readdirSync as readdirSync6, readFileSync as readFileSync8, statSync as statSync4 } from "fs";
|
|
1349
|
+
import { extname as extname2, join as join8, relative as relative4 } from "path";
|
|
1312
1350
|
import { verifyInteractionsInSource } from "@decantr/verifier";
|
|
1313
1351
|
var SCAN_EXTENSIONS = /* @__PURE__ */ new Set([".tsx", ".jsx", ".ts", ".js", ".html", ".mdx"]);
|
|
1314
1352
|
var SKIP_DIRECTORIES = /* @__PURE__ */ new Set([
|
|
@@ -1344,7 +1382,7 @@ function walkSourceTree(rootDir) {
|
|
|
1344
1382
|
function walk2(dir) {
|
|
1345
1383
|
let entries;
|
|
1346
1384
|
try {
|
|
1347
|
-
entries =
|
|
1385
|
+
entries = readdirSync6(dir);
|
|
1348
1386
|
} catch {
|
|
1349
1387
|
return;
|
|
1350
1388
|
}
|
|
@@ -1353,11 +1391,11 @@ function walkSourceTree(rootDir) {
|
|
|
1353
1391
|
const fullPath = join8(dir, entry);
|
|
1354
1392
|
let s;
|
|
1355
1393
|
try {
|
|
1356
|
-
s =
|
|
1394
|
+
s = statSync4(fullPath);
|
|
1357
1395
|
} catch {
|
|
1358
1396
|
continue;
|
|
1359
1397
|
}
|
|
1360
|
-
const relativePath =
|
|
1398
|
+
const relativePath = relative4(rootDir, fullPath) || entry;
|
|
1361
1399
|
if (s.isDirectory()) {
|
|
1362
1400
|
walk2(fullPath);
|
|
1363
1401
|
} else if (s.isFile() && SCAN_EXTENSIONS.has(extname2(entry))) {
|
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
sendProjectHealthCiFailedTelemetry,
|
|
4
4
|
sendProjectHealthPromptTelemetry,
|
|
5
5
|
sendProjectHealthReportTelemetry
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-CAKQWTZS.js";
|
|
7
7
|
|
|
8
8
|
// src/commands/health.ts
|
|
9
9
|
import { execFileSync } from "child_process";
|
|
@@ -330,10 +330,11 @@ function normalizeHealthCategory(category, source) {
|
|
|
330
330
|
return category;
|
|
331
331
|
}
|
|
332
332
|
function contractAssertionApplies(assertion, metadata) {
|
|
333
|
-
|
|
333
|
+
const projectOwnedStyling = metadata.adoptionMode === "contract-only" || metadata.adoptionMode === "style-bridge";
|
|
334
|
+
if (assertion.rule === "tokens-file-present" && projectOwnedStyling) {
|
|
334
335
|
return false;
|
|
335
336
|
}
|
|
336
|
-
if (
|
|
337
|
+
if (projectOwnedStyling && (assertion.rule === "pack-manifest-present" || assertion.rule === "review-pack-present")) {
|
|
337
338
|
return false;
|
|
338
339
|
}
|
|
339
340
|
return true;
|
|
@@ -713,7 +714,7 @@ async function collectBrowserVerification(projectRoot, options, declaredRoutes)
|
|
|
713
714
|
message: "Browser verification was requested, but Playwright is not installed in this project.",
|
|
714
715
|
evidence: ["Expected dependency: playwright or @playwright/test"],
|
|
715
716
|
rule: "browser-playwright-missing",
|
|
716
|
-
suggestedFix: "Install Playwright in the project or
|
|
717
|
+
suggestedFix: "Install Playwright in the project or omit browser/base-url evidence for static-only health checks.",
|
|
717
718
|
baseId: "playwright-missing"
|
|
718
719
|
});
|
|
719
720
|
return {
|
|
@@ -1404,7 +1405,7 @@ async function cmdHealth(projectRoot = process.cwd(), options = {}) {
|
|
|
1404
1405
|
const reason = evidenceBundle.browser.findings[0] ?? "Playwright is not available to Decantr in this project.";
|
|
1405
1406
|
console.log(`${YELLOW}Browser evidence unavailable:${RESET} ${reason}`);
|
|
1406
1407
|
console.log(
|
|
1407
|
-
`${DIM}Static evidence was still written. Install Playwright or
|
|
1408
|
+
`${DIM}Static evidence was still written. Install Playwright or omit --browser/--base-url evidence if screenshots are not needed.${RESET}`
|
|
1408
1409
|
);
|
|
1409
1410
|
}
|
|
1410
1411
|
}
|