@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, | ||
| listWorkspaceAppCandidates | ||
| } from "./chunk-U62GGKUO.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 listWorkspaceAppCandidates(root).map((path) => ({ | ||
| path, | ||
| attached: attached.has(path), | ||
| suggestedAdoptCommand: `decantr adopt --project ${path} --yes` | ||
| })); | ||
| } | ||
| 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 <route> "<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.path} ${DIM}${status}${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 | ||
| }; |
Sorry, the diff of this file is too big to display
| import { | ||
| cmdHeal, | ||
| collectCheckIssues | ||
| } from "./chunk-24JR4ZNG.js"; | ||
| export { | ||
| cmdHeal, | ||
| collectCheckIssues | ||
| }; |
| import { | ||
| cmdHealth, | ||
| collectDesignTokenEvidence, | ||
| createProjectEvidenceBundle, | ||
| createProjectHealthReport, | ||
| formatDiagnosticCatalogJson, | ||
| formatDiagnosticCatalogMarkdown, | ||
| formatDiagnosticCatalogText, | ||
| formatProjectHealthJson, | ||
| formatProjectHealthMarkdown, | ||
| formatProjectHealthText, | ||
| parseHealthArgs, | ||
| renderProjectHealthCiWorkflow, | ||
| shouldFailHealth, | ||
| writeProjectHealthCiWorkflow | ||
| } from "./chunk-U62GGKUO.js"; | ||
| import "./chunk-24JR4ZNG.js"; | ||
| export { | ||
| cmdHealth, | ||
| collectDesignTokenEvidence, | ||
| createProjectEvidenceBundle, | ||
| createProjectHealthReport, | ||
| formatDiagnosticCatalogJson, | ||
| formatDiagnosticCatalogMarkdown, | ||
| formatDiagnosticCatalogText, | ||
| formatProjectHealthJson, | ||
| formatProjectHealthMarkdown, | ||
| formatProjectHealthText, | ||
| parseHealthArgs, | ||
| renderProjectHealthCiWorkflow, | ||
| shouldFailHealth, | ||
| writeProjectHealthCiWorkflow | ||
| }; |
Sorry, the diff of this file is too big to display
| import { | ||
| cmdWorkspace, | ||
| createWorkspaceHealthReport, | ||
| formatWorkspaceHealthMarkdown, | ||
| formatWorkspaceHealthText, | ||
| listWorkspaceCandidates, | ||
| listWorkspaceProjects, | ||
| parseWorkspaceArgs, | ||
| shouldFailWorkspaceHealth | ||
| } from "./chunk-FIWVRMZN.js"; | ||
| import "./chunk-U62GGKUO.js"; | ||
| import "./chunk-24JR4ZNG.js"; | ||
| export { | ||
| cmdWorkspace, | ||
| createWorkspaceHealthReport, | ||
| formatWorkspaceHealthMarkdown, | ||
| formatWorkspaceHealthText, | ||
| listWorkspaceCandidates, | ||
| listWorkspaceProjects, | ||
| parseWorkspaceArgs, | ||
| shouldFailWorkspaceHealth | ||
| }; |
+4
-4
| #!/usr/bin/env node | ||
| import "./chunk-GPGBJFMF.js"; | ||
| import "./chunk-4O74E2PS.js"; | ||
| import "./chunk-SIDKK73N.js"; | ||
| import "./chunk-UDBPH25I.js"; | ||
| import "./chunk-EWUWVQYM.js"; | ||
| import "./chunk-4WVKE2AQ.js"; | ||
| import "./chunk-FIWVRMZN.js"; | ||
| import "./chunk-U62GGKUO.js"; | ||
| import "./chunk-24JR4ZNG.js"; |
+4
-4
@@ -1,5 +0,5 @@ | ||
| import "./chunk-GPGBJFMF.js"; | ||
| import "./chunk-4O74E2PS.js"; | ||
| import "./chunk-SIDKK73N.js"; | ||
| import "./chunk-UDBPH25I.js"; | ||
| import "./chunk-EWUWVQYM.js"; | ||
| import "./chunk-4WVKE2AQ.js"; | ||
| import "./chunk-FIWVRMZN.js"; | ||
| import "./chunk-U62GGKUO.js"; | ||
| import "./chunk-24JR4ZNG.js"; |
+4
-4
| { | ||
| "name": "@decantr/cli", | ||
| "version": "3.5.4", | ||
| "version": "3.5.5", | ||
| "description": "Decantr CLI - adopt, verify, graph, and govern frontend codebases touched by AI agents", | ||
@@ -52,7 +52,7 @@ "keywords": [ | ||
| "ajv": "^8.20.0", | ||
| "@decantr/core": "3.5.0", | ||
| "@decantr/essence-spec": "3.4.0", | ||
| "@decantr/core": "3.5.0", | ||
| "@decantr/registry": "3.4.0", | ||
| "@decantr/telemetry": "3.4.0", | ||
| "@decantr/registry": "3.4.0", | ||
| "@decantr/verifier": "3.5.4" | ||
| "@decantr/verifier": "3.5.5" | ||
| }, | ||
@@ -59,0 +59,0 @@ "scripts": { |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
| import { | ||
| createProjectHealthReport, | ||
| listWorkspaceAppCandidates | ||
| } from "./chunk-EWUWVQYM.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 listWorkspaceAppCandidates(root).map((path) => ({ | ||
| path, | ||
| attached: attached.has(path), | ||
| suggestedAdoptCommand: `decantr adopt --project ${path} --yes` | ||
| })); | ||
| } | ||
| 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 <route> "<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.path} ${DIM}${status}${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 { | ||
| cmdHeal, | ||
| collectCheckIssues | ||
| } from "./chunk-4WVKE2AQ.js"; | ||
| export { | ||
| cmdHeal, | ||
| collectCheckIssues | ||
| }; |
| import { | ||
| cmdHealth, | ||
| collectDesignTokenEvidence, | ||
| createProjectEvidenceBundle, | ||
| createProjectHealthReport, | ||
| formatDiagnosticCatalogJson, | ||
| formatDiagnosticCatalogMarkdown, | ||
| formatDiagnosticCatalogText, | ||
| formatProjectHealthJson, | ||
| formatProjectHealthMarkdown, | ||
| formatProjectHealthText, | ||
| parseHealthArgs, | ||
| renderProjectHealthCiWorkflow, | ||
| shouldFailHealth, | ||
| writeProjectHealthCiWorkflow | ||
| } from "./chunk-EWUWVQYM.js"; | ||
| import "./chunk-4WVKE2AQ.js"; | ||
| export { | ||
| cmdHealth, | ||
| collectDesignTokenEvidence, | ||
| createProjectEvidenceBundle, | ||
| createProjectHealthReport, | ||
| formatDiagnosticCatalogJson, | ||
| formatDiagnosticCatalogMarkdown, | ||
| formatDiagnosticCatalogText, | ||
| formatProjectHealthJson, | ||
| formatProjectHealthMarkdown, | ||
| formatProjectHealthText, | ||
| parseHealthArgs, | ||
| renderProjectHealthCiWorkflow, | ||
| shouldFailHealth, | ||
| writeProjectHealthCiWorkflow | ||
| }; |
Sorry, the diff of this file is too big to display
| import { | ||
| cmdWorkspace, | ||
| createWorkspaceHealthReport, | ||
| formatWorkspaceHealthMarkdown, | ||
| formatWorkspaceHealthText, | ||
| listWorkspaceCandidates, | ||
| listWorkspaceProjects, | ||
| parseWorkspaceArgs, | ||
| shouldFailWorkspaceHealth | ||
| } from "./chunk-UDBPH25I.js"; | ||
| import "./chunk-EWUWVQYM.js"; | ||
| import "./chunk-4WVKE2AQ.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.
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
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.
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
Found 2 instances
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
1276134
0.25%30395
0.29%95
-1.04%+ Added
- Removed
Updated