@decantr/cli 2.9.1 → 2.9.2

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,7 +19,7 @@ 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
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`.
25
25
  Use `decantr init`, `decantr analyze`, `decantr check`, and `decantr health` as advanced primitives when you need direct control over one step.
@@ -138,7 +138,7 @@ decantr showcase verification --json
138
138
 
139
139
  `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
140
 
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`.
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`; if the root manifest has not pinned Decantr yet, it prints the exact install command first.
142
142
 
143
143
  `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
144
 
@@ -173,7 +173,7 @@ decantr export --to figma-tokens
173
173
 
174
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.
175
175
 
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`.
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`. 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
177
 
178
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:
179
179
 
package/dist/bin.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import "./chunk-6UDJDQPT.js";
2
+ import "./chunk-AXMGQ5IB.js";
3
3
  import "./chunk-RXF7ZYGK.js";
4
- import "./chunk-FKM4OQDF.js";
5
- import "./chunk-TMOCTDYY.js";
4
+ import "./chunk-R57DMFLF.js";
5
+ import "./chunk-DX2UDORT.js";
6
6
  import "./chunk-34TZXWIF.js";
@@ -21,15 +21,15 @@ import {
21
21
  formatWorkspaceHealthText,
22
22
  listWorkspaceCandidates,
23
23
  listWorkspaceProjects,
24
- resolveWorkspaceInfo,
25
24
  shouldFailWorkspaceHealth
26
- } from "./chunk-FKM4OQDF.js";
25
+ } from "./chunk-R57DMFLF.js";
27
26
  import {
28
27
  createProjectHealthReport,
29
28
  formatProjectHealthMarkdown,
30
29
  formatProjectHealthText,
30
+ resolveWorkspaceInfo,
31
31
  shouldFailHealth
32
- } from "./chunk-TMOCTDYY.js";
32
+ } from "./chunk-DX2UDORT.js";
33
33
  import {
34
34
  buildGuardRegistryContext,
35
35
  createDoctrineMap,
@@ -3667,6 +3667,12 @@ function detectPackageManager(root) {
3667
3667
  if (existsSync14(join16(root, "bun.lock")) || existsSync14(join16(root, "bun.lockb"))) return "bun";
3668
3668
  return "unknown";
3669
3669
  }
3670
+ function hasWorkspaceMarker(root) {
3671
+ const pkg = readJson(join16(root, "package.json"));
3672
+ return Boolean(
3673
+ existsSync14(join16(root, "pnpm-workspace.yaml")) || existsSync14(join16(root, "turbo.json")) || existsSync14(join16(root, "nx.json")) || pkg?.workspaces
3674
+ );
3675
+ }
3670
3676
  function installCommand(packageManager) {
3671
3677
  switch (packageManager) {
3672
3678
  case "pnpm":
@@ -3681,6 +3687,20 @@ function installCommand(packageManager) {
3681
3687
  return "npm install";
3682
3688
  }
3683
3689
  }
3690
+ function pinCliCommand(packageManager, root) {
3691
+ switch (packageManager) {
3692
+ case "pnpm":
3693
+ return hasWorkspaceMarker(root) ? "pnpm add -D -w @decantr/cli" : "pnpm add -D @decantr/cli";
3694
+ case "yarn":
3695
+ return "yarn add -D @decantr/cli";
3696
+ case "bun":
3697
+ return "bun add -d @decantr/cli";
3698
+ case "npm":
3699
+ return "npm install -D @decantr/cli";
3700
+ default:
3701
+ return "npm install -D @decantr/cli";
3702
+ }
3703
+ }
3684
3704
  function decantrCommand(packageManager) {
3685
3705
  switch (packageManager) {
3686
3706
  case "pnpm":
@@ -3837,7 +3857,7 @@ function writeCiInit(root, options) {
3837
3857
  const provider = options.provider ?? "github";
3838
3858
  if (!localCliPinned(outputRoot)) {
3839
3859
  console.log(
3840
- `${DIM3}No @decantr/cli dependency was found in the workspace root package.json. The generated command still uses the local package-manager binary path; pin @decantr/cli in devDependencies before relying on CI.${RESET3}`
3860
+ `${DIM3}No @decantr/cli dependency was found in the workspace root package.json. Before relying on CI, pin it with: ${pinCliCommand(packageManager, outputRoot)}${RESET3}`
3841
3861
  );
3842
3862
  }
3843
3863
  if (provider === "generic") {
@@ -7944,18 +7964,13 @@ async function printRegistryIntelligenceSummary(namespace, jsonOutput = false) {
7944
7964
  }
7945
7965
  }
7946
7966
  async function printHostedExecutionPackBundle(essencePath, namespace, jsonOutput = false, writeContext = false) {
7947
- const client = getPublicAPIClient();
7948
- const resolvedPath = essencePath ? resolveUserPath(essencePath) : join31(process.cwd(), "decantr.essence.json");
7949
- if (!existsSync29(resolvedPath)) {
7950
- throw new Error(`Essence file not found at ${resolvedPath}`);
7951
- }
7952
- const essence = JSON.parse(readFileSync22(resolvedPath, "utf-8"));
7953
- const bundle = await client.compileExecutionPacks(essence, namespace ? { namespace } : void 0);
7954
- const contextDir = join31(dirname5(resolvedPath), ".decantr", "context");
7967
+ const { resolvedPath, bundle, contextDir } = await compileHostedExecutionPackBundle(
7968
+ essencePath,
7969
+ namespace
7970
+ );
7955
7971
  let writtenContextPaths = [];
7956
7972
  if (writeContext) {
7957
- mkdirSync16(contextDir, { recursive: true });
7958
- const written = writeExecutionPackBundleArtifacts(
7973
+ const written = writeHostedExecutionPackContextArtifacts(
7959
7974
  contextDir,
7960
7975
  bundle
7961
7976
  );
@@ -7990,6 +8005,21 @@ async function printHostedExecutionPackBundle(essencePath, namespace, jsonOutput
7990
8005
  console.log(` ${cyan3(route.path)} -> ${pageLabel} [${patterns}]`);
7991
8006
  }
7992
8007
  }
8008
+ async function compileHostedExecutionPackBundle(essencePath, namespace) {
8009
+ const client = getPublicAPIClient();
8010
+ const resolvedPath = essencePath ? resolveUserPath(essencePath) : join31(process.cwd(), "decantr.essence.json");
8011
+ if (!existsSync29(resolvedPath)) {
8012
+ throw new Error(`Essence file not found at ${resolvedPath}`);
8013
+ }
8014
+ const essence = JSON.parse(readFileSync22(resolvedPath, "utf-8"));
8015
+ const bundle = await client.compileExecutionPacks(essence, namespace ? { namespace } : void 0);
8016
+ const contextDir = join31(dirname5(resolvedPath), ".decantr", "context");
8017
+ return { resolvedPath, bundle, contextDir };
8018
+ }
8019
+ function writeHostedExecutionPackContextArtifacts(contextDir, bundle) {
8020
+ mkdirSync16(contextDir, { recursive: true });
8021
+ return writeExecutionPackBundleArtifacts(contextDir, bundle);
8022
+ }
7993
8023
  function resolvePagePackIdForRoute(essencePath, route) {
7994
8024
  if (!existsSync29(essencePath)) {
7995
8025
  throw new Error(`Essence file not found at ${essencePath}`);
@@ -9761,6 +9791,10 @@ function withoutWorkflowOnlyFlags(args) {
9761
9791
  function withProject(command, projectArg) {
9762
9792
  return projectArg ? `${command} --project ${projectArg}` : command;
9763
9793
  }
9794
+ function compilePacksCommandForProject(projectArg) {
9795
+ const essencePath = projectArg ? `${projectArg}/decantr.essence.json` : "decantr.essence.json";
9796
+ return `decantr registry compile-packs ${essencePath} --write-context`;
9797
+ }
9764
9798
  function firstWorkspaceCandidate(workspaceInfo) {
9765
9799
  return workspaceInfo.appCandidates[0] ?? "apps/web";
9766
9800
  }
@@ -9898,6 +9932,7 @@ async function cmdAdoptWorkflow(args) {
9898
9932
  const runBrowser = flagBoolean(flags, "browser") || Boolean(baseUrl);
9899
9933
  const evidence = flagBoolean(flags, "evidence") || runBrowser;
9900
9934
  const saveBaseline = flagBoolean(flags, "baseline", true) || flagBoolean(flags, "save-baseline");
9935
+ const hydratePacks = flagBoolean(flags, "packs", true) && !flagBoolean(flags, "skip-packs") && !flagBoolean(flags, "offline") && process.env.DECANTR_OFFLINE !== "true";
9901
9936
  const initCi = flagBoolean(flags, "ci") || flagBoolean(flags, "init-ci");
9902
9937
  const assistantBridge = flagString(flags, "assistant-bridge");
9903
9938
  const hasEssence = existsSync29(join31(projectRoot, "decantr.essence.json"));
@@ -9906,6 +9941,9 @@ async function cmdAdoptWorkflow(args) {
9906
9941
  "analyze current app and write .decantr/brownfield intelligence",
9907
9942
  `init --existing ${proposalFlag} as contract-only Brownfield`
9908
9943
  ];
9944
+ if (hydratePacks) {
9945
+ steps.push("hydrate hosted execution packs into the app context");
9946
+ }
9909
9947
  if (runVerify) {
9910
9948
  steps.push(
9911
9949
  runBrowser ? "verify with Project Health, browser evidence, visual manifest, and baseline" : "verify with Project Health and baseline"
@@ -9939,8 +9977,35 @@ async function cmdAdoptWorkflow(args) {
9939
9977
  telemetry: flagBoolean(flags, "telemetry")
9940
9978
  });
9941
9979
  if (process.exitCode && process.exitCode !== 0) return;
9980
+ if (hydratePacks) {
9981
+ try {
9982
+ const { bundle, contextDir } = await compileHostedExecutionPackBundle(
9983
+ join31(projectRoot, "decantr.essence.json")
9984
+ );
9985
+ const written = writeHostedExecutionPackContextArtifacts(
9986
+ contextDir,
9987
+ bundle
9988
+ );
9989
+ console.log(
9990
+ success3(
9991
+ `Hydrated Decantr execution packs (${written.paths.length} files) into ${contextDir}.`
9992
+ )
9993
+ );
9994
+ } catch (e) {
9995
+ console.log(
9996
+ `${YELLOW10}Pack hydration skipped:${RESET16} ${e.message}`
9997
+ );
9998
+ console.log(
9999
+ dim3(
10000
+ `Run ${compilePacksCommandForProject(projectArg)} after adoption if you want hosted page/review packs.`
10001
+ )
10002
+ );
10003
+ }
10004
+ } else if (flagBoolean(flags, "offline") || process.env.DECANTR_OFFLINE === "true") {
10005
+ console.log(dim3("Skipping hosted pack hydration in offline mode."));
10006
+ }
9942
10007
  if (runVerify) {
9943
- const { cmdHealth } = await import("./health-Q7XF3I5Z.js");
10008
+ const { cmdHealth } = await import("./health-LTDSTNOV.js");
9944
10009
  await cmdHealth(projectRoot, {
9945
10010
  browser: runBrowser,
9946
10011
  browserBaseUrl: baseUrl,
@@ -9981,7 +10046,7 @@ async function cmdVerifyWorkflow(args) {
9981
10046
  return;
9982
10047
  }
9983
10048
  if (workspaceMode) {
9984
- const { cmdWorkspace } = await import("./workspace-JA2RZI6V.js");
10049
+ const { cmdWorkspace } = await import("./workspace-53EIHUXB.js");
9985
10050
  await cmdWorkspace(process.cwd(), ["workspace", "health", ...withoutWorkflowOnlyFlags(args)]);
9986
10051
  return;
9987
10052
  }
@@ -10024,7 +10089,7 @@ async function cmdVerifyWorkflow(args) {
10024
10089
  process.exitCode = void 0;
10025
10090
  }
10026
10091
  }
10027
- const { cmdHealth, parseHealthArgs } = await import("./health-Q7XF3I5Z.js");
10092
+ const { cmdHealth, parseHealthArgs } = await import("./health-LTDSTNOV.js");
10028
10093
  await cmdHealth(workspaceInfo.appRoot, parseHealthArgs(healthArgs));
10029
10094
  if (localPatterns) {
10030
10095
  const validation = validateLocalLaw(workspaceInfo.appRoot);
@@ -10090,6 +10155,7 @@ async function cmdTaskWorkflow(args) {
10090
10155
  const { flags, positional } = parseLooseArgs(args);
10091
10156
  const workspaceInfo = resolveWorkflowProject(flags, "task");
10092
10157
  if (!workspaceInfo) return;
10158
+ const projectArg = flagString(flags, "project");
10093
10159
  const routeInput = positional[0];
10094
10160
  if (!routeInput) {
10095
10161
  console.error(
@@ -10158,7 +10224,7 @@ async function cmdTaskWorkflow(args) {
10158
10224
  localLaw,
10159
10225
  changedFiles: currentChangedFiles,
10160
10226
  changedRoutes,
10161
- verifyCommand: "decantr verify --brownfield --local-patterns"
10227
+ verifyCommand: withProject("decantr verify --brownfield --local-patterns", projectArg)
10162
10228
  };
10163
10229
  if (flagBoolean(flags, "json")) {
10164
10230
  console.log(JSON.stringify(context, null, 2));
@@ -10199,7 +10265,7 @@ async function cmdTaskWorkflow(args) {
10199
10265
  console.log("");
10200
10266
  console.log(`${BOLD9}Project-owned local law:${RESET16}`);
10201
10267
  console.log(
10202
- ` ${YELLOW10}Not codified yet.${RESET16} Run ${cyan3("decantr codify --from-audit")} after adoption.`
10268
+ ` ${YELLOW10}Not codified yet.${RESET16} Run ${cyan3(withProject("decantr codify --from-audit", projectArg))} after adoption.`
10203
10269
  );
10204
10270
  }
10205
10271
  if (context.changedFiles.length > 0) {
@@ -10313,7 +10379,7 @@ ${BOLD9}decantr${RESET16} \u2014 Design intelligence for AI-generated UI
10313
10379
  ${BOLD9}Usage:${RESET16}
10314
10380
  decantr setup [--project <path>]
10315
10381
  decantr new <name> [--blueprint=X] [--archetype=X] [--theme=X] [--workflow=greenfield] [--adoption=decantr-css] [--telemetry]
10316
- decantr adopt [--project <path>] [--base-url <url>] [--evidence] [--ci] [--yes]
10382
+ decantr adopt [--project <path>] [--base-url <url>] [--evidence] [--ci] [--no-packs] [--yes]
10317
10383
  decantr task <route> ["task summary"] [--project <path>] [--since origin/main] [--json]
10318
10384
  decantr verify [--project <path>] [--brownfield] [--local-patterns] [health options]
10319
10385
  decantr ci [--project <path>] [--workspace] [--fail-on error|warn|none]
@@ -10673,11 +10739,11 @@ ${BOLD9}Examples:${RESET16}
10673
10739
  }
10674
10740
  function cmdAdoptHelp() {
10675
10741
  console.log(`
