@decantr/cli 2.9.2 → 2.9.4

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
@@ -21,9 +21,11 @@ Use `decantr setup` when you are unsure which path applies. It detects whether t
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
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
  ```
@@ -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,11 +176,11 @@ 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>`, include app-prefixed read targets such as `apps/web/DECANTR.md`, and use root-safe runtime commands such as `pnpm --dir apps/web build` 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
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
- `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:
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, and it distinguishes "attach another app" from the empty-workspace first attach. 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
 
180
185
  ```bash
181
186
  decantr workspace list
package/dist/bin.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import "./chunk-AXMGQ5IB.js";
2
+ import "./chunk-GDGZFGMK.js";
3
3
  import "./chunk-RXF7ZYGK.js";
4
- import "./chunk-R57DMFLF.js";
5
- import "./chunk-DX2UDORT.js";
6
- import "./chunk-34TZXWIF.js";
4
+ import "./chunk-VCUFZB45.js";
5
+ import "./chunk-FACL3NXU.js";
6
+ import "./chunk-WONPNSSI.js";
@@ -3,14 +3,14 @@ import {
3
3
  sendProjectHealthCiFailedTelemetry,
4
4
  sendProjectHealthPromptTelemetry,
5
5
  sendProjectHealthReportTelemetry
6
- } from "./chunk-34TZXWIF.js";
6
+ } from "./chunk-WONPNSSI.js";
7
7
 
8
8
  // src/commands/health.ts
9
9
  import { execFileSync } from "child_process";
10
10
  import { createHash } from "crypto";
11
11
  import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
12
12
  import { createRequire } from "module";
13
- import { dirname as dirname2, isAbsolute, join as join2, relative, resolve as resolve2 } from "path";
13
+ import { dirname as dirname2, isAbsolute as isAbsolute2, join as join2, relative, resolve as resolve2 } from "path";
14
14
  import { fileURLToPath } from "url";
