@decantr/cli 2.9.0 → 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 +4 -3
- package/dist/bin.js +4 -4
- package/dist/{chunk-N7A3WUZ2.js → chunk-AXMGQ5IB.js} +151 -36
- package/dist/{chunk-TMOCTDYY.js → chunk-DX2UDORT.js} +214 -46
- package/dist/{chunk-T5INVSOP.js → chunk-R57DMFLF.js} +17 -93
- package/dist/{chunk-V3XAQWKD.js → chunk-RXF7ZYGK.js} +22 -8
- package/dist/{health-Q7XF3I5Z.js → health-LTDSTNOV.js} +1 -1
- package/dist/index.js +4 -4
- package/dist/{studio-B2Y4TKZ5.js → studio-7E2LJS3A.js} +2 -2
- package/dist/{upgrade-U2BTWJJJ.js → upgrade-VON7Y3LG.js} +1 -1
- package/dist/{workspace-H5QQJCCI.js → workspace-53EIHUXB.js} +2 -2
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -19,8 +19,9 @@ 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
|
+
In monorepos, app-scoped commands accept `--project <app-path>`. Hosted pack hydration also follows the essence path: `decantr registry compile-packs apps/web/decantr.essence.json --write-context` writes into `apps/web/.decantr/context`.
|
|
24
25
|
Use `decantr init`, `decantr analyze`, `decantr check`, and `decantr health` as advanced primitives when you need direct control over one step.
|
|
25
26
|
|
|
26
27
|
Current starter adapter availability:
|
|
@@ -137,7 +138,7 @@ decantr showcase verification --json
|
|
|
137
138
|
|
|
138
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.
|
|
139
140
|
|
|
140
|
-
`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.
|
|
141
142
|
|
|
142
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.
|
|
143
144
|
|
|
@@ -172,7 +173,7 @@ decantr export --to figma-tokens
|
|
|
172
173
|
|
|
173
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.
|
|
174
175
|
|
|
175
|
-
`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`.
|
|
176
177
|
|
|
177
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:
|
|
178
179
|
|
package/dist/bin.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import "./chunk-
|
|
3
|
-
import "./chunk-
|
|
4
|
-
import "./chunk-
|
|
5
|
-
import "./chunk-
|
|
2
|
+
import "./chunk-AXMGQ5IB.js";
|
|
3
|
+
import "./chunk-RXF7ZYGK.js";
|
|
4
|
+
import "./chunk-R57DMFLF.js";
|
|
5
|
+
import "./chunk-DX2UDORT.js";
|
|
6
6
|
import "./chunk-34TZXWIF.js";
|
|
@@ -14,22 +14,22 @@ import {
|
|
|
14
14
|
scaffoldProject,
|
|
15
15
|
syncRegistry,
|
|
16
16
|
writeExecutionPackBundleArtifacts
|
|
17
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-RXF7ZYGK.js";
|
|
18
18
|
import {
|
|
19
19
|
createWorkspaceHealthReport,
|
|
20
20
|
formatWorkspaceHealthMarkdown,
|
|
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") {
|
|
@@ -4194,6 +4214,7 @@ import { existsSync as existsSync16, readdirSync as readdirSync6, readFileSync a
|
|
|
4194
4214
|
import { dirname as dirname3, join as join18, relative as relative4 } from "path";
|
|
4195
4215
|
import { fileURLToPath } from "url";
|
|
4196
4216
|
import { isV4 as isV44 } from "@decantr/essence-spec";
|
|
4217
|
+
import { collectMissingPackManifestFiles } from "@decantr/verifier";
|
|
4197
4218
|
var BOLD3 = "\x1B[1m";
|
|
4198
4219
|
var DIM4 = "\x1B[2m";
|
|
4199
4220
|
var GREEN4 = "\x1B[32m";
|
|
@@ -4339,6 +4360,19 @@ function buildDoctorReport(root, args) {
|
|
|
4339
4360
|
const ciFiles = findCiFiles(workspaceRoot);
|
|
4340
4361
|
const workflowMode = projectJson?.initialized?.workflowMode ?? null;
|
|
4341
4362
|
const adoptionMode = projectJson?.initialized?.adoptionMode ?? null;
|
|
4363
|
+
const missingPackReferences = workspaceMode ? projects.flatMap(
|
|
4364
|
+
(project) => collectMissingPackManifestFiles(join18(workspaceRoot, project.path)).map(
|
|
4365
|
+
(missing) => `${project.path}/${missing.relativePath}`
|
|
4366
|
+
)
|
|
4367
|
+
) : collectMissingPackManifestFiles(appRoot).map((missing) => missing.relativePath);
|
|
4368
|
+
const workspaceProjectsMissingManifest = workspaceMode ? projects.map((project) => project.path).filter((projectPath2) => {
|
|
4369
|
+
const projectContextDir = join18(workspaceRoot, projectPath2, ".decantr", "context");
|
|
4370
|
+
return !existsSync16(join18(projectContextDir, "pack-manifest.json"));
|
|
4371
|
+
}) : [];
|
|
4372
|
+
const workspaceProjectsMissingReviewPack = workspaceMode ? projects.map((project) => project.path).filter((projectPath2) => {
|
|
4373
|
+
const projectContextDir = join18(workspaceRoot, projectPath2, ".decantr", "context");
|
|
4374
|
+
return !existsSync16(join18(projectContextDir, "review-pack.json"));
|
|
4375
|
+
}) : [];
|
|
4342
4376
|
const issues = [];
|
|
4343
4377
|
if (!essenceVersion && !workspaceMode && !workspaceInfo.requiresProjectSelection) {
|
|
4344
4378
|
issues.push({
|
|
@@ -4384,7 +4418,23 @@ function buildDoctorReport(root, args) {
|
|
|
4384
4418
|
category: "generated-artifact",
|
|
4385
4419
|
severity: "warn",
|
|
4386
4420
|
message: "Generated context packs are missing or incomplete.",
|
|
4387
|
-
nextCommand: projectPath ? `decantr
|
|
4421
|
+
nextCommand: projectPath ? `decantr registry compile-packs ${projectPath}/decantr.essence.json --write-context` : "decantr registry compile-packs decantr.essence.json --write-context"
|
|
4422
|
+
});
|
|
4423
|
+
}
|
|
4424
|
+
if (essenceVersion === "4.0.0" && missingPackReferences.length > 0) {
|
|
4425
|
+
issues.push({
|
|
4426
|
+
category: "generated-artifact",
|
|
4427
|
+
severity: "warn",
|
|
4428
|
+
message: `Generated pack manifest references ${missingPackReferences.length} missing file(s).`,
|
|
4429
|
+
nextCommand: projectPath ? `decantr registry compile-packs ${projectPath}/decantr.essence.json --write-context` : "decantr registry compile-packs decantr.essence.json --write-context"
|
|
4430
|
+
});
|
|
4431
|
+
}
|
|
4432
|
+
if (workspaceMode && (workspaceProjectsMissingManifest.length > 0 || workspaceProjectsMissingReviewPack.length > 0 || missingPackReferences.length > 0)) {
|
|
4433
|
+
issues.push({
|
|
4434
|
+
category: "generated-artifact",
|
|
4435
|
+
severity: "warn",
|
|
4436
|
+
message: "One or more attached workspace projects have missing or incomplete generated context packs.",
|
|
4437
|
+
nextCommand: "decantr registry compile-packs <app-path>/decantr.essence.json --write-context"
|
|
4388
4438
|
});
|
|
4389
4439
|
}
|
|
4390
4440
|
if (workflowMode === "brownfield-attach" && !existsSync16(localPatternsPath(appRoot))) {
|
|
@@ -4439,9 +4489,12 @@ function buildDoctorReport(root, args) {
|
|
|
4439
4489
|
appCandidates: candidates
|
|
4440
4490
|
},
|
|
4441
4491
|
generatedArtifacts: {
|
|
4442
|
-
contextDirPresent:
|
|
4443
|
-
|
|
4444
|
-
|
|
4492
|
+
contextDirPresent: workspaceMode ? projects.some(
|
|
4493
|
+
(project) => existsSync16(join18(workspaceRoot, project.path, ".decantr", "context"))
|
|
4494
|
+
) : existsSync16(contextDir),
|
|
4495
|
+
packManifestPresent: workspaceMode ? projects.length > 0 && workspaceProjectsMissingManifest.length === 0 : packManifestPresent,
|
|
4496
|
+
reviewPackPresent: workspaceMode ? projects.length > 0 && workspaceProjectsMissingReviewPack.length === 0 : reviewPackPresent,
|
|
4497
|
+
missingReferencedFiles: missingPackReferences.slice(0, 25)
|
|
4445
4498
|
},
|
|
4446
4499
|
localLaw: {
|
|
4447
4500
|
patternsPresent: existsSync16(localPatternsPath(appRoot)),
|
|
@@ -4481,6 +4534,7 @@ function formatDoctorText(report) {
|
|
|
4481
4534
|
` Context directory: ${report.generatedArtifacts.contextDirPresent ? "present" : "missing"}`,
|
|
4482
4535
|
` Pack manifest: ${report.generatedArtifacts.packManifestPresent ? "present" : "missing"}`,
|
|
4483
4536
|
` Review pack: ${report.generatedArtifacts.reviewPackPresent ? "present" : "missing"}`,
|
|
4537
|
+
` Manifest references: ${report.generatedArtifacts.missingReferencedFiles.length === 0 ? "complete" : `${report.generatedArtifacts.missingReferencedFiles.length} missing`}`,
|
|
4484
4538
|
` Artifact guide: ${report.project.artifactReadmePresent ? "present" : "missing"}`,
|
|
4485
4539
|
"",
|
|
4486
4540
|
`${BOLD3}Local Law:${RESET4}`,
|
|
@@ -5829,6 +5883,7 @@ async function cmdPublish(type, name, projectRoot = process.cwd()) {
|
|
|
5829
5883
|
import { createHash } from "crypto";
|
|
5830
5884
|
import { existsSync as existsSync23, readdirSync as readdirSync7, readFileSync as readFileSync17, statSync as statSync6 } from "fs";
|
|
5831
5885
|
import { join as join25 } from "path";
|
|
5886
|
+
import { collectMissingPackManifestFiles as collectMissingPackManifestFiles2 } from "@decantr/verifier";
|
|
5832
5887
|
import { isV4 as isV46 } from "@decantr/essence-spec";
|
|
5833
5888
|
var GREEN9 = "\x1B[32m";
|
|
5834
5889
|
var RED8 = "\x1B[31m";
|
|
@@ -5891,6 +5946,16 @@ function checkRefreshFreshness(projectRoot) {
|
|
|
5891
5946
|
if (!existsSync23(join25(contextDir, "scaffold.md"))) {
|
|
5892
5947
|
reasons.push(".decantr/context/scaffold.md is missing.");
|
|
5893
5948
|
}
|
|
5949
|
+
if (!existsSync23(join25(contextDir, "pack-manifest.json"))) {
|
|
5950
|
+
reasons.push(".decantr/context/pack-manifest.json is missing.");
|
|
5951
|
+
} else {
|
|
5952
|
+
const missingPackFiles = collectMissingPackManifestFiles2(projectRoot);
|
|
5953
|
+
if (missingPackFiles.length > 0) {
|
|
5954
|
+
reasons.push(
|
|
5955
|
+
`pack-manifest.json references missing files: ${missingPackFiles.slice(0, 5).map((missing) => missing.relativePath).join(", ")}${missingPackFiles.length > 5 ? "..." : ""}`
|
|
5956
|
+
);
|
|
5957
|
+
}
|
|
5958
|
+
}
|
|
5894
5959
|
const newestInput = newestInputMtime(projectRoot);
|
|
5895
5960
|
const staleFiles = generated.filter((path) => fileMtimeMs(path) > 0 && fileMtimeMs(path) < newestInput).map((path) => path.replace(`${projectRoot}/`, ""));
|
|
5896
5961
|
if (staleFiles.length > 0) {
|
|
@@ -7899,18 +7964,13 @@ async function printRegistryIntelligenceSummary(namespace, jsonOutput = false) {
|
|
|
7899
7964
|
}
|
|
7900
7965
|
}
|
|
7901
7966
|
async function printHostedExecutionPackBundle(essencePath, namespace, jsonOutput = false, writeContext = false) {
|
|
7902
|
-
const
|
|
7903
|
-
|
|
7904
|
-
|
|
7905
|
-
|
|
7906
|
-
}
|
|
7907
|
-
const essence = JSON.parse(readFileSync22(resolvedPath, "utf-8"));
|
|
7908
|
-
const bundle = await client.compileExecutionPacks(essence, namespace ? { namespace } : void 0);
|
|
7967
|
+
const { resolvedPath, bundle, contextDir } = await compileHostedExecutionPackBundle(
|
|
7968
|
+
essencePath,
|
|
7969
|
+
namespace
|
|
7970
|
+
);
|
|
7909
7971
|
let writtenContextPaths = [];
|
|
7910
7972
|
if (writeContext) {
|
|
7911
|
-
const
|
|
7912
|
-
mkdirSync16(contextDir, { recursive: true });
|
|
7913
|
-
const written = writeExecutionPackBundleArtifacts(
|
|
7973
|
+
const written = writeHostedExecutionPackContextArtifacts(
|
|
7914
7974
|
contextDir,
|
|
7915
7975
|
bundle
|
|
7916
7976
|
);
|
|
@@ -7934,7 +7994,7 @@ async function printHostedExecutionPackBundle(essencePath, namespace, jsonOutput
|
|
|
7934
7994
|
console.log(` Sections: ${typedBundle.sections.length}`);
|
|
7935
7995
|
console.log(` Mutations: ${typedBundle.mutations.length}`);
|
|
7936
7996
|
if (writeContext) {
|
|
7937
|
-
console.log(` Context bundle: ${
|
|
7997
|
+
console.log(` Context bundle: ${contextDir}`);
|
|
7938
7998
|
console.log(` Files written: ${writtenContextPaths.length}`);
|
|
7939
7999
|
}
|
|
7940
8000
|
console.log("");
|
|
@@ -7945,6 +8005,21 @@ async function printHostedExecutionPackBundle(essencePath, namespace, jsonOutput
|
|
|
7945
8005
|
console.log(` ${cyan3(route.path)} -> ${pageLabel} [${patterns}]`);
|
|
7946
8006
|
}
|
|
7947
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
|
+
}
|
|
7948
8023
|
function resolvePagePackIdForRoute(essencePath, route) {
|
|
7949
8024
|
if (!existsSync29(essencePath)) {
|
|
7950
8025
|
throw new Error(`Essence file not found at ${essencePath}`);
|
|
@@ -7982,7 +8057,7 @@ async function printHostedSelectedExecutionPack(packType, id, essencePath, names
|
|
|
7982
8057
|
);
|
|
7983
8058
|
let writtenContextDir = null;
|
|
7984
8059
|
if (writeContext) {
|
|
7985
|
-
const contextDir = join31(
|
|
8060
|
+
const contextDir = join31(dirname5(resolvedPath), ".decantr", "context");
|
|
7986
8061
|
mkdirSync16(contextDir, { recursive: true });
|
|
7987
8062
|
writeFileSync19(
|
|
7988
8063
|
join31(contextDir, "pack-manifest.json"),
|
|
@@ -8028,7 +8103,7 @@ async function printHostedExecutionPackManifest(essencePath, namespace, jsonOutp
|
|
|
8028
8103
|
);
|
|
8029
8104
|
let writtenContextDir = null;
|
|
8030
8105
|
if (writeContext) {
|
|
8031
|
-
const contextDir = join31(
|
|
8106
|
+
const contextDir = join31(dirname5(resolvedPath), ".decantr", "context");
|
|
8032
8107
|
mkdirSync16(contextDir, { recursive: true });
|
|
8033
8108
|
writeFileSync19(join31(contextDir, "pack-manifest.json"), JSON.stringify(manifest, null, 2) + "\n");
|
|
8034
8109
|
writtenContextDir = contextDir;
|
|
@@ -9716,6 +9791,10 @@ function withoutWorkflowOnlyFlags(args) {
|
|
|
9716
9791
|
function withProject(command, projectArg) {
|
|
9717
9792
|
return projectArg ? `${command} --project ${projectArg}` : command;
|
|
9718
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
|
+
}
|
|
9719
9798
|
function firstWorkspaceCandidate(workspaceInfo) {
|
|
9720
9799
|
return workspaceInfo.appCandidates[0] ?? "apps/web";
|
|
9721
9800
|
}
|
|
@@ -9853,6 +9932,7 @@ async function cmdAdoptWorkflow(args) {
|
|
|
9853
9932
|
const runBrowser = flagBoolean(flags, "browser") || Boolean(baseUrl);
|
|
9854
9933
|
const evidence = flagBoolean(flags, "evidence") || runBrowser;
|
|
9855
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";
|
|
9856
9936
|
const initCi = flagBoolean(flags, "ci") || flagBoolean(flags, "init-ci");
|
|
9857
9937
|
const assistantBridge = flagString(flags, "assistant-bridge");
|
|
9858
9938
|
const hasEssence = existsSync29(join31(projectRoot, "decantr.essence.json"));
|
|
@@ -9861,6 +9941,9 @@ async function cmdAdoptWorkflow(args) {
|
|
|
9861
9941
|
"analyze current app and write .decantr/brownfield intelligence",
|
|
9862
9942
|
`init --existing ${proposalFlag} as contract-only Brownfield`
|
|
9863
9943
|
];
|
|
9944
|
+
if (hydratePacks) {
|
|
9945
|
+
steps.push("hydrate hosted execution packs into the app context");
|
|
9946
|
+
}
|
|
9864
9947
|
if (runVerify) {
|
|
9865
9948
|
steps.push(
|
|
9866
9949
|
runBrowser ? "verify with Project Health, browser evidence, visual manifest, and baseline" : "verify with Project Health and baseline"
|
|
@@ -9894,8 +9977,35 @@ async function cmdAdoptWorkflow(args) {
|
|
|
9894
9977
|
telemetry: flagBoolean(flags, "telemetry")
|
|
9895
9978
|
});
|
|
9896
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
|
+
}
|
|
9897
10007
|
if (runVerify) {
|
|
9898
|
-
const { cmdHealth } = await import("./health-
|
|
10008
|
+
const { cmdHealth } = await import("./health-LTDSTNOV.js");
|
|
9899
10009
|
await cmdHealth(projectRoot, {
|
|
9900
10010
|
browser: runBrowser,
|
|
9901
10011
|
browserBaseUrl: baseUrl,
|
|
@@ -9936,7 +10046,7 @@ async function cmdVerifyWorkflow(args) {
|
|
|
9936
10046
|
return;
|
|
9937
10047
|
}
|
|
9938
10048
|
if (workspaceMode) {
|
|
9939
|
-
const { cmdWorkspace } = await import("./workspace-
|
|
10049
|
+
const { cmdWorkspace } = await import("./workspace-53EIHUXB.js");
|
|
9940
10050
|
await cmdWorkspace(process.cwd(), ["workspace", "health", ...withoutWorkflowOnlyFlags(args)]);
|
|
9941
10051
|
return;
|
|
9942
10052
|
}
|
|
@@ -9979,7 +10089,7 @@ async function cmdVerifyWorkflow(args) {
|
|
|
9979
10089
|
process.exitCode = void 0;
|
|
9980
10090
|
}
|
|
9981
10091
|
}
|
|
9982
|
-
const { cmdHealth, parseHealthArgs } = await import("./health-
|
|
10092
|
+
const { cmdHealth, parseHealthArgs } = await import("./health-LTDSTNOV.js");
|
|
9983
10093
|
await cmdHealth(workspaceInfo.appRoot, parseHealthArgs(healthArgs));
|
|
9984
10094
|
if (localPatterns) {
|
|
9985
10095
|
const validation = validateLocalLaw(workspaceInfo.appRoot);
|
|
@@ -10045,6 +10155,7 @@ async function cmdTaskWorkflow(args) {
|
|
|
10045
10155
|
const { flags, positional } = parseLooseArgs(args);
|
|
10046
10156
|
const workspaceInfo = resolveWorkflowProject(flags, "task");
|
|
10047
10157
|
if (!workspaceInfo) return;
|
|
10158
|
+
const projectArg = flagString(flags, "project");
|
|
10048
10159
|
const routeInput = positional[0];
|
|
10049
10160
|
if (!routeInput) {
|
|
10050
10161
|
console.error(
|
|
@@ -10113,7 +10224,7 @@ async function cmdTaskWorkflow(args) {
|
|
|
10113
10224
|
localLaw,
|
|
10114
10225
|
changedFiles: currentChangedFiles,
|
|
10115
10226
|
changedRoutes,
|
|
10116
|
-
verifyCommand: "decantr verify --brownfield --local-patterns"
|
|
10227
|
+
verifyCommand: withProject("decantr verify --brownfield --local-patterns", projectArg)
|
|
10117
10228
|
};
|
|
10118
10229
|
if (flagBoolean(flags, "json")) {
|
|
10119
10230
|
console.log(JSON.stringify(context, null, 2));
|
|
@@ -10154,7 +10265,7 @@ async function cmdTaskWorkflow(args) {
|
|
|
10154
10265
|
console.log("");
|
|
10155
10266
|
console.log(`${BOLD9}Project-owned local law:${RESET16}`);
|
|
10156
10267
|
console.log(
|
|
10157
|
-
` ${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.`
|
|
10158
10269
|
);
|
|
10159
10270
|
}
|
|
10160
10271
|
if (context.changedFiles.length > 0) {
|
|
@@ -10268,7 +10379,7 @@ ${BOLD9}decantr${RESET16} \u2014 Design intelligence for AI-generated UI
|
|
|
10268
10379
|
${BOLD9}Usage:${RESET16}
|
|
10269
10380
|
decantr setup [--project <path>]
|
|
10270
10381
|
decantr new <name> [--blueprint=X] [--archetype=X] [--theme=X] [--workflow=greenfield] [--adoption=decantr-css] [--telemetry]
|
|
10271
|
-
decantr adopt [--project <path>] [--base-url <url>] [--evidence] [--ci] [--yes]
|
|
10382
|
+
decantr adopt [--project <path>] [--base-url <url>] [--evidence] [--ci] [--no-packs] [--yes]
|
|
10272
10383
|
decantr task <route> ["task summary"] [--project <path>] [--since origin/main] [--json]
|
|
10273
10384
|
decantr verify [--project <path>] [--brownfield] [--local-patterns] [health options]
|
|
10274
10385
|
decantr ci [--project <path>] [--workspace] [--fail-on error|warn|none]
|
|
@@ -10628,11 +10739,11 @@ ${BOLD9}Examples:${RESET16}
|
|
|
10628
10739
|
}
|
|
10629
10740
|
function cmdAdoptHelp() {
|
|
10630
10741
|
console.log(`
|
|
10631
|
-
${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
|
|
10632
10743
|
|
|
10633
10744
|
${BOLD9}Usage:${RESET16}
|
|
10634
|
-
decantr adopt [--project <path>] [--yes] [--dry-run]
|
|
10635
|
-
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]
|
|
10636
10747
|
|
|
10637
10748
|
${BOLD9}Options:${RESET16}
|
|
10638
10749
|
--project App path inside a workspace/monorepo
|
|
@@ -10643,6 +10754,7 @@ ${BOLD9}Options:${RESET16}
|
|
|
10643
10754
|
--baseline Save a health baseline (default)
|
|
10644
10755
|
--no-baseline Skip baseline save
|
|
10645
10756
|
--no-verify Skip the verification step
|
|
10757
|
+
--no-packs Skip hosted execution-pack hydration
|
|
10646
10758
|
--ci, --init-ci Install the Decantr CI gate after adoption
|
|
10647
10759
|
--telemetry Opt this project into privacy-filtered CLI product telemetry
|
|
10648
10760
|
--merge-proposal Merge the observed proposal into an existing essence
|
|
@@ -10921,7 +11033,7 @@ async function main() {
|
|
|
10921
11033
|
break;
|
|
10922
11034
|
}
|
|
10923
11035
|
case "upgrade": {
|
|
10924
|
-
const { cmdUpgrade } = await import("./upgrade-
|
|
11036
|
+
const { cmdUpgrade } = await import("./upgrade-VON7Y3LG.js");
|
|
10925
11037
|
const applyFlag = args.includes("--apply");
|
|
10926
11038
|
await cmdUpgrade(process.cwd(), { apply: applyFlag });
|
|
10927
11039
|
break;
|
|
@@ -10934,9 +11046,12 @@ async function main() {
|
|
|
10934
11046
|
);
|
|
10935
11047
|
}
|
|
10936
11048
|
const { cmdHeal } = await import("./heal-2BDT7TR5.js");
|
|
11049
|
+
const { flags } = parseLooseArgs(args);
|
|
11050
|
+
const workspaceInfo = resolveWorkflowProject(flags, "check");
|
|
11051
|
+
if (!workspaceInfo) break;
|
|
10937
11052
|
const telemetryFlag = args.includes("--telemetry");
|
|
10938
11053
|
const brownfieldFlag = args.includes("--brownfield");
|
|
10939
|
-
await cmdHeal(
|
|
11054
|
+
await cmdHeal(workspaceInfo.appRoot, { telemetry: telemetryFlag, brownfield: brownfieldFlag });
|
|
10940
11055
|
break;
|
|
10941
11056
|
}
|
|
10942
11057
|
case "health": {
|
|
@@ -10945,7 +11060,7 @@ async function main() {
|
|
|
10945
11060
|
cmdHealthHelp();
|
|
10946
11061
|
break;
|
|
10947
11062
|
}
|
|
10948
|
-
const { cmdHealth, parseHealthArgs } = await import("./health-
|
|
11063
|
+
const { cmdHealth, parseHealthArgs } = await import("./health-LTDSTNOV.js");
|
|
10949
11064
|
await cmdHealth(process.cwd(), parseHealthArgs(args));
|
|
10950
11065
|
} catch (e) {
|
|
10951
11066
|
console.error(error3(e.message));
|
|
@@ -10973,7 +11088,7 @@ async function main() {
|
|
|
10973
11088
|
cmdStudioHelp();
|
|
10974
11089
|
break;
|
|
10975
11090
|
}
|
|
10976
|
-
const { cmdStudio, parseStudioArgs } = await import("./studio-
|
|
11091
|
+
const { cmdStudio, parseStudioArgs } = await import("./studio-7E2LJS3A.js");
|
|
10977
11092
|
await cmdStudio(process.cwd(), parseStudioArgs(args));
|
|
10978
11093
|
} catch (e) {
|
|
10979
11094
|
console.error(error3(e.message));
|
|
@@ -10987,7 +11102,7 @@ async function main() {
|
|
|
10987
11102
|
cmdWorkspaceHelp();
|
|
10988
11103
|
break;
|
|
10989
11104
|
}
|
|
10990
|
-
const { cmdWorkspace } = await import("./workspace-
|
|
11105
|
+
const { cmdWorkspace } = await import("./workspace-53EIHUXB.js");
|
|
10991
11106
|
await cmdWorkspace(process.cwd(), args);
|
|
10992
11107
|
} catch (e) {
|
|
10993
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,87 +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
|
-
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")) || allowSourceDirs && (existsSync(join(dir, "src")) || existsSync(join(dir, "app")) || existsSync(join(dir, "pages")))) {
|
|
41
|
-
return true;
|
|
42
|
-
}
|
|
43
|
-
const pkg = readPackageJson(dir);
|
|
44
|
-
const deps = { ...pkg?.dependencies, ...pkg?.devDependencies };
|
|
45
|
-
return Boolean(
|
|
46
|
-
deps.react || deps.next || deps.vue || deps.svelte || deps["@angular/core"] || deps.astro || deps.nuxt
|
|
47
|
-
);
|
|
48
|
-
}
|
|
49
|
-
function listWorkspaceApps(workspaceRoot) {
|
|
50
|
-
const candidates = [];
|
|
51
|
-
for (const base of ["apps", "packages"]) {
|
|
52
|
-
const baseDir = join(workspaceRoot, base);
|
|
53
|
-
if (!existsSync(baseDir)) continue;
|
|
54
|
-
for (const entry of readdirSync(baseDir, { withFileTypes: true })) {
|
|
55
|
-
if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
|
|
56
|
-
const candidate = join(baseDir, entry.name);
|
|
57
|
-
if (looksLikeApp(candidate, { allowSourceDirs: base === "apps" })) {
|
|
58
|
-
candidates.push(`${base}/${entry.name}`);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
return candidates.sort();
|
|
63
|
-
}
|
|
64
|
-
function listWorkspaceAppCandidates(workspaceRoot) {
|
|
65
|
-
return listWorkspaceApps(resolve(workspaceRoot));
|
|
66
|
-
}
|
|
67
|
-
function resolveWorkspaceInfo(cwd, projectArg) {
|
|
68
|
-
const absoluteCwd = resolve(cwd);
|
|
69
|
-
const workspaceRoot = findWorkspaceRoot(absoluteCwd) ?? absoluteCwd;
|
|
70
|
-
const appRoot = projectArg ? resolve(absoluteCwd, projectArg) : absoluteCwd;
|
|
71
|
-
const appCandidates = listWorkspaceApps(workspaceRoot);
|
|
72
|
-
const projectScope = workspaceRoot !== appRoot || appCandidates.length > 0 ? "workspace-app" : "single-app";
|
|
73
|
-
const requiresProjectSelection = !projectArg && workspaceRoot === absoluteCwd && appCandidates.length > 0;
|
|
74
|
-
return {
|
|
75
|
-
cwd: absoluteCwd,
|
|
76
|
-
workspaceRoot,
|
|
77
|
-
appRoot,
|
|
78
|
-
projectScope,
|
|
79
|
-
appCandidates,
|
|
80
|
-
requiresProjectSelection
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// src/commands/workspace.ts
|
|
8
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "fs";
|
|
9
|
+
import { dirname, join, relative, resolve } from "path";
|
|
85
10
|
var BOLD = "\x1B[1m";
|
|
86
11
|
var DIM = "\x1B[2m";
|
|
87
12
|
var GREEN = "\x1B[32m";
|
|
@@ -100,12 +25,12 @@ var DEFAULT_IGNORES = /* @__PURE__ */ new Set([
|
|
|
100
25
|
"playwright-report"
|
|
101
26
|
]);
|
|
102
27
|
function workspaceConfigPath(root) {
|
|
103
|
-
return
|
|
28
|
+
return join(root, ".decantr", "workspace.json");
|
|
104
29
|
}
|
|
105
30
|
function readWorkspaceConfig(root) {
|
|
106
31
|
const path = workspaceConfigPath(root);
|
|
107
|
-
if (!
|
|
108
|
-
return JSON.parse(
|
|
32
|
+
if (!existsSync(path)) return null;
|
|
33
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
109
34
|
}
|
|
110
35
|
function normalizeProjectPath(raw) {
|
|
111
36
|
const normalized = raw.replace(/^\.\/+/, "").replace(/\/+$/, "");
|
|
@@ -124,21 +49,21 @@ function discoverProjectPaths(root, config) {
|
|
|
124
49
|
if (depth > 6) return;
|
|
125
50
|
const rel = relative(root, dir).replace(/\\/g, "/");
|
|
126
51
|
if (rel && [...ignored].some((entry) => rel === entry || rel.startsWith(`${entry}/`))) return;
|
|
127
|
-
if (
|
|
52
|
+
if (existsSync(join(dir, "decantr.essence.json"))) {
|
|
128
53
|
results.add(rel || ".");
|
|
129
54
|
return;
|
|
130
55
|
}
|
|
131
|
-
for (const entry of
|
|
56
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
132
57
|
if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
|
|
133
58
|
if (ignored.has(entry.name)) continue;
|
|
134
|
-
walk(
|
|
59
|
+
walk(join(dir, entry.name), depth + 1);
|
|
135
60
|
}
|
|
136
61
|
}
|
|
137
62
|
walk(root, 0);
|
|
138
63
|
return [...results].sort();
|
|
139
64
|
}
|
|
140
65
|
function listWorkspaceProjects(root = process.cwd()) {
|
|
141
|
-
const workspaceRoot =
|
|
66
|
+
const workspaceRoot = resolve(root);
|
|
142
67
|
const config = readWorkspaceConfig(workspaceRoot);
|
|
143
68
|
const byPath = /* @__PURE__ */ new Map();
|
|
144
69
|
for (const project of config?.projects ?? []) {
|
|
@@ -146,7 +71,7 @@ function listWorkspaceProjects(root = process.cwd()) {
|
|
|
146
71
|
byPath.set(path, {
|
|
147
72
|
id: project.id ?? projectIdFromPath(path),
|
|
148
73
|
path,
|
|
149
|
-
absolutePath:
|
|
74
|
+
absolutePath: resolve(workspaceRoot, path),
|
|
150
75
|
owner: project.owner ?? null,
|
|
151
76
|
tags: project.tags ?? [],
|
|
152
77
|
criticality: project.criticality ?? "normal",
|
|
@@ -159,7 +84,7 @@ function listWorkspaceProjects(root = process.cwd()) {
|
|
|
159
84
|
byPath.set(path, {
|
|
160
85
|
id: projectIdFromPath(path),
|
|
161
86
|
path,
|
|
162
|
-
absolutePath:
|
|
87
|
+
absolutePath: resolve(workspaceRoot, path),
|
|
163
88
|
owner: null,
|
|
164
89
|
tags: [],
|
|
165
90
|
criticality: "normal",
|
|
@@ -226,7 +151,7 @@ async function mapLimited(items, concurrency, fn) {
|
|
|
226
151
|
return results;
|
|
227
152
|
}
|
|
228
153
|
async function createWorkspaceHealthReport(root = process.cwd(), options = {}) {
|
|
229
|
-
const workspaceRoot =
|
|
154
|
+
const workspaceRoot = resolve(root);
|
|
230
155
|
const config = readWorkspaceConfig(workspaceRoot);
|
|
231
156
|
const since = options.since ?? "origin/main";
|
|
232
157
|
const changed = options.changedOnly ? changedPaths(workspaceRoot, since) : /* @__PURE__ */ new Set();
|
|
@@ -406,8 +331,8 @@ async function cmdWorkspace(workspaceRoot = process.cwd(), args = ["workspace"])
|
|
|
406
331
|
const payload = options.json ? `${JSON.stringify(report, null, 2)}
|
|
407
332
|
` : options.markdown ? formatWorkspaceHealthMarkdown(report) : formatWorkspaceHealthText(report);
|
|
408
333
|
if (options.output) {
|
|
409
|
-
mkdirSync(
|
|
410
|
-
writeFileSync(
|
|
334
|
+
mkdirSync(dirname(resolve(workspaceRoot, options.output)), { recursive: true });
|
|
335
|
+
writeFileSync(resolve(workspaceRoot, options.output), payload, "utf-8");
|
|
411
336
|
if (!options.ci)
|
|
412
337
|
console.log(`${GREEN}Wrote Decantr workspace health:${RESET} ${options.output}`);
|
|
413
338
|
} else {
|
|
@@ -419,7 +344,6 @@ async function cmdWorkspace(workspaceRoot = process.cwd(), args = ["workspace"])
|
|
|
419
344
|
}
|
|
420
345
|
|
|
421
346
|
export {
|
|
422
|
-
resolveWorkspaceInfo,
|
|
423
347
|
listWorkspaceProjects,
|
|
424
348
|
listWorkspaceCandidates,
|
|
425
349
|
createWorkspaceHealthReport,
|
|
@@ -4989,7 +4989,8 @@ async function refreshDerivedFiles(projectRoot, essence, registry, prefetchedThe
|
|
|
4989
4989
|
themeData,
|
|
4990
4990
|
themeMode: mode,
|
|
4991
4991
|
voiceTone: storedVoice?.tone ? storedVoice.tone.split(".")[0] + "." : void 0,
|
|
4992
|
-
spatialHints: sectionSpatialHints
|
|
4992
|
+
spatialHints: sectionSpatialHints,
|
|
4993
|
+
adoptionMode: effectiveAdoptionMode
|
|
4993
4994
|
});
|
|
4994
4995
|
const sectionContextPath = join2(contextDir, `section-${section.id}.md`);
|
|
4995
4996
|
writeFileSync2(sectionContextPath, contextContent);
|
|
@@ -5345,7 +5346,8 @@ function generateSectionContext(input) {
|
|
|
5345
5346
|
themeHints,
|
|
5346
5347
|
constraints,
|
|
5347
5348
|
shellInfo,
|
|
5348
|
-
spatialHints
|
|
5349
|
+
spatialHints,
|
|
5350
|
+
adoptionMode
|
|
5349
5351
|
} = input;
|
|
5350
5352
|
const lines = [];
|
|
5351
5353
|
lines.push(`# Section: ${section.id}`);
|
|
@@ -5429,9 +5431,15 @@ function generateSectionContext(input) {
|
|
|
5429
5431
|
const decoratorDefs = input.themeData?.decorator_definitions;
|
|
5430
5432
|
const totalDecoratorCount = decoratorDefs && Object.keys(decoratorDefs).length || decorators.length;
|
|
5431
5433
|
if (totalDecoratorCount > 0) {
|
|
5432
|
-
|
|
5433
|
-
|
|
5434
|
-
|
|
5434
|
+
if (adoptionMode === "contract-only") {
|
|
5435
|
+
lines.push(
|
|
5436
|
+
`**Theme intent:** ${totalDecoratorCount} \`${themeName}-*\` decorator reference(s) exist, but this project is contract-only. Translate the intent into the app's current styling system instead of applying Decantr decorator classes directly.`
|
|
5437
|
+
);
|
|
5438
|
+
} else {
|
|
5439
|
+
lines.push(
|
|
5440
|
+
`**Theme decorators:** ${totalDecoratorCount} \`${themeName}-*\` classes \u2014 full Class/Intent/Apply-to table in \`section-${section.id}-pack.md\` (preferred) and DECANTR.md "Decorator Quick Reference". MUST apply.`
|
|
5441
|
+
);
|
|
5442
|
+
}
|
|
5435
5443
|
lines.push("");
|
|
5436
5444
|
}
|
|
5437
5445
|
if (themeHints) {
|
|
@@ -5454,9 +5462,15 @@ function generateSectionContext(input) {
|
|
|
5454
5462
|
}
|
|
5455
5463
|
const themePrefix = themeName.split("-")[0] || themeName;
|
|
5456
5464
|
lines.push("");
|
|
5457
|
-
|
|
5458
|
-
|
|
5459
|
-
|
|
5465
|
+
if (adoptionMode === "contract-only") {
|
|
5466
|
+
lines.push(
|
|
5467
|
+
"Usage: implement this section through the app's existing styling authority (design-system components, Tailwind/Sass/theme tokens, CVA variants, or accepted local rules). Do not add `@decantr/css`, `css(...)`, `d-*` treatments, or Decantr token CSS unless adoption mode changes."
|
|
5468
|
+
);
|
|
5469
|
+
} else {
|
|
5470
|
+
lines.push(
|
|
5471
|
+
`Usage: \`className={css('_flex _col _gap4') + ' d-surface ${themePrefix}-glass'}\` \u2014 atoms via css(), treatments and theme decorators as plain class strings.`
|
|
5472
|
+
);
|
|
5473
|
+
}
|
|
5460
5474
|
lines.push("");
|
|
5461
5475
|
lines.push("---");
|
|
5462
5476
|
lines.push("");
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import "./chunk-
|
|
2
|
-
import "./chunk-
|
|
3
|
-
import "./chunk-
|
|
4
|
-
import "./chunk-
|
|
1
|
+
import "./chunk-AXMGQ5IB.js";
|
|
2
|
+
import "./chunk-RXF7ZYGK.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-
|
|
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",
|
|
@@ -49,10 +49,10 @@
|
|
|
49
49
|
"dependencies": {
|
|
50
50
|
"ajv": "^8.20.0",
|
|
51
51
|
"@decantr/core": "2.1.0",
|
|
52
|
-
"@decantr/registry": "2.2.0",
|
|
53
52
|
"@decantr/essence-spec": "2.0.1",
|
|
53
|
+
"@decantr/registry": "2.2.0",
|
|
54
54
|
"@decantr/telemetry": "2.2.1",
|
|
55
|
-
"@decantr/verifier": "2.3.
|
|
55
|
+
"@decantr/verifier": "2.3.2"
|
|
56
56
|
},
|
|
57
57
|
"scripts": {
|
|
58
58
|
"build": "tsup",
|