@decantr/cli 1.7.29 → 1.9.0

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.
@@ -0,0 +1,9 @@
1
+ import {
2
+ cmdHeal,
3
+ collectCheckIssues
4
+ } from "./chunk-RSDCWAHD.js";
5
+ import "./chunk-DI2PLOJ6.js";
6
+ export {
7
+ cmdHeal,
8
+ collectCheckIssues
9
+ };
@@ -0,0 +1,20 @@
1
+ import {
2
+ cmdHealth,
3
+ createProjectHealthReport,
4
+ formatProjectHealthJson,
5
+ formatProjectHealthMarkdown,
6
+ formatProjectHealthText,
7
+ parseHealthArgs,
8
+ shouldFailHealth
9
+ } from "./chunk-DONMNPS7.js";
10
+ import "./chunk-RSDCWAHD.js";
11
+ import "./chunk-DI2PLOJ6.js";
12
+ export {
13
+ cmdHealth,
14
+ createProjectHealthReport,
15
+ formatProjectHealthJson,
16
+ formatProjectHealthMarkdown,
17
+ formatProjectHealthText,
18
+ parseHealthArgs,
19
+ shouldFailHealth
20
+ };
package/dist/index.js CHANGED
@@ -1,3 +1,3 @@
1
- import "./chunk-US6RK5QT.js";
2
- import "./chunk-HULA6E2D.js";
1
+ import "./chunk-PKJSI6IH.js";
2
+ import "./chunk-USOO77A5.js";
3
3
  import "./chunk-DI2PLOJ6.js";
