@decantr/cli 1.9.0 → 1.11.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 CHANGED
@@ -97,10 +97,15 @@ decantr health --markdown --output health.md
97
97
  decantr health --ci --fail-on error
98
98
  decantr health --ci --fail-on warn
99
99
  decantr health --prompt <finding-id>
100
+ decantr health init-ci
101
+ decantr health init-ci --fail-on warn --cli-version latest --force
102
+ decantr health init-ci --project apps/registry
100
103
  ```
101
104
 
102
105
  Use `--json` for machines and schema validation, `--markdown` for CI summaries, and `--prompt <finding-id>` when you want a scoped remediation prompt for an AI assistant. `--ci --fail-on error` fails only when blocking errors exist; `--ci --fail-on warn` also fails on warnings.
103
106
 
107
+ `decantr health init-ci` installs `.github/workflows/decantr-health.yml` for GitHub Actions. The generated workflow installs project dependencies, writes `decantr-health.json`, gates with `decantr health --ci --fail-on error --markdown --output decantr-health.md`, appends the markdown report to the GitHub step summary, and uploads both files as artifacts. Use `--force` to replace an existing workflow, `--fail-on warn` for stricter repositories, or `--cli-version <version|latest>` to pin the package used by CI. In monorepos, add `--project <path>` from the repository root; dependency install stays at the root while health runs inside the app contract and uploads artifacts from that project path.
108
+
104
109
  `decantr studio` starts a local-only dashboard powered by the same report. It uses Node built-ins only and serves `GET /`, `GET /api/health`, and `POST /api/refresh`.
105
110
 
106
111
  ```bash
@@ -199,7 +204,7 @@ Recommended read order for AI-assisted scaffolding:
199
204
 
200
205
  Treat the compiled execution packs as the source of truth. Use the narrative docs as secondary explanation, start with the shell and route structure first, and run `decantr check` plus `decantr audit` after implementation.
201
206
 
202
- For a broader health pass, run `decantr health` after `refresh`, before opening a pull request, or inside CI. Findings include remediation commands and can be turned into focused AI prompts with `decantr health --prompt <finding-id>`.
207
+ For a broader health pass, run `decantr health` after `refresh`, before opening a pull request, or inside CI. Install the default GitHub Actions gate with `decantr health init-ci`. Findings include remediation commands and can be turned into focused AI prompts with `decantr health --prompt <finding-id>`.
203
208
 
204
209
  For cold-start harness or certification runs, use only the scaffolded workspace files as the contract. If local scaffold files disagree, stop and report the mismatch rather than relying on repo-global Decantr assumptions.
205
210
 
package/dist/bin.js CHANGED
@@ -1,4 +1,4 @@
1
1
  #!/usr/bin/env node
2
- import "./chunk-PKJSI6IH.js";
2
+ import "./chunk-5RODH77L.js";
3
3
  import "./chunk-USOO77A5.js";
4
4
  import "./chunk-DI2PLOJ6.js";
@@ -6927,6 +6927,7 @@ ${BOLD6}Usage:${RESET13}
6927
6927
  decantr registry get-pack <manifest|scaffold|review|section|page|mutation> [id] [--namespace <namespace>] [--json] [--essence <path>] [--write-context]
6928
6928
  decantr registry critique-file <file> [--namespace <namespace>] [--json] [--essence <path>] [--treatments <path>]
6929
6929
  decantr registry audit-project [--namespace <namespace>] [--json] [--essence <path>] [--dist <path>] [--sources <dir>]
6930
+ decantr health init-ci [--force] [--project <path>] [--fail-on <error|warn|none>] [--cli-version <version|latest>]
6930
6931
  decantr content-health [--json] [--markdown] [--ci]
6931
6932
  decantr rules preview [--project=<path>]
6932
6933
  decantr rules apply [--project=<path>]
@@ -6965,7 +6966,7 @@ ${BOLD6}Commands:${RESET13}
6965
6966
  ${cyan3("magic")} Greenfield-first intent flow; steers existing apps into analyze + init
6966
6967
  ${cyan3("init")} Attach Decantr contract/context files to an existing project or empty workspace
6967
6968
  ${cyan3("status")} Show project status, DNA axioms, and blueprint info