15
15
  import {
16
16
  auditProject,
@@ -20,7 +20,7 @@ import {
20
20
 
21
21
  // src/workspace.ts
22
22
  import { existsSync, readdirSync, readFileSync } from "fs";
23
- import { dirname, join, resolve } from "path";
23
+ import { dirname, isAbsolute, join, resolve } from "path";
24
24
  function readPackageJson(dir) {
25
25
  const path = join(dir, "package.json");
26
26
  if (!existsSync(path)) return null;
@@ -92,7 +92,7 @@ function listWorkspaceAppCandidates(workspaceRoot) {
92
92
  function resolveWorkspaceInfo(cwd, projectArg) {
93
93
  const absoluteCwd = resolve(cwd);
94
94
  const workspaceRoot = findWorkspaceRoot(absoluteCwd) ?? absoluteCwd;
95
- const appRoot = projectArg ? resolve(absoluteCwd, projectArg) : absoluteCwd;
95
+ const appRoot = projectArg ? resolve(isAbsolute(projectArg) ? "/" : workspaceRoot, projectArg) : absoluteCwd;
96
96
  const appCandidates = listWorkspaceApps(workspaceRoot);
97
97
  const projectScope = workspaceRoot !== appRoot || appCandidates.length > 0 ? "workspace-app" : "single-app";
98
98
  const requiresProjectSelection = !projectArg && workspaceRoot === absoluteCwd && appCandidates.length > 0;
@@ -333,6 +333,9 @@ function contractAssertionApplies(assertion, metadata) {
333
333
  if (assertion.rule === "tokens-file-present" && metadata.adoptionMode === "contract-only") {
334
334
  return false;
335
335
  }
336
+ if (metadata.adoptionMode === "contract-only" && (assertion.rule === "pack-manifest-present" || assertion.rule === "review-pack-present")) {
337
+ return false;
338
+ }
336
339
  return true;
337
340
  }
338
341
  function slugify(value) {
@@ -380,20 +383,67 @@ function commandsForFinding(source) {
380
383
  return ["decantr audit", "decantr health"];
381
384
  }
382
385
  }
386
+ function detectPackageManager(root) {
387
+ const pkg = readJsonFile(join2(root, "package.json"));
388
+ const declared = pkg?.packageManager?.split("@")[0];
389
+ if (declared === "pnpm" || declared === "npm" || declared === "yarn" || declared === "bun") {
390
+ return declared;
391
+ }
392
+ if (existsSync2(join2(root, "pnpm-lock.yaml"))) return "pnpm";
393
+ if (existsSync2(join2(root, "package-lock.json"))) return "npm";
394
+ if (existsSync2(join2(root, "yarn.lock"))) return "yarn";
395
+ if (existsSync2(join2(root, "bun.lock")) || existsSync2(join2(root, "bun.lockb"))) return "bun";
396
+ return "unknown";
397
+ }
398
+ function buildCommandForProject(workspaceRoot, projectPath) {
399
+ const packageManager = detectPackageManager(workspaceRoot);
400
+ if (projectPath) {
401
+ switch (packageManager) {
402
+ case "pnpm":
403
+ return `pnpm --dir ${projectPath} build`;
404
+ case "yarn":
405
+ return `yarn --cwd ${projectPath} build`;
406
+ case "bun":
407
+ return `bun --cwd ${projectPath} run build`;
408
+ case "npm":
409
+ return `npm --prefix ${projectPath} run build`;
410
+ default:
411
+ return `cd ${projectPath} && npm run build`;
412
+ }
413
+ }
414
+ switch (packageManager) {
415
+ case "pnpm":
416
+ return "pnpm build";
417
+ case "yarn":
418
+ return "yarn build";
419
+ case "bun":
420
+ return "bun run build";
421
+ case "npm":
422
+ return "npm run build";
423
+ default:
424
+ return "npm run build";
425
+ }
426
+ }
383
427
  function commandContextForProject(projectRoot) {
384
428
  const workspaceInfo = resolveWorkspaceInfo(projectRoot);
385
- const relativeProjectPath = relative(workspaceInfo.workspaceRoot, projectRoot).replace(/\\/g, "/");
386
- const projectPath = relativeProjectPath && !relativeProjectPath.startsWith("..") && !isAbsolute(relativeProjectPath) ? relativeProjectPath : null;
429
+ const relativeProjectPath = relative(workspaceInfo.workspaceRoot, projectRoot).replace(
430
+ /\\/g,
431
+ "/"
432
+ );
433
+ const projectPath = relativeProjectPath && !relativeProjectPath.startsWith("..") && !isAbsolute2(relativeProjectPath) ? relativeProjectPath : null;
387
434
  const projectFlag = projectPath ? ` --project ${projectPath}` : "";
388
435
  const essencePath = projectPath ? `${projectPath}/decantr.essence.json` : "decantr.essence.json";
389
436
  return {
390
437
  projectPath,
438
+ buildCommand: buildCommandForProject(workspaceInfo.workspaceRoot, projectPath),
391
439
  compilePacksCommand: `decantr registry compile-packs ${essencePath} --write-context`,
392
440
  verifyCommand: `decantr verify${projectFlag}`,
393
- ciCommand: `decantr ci${projectFlag} --fail-on error`
441
+ ciCommand: `decantr ci${projectFlag} --fail-on error`,
442
+ promptCommand: (id) => `decantr health${projectFlag} --prompt ${id}`
394
443
  };
395
444
  }
396
445
  function rewriteHealthCommand(command, context) {
446
+ if (command === "npm run build") return context.buildCommand;
397
447
  let rewritten = command.replace(
398
448
  /decantr registry compile-packs decantr\.essence\.json --write-context/g,
399
449
  context.compilePacksCommand
@@ -403,8 +453,14 @@ function rewriteHealthCommand(command, context) {
403
453
  /^decantr init --existing\b/,
404
454
  `decantr init --project ${context.projectPath} --existing`
405
455
  );
406
- rewritten = rewritten.replace(/^decantr analyze\b/, `decantr analyze --project ${context.projectPath}`);
407
- rewritten = rewritten.replace(/^decantr check\b/, `decantr check --project ${context.projectPath}`);
456
+ rewritten = rewritten.replace(
457
+ /^decantr analyze\b/,
458
+ `decantr analyze --project ${context.projectPath}`
459
+ );
460
+ rewritten = rewritten.replace(
461
+ /^decantr check\b/,
462
+ `decantr check --project ${context.projectPath}`
463
+ );
408
464
  rewritten = rewritten.replace(/^decantr audit\b/, context.verifyCommand);
409
465
  rewritten = rewritten.replace(/^decantr health\b/, context.verifyCommand);
410
466
  return rewritten;
@@ -448,17 +504,25 @@ function scopeHealthFindingsToProject(projectRoot, findings) {
448
504
  message: finding.message,
449
505
  evidence: finding.evidence,
450
506
  suggestedFix,
451
- commands
507
+ commands,
508
+ projectPath: context.projectPath
452
509
  })
453
510
  }
454
511
  };
455
512
  });
456
513
  }
457
514
  function buildRemediationPrompt(input) {
515
+ const prefix = input.projectPath ? `${input.projectPath}/` : "";
516
+ const readTargets = [
517
+ `${prefix}DECANTR.md`,
518
+ `${prefix}decantr.essence.json`,
519
+ `${prefix}.decantr/context/scaffold-pack.md`,
520
+ `${prefix}.decantr/context/scaffold.md`
521
+ ];
458
522
  return [
459
523
  "You are fixing one Decantr Project Health finding in this local workspace.",
460
524
  "",
461
- "Read `DECANTR.md`, `decantr.essence.json`, and `.decantr/context/scaffold-pack.md` if they exist. For route or page work, read the matching page/section packs before editing.",
525
+ `Read project-scoped Decantr files if they exist: ${readTargets.map((target) => `\`${target}\``).join(", ")}. For route or page work, read the matching page/section packs before editing.`,
462
526
  "",
463
527
  `Finding: ${input.id}`,
464
528
  `Source: ${input.source}`,
@@ -509,6 +573,69 @@ function createHealthFinding(input) {
509
573
  remediation
510
574
  };
511
575
  }
576
+ function collectContractPackConsistencyFindings(projectRoot, essence, manifest) {
577
+ if (!essence || typeof essence !== "object") return [];
578
+ const record = essence;
579
+ const blueprint = record.blueprint;
580
+ if (!blueprint || typeof blueprint !== "object") return [];
581
+ const bp = blueprint;
582
+ const routes = bp.routes && typeof bp.routes === "object" && !Array.isArray(bp.routes) ? bp.routes : {};
583
+ const routeTargets = new Set(
584
+ Object.values(routes).filter(
585
+ (entry) => Boolean(entry) && typeof entry === "object"
586
+ ).map((entry) => `${String(entry.section ?? "")}/${String(entry.page ?? "")}`)
587
+ );
588
+ const pages = [];
589
+ for (const section of Array.isArray(bp.sections) ? bp.sections : []) {
590
+ if (!section || typeof section !== "object") continue;
591
+ const sectionRecord = section;
592
+ const sectionId = typeof sectionRecord.id === "string" ? sectionRecord.id : "unknown";
593
+ for (const page of Array.isArray(sectionRecord.pages) ? sectionRecord.pages : []) {
594
+ if (!page || typeof page !== "object") continue;
595
+ const pageRecord = page;
596
+ const pageId = typeof pageRecord.id === "string" ? pageRecord.id : "unknown";
597
+ const route = typeof pageRecord.route === "string" ? pageRecord.route : null;
598
+ pages.push({ section: sectionId, page: pageId, route });
599
+ }
600
+ }
601
+ const findings = [];
602
+ const routeLess = pages.filter(
603
+ (page) => !page.route && !routeTargets.has(`${page.section}/${page.page}`)
604
+ );
605
+ if (routeLess.length > 0) {
606
+ findings.push(
607
+ createHealthFinding({
608
+ source: "assertion",
609
+ category: "Contract Route Topology",
610
+ severity: "error",
611
+ message: "One or more blueprint pages have no route and cannot be addressed by task-time context.",
612
+ evidence: routeLess.slice(0, 8).map(
613
+ (page) => `${page.section}/${page.page} has no page.route or blueprint.routes entry`
614
+ ),
615
+ rule: "page-route-required",
616
+ suggestedFix: "Add a route for each page or rerun the add-page flow with a route-aware Decantr CLI.",
617
+ baseId: "page-route-required"
618
+ })
619
+ );
620
+ }
621
+ const pagePackCount = manifest && "pages" in manifest && Array.isArray(manifest.pages) ? manifest.pages.length : 0;
622
+ if (manifest && pages.length !== pagePackCount) {
623
+ const context = commandContextForProject(projectRoot);
624
+ findings.push(
625
+ createHealthFinding({
626
+ source: "pack",
627
+ category: "Generated Artifacts",
628
+ severity: "warn",
629
+ message: `Compiled page pack count (${pagePackCount}) does not match the contract page count (${pages.length}).`,
630
+ evidence: ["Page packs should be regenerated after adding, removing, or re-routing pages."],
631
+ rule: "page-pack-count-mismatch",
632
+ suggestedFix: context.compilePacksCommand,
633
+ baseId: "page-pack-count-mismatch"
634
+ })
635
+ );
636
+ }
637
+ return findings;
638
+ }
512
639
  function countFindings(findings) {
513
640
  return {
514
641
  errorCount: findings.filter((finding) => finding.severity === "error").length,
@@ -541,7 +668,7 @@ function isDuplicateFinding(existing, finding) {
541
668
  }
542
669
  function resolveOptionalPath(projectRoot, path) {
543
670
  if (!path) return void 0;
544
- return isAbsolute(path) ? path : resolve2(projectRoot, path);
671
+ return isAbsolute2(path) ? path : resolve2(projectRoot, path);
545
672
  }
546
673
  function hasProjectPlaywright(projectRoot) {
547
674
  try {
@@ -758,7 +885,7 @@ function parseDecantrCssTokenNames(projectRoot) {
758
885
  function collectDesignTokenEvidence(projectRoot, designTokensPath) {
759
886
  const resolved = resolveOptionalPath(projectRoot, designTokensPath);
760
887
  if (!resolved) return void 0;
761
- const sourceLabel = isAbsolute(designTokensPath ?? "") ? "<design-tokens>" : designTokensPath ?? "<design-tokens>";
888
+ const sourceLabel = isAbsolute2(designTokensPath ?? "") ? "<design-tokens>" : designTokensPath ?? "<design-tokens>";
762
889
  if (!existsSync2(resolved)) {
763
890
  return {
764
891
  source: sourceLabel,
@@ -1033,6 +1160,13 @@ async function createProjectHealthReport(projectRoot = process.cwd(), options =
1033
1160
  }
1034
1161
  const declaredRoutes = collectDeclaredRoutes(audit.essence);
1035
1162
  const manifest = audit.packManifest;
1163
+ for (const consistencyFinding of collectContractPackConsistencyFindings(
1164
+ projectRoot,
1165
+ audit.essence,
1166
+ manifest
1167
+ )) {
1168
+ if (!isDuplicateFinding(seen, consistencyFinding)) findings.push(consistencyFinding);
1169
+ }
1036
1170
  const browserVerification = await collectBrowserVerification(
1037
1171
  projectRoot,
1038
1172
  options,
@@ -1092,6 +1226,7 @@ function colorForStatus(status) {
1092
1226
  }
1093
1227
  function formatProjectHealthText(report) {
1094
1228
  const color = colorForStatus(report.status);
1229
+ const commandContext = commandContextForProject(report.projectRoot);
1095
1230
  const lines = [
1096
1231
  `${BOLD}Decantr Project Health${RESET}`,
1097
1232
  "",
@@ -1121,7 +1256,7 @@ function formatProjectHealthText(report) {
1121
1256
  if (finding.suggestedFix) {
1122
1257
  lines.push(` ${DIM}Fix: ${finding.suggestedFix}${RESET}`);
1123
1258
  }
1124
- lines.push(` ${DIM}Prompt: decantr health --prompt ${finding.id}${RESET}`);
1259
+ lines.push(` ${DIM}Prompt: ${commandContext.promptCommand(finding.id)}${RESET}`);
1125
1260
  }
1126
1261
  }
1127
1262
  lines.push("");
@@ -1130,6 +1265,7 @@ function formatProjectHealthText(report) {
1130
1265
  `;
1131
1266
  }