@@ -0,0 +1,309 @@
1
+ import {
2
+ createProjectHealthReport
3
+ } from "./chunk-DONMNPS7.js";
4
+ import "./chunk-RSDCWAHD.js";
5
+ import "./chunk-DI2PLOJ6.js";
6
+
7
+ // src/commands/studio.ts
8
+ import { createServer } from "http";
9
+ var GREEN = "\x1B[32m";
10
+ var CYAN = "\x1B[36m";
11
+ var RESET = "\x1B[0m";
12
+ function sendJson(res, status, value) {
13
+ const body = JSON.stringify(value, null, 2);
14
+ res.writeHead(status, {
15
+ "Content-Type": "application/json; charset=utf-8",
16
+ "Cache-Control": "no-store"
17
+ });
18
+ res.end(body);
19
+ }
20
+ function sendHtml(res, body) {
21
+ res.writeHead(200, {
22
+ "Content-Type": "text/html; charset=utf-8",
23
+ "Cache-Control": "no-store"
24
+ });
25
+ res.end(body);
26
+ }
27
+ function sendNotFound(res) {
28
+ sendJson(res, 404, { error: "not_found" });
29
+ }
30
+ function studioHtml() {
31
+ return `<!doctype html>
32
+ <html lang="en">
33
+ <head>
34
+ <meta charset="utf-8">
35
+ <meta name="viewport" content="width=device-width, initial-scale=1">
36
+ <title>Decantr Project Health</title>
37
+ <style>
38
+ :root {
39
+ color-scheme: dark;
40
+ --bg: #101014;
41
+ --panel: #181820;
42
+ --panel-2: #20202a;
43
+ --line: #343442;
44
+ --text: #f5f2eb;
45
+ --muted: #ada7bd;
46
+ --good: #5ee2a0;
47
+ --warn: #f2bd61;
48
+ --bad: #ff6f7d;
49
+ --accent: #8ed3ff;
50
+ --coral: #ff8b6a;
51
+ }
52
+ * { box-sizing: border-box; }
53
+ body {
54
+ margin: 0;
55
+ background: radial-gradient(circle at 20% 0%, rgba(255,139,106,0.16), transparent 26rem), var(--bg);
56
+ color: var(--text);
57
+ font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
58
+ line-height: 1.4;
59
+ }
60
+ button, input { font: inherit; }
61
+ .shell { min-height: 100vh; display: grid; grid-template-rows: auto 1fr; }
62
+ header {
63
+ display: flex;
64
+ align-items: center;
65
+ justify-content: space-between;
66
+ gap: 1rem;
67
+ padding: 1rem 1.25rem;
68
+ border-bottom: 1px solid var(--line);
69
+ background: rgba(16,16,20,0.84);
70
+ backdrop-filter: blur(18px);
71
+ position: sticky;
72
+ top: 0;
73
+ z-index: 2;
74
+ }
75
+ h1 { margin: 0; font-size: 1rem; letter-spacing: 0; }
76
+ .subtle { color: var(--muted); font-size: 0.875rem; }
77
+ .button {
78
+ border: 1px solid var(--line);
79
+ background: var(--panel-2);
80
+ color: var(--text);
81
+ border-radius: 8px;
82
+ padding: 0.55rem 0.8rem;
83
+ cursor: pointer;
84
+ }
85
+ .button:hover { border-color: var(--accent); }
86
+ main { display: grid; grid-template-columns: 15rem 1fr; min-height: 0; }
87
+ nav {
88
+ border-right: 1px solid var(--line);
89
+ padding: 1rem;
90
+ background: rgba(24,24,32,0.66);
91
+ }
92
+ .tab {
93
+ width: 100%;
94
+ text-align: left;
95
+ margin: 0 0 0.35rem;
96
+ border: 1px solid transparent;
97
+ border-radius: 8px;
98
+ padding: 0.65rem 0.7rem;
99
+ color: var(--muted);
100
+ background: transparent;
101
+ cursor: pointer;
102
+ }
103
+ .tab[aria-selected="true"] {
104
+ color: var(--text);
105
+ border-color: var(--line);
106
+ background: var(--panel-2);
107
+ }
108
+ .content { padding: 1rem; overflow: auto; }
109
+ .grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 0.75rem; }
110
+ .card {
111
+ border: 1px solid var(--line);
112
+ background: linear-gradient(180deg, var(--panel), rgba(24,24,32,0.74));
113
+ border-radius: 8px;
114
+ padding: 1rem;
115
+ }
116
+ .metric { font-size: 1.85rem; font-weight: 720; }
117
+ .label { color: var(--muted); font-size: 0.78rem; text-transform: uppercase; }
118
+ .status-healthy { color: var(--good); }
119
+ .status-warning { color: var(--warn); }
120
+ .status-error { color: var(--bad); }
121
+ table { width: 100%; border-collapse: collapse; }
122
+ th, td { border-bottom: 1px solid var(--line); padding: 0.7rem; text-align: left; vertical-align: top; }
123
+ th { color: var(--muted); font-size: 0.78rem; text-transform: uppercase; }
124
+ code, pre { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
125
+ pre {
126
+ white-space: pre-wrap;
127
+ border: 1px solid var(--line);
128
+ border-radius: 8px;
129
+ padding: 1rem;
130
+ background: #0c0c10;
131
+ overflow: auto;
132
+ }
133
+ .pill { display: inline-flex; border: 1px solid var(--line); border-radius: 999px; padding: 0.2rem 0.55rem; }
134
+ .stack { display: grid; gap: 0.75rem; }
135
+ .hidden { display: none; }
136
+ @media (max-width: 760px) {
137
+ main { grid-template-columns: 1fr; }
138
+ nav { border-right: 0; border-bottom: 1px solid var(--line); display: grid; grid-template-columns: repeat(2, 1fr); gap: 0.35rem; }
139
+ .grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
140
+ }
141
+ </style>
142
+ </head>
143
+ <body>
144
+ <div class="shell">
145
+ <header>
146
+ <div>
147
+ <h1>Decantr Project Health</h1>
148
+ <div id="project" class="subtle">Loading local contract state...</div>
149
+ </div>
150
+ <button id="refresh" class="button" type="button">Refresh</button>
151
+ </header>
152
+ <main>
153
+ <nav aria-label="Project Health Views">
154
+ <button class="tab" type="button" data-tab="overview" aria-selected="true">Overview</button>
155
+ <button class="tab" type="button" data-tab="routes">Routes</button>
156
+ <button class="tab" type="button" data-tab="drift">Drift</button>
157
+ <button class="tab" type="button" data-tab="findings">Findings</button>
158
+ <button class="tab" type="button" data-tab="remediation">Remediation</button>
159
+ <button class="tab" type="button" data-tab="ci">CI</button>
160
+ <button class="tab" type="button" data-tab="packs">Packs</button>
161
+ </nav>
162
+ <section class="content">
163
+ <div id="overview" class="view stack"></div>
164
+ <div id="routes" class="view stack hidden"></div>
165
+ <div id="drift" class="view stack hidden"></div>
166
+ <div id="findings" class="view stack hidden"></div>
167
+ <div id="remediation" class="view stack hidden"></div>
168
+ <div id="ci" class="view stack hidden"></div>
169
+ <div id="packs" class="view stack hidden"></div>
170
+ </section>
171
+ </main>
172
+ </div>
173
+ <script>
174
+ let report = null;
175
+ const tabs = [...document.querySelectorAll('.tab')];
176
+ const views = [...document.querySelectorAll('.view')];
177
+ function esc(value) {
178
+ return String(value ?? '').replace(/[&<>"']/g, (char) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[char]));
179
+ }
180
+ function metric(label, value, cls = '') {
181
+ return '<div class="card"><div class="label">' + esc(label) + '</div><div class="metric ' + cls + '">' + esc(value) + '</div></div>';
182
+ }
183
+ function table(headers, rows) {
184
+ return '<table><thead><tr>' + headers.map((h) => '<th>' + esc(h) + '</th>').join('') + '</tr></thead><tbody>' +
185
+ rows.map((row) => '<tr>' + row.map((cell) => '<td>' + cell + '</td>').join('') + '</tr>').join('') + '</tbody></table>';
186
+ }
187
+ function render() {
188
+ if (!report) return;
189
+ document.getElementById('project').textContent = report.projectRoot;
190
+ document.getElementById('overview').innerHTML =
191
+ '<div class="grid">' +
192
+ metric('Status', report.status, 'status-' + report.status) +
193
+ metric('Score', report.score + '/100') +
194
+ metric('Errors', report.summary.errorCount, 'status-error') +
195
+ metric('Warnings', report.summary.warnCount, 'status-warning') +
196
+ '</div><div class="card"><div class="label">Workflow</div><p>' + esc(report.summary.workflowMode || 'unknown') + ' / ' + esc(report.summary.adoptionMode || 'unknown') + '</p><p class="subtle">Generated ' + esc(report.generatedAt) + '</p></div>';
197
+ document.getElementById('routes').innerHTML =
198
+ '<div class="card"><div class="label">Route Coverage</div><p>Declared routes: ' + report.routes.declared.length + ' | runtime checked: ' + report.routes.runtimeChecked.length + ' | matched: ' + report.routes.runtimeMatched + '</p></div>' +
199
+ table(['Declared Route'], report.routes.declared.map((route) => ['<code>' + esc(route) + '</code>'])) +
200
+ (report.routes.issues.length ? '<div class="card"><div class="label">Route Issues</div><ul>' + report.routes.issues.map((issue) => '<li>' + esc(issue) + '</li>').join('') + '</ul></div>' : '');
201
+ const drift = report.findings.filter((finding) => finding.source === 'brownfield' || finding.id.includes('drift'));
202
+ document.getElementById('drift').innerHTML = drift.length
203
+ ? table(['Severity', 'Source', 'Message'], drift.map((finding) => [esc(finding.severity), esc(finding.source), esc(finding.message)]))
204
+ : '<div class="card">No drift findings.</div>';
205
+ document.getElementById('findings').innerHTML = report.findings.length
206
+ ? table(['Severity', 'Source', 'Finding', 'Prompt'], report.findings.map((finding) => [
207
+ '<span class="pill">' + esc(finding.severity) + '</span>',
208
+ esc(finding.source),
209
+ '<strong>' + esc(finding.id) + '</strong><br><span class="subtle">' + esc(finding.message) + '</span>',
210
+ '<code>decantr health --prompt ' + esc(finding.id) + '</code>'
211
+ ]))
212
+ : '<div class="card">No findings. Project is healthy.</div>';
213
+ document.getElementById('remediation').innerHTML = report.findings.length
214
+ ? report.findings.map((finding) => '<div class="card"><div class="label">' + esc(finding.id) + '</div><p>' + esc(finding.remediation.summary) + '</p><pre>' + esc(finding.remediation.prompt) + '</pre></div>').join('')
215
+ : '<div class="card">No remediation needed.</div>';
216
+ document.getElementById('ci').innerHTML = '<div class="card"><div class="label">Recommended CI Gate</div><pre>' + esc(report.ci.recommendedCommand) + '</pre></div>';
217
+ document.getElementById('packs').innerHTML =
218
+ '<div class="grid">' +
219
+ metric('Manifest', report.packs.manifestPresent ? 'present' : 'missing') +
220
+ metric('Review', report.packs.reviewPackPresent ? 'present' : 'missing') +
221
+ metric('Sections', report.packs.sectionPackCount) +
222
+ metric('Pages', report.packs.pagePackCount) +
223
+ '</div><div class="card"><div class="label">Generated</div><p>' + esc(report.packs.generatedAt || 'unknown') + '</p></div>';
224
+ }
225
+ async function load(refresh = false) {
226
+ const response = await fetch(refresh ? '/api/refresh' : '/api/health', { method: refresh ? 'POST' : 'GET' });
227
+ report = await response.json();
228
+ render();
229
+ }
230
+ tabs.forEach((tab) => tab.addEventListener('click', () => {
231
+ tabs.forEach((item) => item.setAttribute('aria-selected', String(item === tab)));
232
+ views.forEach((view) => view.classList.toggle('hidden', view.id !== tab.dataset.tab));
233
+ }));
234
+ document.getElementById('refresh').addEventListener('click', () => load(true));
235
+ load().catch((error) => {
236
+ document.getElementById('overview').innerHTML = '<div class="card status-error">Failed to load health report: ' + esc(error.message) + '</div>';
237
+ });
238
+ </script>
239
+ </body>
240
+ </html>`;
241
+ }
242
+ function createStudioRequestHandler(projectRoot) {
243
+ return async function handleStudioRequest(req, res) {
244
+ const url = new URL(req.url ?? "/", "http://localhost");
245
+ try {
246
+ if (req.method === "GET" && url.pathname === "/") {
247
+ sendHtml(res, studioHtml());
248
+ return;
249
+ }
250
+ if (req.method === "GET" && url.pathname === "/api/health") {
251
+ sendJson(res, 200, await createProjectHealthReport(projectRoot));
252
+ return;
253
+ }
254
+ if (req.method === "POST" && url.pathname === "/api/refresh") {
255
+ sendJson(res, 200, await createProjectHealthReport(projectRoot));
256
+ return;
257
+ }
258
+ sendNotFound(res);
259
+ } catch (e) {
260
+ sendJson(res, 500, { error: "health_report_failed", message: e.message });
261
+ }
262
+ };
263
+ }
264
+ async function startStudioServer(projectRoot = process.cwd(), options = {}) {
265
+ const host = options.host ?? "127.0.0.1";
266
+ const port = options.port ?? 4319;
267
+ const server = createServer(createStudioRequestHandler(projectRoot));
268
+ await new Promise((resolve, reject) => {
269
+ server.once("error", reject);
270
+ server.listen(port, host, () => {
271
+ server.off("error", reject);
272
+ resolve();
273
+ });
274
+ });
275
+ const address = server.address();
276
+ const actualPort = typeof address === "object" && address ? address.port : port;
277
+ return { server, url: `http://${host}:${actualPort}` };
278
+ }
279
+ async function cmdStudio(projectRoot = process.cwd(), options = {}) {
280
+ const handle = await startStudioServer(projectRoot, options);
281
+ console.log(`${GREEN}Decantr Studio is running.${RESET}`);
282
+ console.log(`${CYAN}${handle.url}${RESET}`);
283
+ console.log("Press Ctrl+C to stop.");
284
+ }
285
+ function parseStudioArgs(args) {
286
+ const options = {};
287
+ for (let index = 1; index < args.length; index += 1) {
288
+ const arg = args[index];
289
+ if (arg === "--host" && args[index + 1]) {
290
+ options.host = args[++index];
291
+ } else if (arg.startsWith("--host=")) {
292
+ options.host = arg.split("=")[1];
293
+ } else if (arg === "--port" && args[index + 1]) {
294
+ options.port = Number.parseInt(args[++index], 10);
295
+ } else if (arg.startsWith("--port=")) {
296
+ options.port = Number.parseInt(arg.split("=")[1], 10);
297
+ }
298
+ }
299
+ if (options.port !== void 0 && (!Number.isInteger(options.port) || options.port < 0)) {
300
+ throw new Error("Invalid --port value.");
301
+ }
302
+ return options;
303
+ }
304
+ export {
305
+ cmdStudio,
306
+ createStudioRequestHandler,
307
+ parseStudioArgs,
308
+ startStudioServer
309
+ };
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  RegistryClient,
3
3
  refreshDerivedFiles