6968
- ${cyan3("health")} Generate a local Project Health report [--json] [--markdown] [--ci]
6969
+ ${cyan3("health")} Generate a local Project Health report [--json] [--markdown] [--ci]; use health init-ci to install a GitHub Actions gate
6969
6970
  ${cyan3("content-health")} Generate a local registry content health report [--json] [--markdown] [--ci]
6970
6971
  ${cyan3("studio")} Open a local Project Health dashboard backed by the same report
6971
6972
  ${cyan3("sync")} Sync registry content from API
@@ -7005,6 +7006,8 @@ ${BOLD6}Examples:${RESET13}
7005
7006
  decantr rules apply
7006
7007
  decantr status
7007
7008
  decantr health
7009
+ decantr health init-ci
7010
+ decantr health init-ci --project apps/web
7008
7011
  decantr health --ci --fail-on error
7009
7012
  decantr content-health --ci --fail-on error
7010
7013
  decantr studio
@@ -7188,7 +7191,7 @@ async function main() {
7188
7191
  }
7189
7192
  case "health": {
7190
7193
  try {
7191
- const { cmdHealth, parseHealthArgs } = await import("./health-VSL4MROO.js");
7194
+ const { cmdHealth, parseHealthArgs } = await import("./health-3TJYYTX6.js");
7192
7195
  await cmdHealth(process.cwd(), parseHealthArgs(args));
7193
7196
  } catch (e) {
7194
7197
  console.error(error3(e.message));
@@ -7208,7 +7211,7 @@ async function main() {
7208
7211
  }
7209
7212
  case "studio": {
7210
7213
  try {
7211
- const { cmdStudio, parseStudioArgs } = await import("./studio-BCTWKXFH.js");
7214
+ const { cmdStudio, parseStudioArgs } = await import("./studio-7TE7YXFG.js");
7212
7215
  await cmdStudio(process.cwd(), parseStudioArgs(args));
7213
7216
  } catch (e) {
7214
7217
  console.error(error3(e.message));
@@ -3,8 +3,9 @@ import {
3
3
  } from "./chunk-RSDCWAHD.js";
4
4
 
5
5
  // src/commands/health.ts
6
- import { existsSync, readFileSync, writeFileSync } from "fs";
7
- import { join } from "path";
6
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
7
+ import { dirname, join } from "path";
8
+ import { fileURLToPath } from "url";
8
9
  import {
9
10
  auditProject
10
11
  } from "@decantr/verifier";
@@ -16,6 +17,11 @@ var GREEN = "\x1B[32m";
16
17
  var CYAN = "\x1B[36m";
17
18
  var YELLOW = "\x1B[33m";
18
19
  var PROJECT_HEALTH_SCHEMA_URL = "https://decantr.ai/schemas/project-health-report.v1.json";
20
+ var DEFAULT_HEALTH_CI_WORKFLOW_PATH = ".github/workflows/decantr-health.yml";
21
+ var DEFAULT_HEALTH_CI_REPORT_PATH = "decantr-health.md";
22
+ var DEFAULT_HEALTH_CI_JSON_PATH = "decantr-health.json";
23
+ var DEFAULT_HEALTH_CI_CLI_VERSION = "latest";
24
+ var __dirname = dirname(fileURLToPath(import.meta.url));
19
25
  function readProjectMetadata(projectRoot) {
20
26
  const projectJsonPath = join(projectRoot, ".decantr", "project.json");
21
27
  if (!existsSync(projectJsonPath)) {
@@ -34,6 +40,122 @@ function readProjectMetadata(projectRoot) {
34
40
  return { workflowMode: null, adoptionMode: null, autoBrownfield: false };
35
41
  }
36
42
  }
43
+ function loadHealthTemplate(name) {
44
+ const fromDist = join(__dirname, "..", "src", "templates", name);
45
+ if (existsSync(fromDist)) return readFileSync(fromDist, "utf-8");
46
+ const fromSrc = join(__dirname, "..", "templates", name);
47
+ if (existsSync(fromSrc)) return readFileSync(fromSrc, "utf-8");
48
+ const fromCommandSrc = join(__dirname, "..", "..", "templates", name);
49
+ if (existsSync(fromCommandSrc)) return readFileSync(fromCommandSrc, "utf-8");
50
+ throw new Error(`Template not found: ${name}`);
51
+ }
52
+ function renderTemplate(template, vars) {
53
+ let result = template;
54
+ for (const [key, value] of Object.entries(vars)) {
55
+ result = result.replace(new RegExp(`\\{\\{${key}\\}\\}`, "g"), value);
56
+ }
57
+ return result;
58
+ }
59
+ function normalizeCliPackageSpecifier(version) {
60
+ const value = (version || DEFAULT_HEALTH_CI_CLI_VERSION).trim();
61
+ if (!value) return `@decantr/cli@${DEFAULT_HEALTH_CI_CLI_VERSION}`;
62
+ const versionToken = value.startsWith("@decantr/cli@") ? value.slice("@decantr/cli@".length) : value;
63
+ if (!/^[A-Za-z0-9._~^*-]+$/.test(versionToken)) {
64
+ throw new Error(
65
+ "Invalid --cli-version value. Use a package version or dist-tag such as latest, 1.10.0, or next."
66
+ );
67
+ }
68
+ return `@decantr/cli@${versionToken}`;
69
+ }
70
+ function normalizeHealthFailOn(value) {
71
+ const failOn = value ?? "error";
72
+ if (!["error", "warn", "none"].includes(failOn)) {
73
+ throw new Error("Invalid --fail-on value. Use error, warn, or none.");
74
+ }
75
+ return failOn;
76
+ }
77
+ function validateWorkflowPath(value) {
78
+ const normalized = value.trim();
79
+ if (!normalized || normalized.startsWith("/") || normalized.startsWith("-") || normalized.includes("..") || normalized.includes("\\") || /\s/.test(normalized)) {
80
+ throw new Error(
81
+ "Invalid --workflow-path value. Use a relative path without spaces or parent-directory segments."
82
+ );
83
+ }
84
+ return normalized;
85
+ }
86
+ function validateArtifactPath(value, flag) {
87
+ const normalized = value.trim();
88
+ if (!normalized || normalized.startsWith("/") || normalized.startsWith("-") || normalized.includes("..") || normalized.includes("\\") || /\s/.test(normalized)) {
89
+ throw new Error(
90
+ `Invalid ${flag} value. Use a relative artifact path without spaces or parent-directory segments.`
91
+ );
92
+ }
93
+ return normalized;
94
+ }
95
+ function validateProjectPath(value) {
96
+ if (value === void 0) return void 0;
97
+ const raw = value.trim();
98
+ if (!raw || raw === ".") return void 0;
99
+ const normalized = raw.replace(/^\.\/+/, "").replace(/\/+$/, "");
100
+ if (!normalized || normalized.startsWith("/") || normalized.startsWith("-") || normalized.includes("..") || normalized.includes("\\") || /\s/.test(normalized) || !/^[A-Za-z0-9._@/-]+$/.test(normalized)) {
101
+ throw new Error(
102
+ "Invalid --project value. Use a relative project path without spaces or parent-directory segments."
103
+ );
104
+ }
105
+ const segments = normalized.split("/");
106
+ if (segments.some((segment) => !segment || segment === "." || segment === "..")) {
107
+ throw new Error(
108
+ "Invalid --project value. Use a relative project path without empty or parent-directory segments."
109
+ );
110
+ }
111
+ return normalized;
112
+ }
113
+ function prefixArtifactPath(projectPath, artifactPath) {
114
+ return projectPath ? `${projectPath}/${artifactPath}` : artifactPath;
115
+ }
116
+ function renderProjectHealthCiWorkflow(options = {}) {
117
+ const failOn = normalizeHealthFailOn(options.failOn);
118
+ const projectPath = validateProjectPath(options.projectPath);
119
+ const reportPath = validateArtifactPath(
120
+ options.reportPath || DEFAULT_HEALTH_CI_REPORT_PATH,
121
+ "--report-path"
122
+ );
123
+ const jsonPath = validateArtifactPath(options.jsonPath || DEFAULT_HEALTH_CI_JSON_PATH, "--json-path");
124
+ const template = loadHealthTemplate("decantr-health.workflow.yml.template");
125
+ return renderTemplate(template, {
126
+ CLI_PACKAGE: normalizeCliPackageSpecifier(options.cliVersion),
127
+ FAIL_ON: failOn,
128
+ PROJECT_WORKING_DIRECTORY: projectPath ? ` working-directory: ${projectPath}
129
+ ` : "",
130
+ REPORT_PATH: reportPath,
131
+ JSON_PATH: jsonPath,
132
+ REPORT_ARTIFACT_PATH: prefixArtifactPath(projectPath, reportPath),
133
+ JSON_ARTIFACT_PATH: prefixArtifactPath(projectPath, jsonPath)
134
+ });
135
+ }
136
+ function writeProjectHealthCiWorkflow(projectRoot, options = {}) {
137
+ const workflowRelativePath = validateWorkflowPath(
138
+ options.workflowPath || DEFAULT_HEALTH_CI_WORKFLOW_PATH
139
+ );
140
+ const workflowPath = join(projectRoot, workflowRelativePath);
141
+ const alreadyExists = existsSync(workflowPath);
142
+ if (alreadyExists && !options.force) {
143
+ throw new Error(
144
+ `${workflowRelativePath} already exists. Re-run with --force to replace it, or use --workflow-path <file>.`
145
+ );
146
+ }
147
+ mkdirSync(dirname(workflowPath), { recursive: true });
148
+ writeFileSync(workflowPath, renderProjectHealthCiWorkflow(options), "utf-8");
149
+ const projectPath = validateProjectPath(options.projectPath);
150
+ const result = {
151
+ path: workflowRelativePath,
152
+ created: !alreadyExists,
153
+ cliPackage: normalizeCliPackageSpecifier(options.cliVersion),
154
+ failOn: normalizeHealthFailOn(options.failOn)
155
+ };
156
+ if (projectPath) result.projectPath = projectPath;
157
+ return result;
158
+ }
37
159
  function collectDeclaredRoutes(essence) {
38
160
  if (!essence || typeof essence !== "object") return [];
39
161
  const record = essence;
@@ -393,6 +515,22 @@ function shouldFailHealth(report, failOn) {
393
515
  return report.summary.errorCount > 0;
394
516
  }
395
517
  async function cmdHealth(projectRoot = process.cwd(), options = {}) {
518
+ if (options.initCi) {
519
+ try {
520
+ const result = writeProjectHealthCiWorkflow(projectRoot, options.initCi);
521
+ const action = result.created ? "Created" : "Updated";
522
+ console.log(`${GREEN}${action} Decantr Project Health workflow:${RESET} ${result.path}`);
523
+ console.log(`${DIM}CLI package: ${result.cliPackage}${RESET}`);
524
+ if (result.projectPath) {
525
+ console.log(`${DIM}Project: ${result.projectPath}${RESET}`);
526
+ }
527
+ console.log(`${DIM}CI gate: decantr health --ci --fail-on ${result.failOn}${RESET}`);
528
+ } catch (e) {
529
+ console.error(`${RED}${e.message}${RESET}`);
530
+ process.exitCode = 1;
531
+ }
532
+ return;
533
+ }
396
534
  const report = await createProjectHealthReport(projectRoot);
397
535
  if (options.promptId) {
398
536
  const finding = report.findings.find((entry) => entry.id === options.promptId);
@@ -420,6 +558,44 @@ async function cmdHealth(projectRoot = process.cwd(), options = {}) {
420
558
  }
421
559
  function parseHealthArgs(args) {
422
560
  const options = {};
561
+ if (args[1] === "init-ci") {
562
+ options.initCi = {};
563
+ for (let index = 2; index < args.length; index += 1) {
564
+ const arg = args[index];
565
+ if (arg === "--force") {
566
+ options.initCi.force = true;
567
+ } else if (arg === "--fail-on" && args[index + 1]) {
568
+ options.initCi.failOn = args[++index];
569
+ } else if (arg.startsWith("--fail-on=")) {
570
+ options.initCi.failOn = arg.split("=")[1];
571
+ } else if ((arg === "--cli-version" || arg === "--cli") && args[index + 1]) {
572
+ options.initCi.cliVersion = args[++index];
573
+ } else if (arg.startsWith("--cli-version=")) {
574
+ options.initCi.cliVersion = arg.split("=")[1];
575
+ } else if (arg.startsWith("--cli=")) {
576
+ options.initCi.cliVersion = arg.split("=")[1];
577
+ } else if (arg === "--workflow-path" && args[index + 1]) {
578
+ options.initCi.workflowPath = args[++index];
579
+ } else if (arg.startsWith("--workflow-path=")) {
580
+ options.initCi.workflowPath = arg.split("=")[1];
581
+ } else if (arg === "--report-path" && args[index + 1]) {
582
+ options.initCi.reportPath = args[++index];
583
+ } else if (arg.startsWith("--report-path=")) {
584
+ options.initCi.reportPath = arg.split("=")[1];
585
+ } else if (arg === "--json-path" && args[index + 1]) {
586
+ options.initCi.jsonPath = args[++index];
587
+ } else if (arg.startsWith("--json-path=")) {
588
+ options.initCi.jsonPath = arg.split("=")[1];
589
+ } else if (arg === "--project" && args[index + 1]) {
590
+ options.initCi.projectPath = args[++index];
591
+ } else if (arg.startsWith("--project=")) {
592
+ options.initCi.projectPath = arg.split("=")[1];
593
+ }
594
+ }
595
+ normalizeHealthFailOn(options.initCi.failOn);
596
+ validateProjectPath(options.initCi.projectPath);
597
+ return options;
598
+ }
423
599
  for (let index = 1; index < args.length; index += 1) {
424
600
  const arg = args[index];
425
601
  if (arg === "--json") {
@@ -456,6 +632,8 @@ function parseHealthArgs(args) {
456
632
  }
457
633
 
458
634
  export {
635
+ renderProjectHealthCiWorkflow,
636
+ writeProjectHealthCiWorkflow,
459
637
  createProjectHealthReport,
460
638
  formatProjectHealthText,
461
639
  formatProjectHealthMarkdown,
@@ -5,8 +5,10 @@ import {
5
5
  formatProjectHealthMarkdown,
6
6
  formatProjectHealthText,
7
7
  parseHealthArgs,
8
- shouldFailHealth
9
- } from "./chunk-DONMNPS7.js";
8
+ renderProjectHealthCiWorkflow,
9
+ shouldFailHealth,
10
+ writeProjectHealthCiWorkflow
11
+ } from "./chunk-6YCFRZZI.js";
10
12
  import "./chunk-RSDCWAHD.js";
11
13
  import "./chunk-DI2PLOJ6.js";
12
14
  export {
@@ -16,5 +18,7 @@ export {
16
18
  formatProjectHealthMarkdown,
17
19
  formatProjectHealthText,
18
20
  parseHealthArgs,
19
- shouldFailHealth
21
+ renderProjectHealthCiWorkflow,
22
+ shouldFailHealth,
23
+ writeProjectHealthCiWorkflow
20
24
  };
package/dist/index.js CHANGED
@@ -1,3 +1,3 @@
1
- import "./chunk-PKJSI6IH.js";
1
+ import "./chunk-5RODH77L.js";
2
2
  import "./chunk-USOO77A5.js";
3
3
  import "./chunk-DI2PLOJ6.js";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  createProjectHealthReport
3
- } from "./chunk-DONMNPS7.js";
3
+ } from "./chunk-6YCFRZZI.js";
4
4
  import "./chunk-RSDCWAHD.js";
5
5
  import "./chunk-DI2PLOJ6.js";
6
6
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decantr/cli",
3
- "version": "1.9.0",
3
+ "version": "1.11.0",
4
4
  "description": "Decantr CLI - scaffold, audit, inspect Project Health, and maintain Decantr projects from the terminal",
5
5
  "author": "Decantr AI",
6
6
  "license": "MIT",
@@ -30,12 +30,12 @@
30
30
  "access": "public"
31
31
  },
32
32
  "dependencies": {
33
- "ajv": "^8.18.0",
33
+ "ajv": "^8.20.0",
34
34
  "@decantr/core": "1.0.6",
35
- "@decantr/essence-spec": "1.0.7",
36
35
  "@decantr/registry": "1.1.0",
37
- "@decantr/verifier": "1.1.0",
38
- "@decantr/telemetry": "0.1.2"
36
+ "@decantr/telemetry": "0.1.2",
37
+ "@decantr/verifier": "1.1.1",
38
+ "@decantr/essence-spec": "1.0.8"
39
39
  },
40
40
  "scripts": {
41
41
  "build": "tsup",
@@ -115,7 +115,7 @@ Read `.decantr/context/page-{name}-pack.md` for the most local compiled route co
115
115
  ### Validation
116
116
 
117
117
  Run `decantr check` to detect drift violations while editing and `decantr audit` to audit the whole project contract after implementation.
118
- Run `decantr health` for the broader Project Health view before handoff, pull requests, or CI. Use `decantr health --prompt <finding-id>` to generate a scoped remediation prompt for a specific issue, and `decantr studio` to inspect local drift, routes, findings, remediation, CI, and pack state in a localhost dashboard.
118
+ Run `decantr health` for the broader Project Health view before handoff, pull requests, or CI. Use `decantr health init-ci` to install the default GitHub Actions health gate, `decantr health --prompt <finding-id>` to generate a scoped remediation prompt for a specific issue, and `decantr studio` to inspect local drift, routes, findings, remediation, CI, and pack state in a localhost dashboard.
119
119
  Declared command palettes and hotkeys must be implemented, not merely acknowledged.
120
120
 
121
121
  ### Quick Commands
@@ -123,6 +123,7 @@ Declared command palettes and hotkeys must be implemented, not merely acknowledg
123
123
  ```bash
124
124
  decantr status # Project status overview
125
125
  decantr health # Local contract health report
126
+ decantr health init-ci # Install GitHub Actions health gate
126
127
  decantr studio # Local health dashboard
127
128
  decantr check # Detect drift violations
128
129
  decantr get pattern X # Fetch a pattern spec from registry
@@ -0,0 +1,62 @@
1
+ name: Decantr Project Health
2
+
3
+ on:
4
+ pull_request:
5
+ workflow_dispatch:
6
+ push:
7
+ branches:
8
+ - main
9
+
10
+ permissions:
11
+ contents: read
12
+
13
+ jobs:
14
+ health:
15
+ runs-on: ubuntu-latest
16
+ steps:
17
+ - uses: actions/checkout@v6
18
+
19
+ - uses: actions/setup-node@v6
20
+ with:
21
+ node-version: '22'
22
+
23
+ - name: Install dependencies
24
+ shell: bash
25
+ run: |
26
+ if [ -f pnpm-lock.yaml ]; then
27
+ corepack enable
28
+ pnpm install --frozen-lockfile
29
+ elif [ -f package-lock.json ]; then
30
+ npm ci
31
+ elif [ -f yarn.lock ]; then
32
+ corepack enable
33
+ yarn install --frozen-lockfile
34
+ elif [ -f package.json ]; then
35
+ npm install
36
+ else
37
+ echo "No package manifest found; skipping dependency install."
38
+ fi
39
+
40
+ - name: Generate Decantr health JSON
41
+ {{PROJECT_WORKING_DIRECTORY}} run: npx --yes {{CLI_PACKAGE}} health --json --output {{JSON_PATH}}
42
+
43
+ - name: Audit Decantr health
44
+ {{PROJECT_WORKING_DIRECTORY}} run: npx --yes {{CLI_PACKAGE}} health --ci --fail-on {{FAIL_ON}} --markdown --output {{REPORT_PATH}}
45
+
46
+ - name: Publish health summary
47
+ if: always()
48
+ {{PROJECT_WORKING_DIRECTORY}} shell: bash
49
+ run: |
50
+ if [ -f {{REPORT_PATH}} ]; then
51
+ cat {{REPORT_PATH}} >> "$GITHUB_STEP_SUMMARY"
52
+ fi
53
+
54
+ - name: Upload health artifacts
55
+ if: always()
56
+ uses: actions/upload-artifact@v6
57
+ with:
58
+ name: decantr-project-health
59
+ path: |
60
+ {{JSON_ARTIFACT_PATH}}
61
+ {{REPORT_ARTIFACT_PATH}}
62
+ if-no-files-found: ignore