@decantr/cli 2.1.0 → 2.1.2

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