@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.
- package/README.md +46 -0
- package/dist/bin.js +2 -2
- package/dist/chunk-DONMNPS7.js +466 -0
- package/dist/{chunk-US6RK5QT.js → chunk-PKJSI6IH.js} +44 -3
- package/dist/{heal-YHLXO5QL.js → chunk-RSDCWAHD.js} +26 -5
- package/dist/{chunk-HULA6E2D.js → chunk-USOO77A5.js} +10 -1
- package/dist/content-health-QQHBR6XG.js +1057 -0
- package/dist/heal-5JHGCLDX.js +9 -0
- package/dist/health-VSL4MROO.js +20 -0
- package/dist/index.js +2 -2
- package/dist/studio-BCTWKXFH.js +309 -0
- package/dist/{upgrade-EV23CKA3.js → upgrade-4NRDVD5N.js} +1 -1
- package/package.json +6 -5
- package/src/templates/DECANTR.md.template +4 -1
|
@@ -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-
|
|
2
|
-
import "./chunk-
|
|
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) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[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
|
+
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decantr/cli",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Decantr CLI
|
|
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
|
-
"
|
|
33
|
+
"ajv": "^8.18.0",
|
|
34
34
|
"@decantr/core": "1.0.6",
|
|
35
35
|
"@decantr/essence-spec": "1.0.7",
|
|
36
|
-
"@decantr/
|
|
37
|
-
"@decantr/
|
|
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
|
|
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
|