@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 +3 -3
- package/dist/bin.js +3 -3
- package/dist/{chunk-6UDJDQPT.js → chunk-AXMGQ5IB.js} +93 -26
- package/dist/{chunk-TMOCTDYY.js → chunk-DX2UDORT.js} +214 -46
- package/dist/{chunk-FKM4OQDF.js → chunk-R57DMFLF.js} +17 -107
- package/dist/{health-Q7XF3I5Z.js → health-LTDSTNOV.js} +1 -1
- package/dist/index.js +3 -3
- package/dist/{studio-EDQMI6JE.js → studio-7E2LJS3A.js} +2 -2
- package/dist/{workspace-JA2RZI6V.js → workspace-53EIHUXB.js} +2 -2
- package/package.json +4 -4
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
|
|
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-
|
|
2
|
+
import "./chunk-AXMGQ5IB.js";
|
|
3
3
|
import "./chunk-RXF7ZYGK.js";
|
|
4
|
-
import "./chunk-
|
|
5
|
-
import "./chunk-
|
|
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-
|
|
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-
|
|
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.
|
|
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
|
|
7948
|
-
|
|
7949
|
-
|
|
7950
|
-
|
|
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
|
-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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 =
|
|
122
|
+
var __dirname = dirname2(fileURLToPath(import.meta.url));
|
|
33
123
|
function readProjectMetadata(projectRoot) {
|
|
34
|
-
const projectJsonPath =
|
|
35
|
-
if (!
|
|
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(
|
|
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 =
|
|
53
|
-
if (
|
|
54
|
-
const fromSrc =
|
|
55
|
-
if (
|
|
56
|
-
const fromCommandSrc =
|
|
57
|
-
if (
|
|
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 =
|
|
153
|
-
const alreadyExists =
|
|
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(
|
|
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 (!
|
|
342
|
+
if (!existsSync2(path)) return null;
|
|
253
343
|
try {
|
|
254
|
-
return createHash("sha256").update(
|
|
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 (!
|
|
350
|
+
if (!existsSync2(path)) return null;
|
|
261
351
|
try {
|
|
262
|
-
return JSON.parse(
|
|
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 :
|
|
544
|
+
return isAbsolute(path) ? path : resolve2(projectRoot, path);
|
|
381
545
|
}
|
|
382
546
|
function hasProjectPlaywright(projectRoot) {
|
|
383
547
|
try {
|
|
384
|
-
const requireFromProject = createRequire(
|
|
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(
|
|
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(
|
|
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 =
|
|
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 =
|
|
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 =
|
|
535
|
-
mkdirSync(
|
|
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 =
|
|
586
|
-
if (!
|
|
587
|
-
const css =
|
|
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 (!
|
|
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(
|
|
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
|
|
815
|
+
return join2(projectRoot, ".decantr", "health-baseline.json");
|
|
652
816
|
}
|
|
653
817
|
function baselineDiffPath(projectRoot) {
|
|
654
|
-
return
|
|
818
|
+
return join2(projectRoot, ".decantr", "health-baseline-diff.json");
|
|
655
819
|
}
|
|
656
820
|
function screenshotHashes(projectRoot) {
|
|
657
821
|
const manifest = readJsonFile(
|
|
658
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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:
|
|
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:
|
|
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 :
|
|
1094
|
-
mkdirSync(
|
|
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
|
-
|
|
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
|
|
8
|
-
import { dirname
|
|
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
|
|
28
|
+
return join(root, ".decantr", "workspace.json");
|
|
118
29
|
}
|
|
119
30
|
function readWorkspaceConfig(root) {
|
|
120
31
|
const path = workspaceConfigPath(root);
|
|
121
|
-
if (!
|
|
122
|
-
return JSON.parse(
|
|
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 (
|
|
52
|
+
if (existsSync(join(dir, "decantr.essence.json"))) {
|
|
142
53
|
results.add(rel || ".");
|
|
143
54
|
return;
|
|
144
55
|
}
|
|
145
|
-
for (const entry of
|
|
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(
|
|
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 =
|
|
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:
|
|
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:
|
|
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 =
|
|
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(
|
|
424
|
-
writeFileSync(
|
|
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,
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createWorkspaceHealthReport
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-R57DMFLF.js";
|
|
4
4
|
import {
|
|
5
5
|
createProjectHealthReport
|
|
6
|
-
} from "./chunk-
|
|
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-
|
|
11
|
-
import "./chunk-
|
|
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.
|
|
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/
|
|
55
|
-
"@decantr/verifier": "2.3.
|
|
54
|
+
"@decantr/telemetry": "2.2.1",
|
|
55
|
+
"@decantr/verifier": "2.3.2"
|
|
56
56
|
},
|
|
57
57
|
"scripts": {
|
|
58
58
|
"build": "tsup",
|