@decantr/cli 2.8.1 → 2.9.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.
@@ -1,14 +1,87 @@
1
- import {
2
- listWorkspaceAppCandidates
3
- } from "./chunk-VE6N3XWG.js";
4
1
  import {
5
2
  createProjectHealthReport
6
- } from "./chunk-PAF4PBD3.js";
3
+ } from "./chunk-TMOCTDYY.js";
7
4
 
8
5
  // src/commands/workspace.ts
9
6
  import { execFileSync } from "child_process";
10
- import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "fs";
11
- import { dirname, join, relative, resolve } from "path";
7
+ import { existsSync as existsSync2, mkdirSync, readdirSync as readdirSync2, readFileSync as readFileSync2, writeFileSync } from "fs";
8
+ import { dirname as dirname2, join as join2, relative, resolve as resolve2 } from "path";
9
+
10
+ // src/workspace.ts
11
+ import { existsSync, readdirSync, readFileSync } from "fs";
12
+ import { dirname, join, resolve } from "path";
13
+ function readPackageJson(dir) {
14
+ const path = join(dir, "package.json");
15
+ if (!existsSync(path)) return null;
16
+ try {
17
+ return JSON.parse(readFileSync(path, "utf-8"));
18
+ } catch {
19
+ return null;
20
+ }
21
+ }
22
+ function hasWorkspaceMarker(dir) {
23
+ if (existsSync(join(dir, "pnpm-workspace.yaml")) || existsSync(join(dir, "turbo.json")) || existsSync(join(dir, "nx.json"))) {
24
+ return true;
25
+ }
26
+ const pkg = readPackageJson(dir);
27
+ return Boolean(pkg?.workspaces);
28
+ }
29
+ function findWorkspaceRoot(startDir) {
30
+ let current = resolve(startDir);
31
+ while (true) {
32
+ if (hasWorkspaceMarker(current)) return current;
33
+ const parent = dirname(current);
34
+ if (parent === current) return null;
35
+ current = parent;
36
+ }
37
+ }
38
+ function looksLikeApp(dir, options = {}) {
39
+ const allowSourceDirs = options.allowSourceDirs ?? true;
40
+ if (existsSync(join(dir, "next.config.js")) || existsSync(join(dir, "next.config.ts")) || existsSync(join(dir, "next.config.mjs")) || existsSync(join(dir, "vite.config.ts")) || existsSync(join(dir, "vite.config.js")) || existsSync(join(dir, "angular.json")) || existsSync(join(dir, "svelte.config.js")) || existsSync(join(dir, "svelte.config.ts")) || existsSync(join(dir, "astro.config.mjs")) || allowSourceDirs && (existsSync(join(dir, "src")) || existsSync(join(dir, "app")) || existsSync(join(dir, "pages")))) {
41
+ return true;
42
+ }
43
+ const pkg = readPackageJson(dir);
44
+ const deps = { ...pkg?.dependencies, ...pkg?.devDependencies };
45
+ return Boolean(
46
+ deps.react || deps.next || deps.vue || deps.svelte || deps["@angular/core"] || deps.astro || deps.nuxt
47
+ );
48
+ }
49
+ function listWorkspaceApps(workspaceRoot) {
50
+ const candidates = [];
51
+ for (const base of ["apps", "packages"]) {
52
+ const baseDir = join(workspaceRoot, base);
53
+ if (!existsSync(baseDir)) continue;
54
+ for (const entry of readdirSync(baseDir, { withFileTypes: true })) {
55
+ if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
56
+ const candidate = join(baseDir, entry.name);
57
+ if (looksLikeApp(candidate, { allowSourceDirs: base === "apps" })) {
58
+ candidates.push(`${base}/${entry.name}`);
59
+ }
60
+ }
61
+ }
62
+ return candidates.sort();
63
+ }
64
+ function listWorkspaceAppCandidates(workspaceRoot) {
65
+ return listWorkspaceApps(resolve(workspaceRoot));
66
+ }
67
+ function resolveWorkspaceInfo(cwd, projectArg) {
68
+ const absoluteCwd = resolve(cwd);
69
+ const workspaceRoot = findWorkspaceRoot(absoluteCwd) ?? absoluteCwd;
70
+ const appRoot = projectArg ? resolve(absoluteCwd, projectArg) : absoluteCwd;
71
+ const appCandidates = listWorkspaceApps(workspaceRoot);
72
+ const projectScope = workspaceRoot !== appRoot || appCandidates.length > 0 ? "workspace-app" : "single-app";
73
+ const requiresProjectSelection = !projectArg && workspaceRoot === absoluteCwd && appCandidates.length > 0;
74
+ return {
75
+ cwd: absoluteCwd,
76
+ workspaceRoot,
77
+ appRoot,
78
+ projectScope,
79
+ appCandidates,
80
+ requiresProjectSelection
81
+ };
82
+ }
83
+
84
+ // src/commands/workspace.ts
12
85
  var BOLD = "\x1B[1m";
