@decantr/cli
Advanced tools
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
| import { | ||
| createProjectHealthReport, | ||
| listWorkspaceAppCandidateDetails | ||
| } from "./chunk-N7GSDDRH.js"; | ||
| // src/commands/workspace.ts | ||
| import { execFileSync } from "child_process"; | ||
| import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "fs"; | ||
| import { dirname, join, relative, resolve } from "path"; | ||
| import { WORKSPACE_HEALTH_REPORT_V2_SCHEMA_URL } from "@decantr/verifier"; | ||
| var BOLD = "\x1B[1m"; | ||
| var DIM = "\x1B[2m"; | ||
| var GREEN = "\x1B[32m"; | ||
| var RED = "\x1B[31m"; | ||
| var YELLOW = "\x1B[33m"; | ||
| var RESET = "\x1B[0m"; | ||
| var WORKSPACE_HEALTH_SCHEMA_URL = WORKSPACE_HEALTH_REPORT_V2_SCHEMA_URL; | ||
| var DEFAULT_IGNORES = /* @__PURE__ */ new Set([ | ||
| ".git", | ||
| ".next", | ||
| ".turbo", | ||
| ".vercel", | ||
| "coverage", | ||
| "dist", | ||
| "node_modules", | ||
| "playwright-report" | ||
| ]); | ||
| function workspaceConfigPath(root) { | ||
| return join(root, ".decantr", "workspace.json"); | ||
| } | ||
| function readWorkspaceConfig(root) { | ||
| const path = workspaceConfigPath(root); | ||
| if (!existsSync(path)) return null; | ||
| return JSON.parse(readFileSync(path, "utf-8")); | ||
| } | ||
| function normalizeProjectPath(raw) { | ||
| const normalized = raw.replace(/^\.\/+/, "").replace(/\/+$/, ""); | ||
| if (!normalized || normalized.startsWith("/") || normalized.includes("..") || normalized.includes("\\") || /\s/.test(normalized)) { | ||
| throw new Error(`Invalid workspace project path: ${raw}`); | ||
| } | ||
| return normalized; | ||
| } | ||
| function projectIdFromPath(path) { | ||
| return path.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "project"; | ||
| } | ||
| function discoverProjectPaths(root, config) { | ||
| const ignored = /* @__PURE__ */ new Set([...config?.ignore ?? [], ...DEFAULT_IGNORES]); | ||
| const results = /* @__PURE__ */ new Set(); | ||
| function walk(dir, depth) { | ||
| if (depth > 6) return; | ||
| const rel = relative(root, dir).replace(/\\/g, "/"); | ||
| if (rel && [...ignored].some((entry) => rel === entry || rel.startsWith(`${entry}/`))) return; | ||
| if (existsSync(join(dir, "decantr.essence.json"))) { | ||
| results.add(rel || "."); | ||
| return; | ||
| } | ||
| for (const entry of readdirSync(dir, { withFileTypes: true })) { | ||
| if (!entry.isDirectory() || entry.name.startsWith(".")) continue; | ||
| if (ignored.has(entry.name)) continue; | ||
| walk(join(dir, entry.name), depth + 1); | ||
| } | ||
| } | ||
| walk(root, 0); | ||
| return [...results].sort(); | ||
| } | ||
| function listWorkspaceProjects(root = process.cwd()) { | ||
| const workspaceRoot = resolve(root); | ||
| const config = readWorkspaceConfig(workspaceRoot); | ||
| const byPath = /* @__PURE__ */ new Map(); | ||
| for (const project of config?.projects ?? []) { | ||
| const path = normalizeProjectPath(project.path); | ||
| byPath.set(path, { | ||
| id: project.id ?? projectIdFromPath(path), | ||
| path, | ||
| absolutePath: resolve(workspaceRoot, path), | ||
| owner: project.owner ?? null, | ||
| tags: project.tags ?? [], | ||
| criticality: project.criticality ?? "normal", | ||
| browser: project.browser ?? config?.browser ?? false, | ||
| source: "manifest" | ||
| }); | ||
| } | ||
| for (const path of discoverProjectPaths(workspaceRoot, config)) { | ||
| if (byPath.has(path)) continue; | ||
| byPath.set(path, { | ||
| id: projectIdFromPath(path), | ||
| path, | ||
| absolutePath: resolve(workspaceRoot, path), | ||
| owner: null, | ||
| tags: [], | ||
| criticality: "normal", | ||
| browser: config?.browser ?? false, | ||
| source: "auto" | ||
| }); | ||
| } | ||
| return [...byPath.values()].sort((a, b) => a.path.localeCompare(b.path)); | ||
| } | ||
| function listWorkspaceCandidates(root = process.cwd(), projects = listWorkspaceProjects(root)) { | ||
| const attached = new Set(projects.map((project) => project.path)); | ||
| return listWorkspaceAppCandidateDetails(root).map((candidate, index) => ({ | ||
| path: candidate.path, | ||
| attached: attached.has(candidate.path), | ||
| suggestedAdoptCommand: `decantr adopt --project ${candidate.path} --yes`, | ||
| rank: index + 1, | ||
| score: candidate.score, | ||
| category: candidate.category, | ||
| reason: candidate.reason | ||
| })); | ||
| } | ||
| function changedPaths(root, since) { | ||
| try { | ||
| const output = execFileSync("git", ["diff", "--name-only", since, "--"], { | ||
| cwd: root, | ||
| encoding: "utf-8", | ||
| stdio: ["ignore", "pipe", "ignore"] | ||
| }); | ||
| return new Set( | ||
| output.split("\n").map((line) => line.trim()).filter(Boolean) | ||
| ); | ||
| } catch { | ||
| return /* @__PURE__ */ new Set(); | ||
| } | ||
| } | ||
| function projectChanged(project, changed) { | ||
| if (changed.size === 0) return false; | ||
| const prefix = project.path === "." ? "" : `${project.path}/`; | ||
| for (const path of changed) { | ||
| if (project.path === "." || path === project.path || path.startsWith(prefix)) return true; | ||
| } | ||
| return false; | ||
| } | ||
| async function withTimeout(promise, timeoutMs, label) { | ||
| let timeout; | ||
| const timer = new Promise((_, reject) => { | ||
| timeout = setTimeout( | ||
| () => reject(new Error(`${label} timed out after ${timeoutMs}ms`)), | ||
| timeoutMs | ||
| ); | ||
| }); | ||
| try { | ||
| return await Promise.race([promise, timer]); | ||
| } finally { | ||
| if (timeout) clearTimeout(timeout); | ||
| } | ||
| } | ||
| async function mapLimited(items, concurrency, fn) { | ||
| const results = new Array(items.length); | ||
| let next = 0; | ||
| async function worker() { | ||
| while (next < items.length) { | ||
| const index = next++; | ||
| results[index] = await fn(items[index]); | ||
| } | ||
| } | ||
| await Promise.all(Array.from({ length: Math.max(1, concurrency) }, () => worker())); | ||
| return results; | ||
| } | ||
| async function createWorkspaceHealthReport(root = process.cwd(), options = {}) { | ||
| const workspaceRoot = resolve(root); | ||
| const config = readWorkspaceConfig(workspaceRoot); | ||
| const since = options.since ?? "origin/main"; | ||
| const changed = options.changedOnly ? changedPaths(workspaceRoot, since) : /* @__PURE__ */ new Set(); | ||
| const allProjects = listWorkspaceProjects(workspaceRoot); | ||
| const projects = options.changedOnly ? allProjects.filter((project) => projectChanged(project, changed)) : allProjects; | ||
| const concurrency = options.concurrency ?? config?.concurrency ?? 4; | ||
| const timeoutMs = options.timeoutMs ?? config?.timeoutMs ?? 12e4; | ||
| const checked = await mapLimited(projects, concurrency, async (project) => { | ||
| const startedAt = Date.now(); | ||
| try { | ||
| const report = await withTimeout( | ||
| createProjectHealthReport(project.absolutePath, { | ||
| browser: options.browser ?? project.browser | ||
| }), | ||
| timeoutMs, | ||
| project.path | ||
| ); | ||
| return { | ||
| id: project.id, | ||
| path: project.path, | ||
| status: report.status, | ||
| score: report.score, | ||
| errorCount: report.summary.errorCount, | ||
| warnCount: report.summary.warnCount, | ||
| infoCount: report.summary.infoCount, | ||
| findingCount: report.summary.findingCount, | ||
| durationMs: Date.now() - startedAt, | ||
| changed: options.changedOnly ? projectChanged(project, changed) : false, | ||
| source: project.source, | ||
| error: null, | ||
| loopState: report.loop.state, | ||
| loopNextAction: report.loop.nextActions[0] ?? null | ||
| }; | ||
| } catch (error) { | ||
| return { | ||
| id: project.id, | ||
| path: project.path, | ||
| status: "failed", | ||
| score: 0, | ||
| errorCount: 1, | ||
| warnCount: 0, | ||
| infoCount: 0, | ||
| findingCount: 1, | ||
| durationMs: Date.now() - startedAt, | ||
| changed: options.changedOnly ? projectChanged(project, changed) : false, | ||
| source: project.source, | ||
| error: error.message, | ||
| loopState: "blocked_missing_context", | ||
| loopNextAction: "Fix the project health failure, then rerun workspace health." | ||
| }; | ||
| } | ||
| }); | ||
| const summary = { | ||
| projectCount: allProjects.length, | ||
| checkedCount: checked.length, | ||
| healthyCount: checked.filter((project) => project.status === "healthy").length, | ||
| warningCount: checked.filter((project) => project.status === "warning").length, | ||
| errorCount: checked.filter((project) => project.status === "error").length, | ||
| failedCount: checked.filter((project) => project.status === "failed").length | ||
| }; | ||
| const blockedCount = checked.filter( | ||
| (project) => project.loopState?.startsWith("blocked") || project.loopState === "human_resolution_required" | ||
| ).length; | ||
| const repairRequiredCount = checked.filter( | ||
| (project) => project.loopState === "repair_required" | ||
| ).length; | ||
| const workspaceLoopState = checked.length === 0 ? "needs_context" : blockedCount > 0 ? "human_resolution_required" : repairRequiredCount > 0 || summary.errorCount > 0 || summary.warningCount > 0 ? "repair_required" : "verified"; | ||
| const workspaceLoopStatus = workspaceLoopState === "human_resolution_required" || workspaceLoopState.startsWith("blocked") ? "blocked" : summary.errorCount > 0 || summary.failedCount > 0 ? "error" : summary.warningCount > 0 ? "warning" : "healthy"; | ||
| return { | ||
| $schema: WORKSPACE_HEALTH_SCHEMA_URL, | ||
| generatedAt: (/* @__PURE__ */ new Date()).toISOString(), | ||
| workspaceRoot, | ||
| changedOnly: options.changedOnly ?? false, | ||
| since: options.changedOnly ? since : null, | ||
| summary, | ||
| loop: { | ||
| state: workspaceLoopState, | ||
| status: workspaceLoopStatus, | ||
| projectCount: checked.length, | ||
| blockedCount, | ||
| repairRequiredCount, | ||
| nextActions: [ | ||
| workspaceLoopState === "verified" ? "Workspace loop verified." : 'Open the highest-risk project, run `decantr task <target> "<intent>"`, repair, then rerun `decantr verify`.' | ||
| ] | ||
| }, | ||
| projects: checked | ||
| }; | ||
| } | ||
| function formatWorkspaceHealthText(report) { | ||
| const lines = [ | ||
| `${BOLD}Decantr Workspace Health${RESET}`, | ||
| "", | ||
| `Projects: ${report.summary.checkedCount}/${report.summary.projectCount}`, | ||
| `Healthy: ${report.summary.healthyCount} | Warnings: ${report.summary.warningCount} | Errors: ${report.summary.errorCount} | Failed: ${report.summary.failedCount}`, | ||
| `Loop: ${report.loop.state} | blocked ${report.loop.blockedCount} | repair ${report.loop.repairRequiredCount}`, | ||
| "" | ||
| ]; | ||
| for (const project of report.projects) { | ||
| const color = project.status === "healthy" ? GREEN : project.status === "warning" ? YELLOW : RED; | ||
| lines.push( | ||
| `${color}${String(project.status).toUpperCase()}${RESET} ${project.path} score ${project.score}/100 findings ${project.findingCount}` | ||
| ); | ||
| if (project.error) lines.push(` ${DIM}${project.error}${RESET}`); | ||
| } | ||
| return `${lines.join("\n")} | ||
| `; | ||
| } | ||
| function formatWorkspaceHealthMarkdown(report) { | ||
| const lines = [ | ||
| "# Decantr Workspace Health", | ||
| "", | ||
| `- Projects checked: **${report.summary.checkedCount}/${report.summary.projectCount}**`, | ||
| `- Healthy: ${report.summary.healthyCount}`, | ||
| `- Warnings: ${report.summary.warningCount}`, | ||
| `- Errors: ${report.summary.errorCount}`, | ||
| `- Failed: ${report.summary.failedCount}`, | ||
| "", | ||
| "| Project | Status | Score | Findings | Source |", | ||
| "| --- | --- | ---: | ---: | --- |" | ||
| ]; | ||
| for (const project of report.projects) { | ||
| lines.push( | ||
| `| \`${project.path}\` | ${project.status} | ${project.score} | ${project.findingCount} | ${project.source} |` | ||
| ); | ||
| } | ||
| return `${lines.join("\n")} | ||
| `; | ||
| } | ||
| function shouldFailWorkspaceHealth(report, failOn = "error") { | ||
| if (failOn === "none") return false; | ||
| if (report.summary.failedCount > 0 || report.summary.errorCount > 0) return true; | ||
| return failOn === "warn" && report.summary.warningCount > 0; | ||
| } | ||
| function parseHealthFailOn(value) { | ||
| if (value === "warn" || value === "none") return value; | ||
| return "error"; | ||
| } | ||
| function parseWorkspaceArgs(args) { | ||
| const subcommand = args[1] === "health" ? "health" : "list"; | ||
| const options = { subcommand }; | ||
| for (let index = 2; index < args.length; index += 1) { | ||
| const arg = args[index]; | ||
| if (arg === "--json") options.json = true; | ||
| else if (arg === "--markdown") options.markdown = true; | ||
| else if (arg === "--ci") options.ci = true; | ||
| else if (arg === "--browser") options.browser = true; | ||
| else if (arg === "--changed") options.changedOnly = true; | ||
| else if (arg === "--since" && args[index + 1]) options.since = args[++index]; | ||
| else if (arg.startsWith("--since=")) options.since = arg.split("=")[1]; | ||
| else if (arg === "--output" && args[index + 1]) options.output = args[++index]; | ||
| else if (arg.startsWith("--output=")) options.output = arg.split("=")[1]; | ||
| else if (arg === "--fail-on" && args[index + 1]) | ||
| options.failOn = parseHealthFailOn(args[++index]); | ||
| else if (arg.startsWith("--fail-on=")) options.failOn = parseHealthFailOn(arg.split("=")[1]); | ||
| else if (arg === "--concurrency" && args[index + 1]) | ||
| options.concurrency = Number(args[++index]); | ||
| else if (arg.startsWith("--concurrency=")) options.concurrency = Number(arg.split("=")[1]); | ||
| else if (arg === "--timeout-ms" && args[index + 1]) options.timeoutMs = Number(args[++index]); | ||
| else if (arg.startsWith("--timeout-ms=")) options.timeoutMs = Number(arg.split("=")[1]); | ||
| } | ||
| return options; | ||
| } | ||
| async function cmdWorkspace(workspaceRoot = process.cwd(), args = ["workspace"]) { | ||
| const options = parseWorkspaceArgs(args); | ||
| if (options.subcommand === "list") { | ||
| const projects = listWorkspaceProjects(workspaceRoot); | ||
| const candidates = listWorkspaceCandidates(workspaceRoot, projects); | ||
| const unattachedCandidates = candidates.filter((candidate) => !candidate.attached); | ||
| const payload2 = `${JSON.stringify({ projects, candidates }, null, 2)} | ||
| `; | ||
| if (options.json) { | ||
| process.stdout.write(payload2); | ||
| return; | ||
| } | ||
| console.log(`${BOLD}Decantr workspace projects${RESET}`); | ||
| console.log(""); | ||
| console.log("Attached Decantr projects:"); | ||
| if (projects.length === 0) { | ||
| console.log(` ${DIM}(none yet)${RESET}`); | ||
| } else { | ||
| for (const project of projects) { | ||
| console.log(` ${project.path} ${DIM}${project.source}${RESET}`); | ||
| } | ||
| } | ||
| if (candidates.length > 0) { | ||
| console.log(""); | ||
| console.log("App candidates:"); | ||
| for (const candidate of candidates) { | ||
| const status = candidate.attached ? `${GREEN}attached${RESET}` : `${YELLOW}unattached${RESET}`; | ||
| console.log( | ||
| ` #${candidate.rank} ${candidate.path} ${DIM}${status} \xB7 ${candidate.category} \xB7 score ${candidate.score}${RESET}` | ||
| ); | ||
| } | ||
| } | ||
| if (unattachedCandidates.length > 0) { | ||
| console.log(""); | ||
| console.log(projects.length > 0 ? "Attach another app:" : "Start by attaching one app:"); | ||
| console.log(` ${unattachedCandidates[0].suggestedAdoptCommand}`); | ||
| } | ||
| return; | ||
| } | ||
| const report = await createWorkspaceHealthReport(workspaceRoot, options); | ||
| const payload = options.json ? `${JSON.stringify(report, null, 2)} | ||
| ` : options.markdown ? formatWorkspaceHealthMarkdown(report) : formatWorkspaceHealthText(report); | ||
| if (options.output) { | ||
| mkdirSync(dirname(resolve(workspaceRoot, options.output)), { recursive: true }); | ||
| writeFileSync(resolve(workspaceRoot, options.output), payload, "utf-8"); | ||
| if (!options.ci) | ||
| console.log(`${GREEN}Wrote Decantr workspace health:${RESET} ${options.output}`); | ||
| } else { | ||
| process.stdout.write(payload); | ||
| } | ||
| if (options.ci && shouldFailWorkspaceHealth(report, options.failOn ?? "error")) { | ||
| process.exitCode = 1; | ||
| } | ||
| } | ||
| export { | ||
| listWorkspaceProjects, | ||
| listWorkspaceCandidates, | ||
| createWorkspaceHealthReport, | ||
| formatWorkspaceHealthText, | ||
| formatWorkspaceHealthMarkdown, | ||
| shouldFailWorkspaceHealth, | ||
| parseWorkspaceArgs, | ||
| cmdWorkspace | ||
| }; |
| import { | ||
| cmdHealth, | ||
| collectDesignTokenEvidence, | ||
| createProjectEvidenceBundle, | ||
| createProjectHealthReport, | ||
| evaluateHealthBaselineGate, | ||
| formatDiagnosticCatalogJson, | ||
| formatDiagnosticCatalogMarkdown, | ||
| formatDiagnosticCatalogText, | ||
| formatProjectHealthJson, | ||
| formatProjectHealthMarkdown, | ||
| formatProjectHealthText, | ||
| parseHealthArgs, | ||
| renderProjectHealthCiWorkflow, | ||
| shouldFailHealth, | ||
| shouldFailHealthBaselineGate, | ||
| writeProjectHealthCiWorkflow | ||
| } from "./chunk-N7GSDDRH.js"; | ||
| import "./chunk-BVKXRTYP.js"; | ||
| export { | ||
| cmdHealth, | ||
| collectDesignTokenEvidence, | ||
| createProjectEvidenceBundle, | ||
| createProjectHealthReport, | ||
| evaluateHealthBaselineGate, | ||
| formatDiagnosticCatalogJson, | ||
| formatDiagnosticCatalogMarkdown, | ||
| formatDiagnosticCatalogText, | ||
| formatProjectHealthJson, | ||
| formatProjectHealthMarkdown, | ||
| formatProjectHealthText, | ||
| parseHealthArgs, | ||
| renderProjectHealthCiWorkflow, | ||
| shouldFailHealth, | ||
| shouldFailHealthBaselineGate, | ||
| writeProjectHealthCiWorkflow | ||
| }; |
Sorry, the diff of this file is too big to display
| import { | ||
| cmdWorkspace, | ||
| createWorkspaceHealthReport, | ||
| formatWorkspaceHealthMarkdown, | ||
| formatWorkspaceHealthText, | ||
| listWorkspaceCandidates, | ||
| listWorkspaceProjects, | ||
| parseWorkspaceArgs, | ||
| shouldFailWorkspaceHealth | ||
| } from "./chunk-ZM6SAB4Y.js"; | ||
| import "./chunk-N7GSDDRH.js"; | ||
| import "./chunk-BVKXRTYP.js"; | ||
| export { | ||
| cmdWorkspace, | ||
| createWorkspaceHealthReport, | ||
| formatWorkspaceHealthMarkdown, | ||
| formatWorkspaceHealthText, | ||
| listWorkspaceCandidates, | ||
| listWorkspaceProjects, | ||
| parseWorkspaceArgs, | ||
| shouldFailWorkspaceHealth | ||
| }; |
+3
-3
| #!/usr/bin/env node | ||
| import "./chunk-4FKGE5XD.js"; | ||
| import "./chunk-OMPJDJWW.js"; | ||
| import "./chunk-ZZDLYY72.js"; | ||
| import "./chunk-FY2VQ6LD.js"; | ||
| import "./chunk-72FITODT.js"; | ||
| import "./chunk-ZM6SAB4Y.js"; | ||
| import "./chunk-N7GSDDRH.js"; | ||
| import "./chunk-BVKXRTYP.js"; |
+3
-3
@@ -1,5 +0,5 @@ | ||
| import "./chunk-4FKGE5XD.js"; | ||
| import "./chunk-OMPJDJWW.js"; | ||
| import "./chunk-ZZDLYY72.js"; | ||
| import "./chunk-FY2VQ6LD.js"; | ||
| import "./chunk-72FITODT.js"; | ||
| import "./chunk-ZM6SAB4Y.js"; | ||
| import "./chunk-N7GSDDRH.js"; | ||
| import "./chunk-BVKXRTYP.js"; |
+3
-3
@@ -14,6 +14,6 @@ { | ||
| "@decantr/telemetry": "3.8.1", | ||
| "@decantr/verifier": "3.10.0", | ||
| "@decantr/verifier": "3.11.0", | ||
| "ajv": "^8.20.0" | ||
| }, | ||
| "description": "Authority-aware UI change control for frontend codebases touched by AI agents", | ||
| "description": "Diff-first UI change assurance and authority-aware governance for frontend codebases touched by AI agents", | ||
| "engines": { | ||
@@ -66,3 +66,3 @@ "node": ">=20.19.0" | ||
| "type": "module", | ||
| "version": "3.10.0" | ||
| "version": "3.11.0" | ||
| } |
+26
-10
@@ -10,5 +10,5 @@ # @decantr/cli | ||
| The current published stable version is Decantr 3.10.0. It supports independent UI authority axes, route and non-route target preparation, compatible route-backed `TaskCapsuleV1`, adoption truth, explicit CI v3, and governance deltas, but it is not quantitatively adoption-proven. | ||
| The current stable version is Decantr 3.11.0. Bare `decantr verify` is now zero-write Changed-UI Assurance: it scopes the current Git change, auto-selects one changed UI app when provable, and reports at most three source-anchored findings. It is not quantitatively adoption-proven. | ||
| The primary CLI loop is **Observe -> Prepare -> Verify -> Report** through `scan`, `task <target>`, `verify`, and `ci`. Routes are one UI surface among layouts, components, stories, overlays, flows, packages, runtime states, and exact files. Shipped 3.10 behavior must not be presented as evidence that Decantr improves frontier models; that remains a separate research question. | ||
| The zero-setup entry point is `verify`. The deeper CLI loop remains **Observe -> Prepare -> Verify -> Report** through `scan`, `task <target>`, `verify --full`, and `ci`. Routes are one UI surface among layouts, components, stories, overlays, flows, packages, runtime states, and exact files. Shipped behavior must not be presented as evidence that Decantr improves frontier models; that remains a separate research question. | ||
@@ -24,3 +24,4 @@ ## Install | ||
| ```bash | ||
| npx @decantr/cli scan | ||
| npx @decantr/cli@3.11.0 verify | ||
| npx @decantr/cli@3.11.0 scan | ||
| npx @decantr/cli new my-app --blueprint=esports-hq | ||
@@ -31,3 +32,3 @@ ``` | ||
| Use `decantr task <target> "<intent>"` to **Prepare** one bounded change. Attached graph-backed routes retain `TaskCapsuleV1`; an exact surface ID, component, layout, overlay, story, package, or `file:<path>` selector can return read-only discovery context before adoption. Unknown, ambiguous, unresolved, or non-taskable targets fail closed; static non-route evidence is normally `limited` unless runtime reachability proves more. | ||
| Use `decantr verify` to **Verify** the edit against the available project evidence. Use `decantr ci` to **Report** typed evidence; CI v3 remains explicit through `--report-version v3`, and missing or incompatible proof remains `not_proven`. | ||
| Use bare `decantr verify` to check only the current UI change with no adoption or write. Use `decantr verify --full` for the previous Project Health workflow. Use `decantr ci` to **Report** typed evidence; explicit CI v3 includes the same verifier-owned change-assurance report, and missing or incompatible proof remains `not_proven`. | ||
| Use `decantr setup` when you are unsure which attach or compatibility path applies. It detects whether the repo is empty, already attached, or a Brownfield app and recommends the right entry path. | ||
@@ -42,3 +43,3 @@ Use `decantr new` for a greenfield workspace in a fresh directory. With a blueprint or archetype it creates a contract-only Decantr workspace by default; runnable legacy Decantr CSS adapters require explicit `--adoption=decantr-css`. | ||
| Compatibility Decantr CSS starter adapters, only when `--adoption=decantr-css` is explicit. These receive no 3.10 feature investment: | ||
| Compatibility Decantr CSS starter adapters, only when `--adoption=decantr-css` is explicit. These receive no 3.11 feature investment: | ||
@@ -58,2 +59,4 @@ - `react-vite` is the React + Vite runnable bootstrap adapter | ||
| decantr setup | ||
| decantr verify | ||
| decantr verify --since origin/main --ci | ||
| decantr scan | ||
@@ -146,4 +149,14 @@ decantr scan --project apps/web --json | ||
| ## Shipped 3.10 Additions | ||
| ## Shipped 3.11 Addition | ||
| - bare `decantr verify` selects `change-assurance-report.v1` with complete Git change scope and zero writes | ||
| - exactly one changed app is auto-selected when provable; ambiguous multi-app work fails closed | ||
| - default output is capped at three consequential authority, component-reuse, or style findings with exact source lines and repair targets | ||
| - tests, fixtures, stories, generated files, build output, and sibling apps cannot become production authority | ||
| - explicit CI v3 and MCP `decantr_verify` action `changes` carry the same verifier-owned report | ||
| Primitive-reuse enforcement is strongest for JSX/TSX in 3.11. Angular, Vue, and other template parity remains an explicit limitation. See the [Change Assurance contract](https://decantr.ai/reference/change-assurance.md). | ||
| ## Shipped 3.10 Foundation | ||
| - independent UI-surface readiness and evidence-adapter data in source-tree scan reports | ||
@@ -153,7 +166,7 @@ - target-based non-route and pre-adoption discovery context through `ui-surface-task-context.v1` | ||
| These additions ship in 3.10.0. Their deterministic behavior is covered by tests and regression replays; they do not establish a frontier-model improvement claim. | ||
| These additions shipped in 3.10.0 and remain the authority foundation beneath 3.11. Their deterministic behavior is covered by tests and regression replays; it does not establish a frontier-model improvement claim. | ||
| ## Security And Permissions | ||
| The CLI is intentionally a local project inspector and artifact writer. It reads selected project/workspace files, package manifests, routing/style/config files, `.decantr` artifacts, and Decantr cache/config files. It writes `decantr.essence.json`, `DECANTR.md`, `.decantr/*`, generated context packs, `.decantr/graph/*` typed graph artifacts, optional CI workflows/snippets, optional Cursor MCP/rule files, optional style/export files, and auth/telemetry config only when explicitly requested. `decantr scan` is the exception by design: it reads and prints only, and does not create `.decantr`, save reports, upload source, run package scripts, or install dependencies. | ||
| The CLI is intentionally a local project inspector and artifact writer. It reads selected project/workspace files, package manifests, routing/style/config files, `.decantr` artifacts, and Decantr cache/config files. It writes `decantr.essence.json`, `DECANTR.md`, `.decantr/*`, generated context packs, `.decantr/graph/*` typed graph artifacts, optional CI workflows/snippets, optional Cursor MCP/rule files, optional style/export files, and auth/telemetry config only when explicitly requested. Bare `decantr verify` and `decantr scan` read and print only; changed-UI verify writes only when an explicit `--output` is supplied. Neither path creates `.decantr`, uploads source, runs package scripts, or installs dependencies. | ||
@@ -166,2 +179,5 @@ Telemetry is disabled by default. Content API reads and pack hydration are explicit command paths; hosted critique/audit uploads are retired. Screenshots and Evidence Bundles stay local. Release audits prove the installed package with `npm pack --dry-run --json`. See [security permissions](https://decantr.ai/reference/security-permissions.md). | ||
| decantr setup | ||
| decantr verify | ||
| decantr verify --since origin/main --ci | ||
| decantr verify --full --brownfield --local-patterns | ||
| decantr scan | ||
@@ -222,3 +238,3 @@ decantr scan --project apps/web | ||
| `decantr verify` is the workflow command most users should run locally after edits. It delegates to Project Health, can add Brownfield guard validation with `--brownfield`, requires an accepted local pattern pack with `--local-patterns`, scans `.decantr/rules.json` when present, supports workspace mode, and writes evidence to `.decantr/evidence/latest.json` by default when `--evidence` is used. | ||
| Bare `decantr verify` is the workflow command most users should run locally after edits. It performs Changed-UI Assurance over the current Git scope and writes nothing. `--project`, `--since`, `--json`, `--markdown`, `--ci`, and an explicit `--output` refine that workflow. It selects Project Health when `--full` or an existing health-only flag is present; full mode can add Brownfield guard validation with `--brownfield`, require an accepted local pattern pack with `--local-patterns`, scan `.decantr/rules.json`, support workspace mode, and write evidence when `--evidence` is used. | ||
@@ -229,3 +245,3 @@ `decantr doctor` explains project/workspace state, adoption mode, adoption lane, generated artifacts, typed graph readiness, local law, visual evidence, design authority signals, CI wiring, and an ordered next-step queue. 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. | ||
| `decantr ci` is the blessed non-mutating automation gate. V2 remains the default and preserves its shipped baseline behavior. Explicit `--report-version v3` emits `decantr-ci-report.v3` with the existing health evidence plus verifier-owned `AdoptionTruthV1` and `GovernanceDeltaV1`; project mode may add `--since <git-ref>` for changed-file scope, and workspace mode carries per-project contracts plus a deterministic aggregate gate. Missing, stale, or incompatible baseline/change evidence is `not_proven`, not an empty successful delta, and is non-passing unless `--fail-on none` is explicit. `decantr ci init --report-version v3` opts a generated workflow into v3 and configures full Git history/base-ref collection; existing and newly generated workflows stay on v2 without that flag. | ||
| `decantr ci` is the blessed non-mutating automation gate. V2 remains the default and preserves its shipped baseline behavior. Explicit `--report-version v3` emits `decantr-ci-report.v3` with the existing health evidence plus verifier-owned `AdoptionTruthV1`, `GovernanceDeltaV1`, and Changed-UI Assurance; project mode may add `--since <git-ref>` for changed-file scope, and workspace mode carries per-project contracts plus a deterministic aggregate gate. Missing, stale, or incompatible baseline/change evidence is `not_proven`, not an empty successful delta, and is non-passing unless `--fail-on none` is explicit. `decantr ci init --report-version v3` opts a generated workflow into v3 and configures full Git history/base-ref collection; existing and newly generated workflows stay on v2 without that flag. | ||
@@ -232,0 +248,0 @@ `decantr health` remains the advanced project observability primitive. It composes the existing verifier audit, guard checks, brownfield route drift checks, runtime evidence, component reuse drift, accepted style bridge drift, accepted behavior-obligation checks, typed Contract graph freshness, and execution-pack files into a v2 `ProjectHealthReport` with status, score, route summary, pack summary, findings, stable diagnostic codes, typed repair IDs, evidence tier, authority resolution, loop readiness, and AI-ready remediation prompts. The graph freshness slice emits `GRAPH001` / `regenerate-typed-graph` when an attached app has missing, stale, or non-derivable `.decantr/graph` artifacts. The component reuse slice emits `COMP001` / `import-existing-component` when production source locally redeclares a common primitive that already exists as an exported reusable component, and `COMP010` / `replace-raw-control-with-local-component` when production JSX renders generic raw controls such as `<button>` or text-like `<input>` while the project already owns a reusable primitive. Specialized inputs such as file, hidden, checkbox, radio, color, range, and Dropzone `getInputProps()` controls are not treated as generic `Input` drift. Source audits also exclude tests, fixtures, generated files, and testing directories; explicit router guards satisfy protected-surface topology; generic callback utilities and fixed-position components need semantic evidence before they are treated as auth callbacks or dialogs. The behavior-obligation slice emits `A11Y010`, `A11Y011`, `INT010`, `INT011`, `INT012`, `INT013`, and `COMP020` for high-confidence dialog/form regressions such as missing accessible names, missing label associations, missing visible destructive consequence copy, missing cancel affordances, missing submitting guards, implicit form button types, or bypassed project-owned interaction primitives. The style bridge slice emits `TOKEN010` / `replace-arbitrary-style-with-bridge-token` when production JSX, common class helpers, hardcoded inline color styles, or hardcoded visual values in CSS/module stylesheets bypass `.decantr/style-bridge.json` after it has been accepted as project-owned style authority. The baseline slice emits `VISUAL010` / `review-visual-baseline-drift` when `--since-baseline` detects changed screenshot hashes. When `.decantr/graph/graph.snapshot.json` exists, each finding is anchored to the most specific graph node Decantr can resolve, and JSON, markdown, text output, repair prompts, and Evidence Bundles carry that anchor. `decantr graph` also writes content-addressed history snapshots under `.decantr/graph/snapshots/` so repeated graph runs can be replayed across an AI edit sequence. When `.decantr/analysis.json` exists, `decantr graph` links observed routes/pages to implementation source artifacts and links exported reusable component declarations to their source files. When browser evidence writes `.decantr/evidence/visual-manifest.json`, `decantr graph` ingests it as local route/page Evidence nodes without uploading screenshots. When `.decantr/evidence/latest.json` exists, `decantr graph` can also materialize saved findings, evidence strings, graph anchors, repair IDs, and referenced repair/read target files as typed graph nodes and edges. Health-baseline diffs remain continuity artifacts and are deliberately not graph inputs, preventing a continuity check from making the graph stale. |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
| import { | ||
| createProjectHealthReport, | ||
| listWorkspaceAppCandidateDetails | ||
| } from "./chunk-72FITODT.js"; | ||
| // src/commands/workspace.ts | ||
| import { execFileSync } from "child_process"; | ||
| import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "fs"; | ||
| import { dirname, join, relative, resolve } from "path"; | ||
| import { WORKSPACE_HEALTH_REPORT_V2_SCHEMA_URL } from "@decantr/verifier"; | ||
| var BOLD = "\x1B[1m"; | ||
| var DIM = "\x1B[2m"; | ||
| var GREEN = "\x1B[32m"; | ||
| var RED = "\x1B[31m"; | ||
| var YELLOW = "\x1B[33m"; | ||
| var RESET = "\x1B[0m"; | ||
| var WORKSPACE_HEALTH_SCHEMA_URL = WORKSPACE_HEALTH_REPORT_V2_SCHEMA_URL; | ||
| var DEFAULT_IGNORES = /* @__PURE__ */ new Set([ | ||
| ".git", | ||
| ".next", | ||
| ".turbo", | ||
| ".vercel", | ||
| "coverage", | ||
| "dist", | ||
| "node_modules", | ||
| "playwright-report" | ||
| ]); | ||
| function workspaceConfigPath(root) { | ||
| return join(root, ".decantr", "workspace.json"); | ||
| } | ||
| function readWorkspaceConfig(root) { | ||
| const path = workspaceConfigPath(root); | ||
| if (!existsSync(path)) return null; | ||
| return JSON.parse(readFileSync(path, "utf-8")); | ||
| } | ||
| function normalizeProjectPath(raw) { | ||
| const normalized = raw.replace(/^\.\/+/, "").replace(/\/+$/, ""); | ||
| if (!normalized || normalized.startsWith("/") || normalized.includes("..") || normalized.includes("\\") || /\s/.test(normalized)) { | ||
| throw new Error(`Invalid workspace project path: ${raw}`); | ||
| } | ||
| return normalized; | ||
| } | ||
| function projectIdFromPath(path) { | ||
| return path.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "project"; | ||
| } | ||
| function discoverProjectPaths(root, config) { | ||
| const ignored = /* @__PURE__ */ new Set([...config?.ignore ?? [], ...DEFAULT_IGNORES]); | ||
| const results = /* @__PURE__ */ new Set(); | ||
| function walk(dir, depth) { | ||
| if (depth > 6) return; | ||
| const rel = relative(root, dir).replace(/\\/g, "/"); | ||
| if (rel && [...ignored].some((entry) => rel === entry || rel.startsWith(`${entry}/`))) return; | ||
| if (existsSync(join(dir, "decantr.essence.json"))) { | ||
| results.add(rel || "."); | ||
| return; | ||
| } | ||
| for (const entry of readdirSync(dir, { withFileTypes: true })) { | ||
| if (!entry.isDirectory() || entry.name.startsWith(".")) continue; | ||
| if (ignored.has(entry.name)) continue; | ||
| walk(join(dir, entry.name), depth + 1); | ||
| } | ||
| } | ||
| walk(root, 0); | ||
| return [...results].sort(); | ||
| } | ||
| function listWorkspaceProjects(root = process.cwd()) { | ||
| const workspaceRoot = resolve(root); | ||
| const config = readWorkspaceConfig(workspaceRoot); | ||
| const byPath = /* @__PURE__ */ new Map(); | ||
| for (const project of config?.projects ?? []) { | ||
| const path = normalizeProjectPath(project.path); | ||
| byPath.set(path, { | ||
| id: project.id ?? projectIdFromPath(path), | ||
| path, | ||
| absolutePath: resolve(workspaceRoot, path), | ||
| owner: project.owner ?? null, | ||
| tags: project.tags ?? [], | ||
| criticality: project.criticality ?? "normal", | ||
| browser: project.browser ?? config?.browser ?? false, | ||
| source: "manifest" | ||
| }); | ||
| } | ||
| for (const path of discoverProjectPaths(workspaceRoot, config)) { | ||
| if (byPath.has(path)) continue; | ||
| byPath.set(path, { | ||
| id: projectIdFromPath(path), | ||
| path, | ||
| absolutePath: resolve(workspaceRoot, path), | ||
| owner: null, | ||
| tags: [], | ||
| criticality: "normal", | ||
| browser: config?.browser ?? false, | ||
| source: "auto" | ||
| }); | ||
| } | ||
| return [...byPath.values()].sort((a, b) => a.path.localeCompare(b.path)); | ||
| } | ||
| function listWorkspaceCandidates(root = process.cwd(), projects = listWorkspaceProjects(root)) { | ||
| const attached = new Set(projects.map((project) => project.path)); | ||
| return listWorkspaceAppCandidateDetails(root).map((candidate, index) => ({ | ||
| path: candidate.path, | ||
| attached: attached.has(candidate.path), | ||
| suggestedAdoptCommand: `decantr adopt --project ${candidate.path} --yes`, | ||
| rank: index + 1, | ||
| score: candidate.score, | ||
| category: candidate.category, | ||
| reason: candidate.reason | ||
| })); | ||
| } | ||
| function changedPaths(root, since) { | ||
| try { | ||
| const output = execFileSync("git", ["diff", "--name-only", since, "--"], { | ||
| cwd: root, | ||
| encoding: "utf-8", | ||
| stdio: ["ignore", "pipe", "ignore"] | ||
| }); | ||
| return new Set( | ||
| output.split("\n").map((line) => line.trim()).filter(Boolean) | ||
| ); | ||
| } catch { | ||
| return /* @__PURE__ */ new Set(); | ||
| } | ||
| } | ||
| function projectChanged(project, changed) { | ||
| if (changed.size === 0) return false; | ||
| const prefix = project.path === "." ? "" : `${project.path}/`; | ||
| for (const path of changed) { | ||
| if (project.path === "." || path === project.path || path.startsWith(prefix)) return true; | ||
| } | ||
| return false; | ||
| } | ||
| async function withTimeout(promise, timeoutMs, label) { | ||
| let timeout; | ||
| const timer = new Promise((_, reject) => { | ||
| timeout = setTimeout( | ||
| () => reject(new Error(`${label} timed out after ${timeoutMs}ms`)), | ||
| timeoutMs | ||
| ); | ||
| }); | ||
| try { | ||
| return await Promise.race([promise, timer]); | ||
| } finally { | ||
| if (timeout) clearTimeout(timeout); | ||
| } | ||
| } | ||
| async function mapLimited(items, concurrency, fn) { | ||
| const results = new Array(items.length); | ||
| let next = 0; | ||
| async function worker() { | ||
| while (next < items.length) { | ||
| const index = next++; | ||
| results[index] = await fn(items[index]); | ||
| } | ||
| } | ||
| await Promise.all(Array.from({ length: Math.max(1, concurrency) }, () => worker())); | ||
| return results; | ||
| } | ||
| async function createWorkspaceHealthReport(root = process.cwd(), options = {}) { | ||
| const workspaceRoot = resolve(root); | ||
| const config = readWorkspaceConfig(workspaceRoot); | ||
| const since = options.since ?? "origin/main"; | ||
| const changed = options.changedOnly ? changedPaths(workspaceRoot, since) : /* @__PURE__ */ new Set(); | ||
| const allProjects = listWorkspaceProjects(workspaceRoot); | ||
| const projects = options.changedOnly ? allProjects.filter((project) => projectChanged(project, changed)) : allProjects; | ||
| const concurrency = options.concurrency ?? config?.concurrency ?? 4; | ||
| const timeoutMs = options.timeoutMs ?? config?.timeoutMs ?? 12e4; | ||
| const checked = await mapLimited(projects, concurrency, async (project) => { | ||
| const startedAt = Date.now(); | ||
| try { | ||
| const report = await withTimeout( | ||
| createProjectHealthReport(project.absolutePath, { | ||
| browser: options.browser ?? project.browser | ||
| }), | ||
| timeoutMs, | ||
| project.path | ||
| ); | ||
| return { | ||
| id: project.id, | ||
| path: project.path, | ||
| status: report.status, | ||
| score: report.score, | ||
| errorCount: report.summary.errorCount, | ||
| warnCount: report.summary.warnCount, | ||
| infoCount: report.summary.infoCount, | ||
| findingCount: report.summary.findingCount, | ||
| durationMs: Date.now() - startedAt, | ||
| changed: options.changedOnly ? projectChanged(project, changed) : false, | ||
| source: project.source, | ||
| error: null, | ||
| loopState: report.loop.state, | ||
| loopNextAction: report.loop.nextActions[0] ?? null | ||
| }; | ||
| } catch (error) { | ||
| return { | ||
| id: project.id, | ||
| path: project.path, | ||
| status: "failed", | ||
| score: 0, | ||
| errorCount: 1, | ||
| warnCount: 0, | ||
| infoCount: 0, | ||
| findingCount: 1, | ||
| durationMs: Date.now() - startedAt, | ||
| changed: options.changedOnly ? projectChanged(project, changed) : false, | ||
| source: project.source, | ||
| error: error.message, | ||
| loopState: "blocked_missing_context", | ||
| loopNextAction: "Fix the project health failure, then rerun workspace health." | ||
| }; | ||
| } | ||
| }); | ||
| const summary = { | ||
| projectCount: allProjects.length, | ||
| checkedCount: checked.length, | ||
| healthyCount: checked.filter((project) => project.status === "healthy").length, | ||
| warningCount: checked.filter((project) => project.status === "warning").length, | ||
| errorCount: checked.filter((project) => project.status === "error").length, | ||
| failedCount: checked.filter((project) => project.status === "failed").length | ||
| }; | ||
| const blockedCount = checked.filter( | ||
| (project) => project.loopState?.startsWith("blocked") || project.loopState === "human_resolution_required" | ||
| ).length; | ||
| const repairRequiredCount = checked.filter( | ||
| (project) => project.loopState === "repair_required" | ||
| ).length; | ||
| const workspaceLoopState = checked.length === 0 ? "needs_context" : blockedCount > 0 ? "human_resolution_required" : repairRequiredCount > 0 || summary.errorCount > 0 || summary.warningCount > 0 ? "repair_required" : "verified"; | ||
| const workspaceLoopStatus = workspaceLoopState === "human_resolution_required" || workspaceLoopState.startsWith("blocked") ? "blocked" : summary.errorCount > 0 || summary.failedCount > 0 ? "error" : summary.warningCount > 0 ? "warning" : "healthy"; | ||
| return { | ||
| $schema: WORKSPACE_HEALTH_SCHEMA_URL, | ||
| generatedAt: (/* @__PURE__ */ new Date()).toISOString(), | ||
| workspaceRoot, | ||
| changedOnly: options.changedOnly ?? false, | ||
| since: options.changedOnly ? since : null, | ||
| summary, | ||
| loop: { | ||
| state: workspaceLoopState, | ||
| status: workspaceLoopStatus, | ||
| projectCount: checked.length, | ||
| blockedCount, | ||
| repairRequiredCount, | ||
| nextActions: [ | ||
| workspaceLoopState === "verified" ? "Workspace loop verified." : 'Open the highest-risk project, run `decantr task <target> "<intent>"`, repair, then rerun `decantr verify`.' | ||
| ] | ||
| }, | ||
| projects: checked | ||
| }; | ||
| } | ||
| function formatWorkspaceHealthText(report) { | ||
| const lines = [ | ||
| `${BOLD}Decantr Workspace Health${RESET}`, | ||
| "", | ||
| `Projects: ${report.summary.checkedCount}/${report.summary.projectCount}`, | ||
| `Healthy: ${report.summary.healthyCount} | Warnings: ${report.summary.warningCount} | Errors: ${report.summary.errorCount} | Failed: ${report.summary.failedCount}`, | ||
| `Loop: ${report.loop.state} | blocked ${report.loop.blockedCount} | repair ${report.loop.repairRequiredCount}`, | ||
| "" | ||
| ]; | ||
| for (const project of report.projects) { | ||
| const color = project.status === "healthy" ? GREEN : project.status === "warning" ? YELLOW : RED; | ||
| lines.push( | ||
| `${color}${String(project.status).toUpperCase()}${RESET} ${project.path} score ${project.score}/100 findings ${project.findingCount}` | ||
| ); | ||
| if (project.error) lines.push(` ${DIM}${project.error}${RESET}`); | ||
| } | ||
| return `${lines.join("\n")} | ||
| `; | ||
| } | ||
| function formatWorkspaceHealthMarkdown(report) { | ||
| const lines = [ | ||
| "# Decantr Workspace Health", | ||
| "", | ||
| `- Projects checked: **${report.summary.checkedCount}/${report.summary.projectCount}**`, | ||
| `- Healthy: ${report.summary.healthyCount}`, | ||
| `- Warnings: ${report.summary.warningCount}`, | ||
| `- Errors: ${report.summary.errorCount}`, | ||
| `- Failed: ${report.summary.failedCount}`, | ||
| "", | ||
| "| Project | Status | Score | Findings | Source |", | ||
| "| --- | --- | ---: | ---: | --- |" | ||
| ]; | ||
| for (const project of report.projects) { | ||
| lines.push( | ||
| `| \`${project.path}\` | ${project.status} | ${project.score} | ${project.findingCount} | ${project.source} |` | ||
| ); | ||
| } | ||
| return `${lines.join("\n")} | ||
| `; | ||
| } | ||
| function shouldFailWorkspaceHealth(report, failOn = "error") { | ||
| if (failOn === "none") return false; | ||
| if (report.summary.failedCount > 0 || report.summary.errorCount > 0) return true; | ||
| return failOn === "warn" && report.summary.warningCount > 0; | ||
| } | ||
| function parseHealthFailOn(value) { | ||
| if (value === "warn" || value === "none") return value; | ||
| return "error"; | ||
| } | ||
| function parseWorkspaceArgs(args) { | ||
| const subcommand = args[1] === "health" ? "health" : "list"; | ||
| const options = { subcommand }; | ||
| for (let index = 2; index < args.length; index += 1) { | ||
| const arg = args[index]; | ||
| if (arg === "--json") options.json = true; | ||
| else if (arg === "--markdown") options.markdown = true; | ||
| else if (arg === "--ci") options.ci = true; | ||
| else if (arg === "--browser") options.browser = true; | ||
| else if (arg === "--changed") options.changedOnly = true; | ||
| else if (arg === "--since" && args[index + 1]) options.since = args[++index]; | ||
| else if (arg.startsWith("--since=")) options.since = arg.split("=")[1]; | ||
| else if (arg === "--output" && args[index + 1]) options.output = args[++index]; | ||
| else if (arg.startsWith("--output=")) options.output = arg.split("=")[1]; | ||
| else if (arg === "--fail-on" && args[index + 1]) | ||
| options.failOn = parseHealthFailOn(args[++index]); | ||
| else if (arg.startsWith("--fail-on=")) options.failOn = parseHealthFailOn(arg.split("=")[1]); | ||
| else if (arg === "--concurrency" && args[index + 1]) | ||
| options.concurrency = Number(args[++index]); | ||
| else if (arg.startsWith("--concurrency=")) options.concurrency = Number(arg.split("=")[1]); | ||
| else if (arg === "--timeout-ms" && args[index + 1]) options.timeoutMs = Number(args[++index]); | ||
| else if (arg.startsWith("--timeout-ms=")) options.timeoutMs = Number(arg.split("=")[1]); | ||
| } | ||
| return options; | ||
| } | ||
| async function cmdWorkspace(workspaceRoot = process.cwd(), args = ["workspace"]) { | ||
| const options = parseWorkspaceArgs(args); | ||
| if (options.subcommand === "list") { | ||
| const projects = listWorkspaceProjects(workspaceRoot); | ||
| const candidates = listWorkspaceCandidates(workspaceRoot, projects); | ||
| const unattachedCandidates = candidates.filter((candidate) => !candidate.attached); | ||
| const payload2 = `${JSON.stringify({ projects, candidates }, null, 2)} | ||
| `; | ||
| if (options.json) { | ||
| process.stdout.write(payload2); | ||
| return; | ||
| } | ||
| console.log(`${BOLD}Decantr workspace projects${RESET}`); | ||
| console.log(""); | ||
| console.log("Attached Decantr projects:"); | ||
| if (projects.length === 0) { | ||
| console.log(` ${DIM}(none yet)${RESET}`); | ||
| } else { | ||
| for (const project of projects) { | ||
| console.log(` ${project.path} ${DIM}${project.source}${RESET}`); | ||
| } | ||
| } | ||
| if (candidates.length > 0) { | ||
| console.log(""); | ||
| console.log("App candidates:"); | ||
| for (const candidate of candidates) { | ||
| const status = candidate.attached ? `${GREEN}attached${RESET}` : `${YELLOW}unattached${RESET}`; | ||
| console.log( | ||
| ` #${candidate.rank} ${candidate.path} ${DIM}${status} \xB7 ${candidate.category} \xB7 score ${candidate.score}${RESET}` | ||
| ); | ||
| } | ||
| } | ||
| if (unattachedCandidates.length > 0) { | ||
| console.log(""); | ||
| console.log(projects.length > 0 ? "Attach another app:" : "Start by attaching one app:"); | ||
| console.log(` ${unattachedCandidates[0].suggestedAdoptCommand}`); | ||
| } | ||
| return; | ||
| } | ||
| const report = await createWorkspaceHealthReport(workspaceRoot, options); | ||
| const payload = options.json ? `${JSON.stringify(report, null, 2)} | ||
| ` : options.markdown ? formatWorkspaceHealthMarkdown(report) : formatWorkspaceHealthText(report); | ||
| if (options.output) { | ||
| mkdirSync(dirname(resolve(workspaceRoot, options.output)), { recursive: true }); | ||
| writeFileSync(resolve(workspaceRoot, options.output), payload, "utf-8"); | ||
| if (!options.ci) | ||
| console.log(`${GREEN}Wrote Decantr workspace health:${RESET} ${options.output}`); | ||
| } else { | ||
| process.stdout.write(payload); | ||
| } | ||
| if (options.ci && shouldFailWorkspaceHealth(report, options.failOn ?? "error")) { | ||
| process.exitCode = 1; | ||
| } | ||
| } | ||
| export { | ||
| listWorkspaceProjects, | ||
| listWorkspaceCandidates, | ||
| createWorkspaceHealthReport, | ||
| formatWorkspaceHealthText, | ||
| formatWorkspaceHealthMarkdown, | ||
| shouldFailWorkspaceHealth, | ||
| parseWorkspaceArgs, | ||
| cmdWorkspace | ||
| }; |
| import { | ||
| cmdHealth, | ||
| collectDesignTokenEvidence, | ||
| createProjectEvidenceBundle, | ||
| createProjectHealthReport, | ||
| evaluateHealthBaselineGate, | ||
| formatDiagnosticCatalogJson, | ||
| formatDiagnosticCatalogMarkdown, | ||
| formatDiagnosticCatalogText, | ||
| formatProjectHealthJson, | ||
| formatProjectHealthMarkdown, | ||
| formatProjectHealthText, | ||
| parseHealthArgs, | ||
| renderProjectHealthCiWorkflow, | ||
| shouldFailHealth, | ||
| shouldFailHealthBaselineGate, | ||
| writeProjectHealthCiWorkflow | ||
| } from "./chunk-72FITODT.js"; | ||
| import "./chunk-BVKXRTYP.js"; | ||
| export { | ||
| cmdHealth, | ||
| collectDesignTokenEvidence, | ||
| createProjectEvidenceBundle, | ||
| createProjectHealthReport, | ||
| evaluateHealthBaselineGate, | ||
| formatDiagnosticCatalogJson, | ||
| formatDiagnosticCatalogMarkdown, | ||
| formatDiagnosticCatalogText, | ||
| formatProjectHealthJson, | ||
| formatProjectHealthMarkdown, | ||
| formatProjectHealthText, | ||
| parseHealthArgs, | ||
| renderProjectHealthCiWorkflow, | ||
| shouldFailHealth, | ||
| shouldFailHealthBaselineGate, | ||
| writeProjectHealthCiWorkflow | ||
| }; |
Sorry, the diff of this file is too big to display
| import { | ||
| cmdWorkspace, | ||
| createWorkspaceHealthReport, | ||
| formatWorkspaceHealthMarkdown, | ||
| formatWorkspaceHealthText, | ||
| listWorkspaceCandidates, | ||
| listWorkspaceProjects, | ||
| parseWorkspaceArgs, | ||
| shouldFailWorkspaceHealth | ||
| } from "./chunk-FY2VQ6LD.js"; | ||
| import "./chunk-72FITODT.js"; | ||
| import "./chunk-BVKXRTYP.js"; | ||
| export { | ||
| cmdWorkspace, | ||
| createWorkspaceHealthReport, | ||
| formatWorkspaceHealthMarkdown, | ||
| formatWorkspaceHealthText, | ||
| listWorkspaceCandidates, | ||
| listWorkspaceProjects, | ||
| parseWorkspaceArgs, | ||
| shouldFailWorkspaceHealth | ||
| }; |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
1432831
0.74%34078
0.79%444
3.74%96
2.13%+ Added
- Removed
Updated