@decantr/cli 2.9.1 → 2.9.3

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 CHANGED
@@ -19,11 +19,13 @@ npx @decantr/cli new my-app --blueprint=esports-hq
19
19
 
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 next command.
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
- 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, and only applies it when you explicitly accept or merge it.
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
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 and local rules such as button/card/shell/theme standards to appear in future task context and verification.
24
- In monorepos, app-scoped commands accept `--project <app-path>`. 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`.
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
 
27
+ App-scoped primitives now share the same `--project` posture as the primary workflow commands. From a workspace root, `health`, `status`, `upgrade`, `add`, `remove`, `theme`, `export`, `suggest`, `magic`, `rules`, and `telemetry` target the selected app instead of the root. Task/read paths, local-law summaries, and refresh change summaries are printed as openable workspace paths. Nonexistent project paths fail immediately, and Brownfield adoption refuses component packages unless you intentionally pass `--force-package`.
28
+
27
29
  Current starter adapter availability:
28
30
 
29
31
  - `react-vite` is the React + Vite runnable bootstrap adapter
@@ -128,6 +130,8 @@ decantr list blueprints --blueprint-set featured
128
130
  decantr list blueprints --blueprint-set certified
129
131
  decantr search dashboard --type blueprint --blueprint-set labs
130
132
  decantr suggest "recipe feed with infinite scroll" --route /feed --from-code
133
+ decantr suggest --from-code --file app/page.tsx --project apps/web
134
+ decantr suggest "standardize buttons" --project apps/web
131
135
  decantr list patterns
132
136
  decantr showcase verification --json
133
137
  ```
@@ -138,7 +142,7 @@ decantr showcase verification --json
138
142
 
139
143
  `decantr doctor` explains project/workspace state, adoption mode, generated artifacts, local law, visual evidence, design authority signals, CI wiring, and the next command to run. It is the command to reach for when an app is in a monorepo, has stale Decantr files, or someone is not sure what Decantr expects next.
140
144
 
141
- `decantr ci` is the blessed non-mutating automation gate. It runs the Project Health surface with adoption-mode-aware local law checks and emits a schema-backed CI report. `decantr ci init` writes root GitHub workflows or portable generic snippets using the detected package manager and pinned local CLI command instead of `@latest`.
145
+ `decantr ci` is the blessed non-mutating automation gate. It runs the Project Health surface with adoption-mode-aware local law checks and emits a schema-backed CI report. `decantr ci init` writes root GitHub workflows or portable generic snippets using the detected package manager and pinned local CLI command instead of `@latest`; if the root manifest has not pinned Decantr yet, it prints the exact install command first.
142
146
 
143
147
  `decantr health` remains the advanced project observability primitive. It composes the existing verifier audit, guard checks, brownfield route drift checks, runtime evidence, and execution-pack files into a `ProjectHealthReport` with a status, score, route summary, pack summary, findings, and AI-ready remediation prompts.
144
148
 
@@ -156,6 +160,7 @@ decantr health
156
160
  decantr health --format json
157
161
  decantr health --markdown --output health.md
158
162
  decantr health --prompt <finding-id>
163
+ decantr health --project apps/registry --prompt <finding-id>
159
164
  decantr health --evidence --output .decantr/evidence/latest.json
160
165
  decantr health --browser --base-url http://localhost:3000 --evidence
161
166
  decantr health --save-baseline
@@ -171,9 +176,9 @@ decantr verify --workspace --changed --since origin/main
171
176
  decantr export --to figma-tokens