10676
- ${BOLD9}decantr adopt${RESET16} \u2014 Brownfield one-liner: analyze, attach, verify, and show the operating loop
10742
+ ${BOLD9}decantr adopt${RESET16} \u2014 Brownfield one-liner: analyze, attach, hydrate packs, verify, and show the operating loop
10677
10743
 
10678
10744
  ${BOLD9}Usage:${RESET16}
10679
- decantr adopt [--project <path>] [--yes] [--dry-run]
10680
- decantr adopt [--project <path>] --base-url <url> [--evidence] [--ci] [--yes]
10745
+ decantr adopt [--project <path>] [--yes] [--dry-run] [--no-packs]
10746
+ decantr adopt [--project <path>] --base-url <url> [--evidence] [--ci] [--yes] [--no-packs]
10681
10747
 
10682
10748
  ${BOLD9}Options:${RESET16}
10683
10749
  --project App path inside a workspace/monorepo
@@ -10688,6 +10754,7 @@ ${BOLD9}Options:${RESET16}
10688
10754
  --baseline Save a health baseline (default)
10689
10755
  --no-baseline Skip baseline save
10690
10756
  --no-verify Skip the verification step
10757
+ --no-packs Skip hosted execution-pack hydration
10691
10758
  --ci, --init-ci Install the Decantr CI gate after adoption