13
86
  var DIM = "\x1B[2m";
14
87
  var GREEN = "\x1B[32m";
@@ -27,12 +100,12 @@ var DEFAULT_IGNORES = /* @__PURE__ */ new Set([
27
100
  "playwright-report"
28
101
  ]);
29
102
  function workspaceConfigPath(root) {
30
- return join(root, ".decantr", "workspace.json");
103
+ return join2(root, ".decantr", "workspace.json");
31
104
  }
32
105
  function readWorkspaceConfig(root) {
33
106
  const path = workspaceConfigPath(root);
34
- if (!existsSync(path)) return null;
35
- return JSON.parse(readFileSync(path, "utf-8"));
107
+ if (!existsSync2(path)) return null;
108
+ return JSON.parse(readFileSync2(path, "utf-8"));
36
109
  }
37
110
  function normalizeProjectPath(raw) {
38
111
  const normalized = raw.replace(/^\.\/+/, "").replace(/\/+$/, "");
@@ -51,21 +124,21 @@ function discoverProjectPaths(root, config) {
51
124
  if (depth > 6) return;
52
125
  const rel = relative(root, dir).replace(/\\/g, "/");
53
126
  if (rel && [...ignored].some((entry) => rel === entry || rel.startsWith(`${entry}/`))) return;
54
- if (existsSync(join(dir, "decantr.essence.json"))) {
127
+ if (existsSync2(join2(dir, "decantr.essence.json"))) {
55
128
  results.add(rel || ".");
56
129
  return;
57
130
  }
58
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
131
+ for (const entry of readdirSync2(dir, { withFileTypes: true })) {
59
132
  if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
60
133
  if (ignored.has(entry.name)) continue;
61
- walk(join(dir, entry.name), depth + 1);
134
+ walk(join2(dir, entry.name), depth + 1);
62
135
  }
63
136
  }
64
137
  walk(root, 0);
65
138
  return [...results].sort();
66
139
  }
67
140
  function listWorkspaceProjects(root = process.cwd()) {
68
- const workspaceRoot = resolve(root);
141
+ const workspaceRoot = resolve2(root);
69
142
  const config = readWorkspaceConfig(workspaceRoot);
70
143
  const byPath = /* @__PURE__ */ new Map();
71
144
  for (const project of config?.projects ?? []) {
@@ -73,7 +146,7 @@ function listWorkspaceProjects(root = process.cwd()) {
73
146
  byPath.set(path, {
74
147
  id: project.id ?? projectIdFromPath(path),
75
148
  path,
76
- absolutePath: resolve(workspaceRoot, path),
149
+ absolutePath: resolve2(workspaceRoot, path),
77
150
  owner: project.owner ?? null,
78
151
  tags: project.tags ?? [],
79
152
  criticality: project.criticality ?? "normal",
@@ -86,7 +159,7 @@ function listWorkspaceProjects(root = process.cwd()) {
86
159
  byPath.set(path, {
87
160
  id: projectIdFromPath(path),
88
161
  path,
89
- absolutePath: resolve(workspaceRoot, path),
162
+ absolutePath: resolve2(workspaceRoot, path),
90
163
  owner: null,
91
164
  tags: [],
92
165
  criticality: "normal",
@@ -153,7 +226,7 @@ async function mapLimited(items, concurrency, fn) {
153
226
  return results;
154
227
  }
155
228
  async function createWorkspaceHealthReport(root = process.cwd(), options = {}) {
156
- const workspaceRoot = resolve(root);
229
+ const workspaceRoot = resolve2(root);
157
230
  const config = readWorkspaceConfig(workspaceRoot);
158
231
  const since = options.since ?? "origin/main";
159
232
  const changed = options.changedOnly ? changedPaths(workspaceRoot, since) : /* @__PURE__ */ new Set();
@@ -333,8 +406,8 @@ async function cmdWorkspace(workspaceRoot = process.cwd(), args = ["workspace"])
333
406
  const payload = options.json ? `${JSON.stringify(report, null, 2)}
334
407
  ` : options.markdown ? formatWorkspaceHealthMarkdown(report) : formatWorkspaceHealthText(report);
335
408
  if (options.output) {
336
- mkdirSync(dirname(resolve(workspaceRoot, options.output)), { recursive: true });
337
- writeFileSync(resolve(workspaceRoot, options.output), payload, "utf-8");
409
+ mkdirSync(dirname2(resolve2(workspaceRoot, options.output)), { recursive: true });
410
+ writeFileSync(resolve2(workspaceRoot, options.output), payload, "utf-8");
338
411
  if (!options.ci)
339
412
  console.log(`${GREEN}Wrote Decantr workspace health:${RESET} ${options.output}`);
340
413
  } else {
@@ -346,6 +419,7 @@ async function cmdWorkspace(workspaceRoot = process.cwd(), args = ["workspace"])
346
419
  }
347
420
 
348
421
  export {
422
+ resolveWorkspaceInfo,
349
423
  listWorkspaceProjects,
350
424
  listWorkspaceCandidates,
351
425
  createWorkspaceHealthReport,
@@ -1,11 +1,9 @@
1
1
  import {
2
- collectCheckIssues
3
- } from "./chunk-3TH5PLFO.js";
4
- import {
2
+ collectCheckIssues,
5
3
  sendProjectHealthCiFailedTelemetry,
6
4
  sendProjectHealthPromptTelemetry,
7
5
  sendProjectHealthReportTelemetry
8
- } from "./chunk-KT2ROK2D.js";
6
+ } from "./chunk-34TZXWIF.js";
9
7
 
10
8
  // src/commands/health.ts
11
9
  import { execFileSync } from "child_process";
@@ -227,6 +225,26 @@ function sourceFromCheckIssue(issue) {
227
225
  if (issue.rule.includes("interaction")) return "interaction";
228
226
  return "check";
229
227
  }
228
+ function normalizeHealthCategory(category, source) {
229
+ const lower = category.toLowerCase();
230
+ if (source === "pack" || lower.includes("execution pack") || lower.includes("review contract") || lower.includes("context")) {
231
+ return "Generated Artifact";
232
+ }
233
+ if (source === "brownfield") return "Brownfield Contract";
234
+ if (source === "design-token" || lower.includes("design-token")) return "Design Token";
235
+ if (lower.includes("accessibility")) return "Accessibility";
236
+ if (source === "runtime") return "Runtime";
237
+ if (source === "browser") return "Visual Evidence";
238
+ if (source === "interaction") return "Interaction";
239
+ if (source === "assertion") return `Contract ${category}`;
240
+ return category;
241
+ }
242
+ function contractAssertionApplies(assertion, metadata) {
243
+ if (assertion.rule === "tokens-file-present" && metadata.adoptionMode === "contract-only") {
244
+ return false;
245
+ }
246
+ return true;
247
+ }
230
248
  function slugify(value) {
231
249
  return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80);
232
250
  }
@@ -298,13 +316,14 @@ function createHealthFinding(input) {
298
316
  const idBase = input.baseId || input.rule || `${input.category}-${input.message}`;
299
317
  const id = `${input.source}-${slugify(idBase)}`;
300
318
  const commands = commandsForFinding(input.source);
319
+ const category = normalizeHealthCategory(input.category, input.source);
301
320
  const remediation = {
302
- summary: input.suggestedFix || `Resolve ${input.category.toLowerCase()} finding.`,
321
+ summary: input.suggestedFix || `Resolve ${category.toLowerCase()} finding.`,
303
322
  commands,
304
323
  prompt: buildRemediationPrompt({
305
324
  id,
306
325
  source: input.source,
307
- category: input.category,
326
+ category,
308
327
  severity: input.severity,
309
328
  message: input.message,
310
329
  evidence: input.evidence ?? [],
@@ -315,7 +334,7 @@ function createHealthFinding(input) {
315
334
  return {
316
335
  id,
317
336
  source: input.source,
318
- category: input.category,
337
+ category,
319
338
  severity: input.severity,
320
339
  message: input.message,
321
340
  evidence: input.evidence ?? [],
@@ -787,6 +806,7 @@ async function createProjectHealthReport(projectRoot = process.cwd(), options =
787
806
  if (!isDuplicateFinding(seen, healthFinding)) findings.push(healthFinding);
788
807
  }
789
808
  for (const contractAssertion of createContractAssertions(projectRoot, audit)) {
809
+ if (!contractAssertionApplies(contractAssertion, metadata)) continue;
790
810
  if (contractAssertion.status !== "failed") continue;
791
811
  const healthFinding = createHealthFinding({
792
812
  source: "assertion",
@@ -893,7 +913,7 @@ async function createProjectHealthReport(projectRoot = process.cwd(), options =
893
913
  generatedAt: typeof manifest?.generatedAt === "string" ? manifest.generatedAt : null
894
914
  },
895
915
  ci: {
896
- recommendedCommand: "decantr health --ci --fail-on error",
916
+ recommendedCommand: "decantr ci --fail-on error",
897
917
  failOn: "error"
898
918
  },
899
919
  findings
@@ -172,7 +172,9 @@ function statusFromCounts(counts) {
172
172
  function scoreFromCounts(counts) {
173
173
  const warningPenalty = Math.min(counts.warnCount * 2, 75);
174
174
  const infoPenalty = Math.min(counts.infoCount * 0.5, 10);
175
- return Math.round(Math.max(0, Math.min(100, 100 - counts.errorCount * 15 - warningPenalty - infoPenalty)));
175
+ return Math.round(
176
+ Math.max(0, Math.min(100, 100 - counts.errorCount * 15 - warningPenalty - infoPenalty))
177
+ );
176
178
  }
177
179
  function percentage(count, total) {
178
180
  if (total === 0) return 1;
@@ -728,7 +730,9 @@ async function createContentHealthReport(contentRoot = process.cwd(), options =
728
730
  category: "Content Root",
729
731
  severity: "error",
730
732
  message: "No Decantr registry content was found in this directory.",
731
- evidence: ["Expected one or more of patterns/, themes/, blueprints/, archetypes/, shells/."],
733
+ evidence: [
734
+ "Expected one or more of patterns/, themes/, blueprints/, archetypes/, shells/."
735
+ ],
732
736
  rule: "content-root-empty",
733
737
  suggestedFix: "Run this command from a decantr-content style repository.",
734
738
  baseId: "content-root-empty"
@@ -828,11 +832,15 @@ async function createContentHealthReport(contentRoot = process.cwd(), options =
828
832
  patterns.length
829
833
  ),
830
834
  patternInteractionCoverage: percentage(
831
- patterns.filter((item) => Array.isArray(item.data.interactions) && item.data.interactions.length > 0).length,
835
+ patterns.filter(
836
+ (item) => Array.isArray(item.data.interactions) && item.data.interactions.length > 0
837
+ ).length,
832
838
  patterns.length
833
839
  ),
834
840
  themeDecoratorCoverage: percentage(
835
- themes.filter((item) => isRecord(item.data.decorators) && Object.keys(item.data.decorators).length > 0).length,
841
+ themes.filter(
842
+ (item) => isRecord(item.data.decorators) && Object.keys(item.data.decorators).length > 0
843
+ ).length,
836
844
  themes.length
837
845
  ),
838
846
  blueprintPersonalityCoverage: percentage(
@@ -842,7 +850,10 @@ async function createContentHealthReport(contentRoot = process.cwd(), options =
842
850
  }).length,
843
851
  blueprints.length
844
852
  ),
845
- blueprintVoiceCoverage: percentage(blueprints.filter((item) => isRecord(item.data.voice)).length, blueprints.length),
853
+ blueprintVoiceCoverage: percentage(
854
+ blueprints.filter((item) => isRecord(item.data.voice)).length,
855
+ blueprints.length
856
+ ),
846
857
  archetypePageBriefCoverage: percentage(
847
858
  archetypes.filter((item) => isRecord(item.data.page_briefs)).length,
848
859
  archetypes.length
@@ -896,10 +907,12 @@ function formatContentHealthText(report) {
896
907
  );
897
908
  if (finding.file) lines.push(` ${DIM}${finding.file}${RESET}`);
898
909
  if (finding.suggestedFix) lines.push(` ${DIM}Fix: ${finding.suggestedFix}${RESET}`);
899
- lines.push(` ${DIM}Prompt: decantr content-health --prompt ${finding.id}${RESET}`);
910
+ lines.push(` ${DIM}Prompt: decantr content check --prompt ${finding.id}${RESET}`);
900
911
  }
901
912
  if (report.findings.length > 40) {
902
- lines.push(` ${DIM}Showing first 40 of ${report.findings.length} findings. Use --json for the full report.${RESET}`);
913
+ lines.push(
914
+ ` ${DIM}Showing first 40 of ${report.findings.length} findings. Use --json for the full report.${RESET}`
915
+ );
903
916
  }
904
917
  }
905
918
  lines.push("");
@@ -931,10 +944,14 @@ function formatContentHealthMarkdown(report) {
931
944
  lines.push("");
932
945
  lines.push("## Quality Coverage");
933
946
  lines.push("");
934
- lines.push(`- Pattern visual guidance: ${percentLabel(report.quality.patternVisualBriefCoverage)}`);
947
+ lines.push(
948
+ `- Pattern visual guidance: ${percentLabel(report.quality.patternVisualBriefCoverage)}`
949
+ );
935
950
  lines.push(`- Pattern interactions: ${percentLabel(report.quality.patternInteractionCoverage)}`);
936
951
  lines.push(`- Theme decorators: ${percentLabel(report.quality.themeDecoratorCoverage)}`);
937
- lines.push(`- Blueprint personality: ${percentLabel(report.quality.blueprintPersonalityCoverage)}`);
952
+ lines.push(
953
+ `- Blueprint personality: ${percentLabel(report.quality.blueprintPersonalityCoverage)}`
954
+ );
938
955
  lines.push(`- Blueprint voice: ${percentLabel(report.quality.blueprintVoiceCoverage)}`);
939
956
  lines.push(`- Archetype page briefs: ${percentLabel(report.quality.archetypePageBriefCoverage)}`);
940
957
  lines.push("");
@@ -958,7 +975,7 @@ function formatContentHealthMarkdown(report) {
958
975
  lines.push("- Evidence:");
959
976
  for (const evidence of finding.evidence) lines.push(` - ${evidence}`);
960
977
  }
961
- lines.push(`- Prompt: \`decantr content-health --prompt ${finding.id}\``);
978
+ lines.push(`- Prompt: \`decantr content check --prompt ${finding.id}\``);
962
979
  lines.push("");
963
980
  }
964
981
  }
@@ -1,8 +1,7 @@
1
1
  import {
2
2
  cmdHeal,
3
3
  collectCheckIssues
4
- } from "./chunk-3TH5PLFO.js";
5
- import "./chunk-KT2ROK2D.js";
4
+ } from "./chunk-34TZXWIF.js";
6
5
  export {
7
6
  cmdHeal,
8
7
  collectCheckIssues
@@ -10,9 +10,8 @@ import {
10
10
  renderProjectHealthCiWorkflow,
11
11
  shouldFailHealth,
12
12
  writeProjectHealthCiWorkflow
13
- } from "./chunk-PAF4PBD3.js";
14
- import "./chunk-3TH5PLFO.js";
15
- import "./chunk-KT2ROK2D.js";
13
+ } from "./chunk-TMOCTDYY.js";
14
+ import "./chunk-34TZXWIF.js";
16
15
  export {
17
16
  cmdHealth,
18
17
  collectDesignTokenEvidence,
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
- import "./chunk-RKZMHS2K.js";
1
+ import "./chunk-N7A3WUZ2.js";
2
2
  import "./chunk-V3XAQWKD.js";
3
- import "./chunk-VE6N3XWG.js";
4
- import "./chunk-KT2ROK2D.js";
3
+ import "./chunk-T5INVSOP.js";
4
+ import "./chunk-TMOCTDYY.js";
5
+ import "./chunk-34TZXWIF.js";
@@ -1,15 +1,13 @@
1
1
  import {
2
2
  createWorkspaceHealthReport
3
- } from "./chunk-FV6DGYD7.js";
4
- import "./chunk-VE6N3XWG.js";
3
+ } from "./chunk-T5INVSOP.js";
5
4
  import {
6
5
  createProjectHealthReport
7
- } from "./chunk-PAF4PBD3.js";
8
- import "./chunk-3TH5PLFO.js";
6
+ } from "./chunk-TMOCTDYY.js";
9
7
  import {
10
8
  sendStudioHealthRefreshedTelemetry,
11
9
  sendStudioStartedTelemetry
12
- } from "./chunk-KT2ROK2D.js";
10
+ } from "./chunk-34TZXWIF.js";
13
11
 
14
12
  // src/commands/studio.ts
15
13
  import { readFileSync } from "fs";
@@ -7,11 +7,9 @@ import {
7
7
  listWorkspaceProjects,
8
8
  parseWorkspaceArgs,
9
9
  shouldFailWorkspaceHealth
10
- } from "./chunk-FV6DGYD7.js";
11
- import "./chunk-VE6N3XWG.js";
12
- import "./chunk-PAF4PBD3.js";
13
- import "./chunk-3TH5PLFO.js";
14
- import "./chunk-KT2ROK2D.js";
10
+ } from "./chunk-T5INVSOP.js";
11
+ import "./chunk-TMOCTDYY.js";
12
+ import "./chunk-34TZXWIF.js";
15
13
  export {
16
14
  cmdWorkspace,
17
15
  createWorkspaceHealthReport,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decantr/cli",
3
- "version": "2.8.1",
3
+ "version": "2.9.0",
4
4
  "description": "Decantr CLI - scaffold, audit, inspect Project Health, and maintain Decantr projects from the terminal",
5
5
  "keywords": [
6
6
  "decantr",
@@ -49,10 +49,10 @@
49
49
  "dependencies": {
50
50
  "ajv": "^8.20.0",
51
51
  "@decantr/core": "2.1.0",
52
+ "@decantr/registry": "2.2.0",
52
53
  "@decantr/essence-spec": "2.0.1",
53
54
  "@decantr/telemetry": "2.2.1",
54
- "@decantr/verifier": "2.2.0",
55
- "@decantr/registry": "2.2.0"
55
+ "@decantr/verifier": "2.3.0"
56
56
  },
57
57
  "scripts": {
58
58
  "build": "tsup",