172
177
  ```
173
178
 
174
- Use `--json` for machines and schema validation, `--markdown` for summaries, `--evidence` for the privacy-redacted Evidence Bundle, and `--prompt <finding-id>` when you want a scoped remediation prompt for an AI assistant. The prompt command prints instructions only; it does not modify source files. `--browser` uses a project-local Playwright install and a supplied base URL to capture local route screenshots under `.decantr/evidence/screenshots/` and write `.decantr/evidence/visual-manifest.json`; missing Playwright becomes a setup finding, not a crash. `--save-baseline` writes `.decantr/health-baseline.json`; `--since-baseline` writes `.decantr/health-baseline-diff.json` with changed files, route impact, finding deltas, screenshot hash drift, and contract drift. `--design-tokens <path>` compares a Tokens Studio/Figma token JSON export against Decantr CSS token names. `decantr ci --fail-on error` fails only when blocking errors exist; `decantr ci --fail-on warn` also fails on warnings.
179
+ Use `--json` for machines and schema validation, `--markdown` for summaries, `--evidence` for the privacy-redacted Evidence Bundle, and `--prompt <finding-id>` when you want a scoped remediation prompt for an AI assistant. The prompt command prints instructions only; it does not modify source files. In monorepos, prompt commands preserve `--project <path>` so the finding resolves from the same app that produced it. `--browser` uses a project-local Playwright install and a supplied base URL to capture local route screenshots under `.decantr/evidence/screenshots/` and write `.decantr/evidence/visual-manifest.json`; missing Playwright becomes a visible setup finding/message, not a crash or silent skip. `--save-baseline` writes `.decantr/health-baseline.json`; `--since-baseline` writes `.decantr/health-baseline-diff.json` with changed files, route impact, finding deltas, screenshot hash drift, and contract drift. `--design-tokens <path>` compares a Tokens Studio/Figma token JSON export against Decantr CSS token names. `decantr ci --fail-on error` fails only when blocking errors exist; `decantr ci --fail-on warn` also fails on warnings.
175
180
 
176
- `decantr ci init` installs `.github/workflows/decantr-ci.yml` for GitHub Actions. The generated workflow installs dependencies at the workspace root, writes JSON/markdown CI artifacts, gates with `decantr ci`, appends the markdown report to the GitHub step summary, and uploads both files as artifacts. Use `--force` to replace an existing workflow or `--fail-on warn` for stricter repositories. In monorepos, add `--project <path>` from the repository root; dependency install stays at the root while CI evaluates the app contract and uploads app-scoped artifacts. Use `--workspace` to generate an aggregate gate. Use `--provider generic` for Jenkins, Please, Buildkite, GitLab, Azure DevOps, or internal deployment tools. Generated CI uses the pinned local package-manager command and does not depend on `@latest`.
181
+ `decantr ci init` installs `.github/workflows/decantr-ci.yml` for GitHub Actions. The generated workflow installs dependencies at the workspace root, writes JSON/markdown CI artifacts, gates with `decantr ci`, appends the markdown report to the GitHub step summary, and uploads both files as artifacts. Use `--force` to replace an existing workflow or `--fail-on warn` for stricter repositories. In monorepos, add `--project <path>` from the repository root; dependency install stays at the root while CI evaluates the app contract and uploads app-scoped artifacts. Use `--workspace` to generate an aggregate gate. Use `--provider generic` for Jenkins, Please, Buildkite, GitLab, Azure DevOps, or internal deployment tools. Generated CI uses the pinned local package-manager command and does not depend on `@latest`. Project Health remediation prompts are also monorepo-aware, so missing-pack fixes use `apps/web/decantr.essence.json` and CI recommendations include `--project apps/web`.
177
182
 
178
183
  `decantr workspace` is the monorepo reliability namespace. Before attach, `workspace list` shows app candidates. After attach, it also discovers Decantr projects from `.decantr/workspace.json` or by finding `decantr.essence.json` files. Workspace health runs projects with deterministic ordering, concurrency, per-project timeout, failure isolation, and aggregate JSON, and can limit a run to changed projects:
179
184
 
package/dist/bin.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import "./chunk-6UDJDQPT.js";
2
+ import "./chunk-VMNUJOEH.js";
3
3
  import "./chunk-RXF7ZYGK.js";
4
- import "./chunk-FKM4OQDF.js";
5
- import "./chunk-TMOCTDYY.js";
4
+ import "./chunk-ARR3EPS2.js";
5
+ import "./chunk-XZFKK6V7.js";
6
6
  import "./chunk-34TZXWIF.js";
@@ -1,101 +1,12 @@
1
1
  import {
2
- createProjectHealthReport
3
- } from "./chunk-TMOCTDYY.js";
2
+ createProjectHealthReport,
3
+ listWorkspaceAppCandidates
4
+ } from "./chunk-XZFKK6V7.js";
4
5
 
5
6
  // src/commands/workspace.ts
6
7
  import { execFileSync } from "child_process";
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
- const allowPackageDeps = options.allowPackageDeps ?? true;
41
- const pkg = readPackageJson(dir);
42
- const deps = { ...pkg?.dependencies, ...pkg?.devDependencies };
43
- const hasFrontendDependency = Boolean(
44
- deps.react || deps["react-dom"] || deps.next || deps.vue || deps.svelte || deps["@angular/core"] || deps.astro || deps.nuxt
45
- );
46
- const hasServerOnlyDependency = Boolean(
47
- deps.hono || deps.express || deps.fastify || deps.koa || deps["@hapi/hapi"]
48
- );
49
- 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"))) {
50
- return true;
51
- }
52
- if (allowSourceDirs && (existsSync(join(dir, "src")) || existsSync(join(dir, "app")) || existsSync(join(dir, "pages")))) {
53
- if (hasFrontendDependency) return true;
54
- if (hasServerOnlyDependency) return false;
55
- return true;
56
- }
57
- if (!allowPackageDeps) return false;
58
- return hasFrontendDependency;
59
- }
60
- function listWorkspaceApps(workspaceRoot) {
61
- const candidates = [];
62
- for (const base of ["apps", "packages"]) {
63
- const baseDir = join(workspaceRoot, base);
64
- if (!existsSync(baseDir)) continue;
65
- for (const entry of readdirSync(baseDir, { withFileTypes: true })) {
66
- if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
67
- const candidate = join(baseDir, entry.name);
68
- if (looksLikeApp(candidate, {
69
- allowSourceDirs: base === "apps",
70
- allowPackageDeps: base === "apps"
71
- })) {
72
- candidates.push(`${base}/${entry.name}`);
73
- }
74
- }
75
- }
76
- return candidates.sort();
77
- }
78
- function listWorkspaceAppCandidates(workspaceRoot) {
79
- return listWorkspaceApps(resolve(workspaceRoot));
80
- }
81
- function resolveWorkspaceInfo(cwd, projectArg) {
82
- const absoluteCwd = resolve(cwd);
83
- const workspaceRoot = findWorkspaceRoot(absoluteCwd) ?? absoluteCwd;
84
- const appRoot = projectArg ? resolve(absoluteCwd, projectArg) : absoluteCwd;
85
- const appCandidates = listWorkspaceApps(workspaceRoot);
86
- const projectScope = workspaceRoot !== appRoot || appCandidates.length > 0 ? "workspace-app" : "single-app";
87
- const requiresProjectSelection = !projectArg && workspaceRoot === absoluteCwd && appCandidates.length > 0;
88
- return {
89
- cwd: absoluteCwd,
90
- workspaceRoot,
91
- appRoot,
92
- projectScope,
93
- appCandidates,
94
- requiresProjectSelection
95
- };
96
- }
97
-
98
- // src/commands/workspace.ts
8
+ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "fs";
9
+ import { dirname, join, relative, resolve } from "path";
99
10
  var BOLD = "\x1B[1m";
100
11
  var DIM = "\x1B[2m";
101
12
  var GREEN = "\x1B[32m";
@@ -114,12 +25,12 @@ var DEFAULT_IGNORES = /* @__PURE__ */ new Set([
114
25
  "playwright-report"
115
26
  ]);
116
27
  function workspaceConfigPath(root) {
117
- return join2(root, ".decantr", "workspace.json");
28
+ return join(root, ".decantr", "workspace.json");
118
29
  }
119
30
  function readWorkspaceConfig(root) {
120
31
  const path = workspaceConfigPath(root);
121
- if (!existsSync2(path)) return null;
122
- return JSON.parse(readFileSync2(path, "utf-8"));
32
+ if (!existsSync(path)) return null;
33
+ return JSON.parse(readFileSync(path, "utf-8"));
123
34
  }
124
35
  function normalizeProjectPath(raw) {
125
36
  const normalized = raw.replace(/^\.\/+/, "").replace(/\/+$/, "");
@@ -138,21 +49,21 @@ function discoverProjectPaths(root, config) {
138
49
  if (depth > 6) return;
139
50
  const rel = relative(root, dir).replace(/\\/g, "/");
140
51
  if (rel && [...ignored].some((entry) => rel === entry || rel.startsWith(`${entry}/`))) return;
141
- if (existsSync2(join2(dir, "decantr.essence.json"))) {
52
+ if (existsSync(join(dir, "decantr.essence.json"))) {
142
53
  results.add(rel || ".");
143
54
  return;
144
55
  }
145
- for (const entry of readdirSync2(dir, { withFileTypes: true })) {
56
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
146
57
  if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
147
58
  if (ignored.has(entry.name)) continue;
148
- walk(join2(dir, entry.name), depth + 1);
59
+ walk(join(dir, entry.name), depth + 1);
149
60
  }
150
61
  }
151
62
  walk(root, 0);
152
63
  return [...results].sort();
153
64
  }
154
65
  function listWorkspaceProjects(root = process.cwd()) {
155
- const workspaceRoot = resolve2(root);
66
+ const workspaceRoot = resolve(root);
156
67
  const config = readWorkspaceConfig(workspaceRoot);
157
68
  const byPath = /* @__PURE__ */ new Map();
158
69
  for (const project of config?.projects ?? []) {
@@ -160,7 +71,7 @@ function listWorkspaceProjects(root = process.cwd()) {
160
71
  byPath.set(path, {
161
72
  id: project.id ?? projectIdFromPath(path),
162
73
  path,
163
- absolutePath: resolve2(workspaceRoot, path),
74
+ absolutePath: resolve(workspaceRoot, path),
164
75
  owner: project.owner ?? null,
165
76
  tags: project.tags ?? [],
166
77
  criticality: project.criticality ?? "normal",
@@ -173,7 +84,7 @@ function listWorkspaceProjects(root = process.cwd()) {
173
84
  byPath.set(path, {
174
85
  id: projectIdFromPath(path),
175
86
  path,
176
- absolutePath: resolve2(workspaceRoot, path),
87
+ absolutePath: resolve(workspaceRoot, path),
177
88
  owner: null,
178
89
  tags: [],
179
90
  criticality: "normal",
@@ -240,7 +151,7 @@ async function mapLimited(items, concurrency, fn) {
240
151
  return results;
241
152
  }
242
153
  async function createWorkspaceHealthReport(root = process.cwd(), options = {}) {
243
- const workspaceRoot = resolve2(root);
154
+ const workspaceRoot = resolve(root);
244
155
  const config = readWorkspaceConfig(workspaceRoot);
245
156
  const since = options.since ?? "origin/main";
246
157
  const changed = options.changedOnly ? changedPaths(workspaceRoot, since) : /* @__PURE__ */ new Set();
@@ -420,8 +331,8 @@ async function cmdWorkspace(workspaceRoot = process.cwd(), args = ["workspace"])
420
331
  const payload = options.json ? `${JSON.stringify(report, null, 2)}
421
332
  ` : options.markdown ? formatWorkspaceHealthMarkdown(report) : formatWorkspaceHealthText(report);
422
333
  if (options.output) {
423
- mkdirSync(dirname2(resolve2(workspaceRoot, options.output)), { recursive: true });
424
- writeFileSync(resolve2(workspaceRoot, options.output), payload, "utf-8");
334
+ mkdirSync(dirname(resolve(workspaceRoot, options.output)), { recursive: true });
335
+ writeFileSync(resolve(workspaceRoot, options.output), payload, "utf-8");
425
336
  if (!options.ci)
426
337
  console.log(`${GREEN}Wrote Decantr workspace health:${RESET} ${options.output}`);
427
338
  } else {
@@ -433,7 +344,6 @@ async function cmdWorkspace(workspaceRoot = process.cwd(), args = ["workspace"])
433
344
  }
434
345
 
435
346
  export {
436
- resolveWorkspaceInfo,
437
347
  listWorkspaceProjects,
438
348
  listWorkspaceCandidates,
439
349
  createWorkspaceHealthReport,