10692
10759
  --telemetry Opt this project into privacy-filtered CLI product telemetry
10693
10760
  --merge-proposal Merge the observed proposal into an existing essence
@@ -10993,7 +11060,7 @@ async function main() {
10993
11060
  cmdHealthHelp();
10994
11061
  break;
10995
11062
  }
10996
- const { cmdHealth, parseHealthArgs } = await import("./health-Q7XF3I5Z.js");
11063
+ const { cmdHealth, parseHealthArgs } = await import("./health-LTDSTNOV.js");
10997
11064
  await cmdHealth(process.cwd(), parseHealthArgs(args));
10998
11065
  } catch (e) {
10999
11066
  console.error(error3(e.message));
@@ -11021,7 +11088,7 @@ async function main() {
11021
11088
  cmdStudioHelp();
11022
11089
  break;
11023
11090
  }
11024
- const { cmdStudio, parseStudioArgs } = await import("./studio-EDQMI6JE.js");
11091
+ const { cmdStudio, parseStudioArgs } = await import("./studio-7E2LJS3A.js");
11025
11092
  await cmdStudio(process.cwd(), parseStudioArgs(args));
11026
11093
  } catch (e) {
11027
11094
  console.error(error3(e.message));
@@ -11035,7 +11102,7 @@ async function main() {
11035
11102
  cmdWorkspaceHelp();
11036
11103
  break;
11037
11104
  }
11038
- const { cmdWorkspace } = await import("./workspace-JA2RZI6V.js");
11105
+ const { cmdWorkspace } = await import("./workspace-53EIHUXB.js");
11039
11106
  await cmdWorkspace(process.cwd(), args);
11040
11107
  } catch (e) {
11041
11108
  console.error(error3(e.message));
@@ -8,15 +8,105 @@ import {
8
8
  // src/commands/health.ts
9
9
  import { execFileSync } from "child_process";
10
10
  import { createHash } from "crypto";
11
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
11
+ import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
12
12
  import { createRequire } from "module";
13
- import { dirname, isAbsolute, join, resolve } from "path";
13
+ import { dirname as dirname2, isAbsolute, join as join2, relative, resolve as resolve2 } from "path";
14
14
  import { fileURLToPath } from "url";
15
15
  import {
16
16
  auditProject,
17
17
  createContractAssertions,
18
18
  createEvidenceBundle
19
19
  } from "@decantr/verifier";
20
+
21
+ // src/workspace.ts
22
+ import { existsSync, readdirSync, readFileSync } from "fs";
23
+ import { dirname, join, resolve } from "path";
24
+ function readPackageJson(dir) {
25
+ const path = join(dir, "package.json");
26
+ if (!existsSync(path)) return null;
27
+ try {
28
+ return JSON.parse(readFileSync(path, "utf-8"));
29
+ } catch {
30
+ return null;
31
+ }
32
+ }
33
+ function hasWorkspaceMarker(dir) {
34
+ if (existsSync(join(dir, "pnpm-workspace.yaml")) || existsSync(join(dir, "turbo.json")) || existsSync(join(dir, "nx.json"))) {
35
+ return true;
36
+ }
37
+ const pkg = readPackageJson(dir);
38
+ return Boolean(pkg?.workspaces);
39
+ }
40
+ function findWorkspaceRoot(startDir) {
41
+ let current = resolve(startDir);
42
+ while (true) {
43
+ if (hasWorkspaceMarker(current)) return current;
44
+ const parent = dirname(current);
45
+ if (parent === current) return null;
46
+ current = parent;
47
+ }
48
+ }
49
+ function looksLikeApp(dir, options = {}) {
50
+ const allowSourceDirs = options.allowSourceDirs ?? true;
51
+ const allowPackageDeps = options.allowPackageDeps ?? true;
52
+ const pkg = readPackageJson(dir);
53
+ const deps = { ...pkg?.dependencies, ...pkg?.devDependencies };
54
+ const hasFrontendDependency = Boolean(
55
+ deps.react || deps["react-dom"] || deps.next || deps.vue || deps.svelte || deps["@angular/core"] || deps.astro || deps.nuxt
56
+ );
57
+ const hasServerOnlyDependency = Boolean(
58
+ deps.hono || deps.express || deps.fastify || deps.koa || deps["@hapi/hapi"]
59
+ );
60
+ 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"))) {
61
+ return true;
62
+ }
63
+ if (allowSourceDirs && (existsSync(join(dir, "src")) || existsSync(join(dir, "app")) || existsSync(join(dir, "pages")))) {
64
+ if (hasFrontendDependency) return true;
65
+ if (hasServerOnlyDependency) return false;
66
+ return true;
67
+ }
68
+ if (!allowPackageDeps) return false;
69
+ return hasFrontendDependency;
70
+ }
71
+ function listWorkspaceApps(workspaceRoot) {
72
+ const candidates = [];
73
+ for (const base of ["apps", "packages"]) {
74
+ const baseDir = join(workspaceRoot, base);
75
+ if (!existsSync(baseDir)) continue;
76
+ for (const entry of readdirSync(baseDir, { withFileTypes: true })) {
77
+ if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
78
+ const candidate = join(baseDir, entry.name);
79
+ if (looksLikeApp(candidate, {
80
+ allowSourceDirs: base === "apps",
81
+ allowPackageDeps: base === "apps"
82
+ })) {
83
+ candidates.push(`${base}/${entry.name}`);
84
+ }
85
+ }
86
+ }
87
+ return candidates.sort();
88
+ }
89
+ function listWorkspaceAppCandidates(workspaceRoot) {
90
+ return listWorkspaceApps(resolve(workspaceRoot));
91
+ }
92
+ function resolveWorkspaceInfo(cwd, projectArg) {
93
+ const absoluteCwd = resolve(cwd);
94
+ const workspaceRoot = findWorkspaceRoot(absoluteCwd) ?? absoluteCwd;
95
+ const appRoot = projectArg ? resolve(absoluteCwd, projectArg) : absoluteCwd;
96
+ const appCandidates = listWorkspaceApps(workspaceRoot);
97
+ const projectScope = workspaceRoot !== appRoot || appCandidates.length > 0 ? "workspace-app" : "single-app";
98
+ const requiresProjectSelection = !projectArg && workspaceRoot === absoluteCwd && appCandidates.length > 0;
99
+ return {
100
+ cwd: absoluteCwd,
101
+ workspaceRoot,
102
+ appRoot,
103
+ projectScope,
104
+ appCandidates,
105
+ requiresProjectSelection
106
+ };
107
+ }
108
+
109
+ // src/commands/health.ts
20
110
  var BOLD = "\x1B[1m";
21
111
  var DIM = "\x1B[2m";
22
112
  var RESET = "\x1B[0m";
@@ -29,14 +119,14 @@ var DEFAULT_HEALTH_CI_WORKFLOW_PATH = ".github/workflows/decantr-health.yml";
29
119
  var DEFAULT_HEALTH_CI_REPORT_PATH = "decantr-health.md";
30
120
  var DEFAULT_HEALTH_CI_JSON_PATH = "decantr-health.json";
31
121
  var DEFAULT_HEALTH_CI_CLI_VERSION = "latest";
32
- var __dirname = dirname(fileURLToPath(import.meta.url));
122
+ var __dirname = dirname2(fileURLToPath(import.meta.url));
33
123
  function readProjectMetadata(projectRoot) {
34
- const projectJsonPath = join(projectRoot, ".decantr", "project.json");
35
- if (!existsSync(projectJsonPath)) {
124
+ const projectJsonPath = join2(projectRoot, ".decantr", "project.json");
125
+ if (!existsSync2(projectJsonPath)) {
36
126
  return { workflowMode: null, adoptionMode: null, autoBrownfield: false };
37
127
  }
38
128
  try {
39
- const data = JSON.parse(readFileSync(projectJsonPath, "utf-8"));
129
+ const data = JSON.parse(readFileSync2(projectJsonPath, "utf-8"));
40
130
  const workflowMode = typeof data.initialized?.workflowMode === "string" ? data.initialized.workflowMode : null;
41
131
  const adoptionMode = typeof data.initialized?.adoptionMode === "string" ? data.initialized.adoptionMode : null;
42
132
  return {
@@ -49,12 +139,12 @@ function readProjectMetadata(projectRoot) {
49
139
  }
50
140
  }
51
141
  function loadHealthTemplate(name) {
52
- const fromDist = join(__dirname, "..", "src", "templates", name);
53
- if (existsSync(fromDist)) return readFileSync(fromDist, "utf-8");
54
- const fromSrc = join(__dirname, "..", "templates", name);
55
- if (existsSync(fromSrc)) return readFileSync(fromSrc, "utf-8");
56
- const fromCommandSrc = join(__dirname, "..", "..", "templates", name);
57
- if (existsSync(fromCommandSrc)) return readFileSync(fromCommandSrc, "utf-8");
142
+ const fromDist = join2(__dirname, "..", "src", "templates", name);
143
+ if (existsSync2(fromDist)) return readFileSync2(fromDist, "utf-8");
144
+ const fromSrc = join2(__dirname, "..", "templates", name);
145
+ if (existsSync2(fromSrc)) return readFileSync2(fromSrc, "utf-8");
146
+ const fromCommandSrc = join2(__dirname, "..", "..", "templates", name);
147
+ if (existsSync2(fromCommandSrc)) return readFileSync2(fromCommandSrc, "utf-8");
58
148
  throw new Error(`Template not found: ${name}`);
59
149
  }
60
150
  function renderTemplate(template, vars) {
@@ -149,14 +239,14 @@ function writeProjectHealthCiWorkflow(projectRoot, options = {}) {
149
239
  const workflowRelativePath = validateWorkflowPath(
150
240
  options.workflowPath || DEFAULT_HEALTH_CI_WORKFLOW_PATH
151
241
  );
152
- const workflowPath = join(projectRoot, workflowRelativePath);
153
- const alreadyExists = existsSync(workflowPath);
242
+ const workflowPath = join2(projectRoot, workflowRelativePath);
243
+ const alreadyExists = existsSync2(workflowPath);
154
244
  if (alreadyExists && !options.force) {
155
245
  throw new Error(
156
246
  `${workflowRelativePath} already exists. Re-run with --force to replace it, or use --workflow-path <file>.`
157
247
  );
158
248
  }
159
- mkdirSync(dirname(workflowPath), { recursive: true });
249
+ mkdirSync(dirname2(workflowPath), { recursive: true });
160
250
  writeFileSync(workflowPath, renderProjectHealthCiWorkflow(options), "utf-8");
161
251
  const projectPath = options.workspace ? void 0 : validateProjectPath(options.projectPath);
162
252
  const result = {
@@ -249,17 +339,17 @@ function slugify(value) {
249
339
  return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80);
250
340
  }
251
341
  function hashFile(path) {
252
- if (!existsSync(path)) return null;
342
+ if (!existsSync2(path)) return null;
253
343
  try {
254
- return createHash("sha256").update(readFileSync(path)).digest("hex");
344
+ return createHash("sha256").update(readFileSync2(path)).digest("hex");
255
345
  } catch {
256
346
  return null;
257
347
  }
258
348
  }
259
349
  function readJsonFile(path) {
260
- if (!existsSync(path)) return null;
350
+ if (!existsSync2(path)) return null;
261
351
  try {
262
- return JSON.parse(readFileSync(path, "utf-8"));
352
+ return JSON.parse(readFileSync2(path, "utf-8"));
263
353
  } catch {
264
354
  return null;
265
355
  }
@@ -290,6 +380,80 @@ function commandsForFinding(source) {
290
380
  return ["decantr audit", "decantr health"];
291
381
  }
292
382
  }
383
+ function commandContextForProject(projectRoot) {
384
+ const workspaceInfo = resolveWorkspaceInfo(projectRoot);
385
+ const relativeProjectPath = relative(workspaceInfo.workspaceRoot, projectRoot).replace(/\\/g, "/");
386
+ const projectPath = relativeProjectPath && !relativeProjectPath.startsWith("..") && !isAbsolute(relativeProjectPath) ? relativeProjectPath : null;
387
+ const projectFlag = projectPath ? ` --project ${projectPath}` : "";
388
+ const essencePath = projectPath ? `${projectPath}/decantr.essence.json` : "decantr.essence.json";
389
+ return {
390
+ projectPath,
391
+ compilePacksCommand: `decantr registry compile-packs ${essencePath} --write-context`,
392
+ verifyCommand: `decantr verify${projectFlag}`,
393
+ ciCommand: `decantr ci${projectFlag} --fail-on error`
394
+ };
395
+ }
396
+ function rewriteHealthCommand(command, context) {
397
+ let rewritten = command.replace(
398
+ /decantr registry compile-packs decantr\.essence\.json --write-context/g,
399
+ context.compilePacksCommand
400
+ );
401
+ if (!context.projectPath) return rewritten;
402
+ rewritten = rewritten.replace(
403
+ /^decantr init --existing\b/,
404
+ `decantr init --project ${context.projectPath} --existing`
405
+ );
406
+ rewritten = rewritten.replace(/^decantr analyze\b/, `decantr analyze --project ${context.projectPath}`);
407
+ rewritten = rewritten.replace(/^decantr check\b/, `decantr check --project ${context.projectPath}`);
408
+ rewritten = rewritten.replace(/^decantr audit\b/, context.verifyCommand);
409
+ rewritten = rewritten.replace(/^decantr health\b/, context.verifyCommand);
410
+ return rewritten;
411
+ }
412
+ function rewriteSuggestedFixForProject(suggestedFix, context) {
413
+ if (!suggestedFix) return suggestedFix;
414
+ return suggestedFix.replace(
415
+ /decantr registry compile-packs decantr\.essence\.json --write-context/g,
416
+ context.compilePacksCommand
417
+ );
418
+ }
419
+ function commandsForProjectFinding(finding, context) {
420
+ const isPackHydrationFinding = finding.source === "pack" || /pack-manifest|review-pack|compile-packs/i.test(
421
+ `${finding.id} ${finding.rule ?? ""} ${finding.suggestedFix ?? ""}`
422
+ );
423
+ if (isPackHydrationFinding) {
424
+ return [context.compilePacksCommand, context.verifyCommand];
425
+ }
426
+ return [
427
+ ...new Set(
428
+ finding.remediation.commands.map((command) => rewriteHealthCommand(command, context))
429
+ )
430
+ ];
431
+ }
432
+ function scopeHealthFindingsToProject(projectRoot, findings) {
433
+ const context = commandContextForProject(projectRoot);
434
+ return findings.map((finding) => {
435
+ const suggestedFix = rewriteSuggestedFixForProject(finding.suggestedFix, context);
436
+ const commands = commandsForProjectFinding(finding, context);
437
+ return {
438
+ ...finding,
439
+ suggestedFix,
440
+ remediation: {
441
+ summary: suggestedFix || finding.remediation.summary,
442
+ commands,
443
+ prompt: buildRemediationPrompt({
444
+ id: finding.id,
445
+ source: finding.source,
446
+ category: finding.category,
447
+ severity: finding.severity,
448
+ message: finding.message,
449
+ evidence: finding.evidence,
450
+ suggestedFix,
451
+ commands
452
+ })
453
+ }
454
+ };
455
+ });
456
+ }
293
457
  function buildRemediationPrompt(input) {
294
458
  return [
295
459
  "You are fixing one Decantr Project Health finding in this local workspace.",
@@ -377,16 +541,16 @@ function isDuplicateFinding(existing, finding) {
377
541
  }
378
542
  function resolveOptionalPath(projectRoot, path) {
379
543
  if (!path) return void 0;
380
- return isAbsolute(path) ? path : resolve(projectRoot, path);
544
+ return isAbsolute(path) ? path : resolve2(projectRoot, path);
381
545
  }
382
546
  function hasProjectPlaywright(projectRoot) {
383
547
  try {
384
- const requireFromProject = createRequire(join(projectRoot, "package.json"));
548
+ const requireFromProject = createRequire(join2(projectRoot, "package.json"));
385
549
  requireFromProject.resolve("playwright");
386
550
  return true;
387
551
  } catch {
388
552
  try {
389
- const requireFromProject = createRequire(join(projectRoot, "package.json"));
553
+ const requireFromProject = createRequire(join2(projectRoot, "package.json"));
390
554
  requireFromProject.resolve("@playwright/test");
391
555
  return true;
392
556
  } catch {
@@ -395,7 +559,7 @@ function hasProjectPlaywright(projectRoot) {
395
559
  }
396
560
  }
397
561
  function loadProjectPlaywright(projectRoot) {
398
- const requireFromProject = createRequire(join(projectRoot, "package.json"));
562
+ const requireFromProject = createRequire(join2(projectRoot, "package.json"));
399
563
  for (const packageName of ["playwright", "@playwright/test"]) {
400
564
  try {
401
565
  const loaded = requireFromProject(packageName);
@@ -485,7 +649,7 @@ async function collectBrowserVerification(projectRoot, options, declaredRoutes)
485
649
  const screenshots = [];
486
650
  const browserFindings = [];
487
651
  const visualRoutes = [];
488
- const screenshotDir = join(projectRoot, ".decantr", "evidence", "screenshots");
652
+ const screenshotDir = join2(projectRoot, ".decantr", "evidence", "screenshots");
489
653
  mkdirSync(screenshotDir, { recursive: true });
490
654
  let browser = null;
491
655
  try {
@@ -496,7 +660,7 @@ async function collectBrowserVerification(projectRoot, options, declaredRoutes)
496
660
  const relativePath = browserScreenshotRelativePath(route);
497
661
  try {
498
662
  await page.goto(url, { waitUntil: "networkidle", timeout: 15e3 });
499
- const absoluteScreenshotPath = join(projectRoot, relativePath);
663
+ const absoluteScreenshotPath = join2(projectRoot, relativePath);
500
664
  await page.screenshot({ path: absoluteScreenshotPath, fullPage: true });
501
665
  screenshots.push(relativePath);
502
666
  visualRoutes.push({
@@ -531,8 +695,8 @@ async function collectBrowserVerification(projectRoot, options, declaredRoutes)
531
695
  baseUrl: options.browserBaseUrl,
532
696
  routes: visualRoutes
533
697
  };
534
- const visualManifestPath = join(projectRoot, ".decantr", "evidence", "visual-manifest.json");
535
- mkdirSync(dirname(visualManifestPath), { recursive: true });
698
+ const visualManifestPath = join2(projectRoot, ".decantr", "evidence", "visual-manifest.json");
699
+ mkdirSync(dirname2(visualManifestPath), { recursive: true });
536
700
  writeFileSync(visualManifestPath, JSON.stringify(visualManifest, null, 2) + "\n", "utf-8");
537
701
  if (browserFindings.length > 0) {
538
702
  const finding = createHealthFinding({
@@ -582,9 +746,9 @@ function flattenDesignTokenKeys(value, prefix = "") {
582
746
  return keys;
583
747
  }
584
748
  function parseDecantrCssTokenNames(projectRoot) {
585
- const tokensPath = join(projectRoot, "src", "styles", "tokens.css");
586
- if (!existsSync(tokensPath)) return [];
587
- const css = readFileSync(tokensPath, "utf-8");
749
+ const tokensPath = join2(projectRoot, "src", "styles", "tokens.css");
750
+ if (!existsSync2(tokensPath)) return [];
751
+ const css = readFileSync2(tokensPath, "utf-8");
588
752
  const names = /* @__PURE__ */ new Set();
589
753
  for (const match of css.matchAll(/(--d-[\w-]+)\s*:/g)) {
590
754
  names.add(match[1]);
@@ -595,7 +759,7 @@ function collectDesignTokenEvidence(projectRoot, designTokensPath) {
595
759
  const resolved = resolveOptionalPath(projectRoot, designTokensPath);
596
760
  if (!resolved) return void 0;
597
761
  const sourceLabel = isAbsolute(designTokensPath ?? "") ? "<design-tokens>" : designTokensPath ?? "<design-tokens>";
598
- if (!existsSync(resolved)) {
762
+ if (!existsSync2(resolved)) {
599
763
  return {
600
764
  source: sourceLabel,
601
765
  status: "error",
@@ -605,7 +769,7 @@ function collectDesignTokenEvidence(projectRoot, designTokensPath) {
605
769
  };
606
770
  }
607
771
  const decantrTokens = parseDecantrCssTokenNames(projectRoot);
608
- const parsed = JSON.parse(readFileSync(resolved, "utf-8"));
772
+ const parsed = JSON.parse(readFileSync2(resolved, "utf-8"));
609
773
  const designKeys = flattenDesignTokenKeys(parsed);
610
774
  const missing = decantrTokens.filter((token) => {
611
775
  const bare = token.replace(/^--/, "");
@@ -648,19 +812,19 @@ function collectDesignTokenFinding(projectRoot, designTokensPath) {
648
812
  });
649
813
  }
650
814
  function baselinePath(projectRoot) {
651
- return join(projectRoot, ".decantr", "health-baseline.json");
815
+ return join2(projectRoot, ".decantr", "health-baseline.json");
652
816
  }
653
817
  function baselineDiffPath(projectRoot) {
654
- return join(projectRoot, ".decantr", "health-baseline-diff.json");
818
+ return join2(projectRoot, ".decantr", "health-baseline-diff.json");
655
819
  }
656
820
  function screenshotHashes(projectRoot) {
657
821
  const manifest = readJsonFile(
658
- join(projectRoot, ".decantr", "evidence", "visual-manifest.json")
822
+ join2(projectRoot, ".decantr", "evidence", "visual-manifest.json")
659
823
  );
660
824
  if (manifest?.routes) {
661
825
  return manifest.routes.filter((route) => typeof route.screenshot === "string").map((route) => ({
662
826
  path: route.screenshot,
663
- hash: route.screenshotHash ?? hashFile(join(projectRoot, route.screenshot))
827
+ hash: route.screenshotHash ?? hashFile(join2(projectRoot, route.screenshot))
664
828
  }));
665
829
  }
666
830
  return [];
@@ -688,7 +852,7 @@ function changedFilesSinceBaseline(projectRoot) {
688
852
  }
689
853
  function routeImpactsFromChangedFiles(report, changedFiles) {
690
854
  const analysis = readJsonFile(
691
- join(report.projectRoot, ".decantr", "analysis.json")
855
+ join2(report.projectRoot, ".decantr", "analysis.json")
692
856
  );
693
857
  const routeEntries = analysis?.routes?.routes ?? [];
694
858
  const impacted = /* @__PURE__ */ new Set();
@@ -721,7 +885,7 @@ function createHealthBaseline(projectRoot, report) {
721
885
  }
722
886
  function saveHealthBaseline(projectRoot, report) {
723
887
  const path = baselinePath(projectRoot);
724
- mkdirSync(dirname(path), { recursive: true });
888
+ mkdirSync(dirname2(path), { recursive: true });
725
889
  writeFileSync(
726
890
  path,
727
891
  JSON.stringify(createHealthBaseline(projectRoot, report), null, 2) + "\n",
@@ -760,7 +924,7 @@ function compareHealthBaseline(projectRoot, report) {
760
924
  }
761
925
  function saveHealthBaselineComparison(projectRoot, comparison) {
762
926
  const path = baselineDiffPath(projectRoot);
763
- mkdirSync(dirname(path), { recursive: true });
927
+ mkdirSync(dirname2(path), { recursive: true });
764
928
  writeFileSync(path, JSON.stringify(comparison, null, 2) + "\n", "utf-8");
765
929
  return path;
766
930
  }
@@ -877,7 +1041,9 @@ async function createProjectHealthReport(projectRoot = process.cwd(), options =
877
1041
  if (browserVerification?.finding && !isDuplicateFinding(seen, browserVerification.finding)) {
878
1042
  findings.push(browserVerification.finding);
879
1043
  }
880
- const finalCounts = countFindings(findings);
1044
+ const scopedFindings = scopeHealthFindingsToProject(projectRoot, findings);
1045
+ const finalCounts = countFindings(scopedFindings);
1046
+ const commandContext = commandContextForProject(projectRoot);
881
1047
  return {
882
1048
  $schema: PROJECT_HEALTH_SCHEMA_URL,
883
1049
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -901,7 +1067,7 @@ async function createProjectHealthReport(projectRoot = process.cwd(), options =
901
1067
  runtimeChecked: audit.runtimeAudit.routeHintsChecked,
902
1068
  runtimeMatched: audit.runtimeAudit.routeHintsMatched,
903
1069
  runtimeCoverageOk: audit.summary.runtimeAuditChecked ? audit.runtimeAudit.routeHintsCoverageOk : null,
904
- issues: routeIssuesFromFindings(findings)
1070
+ issues: routeIssuesFromFindings(scopedFindings)
905
1071
  },
906
1072
  packs: {
907
1073
  manifestPresent: Boolean(manifest),
@@ -913,10 +1079,10 @@ async function createProjectHealthReport(projectRoot = process.cwd(), options =
913
1079
  generatedAt: typeof manifest?.generatedAt === "string" ? manifest.generatedAt : null
914
1080
  },
915
1081
  ci: {
916
- recommendedCommand: "decantr ci --fail-on error",
1082
+ recommendedCommand: commandContext.ciCommand,
917
1083
  failOn: "error"
918
1084
  },
919
- findings
1085
+ findings: scopedFindings
920
1086
  };
921
1087
  }
922
1088
  function colorForStatus(status) {
@@ -1014,7 +1180,7 @@ async function createProjectEvidenceBundle(projectRoot, report, options = {}) {
1014
1180
  report,
1015
1181
  audit,
1016
1182
  assertions,
1017
- workspaceConfigPath: existsSync(join(projectRoot, ".decantr", "workspace.json")) ? join(projectRoot, ".decantr", "workspace.json") : null,
1183
+ workspaceConfigPath: existsSync2(join2(projectRoot, ".decantr", "workspace.json")) ? join2(projectRoot, ".decantr", "workspace.json") : null,
1018
1184
  designTokensPath: resolveOptionalPath(projectRoot, options.designTokensPath) ?? null,
1019
1185
  browser: await browserEvidenceFromOptions(projectRoot, options, report.routes.declared),
1020
1186
  designTokens: collectDesignTokenEvidence(projectRoot, options.designTokensPath)
@@ -1090,8 +1256,8 @@ async function cmdHealth(projectRoot = process.cwd(), options = {}) {
1090
1256
  ` : format === "json" ? formatProjectHealthJson(report) : format === "markdown" ? formatProjectHealthMarkdown(report) : formatProjectHealthText(report);
1091
1257
  const payload = baselineComparison && !options.evidence && format === "text" ? `${basePayload}${formatBaselineComparisonText(baselineComparison)}` : basePayload;
1092
1258
  if (options.output) {
1093
- const outputPath = isAbsolute(options.output) ? options.output : join(projectRoot, options.output);
1094
- mkdirSync(dirname(outputPath), { recursive: true });
1259
+ const outputPath = isAbsolute(options.output) ? options.output : join2(projectRoot, options.output);
1260
+ mkdirSync(dirname2(outputPath), { recursive: true });
1095
1261
  writeFileSync(outputPath, payload, "utf-8");
1096
1262
  if (!options.ci) {
1097
1263
  console.log(
@@ -1236,6 +1402,8 @@ function parseHealthArgs(args) {
1236
1402
  }
1237
1403
 
1238
1404
  export {
1405
+ listWorkspaceAppCandidates,
1406
+ resolveWorkspaceInfo,
1239
1407
  renderProjectHealthCiWorkflow,
1240
1408
  writeProjectHealthCiWorkflow,
1241
1409
  collectDesignTokenEvidence,
@@ -1,101 +1,12 @@
1
1
  import {
2
- createProjectHealthReport
3
- } from "./chunk-TMOCTDYY.js";
2
+ createProjectHealthReport,
3
+ listWorkspaceAppCandidates
4
+ } from "./chunk-DX2UDORT.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,
@@ -10,7 +10,7 @@ import {
10
10
  renderProjectHealthCiWorkflow,
11
11
  shouldFailHealth,
12
12
  writeProjectHealthCiWorkflow
13
- } from "./chunk-TMOCTDYY.js";
13
+ } from "./chunk-DX2UDORT.js";
14
14
  import "./chunk-34TZXWIF.js";
15
15
  export {
16
16
  cmdHealth,
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import "./chunk-6UDJDQPT.js";
1
+ import "./chunk-AXMGQ5IB.js";
2
2
  import "./chunk-RXF7ZYGK.js";
3
- import "./chunk-FKM4OQDF.js";
4
- import "./chunk-TMOCTDYY.js";
3
+ import "./chunk-R57DMFLF.js";
4
+ import "./chunk-DX2UDORT.js";
5
5
  import "./chunk-34TZXWIF.js";
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  createWorkspaceHealthReport
3
- } from "./chunk-FKM4OQDF.js";
3
+ } from "./chunk-R57DMFLF.js";
4
4
  import {
5
5
  createProjectHealthReport
6
- } from "./chunk-TMOCTDYY.js";
6
+ } from "./chunk-DX2UDORT.js";
7
7
  import {
8
8
  sendStudioHealthRefreshedTelemetry,
9
9
  sendStudioStartedTelemetry
@@ -7,8 +7,8 @@ import {
7
7
  listWorkspaceProjects,
8
8
  parseWorkspaceArgs,
9
9
  shouldFailWorkspaceHealth
10
- } from "./chunk-FKM4OQDF.js";
11
- import "./chunk-TMOCTDYY.js";
10
+ } from "./chunk-R57DMFLF.js";
11
+ import "./chunk-DX2UDORT.js";
12
12
  import "./chunk-34TZXWIF.js";
13
13
  export {
14
14
  cmdWorkspace,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decantr/cli",
3
- "version": "2.9.1",
3
+ "version": "2.9.2",
4
4
  "description": "Decantr CLI - scaffold, audit, inspect Project Health, and maintain Decantr projects from the terminal",
5
5
  "keywords": [
6
6
  "decantr",
@@ -48,11 +48,11 @@
48
48
  },
49
49
  "dependencies": {
50
50
  "ajv": "^8.20.0",
51
+ "@decantr/core": "2.1.0",
51
52
  "@decantr/essence-spec": "2.0.1",
52
- "@decantr/telemetry": "2.2.1",
53
53
  "@decantr/registry": "2.2.0",
54
- "@decantr/core": "2.1.0",
55
- "@decantr/verifier": "2.3.1"
54
+ "@decantr/telemetry": "2.2.1",
55
+ "@decantr/verifier": "2.3.2"
56
56
  },
57
57
  "scripts": {
58
58
  "build": "tsup",