4
- } from "./chunk-HULA6E2D.js";
4
+ } from "./chunk-USOO77A5.js";
5
5
 
6
6
  // src/commands/upgrade.ts
7
7
  import { existsSync, readFileSync, writeFileSync } from "fs";
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@decantr/cli",
3
- "version": "1.7.29",
4
- "description": "Decantr CLI scaffold, audit, and maintain Decantr projects from the terminal",
3
+ "version": "1.9.0",
4
+ "description": "Decantr CLI - scaffold, audit, inspect Project Health, and maintain Decantr projects from the terminal",
5
5
  "author": "Decantr AI",
6
6
  "license": "MIT",
7
7
  "bugs": {
@@ -30,11 +30,12 @@
30
30
  "access": "public"
31
31
  },
32
32
  "dependencies": {
33
- "@decantr/telemetry": "0.1.2",
33
+ "ajv": "^8.18.0",
34
34
  "@decantr/core": "1.0.6",
35
35
  "@decantr/essence-spec": "1.0.7",
36
- "@decantr/verifier": "1.0.6",
37
- "@decantr/registry": "1.0.4"
36
+ "@decantr/registry": "1.1.0",
37
+ "@decantr/verifier": "1.1.0",
38
+ "@decantr/telemetry": "0.1.2"
38
39
  },
39
40
  "scripts": {
40
41
  "build": "tsup",
@@ -115,12 +115,15 @@ Read `.decantr/context/page-{name}-pack.md` for the most local compiled route co
115
115
  ### Validation
116
116
 
117
117
  Run `decantr check` to detect drift violations while editing and `decantr audit` to audit the whole project contract after implementation.
118
+ Run `decantr health` for the broader Project Health view before handoff, pull requests, or CI. Use `decantr health --prompt <finding-id>` to generate a scoped remediation prompt for a specific issue, and `decantr studio` to inspect local drift, routes, findings, remediation, CI, and pack state in a localhost dashboard.
118
119
  Declared command palettes and hotkeys must be implemented, not merely acknowledged.
119
120
 
120
121
  ### Quick Commands
121
122
 
122
123
  ```bash
123
- decantr status # Project health
124
+ decantr status # Project status overview
125
+ decantr health # Local contract health report
126
+ decantr studio # Local health dashboard
124
127
  decantr check # Detect drift violations
125
128
  decantr get pattern X # Fetch a pattern spec from registry
126
129
  decantr get theme X # Fetch theme details and decorators