1132
1267
  function formatProjectHealthMarkdown(report) {
1268
+ const commandContext = commandContextForProject(report.projectRoot);
1133
1269
  const lines = [
1134
1270
  "# Decantr Project Health",
1135
1271
  "",
@@ -1158,7 +1294,7 @@ function formatProjectHealthMarkdown(report) {
1158
1294
  lines.push("- Evidence:");
1159
1295
  for (const evidence of finding.evidence) lines.push(` - ${evidence}`);
1160
1296
  }
1161
- lines.push(`- Prompt: \`decantr health --prompt ${finding.id}\``);
1297
+ lines.push(`- Prompt: \`${commandContext.promptCommand(finding.id)}\``);
1162
1298
  lines.push("");
1163
1299
  }
1164
1300
  }
@@ -1252,17 +1388,25 @@ async function cmdHealth(projectRoot = process.cwd(), options = {}) {
1252
1388
  }
1253
1389
  const format = resolveFormat(options);
1254
1390
  const failOn = options.failOn ?? "error";
1255
- const basePayload = options.evidence ? `${JSON.stringify(await createProjectEvidenceBundle(projectRoot, report, reportOptions), null, 2)}
1391
+ const evidenceBundle = options.evidence ? await createProjectEvidenceBundle(projectRoot, report, reportOptions) : null;
1392
+ const basePayload = options.evidence ? `${JSON.stringify(evidenceBundle, null, 2)}
1256
1393
  ` : format === "json" ? formatProjectHealthJson(report) : format === "markdown" ? formatProjectHealthMarkdown(report) : formatProjectHealthText(report);
1257
1394
  const payload = baselineComparison && !options.evidence && format === "text" ? `${basePayload}${formatBaselineComparisonText(baselineComparison)}` : basePayload;
1258
1395
  if (options.output) {
1259
- const outputPath = isAbsolute(options.output) ? options.output : join2(projectRoot, options.output);
1396
+ const outputPath = isAbsolute2(options.output) ? options.output : join2(projectRoot, options.output);
1260
1397
  mkdirSync(dirname2(outputPath), { recursive: true });
1261
1398
  writeFileSync(outputPath, payload, "utf-8");
1262
1399
  if (!options.ci) {
1263
1400
  console.log(
1264
1401
  `${GREEN}Wrote Decantr ${options.evidence ? "evidence bundle" : "health report"}:${RESET} ${options.output}`
1265
1402
  );
1403
+ if (options.browser && evidenceBundle?.browser?.status === "unavailable") {
1404
+ const reason = evidenceBundle.browser.findings[0] ?? "Playwright is not available to Decantr in this project.";
1405
+ console.log(`${YELLOW}Browser evidence unavailable:${RESET} ${reason}`);
1406
+ console.log(
1407
+ `${DIM}Static evidence was still written. Install Playwright or rerun without --browser if screenshots are not needed.${RESET}`
1408
+ );
1409
+ }
1266
1410
  }
1267
1411
  } else {
1268
1412
  process.stdout.write(payload);