@decantr/cli 1.7.28 → 1.8.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 +44 -6
- package/dist/bin.js +3 -3
- package/dist/chunk-DI2PLOJ6.js +1418 -0
- package/dist/chunk-DONMNPS7.js +466 -0
- package/dist/chunk-RSDCWAHD.js +328 -0
- package/dist/{chunk-GCDFX7UE.js → chunk-USOO77A5.js} +15 -5
- package/dist/{chunk-56VBV4MT.js → chunk-Y45MCRGI.js} +1298 -774
- package/dist/heal-5JHGCLDX.js +9 -0
- package/dist/health-VSL4MROO.js +20 -0
- package/dist/index.js +3 -3
- package/dist/studio-BCTWKXFH.js +309 -0
- package/dist/{upgrade-XMY6LIPS.js → upgrade-4NRDVD5N.js} +1 -1
- package/package.json +5 -5
- package/src/templates/DECANTR.md.template +4 -1
- package/dist/chunk-RRRHQ45P.js +0 -397
- package/dist/heal-SWUFIYU5.js +0 -99
package/dist/chunk-RRRHQ45P.js
DELETED
|
@@ -1,397 +0,0 @@
|
|
|
1
|
-
// src/lib/scan-interactions.ts
|
|
2
|
-
import { existsSync, readdirSync, readFileSync, statSync } from "fs";
|
|
3
|
-
import { extname, join } from "path";
|
|
4
|
-
import { verifyInteractionsInSource } from "@decantr/verifier";
|
|
5
|
-
var SCAN_EXTENSIONS = /* @__PURE__ */ new Set([".tsx", ".jsx", ".ts", ".js", ".html", ".mdx"]);
|
|
6
|
-
var SKIP_DIRECTORIES = /* @__PURE__ */ new Set([
|
|
7
|
-
"node_modules",
|
|
8
|
-
".decantr",
|
|
9
|
-
".git",
|
|
10
|
-
"dist",
|
|
11
|
-
"build",
|
|
12
|
-
".next",
|
|
13
|
-
".turbo",
|
|
14
|
-
"coverage",
|
|
15
|
-
".cache"
|
|
16
|
-
]);
|
|
17
|
-
var MAX_FILE_SIZE = 1024 * 1024;
|
|
18
|
-
function walkSourceTree(rootDir) {
|
|
19
|
-
const sources = /* @__PURE__ */ new Map();
|
|
20
|
-
function walk(dir) {
|
|
21
|
-
let entries;
|
|
22
|
-
try {
|
|
23
|
-
entries = readdirSync(dir);
|
|
24
|
-
} catch {
|
|
25
|
-
return;
|
|
26
|
-
}
|
|
27
|
-
for (const entry of entries) {
|
|
28
|
-
if (SKIP_DIRECTORIES.has(entry)) continue;
|
|
29
|
-
const fullPath = join(dir, entry);
|
|
30
|
-
let s;
|
|
31
|
-
try {
|
|
32
|
-
s = statSync(fullPath);
|
|
33
|
-
} catch {
|
|
34
|
-
continue;
|
|
35
|
-
}
|
|
36
|
-
if (s.isDirectory()) {
|
|
37
|
-
walk(fullPath);
|
|
38
|
-
} else if (s.isFile() && SCAN_EXTENSIONS.has(extname(entry))) {
|
|
39
|
-
if (s.size > MAX_FILE_SIZE) continue;
|
|
40
|
-
try {
|
|
41
|
-
sources.set(fullPath, readFileSync(fullPath, "utf8"));
|
|
42
|
-
} catch {
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
walk(rootDir);
|
|
48
|
-
return sources;
|
|
49
|
-
}
|
|
50
|
-
function collectDeclaredInteractions(projectRoot) {
|
|
51
|
-
const manifestPath = join(projectRoot, ".decantr", "context", "pack-manifest.json");
|
|
52
|
-
if (!existsSync(manifestPath)) return [];
|
|
53
|
-
let manifest;
|
|
54
|
-
try {
|
|
55
|
-
manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
56
|
-
} catch {
|
|
57
|
-
return [];
|
|
58
|
-
}
|
|
59
|
-
const all = [];
|
|
60
|
-
const pages = manifest.pages ?? [];
|
|
61
|
-
const contextDir = join(projectRoot, ".decantr", "context");
|
|
62
|
-
for (const page of pages) {
|
|
63
|
-
const packPath = join(contextDir, page.json);
|
|
64
|
-
if (!existsSync(packPath)) continue;
|
|
65
|
-
let pack;
|
|
66
|
-
try {
|
|
67
|
-
pack = JSON.parse(readFileSync(packPath, "utf8"));
|
|
68
|
-
} catch {
|
|
69
|
-
continue;
|
|
70
|
-
}
|
|
71
|
-
const patterns = pack.data?.patterns ?? [];
|
|
72
|
-
for (const pat of patterns) {
|
|
73
|
-
if (Array.isArray(pat.interactions)) {
|
|
74
|
-
all.push(...pat.interactions);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
return all;
|
|
79
|
-
}
|
|
80
|
-
function scanProjectInteractions(projectRoot) {
|
|
81
|
-
const declared = collectDeclaredInteractions(projectRoot);
|
|
82
|
-
if (declared.length === 0) return [];
|
|
83
|
-
const sources = walkSourceTree(projectRoot);
|
|
84
|
-
if (sources.size === 0) return [];
|
|
85
|
-
const missing = verifyInteractionsInSource(declared, sources);
|
|
86
|
-
return missing.map(({ interaction, suggestion }) => `${interaction} \u2192 ${suggestion}`);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// src/guard-context.ts
|
|
90
|
-
import { existsSync as existsSync2, readdirSync as readdirSync2, readFileSync as readFileSync2 } from "fs";
|
|
91
|
-
import { join as join2 } from "path";
|
|
92
|
-
function loadJsonEntries(dir) {
|
|
93
|
-
if (!existsSync2(dir)) return [];
|
|
94
|
-
try {
|
|
95
|
-
return readdirSync2(dir).filter((file) => file.endsWith(".json") && file !== "index.json").map((file) => JSON.parse(readFileSync2(join2(dir, file), "utf-8")));
|
|
96
|
-
} catch {
|
|
97
|
-
return [];
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
function buildGuardRegistryContext(projectRoot = process.cwd()) {
|
|
101
|
-
const themeRegistry = /* @__PURE__ */ new Map();
|
|
102
|
-
const patternRegistry = /* @__PURE__ */ new Map();
|
|
103
|
-
const cacheDir = join2(projectRoot, ".decantr", "cache");
|
|
104
|
-
const customDir = join2(projectRoot, ".decantr", "custom");
|
|
105
|
-
for (const data of loadJsonEntries(join2(cacheDir, "@official", "themes"))) {
|
|
106
|
-
if (typeof data.id === "string" && !themeRegistry.has(data.id)) {
|
|
107
|
-
themeRegistry.set(data.id, {
|
|
108
|
-
modes: Array.isArray(data.modes) ? data.modes.filter((mode) => typeof mode === "string") : ["light", "dark"]
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
for (const data of loadJsonEntries(join2(customDir, "themes"))) {
|
|
113
|
-
if (typeof data.id === "string") {
|
|
114
|
-
themeRegistry.set(`custom:${data.id}`, {
|
|
115
|
-
modes: Array.isArray(data.modes) ? data.modes.filter((mode) => typeof mode === "string") : ["light", "dark"]
|
|
116
|
-
});
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
for (const data of loadJsonEntries(join2(cacheDir, "@official", "patterns"))) {
|
|
120
|
-
if (typeof data.id === "string" && !patternRegistry.has(data.id)) {
|
|
121
|
-
patternRegistry.set(data.id, data);
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
for (const data of loadJsonEntries(join2(customDir, "patterns"))) {
|
|
125
|
-
if (typeof data.id === "string") {
|
|
126
|
-
patternRegistry.set(data.id, data);
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
return { themeRegistry, patternRegistry };
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// src/telemetry.ts
|
|
133
|
-
import { randomUUID } from "crypto";
|
|
134
|
-
import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync3, writeFileSync } from "fs";
|
|
135
|
-
import { homedir } from "os";
|
|
136
|
-
import { dirname, join as join3 } from "path";
|
|
137
|
-
import { fileURLToPath } from "url";
|
|
138
|
-
import {
|
|
139
|
-
createFetchTelemetrySink,
|
|
140
|
-
createTelemetryClient,
|
|
141
|
-
isTelemetryActorType
|
|
142
|
-
} from "@decantr/telemetry";
|
|
143
|
-
var TELEMETRY_ENDPOINT = "https://api.decantr.ai/v1/telemetry/guard";
|
|
144
|
-
var DEFAULT_TELEMETRY_EVENTS_ENDPOINT = "https://api.decantr.ai/v1/telemetry/events";
|
|
145
|
-
var TELEMETRY_TIMEOUT_MS = 3e3;
|
|
146
|
-
var DNA_RULES = /* @__PURE__ */ new Set(["theme", "style", "density", "accessibility", "theme-mode"]);
|
|
147
|
-
async function sendGuardMetrics(metrics) {
|
|
148
|
-
try {
|
|
149
|
-
const controller = new AbortController();
|
|
150
|
-
const timer = setTimeout(() => controller.abort(), TELEMETRY_TIMEOUT_MS);
|
|
151
|
-
await fetch(TELEMETRY_ENDPOINT, {
|
|
152
|
-
method: "POST",
|
|
153
|
-
headers: { "Content-Type": "application/json" },
|
|
154
|
-
body: JSON.stringify(metrics),
|
|
155
|
-
signal: controller.signal
|
|
156
|
-
});
|
|
157
|
-
clearTimeout(timer);
|
|
158
|
-
} catch {
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
function isOptedIn(projectRoot) {
|
|
162
|
-
const projectJsonPath = join3(projectRoot, ".decantr", "project.json");
|
|
163
|
-
if (!existsSync3(projectJsonPath)) return false;
|
|
164
|
-
try {
|
|
165
|
-
const data = JSON.parse(readFileSync3(projectJsonPath, "utf-8"));
|
|
166
|
-
return data.telemetry === true;
|
|
167
|
-
} catch {
|
|
168
|
-
return false;
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
function optIn(projectRoot) {
|
|
172
|
-
const projectJsonPath = join3(projectRoot, ".decantr", "project.json");
|
|
173
|
-
let data = {};
|
|
174
|
-
if (existsSync3(projectJsonPath)) {
|
|
175
|
-
try {
|
|
176
|
-
data = JSON.parse(readFileSync3(projectJsonPath, "utf-8"));
|
|
177
|
-
} catch {
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
data.telemetry = true;
|
|
181
|
-
mkdirSync(dirname(projectJsonPath), { recursive: true });
|
|
182
|
-
writeFileSync(projectJsonPath, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
183
|
-
}
|
|
184
|
-
async function sendCliCommandTelemetry(input) {
|
|
185
|
-
const projectRoot = input.projectRoot ?? process.cwd();
|
|
186
|
-
const command = normalizeCommand(input.args[0]);
|
|
187
|
-
if (!isOptedIn(projectRoot) || !command || command === "help" || command === "version") {
|
|
188
|
-
return;
|
|
189
|
-
}
|
|
190
|
-
const identities = ensureTelemetryIdentities(projectRoot);
|
|
191
|
-
if (!identities) {
|
|
192
|
-
return;
|
|
193
|
-
}
|
|
194
|
-
const client = createTelemetryClient({
|
|
195
|
-
sink: createFetchTelemetrySink({
|
|
196
|
-
endpoint: getTelemetryEventsEndpoint(),
|
|
197
|
-
timeoutMs: TELEMETRY_TIMEOUT_MS
|
|
198
|
-
})
|
|
199
|
-
});
|
|
200
|
-
const event = {
|
|
201
|
-
name: "cli.command.completed",
|
|
202
|
-
context: {
|
|
203
|
-
source: "cli",
|
|
204
|
-
actorType: getTelemetryActorType(),
|
|
205
|
-
environment: "production",
|
|
206
|
-
decantrVersion: getCliVersion(),
|
|
207
|
-
installId: identities.installId,
|
|
208
|
-
projectId: identities.projectId,
|
|
209
|
-
registrySource: inferRegistrySource(input.args)
|
|
210
|
-
},
|
|
211
|
-
properties: {
|
|
212
|
-
command,
|
|
213
|
-
success: input.success,
|
|
214
|
-
durationMs: input.durationMs,
|
|
215
|
-
adoptionMode: inferAdoptionMode(input.args),
|
|
216
|
-
errorCode: input.success ? void 0 : "cli_command_failed",
|
|
217
|
-
offline: input.args.includes("--offline"),
|
|
218
|
-
projectScope: inferProjectScope(projectRoot),
|
|
219
|
-
registrySource: inferRegistrySource(input.args),
|
|
220
|
-
targetFramework: inferFlagValue(input.args, "--target"),
|
|
221
|
-
workflowMode: inferWorkflowMode(input.args)
|
|
222
|
-
}
|
|
223
|
-
};
|
|
224
|
-
try {
|
|
225
|
-
await client.capture(event);
|
|
226
|
-
} catch {
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
function collectMetrics(essence, issues) {
|
|
230
|
-
const dna = essence.dna ?? {};
|
|
231
|
-
const blueprint = essence.blueprint ?? {};
|
|
232
|
-
const meta = essence.meta ?? {};
|
|
233
|
-
const guard = meta.guard ?? {};
|
|
234
|
-
const theme = dna.theme ?? {};
|
|
235
|
-
const sections = blueprint.sections ?? [];
|
|
236
|
-
const routes = blueprint.routes ?? {};
|
|
237
|
-
const byRule = {};
|
|
238
|
-
let dnaCount = 0;
|
|
239
|
-
let blueprintCount = 0;
|
|
240
|
-
for (const issue of issues) {
|
|
241
|
-
byRule[issue.rule] = (byRule[issue.rule] ?? 0) + 1;
|
|
242
|
-
if (DNA_RULES.has(issue.rule)) {
|
|
243
|
-
dnaCount++;
|
|
244
|
-
} else {
|
|
245
|
-
blueprintCount++;
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
return {
|
|
249
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
250
|
-
cli_version: getCliVersion(),
|
|
251
|
-
essence_version: essence.version ?? "unknown",
|
|
252
|
-
guard_mode: guard.mode ?? "unknown",
|
|
253
|
-
violations: {
|
|
254
|
-
dna: dnaCount,
|
|
255
|
-
blueprint: blueprintCount,
|
|
256
|
-
by_rule: byRule
|
|
257
|
-
},
|
|
258
|
-
resolution_rate: 0,
|
|
259
|
-
sections_count: sections.length,
|
|
260
|
-
routes_count: Object.keys(routes).length,
|
|
261
|
-
theme: theme.id ?? "unknown"
|
|
262
|
-
};
|
|
263
|
-
}
|
|
264
|
-
function ensureTelemetryIdentities(projectRoot) {
|
|
265
|
-
const installId = getOrCreateInstallId();
|
|
266
|
-
const projectJsonPath = join3(projectRoot, ".decantr", "project.json");
|
|
267
|
-
if (!existsSync3(projectJsonPath)) {
|
|
268
|
-
return null;
|
|
269
|
-
}
|
|
270
|
-
try {
|
|
271
|
-
const data = JSON.parse(readFileSync3(projectJsonPath, "utf-8"));
|
|
272
|
-
let projectId = typeof data.telemetryProjectId === "string" ? data.telemetryProjectId : void 0;
|
|
273
|
-
if (!projectId) {
|
|
274
|
-
projectId = `project_${randomUUID()}`;
|
|
275
|
-
data.telemetryProjectId = projectId;
|
|
276
|
-
writeFileSync(projectJsonPath, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
277
|
-
}
|
|
278
|
-
return { installId, projectId };
|
|
279
|
-
} catch {
|
|
280
|
-
return null;
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
function getOrCreateInstallId() {
|
|
284
|
-
const configDir = getConfigDir();
|
|
285
|
-
const configPath = join3(configDir, "config.json");
|
|
286
|
-
try {
|
|
287
|
-
if (existsSync3(configPath)) {
|
|
288
|
-
const data = JSON.parse(readFileSync3(configPath, "utf-8"));
|
|
289
|
-
if (typeof data.telemetryInstallId === "string") {
|
|
290
|
-
return data.telemetryInstallId;
|
|
291
|
-
}
|
|
292
|
-
const installId2 = `install_${randomUUID()}`;
|
|
293
|
-
data.telemetryInstallId = installId2;
|
|
294
|
-
writeFileSync(configPath, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
295
|
-
return installId2;
|
|
296
|
-
}
|
|
297
|
-
mkdirSync(configDir, { recursive: true });
|
|
298
|
-
const installId = `install_${randomUUID()}`;
|
|
299
|
-
writeFileSync(
|
|
300
|
-
configPath,
|
|
301
|
-
JSON.stringify({ telemetryInstallId: installId }, null, 2) + "\n",
|
|
302
|
-
"utf-8"
|
|
303
|
-
);
|
|
304
|
-
return installId;
|
|
305
|
-
} catch {
|
|
306
|
-
return `install_${randomUUID()}`;
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
function getConfigDir() {
|
|
310
|
-
return process.env.DECANTR_CONFIG_DIR || join3(homedir(), ".config", "decantr");
|
|
311
|
-
}
|
|
312
|
-
function getTelemetryEventsEndpoint() {
|
|
313
|
-
return process.env.DECANTR_TELEMETRY_ENDPOINT || DEFAULT_TELEMETRY_EVENTS_ENDPOINT;
|
|
314
|
-
}
|
|
315
|
-
function getTelemetryActorType() {
|
|
316
|
-
const configured = process.env.DECANTR_TELEMETRY_ACTOR_TYPE;
|
|
317
|
-
return isTelemetryActorType(configured) ? configured : "customer";
|
|
318
|
-
}
|
|
319
|
-
function normalizeCommand(command) {
|
|
320
|
-
if (!command) return null;
|
|
321
|
-
if (command === "--help" || command === "-h") return "help";
|
|
322
|
-
if (command === "--version" || command === "-v") return "version";
|
|
323
|
-
return command;
|
|
324
|
-
}
|
|
325
|
-
function inferFlagValue(args, flag) {
|
|
326
|
-
const equalsPrefix = `${flag}=`;
|
|
327
|
-
const inline = args.find((arg) => arg.startsWith(equalsPrefix));
|
|
328
|
-
if (inline) {
|
|
329
|
-
return inline.slice(equalsPrefix.length) || void 0;
|
|
330
|
-
}
|
|
331
|
-
const index = args.indexOf(flag);
|
|
332
|
-
if (index !== -1 && args[index + 1] && !args[index + 1].startsWith("-")) {
|
|
333
|
-
return args[index + 1];
|
|
334
|
-
}
|
|
335
|
-
return void 0;
|
|
336
|
-
}
|
|
337
|
-
function inferAdoptionMode(args) {
|
|
338
|
-
const value = inferFlagValue(args, "--adoption");
|
|
339
|
-
if (value === "contract-only" || value === "decantr-css" || value === "style-bridge") {
|
|
340
|
-
return value;
|
|
341
|
-
}
|
|
342
|
-
return void 0;
|
|
343
|
-
}
|
|
344
|
-
function inferWorkflowMode(args) {
|
|
345
|
-
const value = inferFlagValue(args, "--workflow");
|
|
346
|
-
if (value === "greenfield" || value === "greenfield-scaffold") {
|
|
347
|
-
return "greenfield-scaffold";
|
|
348
|
-
}
|
|
349
|
-
if (value === "contract" || value === "greenfield-contract-only") {
|
|
350
|
-
return "greenfield-contract-only";
|
|
351
|
-
}
|
|
352
|
-
if (value === "brownfield" || value === "brownfield-attach") {
|
|
353
|
-
return "brownfield-attach";
|
|
354
|
-
}
|
|
355
|
-
if (value === "hybrid" || value === "hybrid-compose") {
|
|
356
|
-
return "hybrid-compose";
|
|
357
|
-
}
|
|
358
|
-
return void 0;
|
|
359
|
-
}
|
|
360
|
-
function inferRegistrySource(args) {
|
|
361
|
-
if (args.includes("--offline")) {
|
|
362
|
-
return "cache";
|
|
363
|
-
}
|
|
364
|
-
if (args.some((arg) => arg === "--registry" || arg.startsWith("--registry="))) {
|
|
365
|
-
return "custom";
|
|
366
|
-
}
|
|
367
|
-
return "official";
|
|
368
|
-
}
|
|
369
|
-
function inferProjectScope(projectRoot) {
|
|
370
|
-
return existsSync3(join3(projectRoot, "pnpm-workspace.yaml")) || existsSync3(join3(projectRoot, "turbo.json")) || existsSync3(join3(projectRoot, "lerna.json")) ? "workspace-app" : "single-app";
|
|
371
|
-
}
|
|
372
|
-
function getCliVersion() {
|
|
373
|
-
try {
|
|
374
|
-
const here = dirname(fileURLToPath(import.meta.url));
|
|
375
|
-
const candidates = [join3(here, "..", "package.json"), join3(here, "..", "..", "package.json")];
|
|
376
|
-
for (const candidate of candidates) {
|
|
377
|
-
if (existsSync3(candidate)) {
|
|
378
|
-
const pkg = JSON.parse(readFileSync3(candidate, "utf-8"));
|
|
379
|
-
if (pkg.version) {
|
|
380
|
-
return pkg.version;
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
} catch {
|
|
385
|
-
}
|
|
386
|
-
return "unknown";
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
export {
|
|
390
|
-
scanProjectInteractions,
|
|
391
|
-
buildGuardRegistryContext,
|
|
392
|
-
sendGuardMetrics,
|
|
393
|
-
isOptedIn,
|
|
394
|
-
optIn,
|
|
395
|
-
sendCliCommandTelemetry,
|
|
396
|
-
collectMetrics
|
|
397
|
-
};
|
package/dist/heal-SWUFIYU5.js
DELETED
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
buildGuardRegistryContext,
|
|
3
|
-
collectMetrics,
|
|
4
|
-
isOptedIn,
|
|
5
|
-
optIn,
|
|
6
|
-
scanProjectInteractions,
|
|
7
|
-
sendGuardMetrics
|
|
8
|
-
} from "./chunk-RRRHQ45P.js";
|
|
9
|
-
|
|
10
|
-
// src/commands/heal.ts
|
|
11
|
-
import { existsSync, readFileSync } from "fs";
|
|
12
|
-
import { join } from "path";
|
|
13
|
-
import { evaluateGuard, validateEssence } from "@decantr/essence-spec";
|
|
14
|
-
var GREEN = "\x1B[32m";
|
|
15
|
-
var RED = "\x1B[31m";
|
|
16
|
-
var YELLOW = "\x1B[33m";
|
|
17
|
-
var CYAN = "\x1B[36m";
|
|
18
|
-
var RESET = "\x1B[0m";
|
|
19
|
-
var DIM = "\x1B[2m";
|
|
20
|
-
async function cmdHeal(projectRoot = process.cwd(), options = {}) {
|
|
21
|
-
const essencePath = join(projectRoot, "decantr.essence.json");
|
|
22
|
-
if (!existsSync(essencePath)) {
|
|
23
|
-
console.error("No decantr.essence.json found. Run `decantr init` first.");
|
|
24
|
-
process.exitCode = 1;
|
|
25
|
-
return;
|
|
26
|
-
}
|
|
27
|
-
const essence = JSON.parse(readFileSync(essencePath, "utf-8"));
|
|
28
|
-
console.log("Scanning for issues...\n");
|
|
29
|
-
const issues = [];
|
|
30
|
-
const validation = validateEssence(essence);
|
|
31
|
-
if (!validation.valid) {
|
|
32
|
-
for (const err of validation.errors) {
|
|
33
|
-
issues.push({
|
|
34
|
-
type: "error",
|
|
35
|
-
rule: "schema",
|
|
36
|
-
message: err
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
let interactionIssues = [];
|
|
41
|
-
try {
|
|
42
|
-
interactionIssues = scanProjectInteractions(projectRoot);
|
|
43
|
-
} catch {
|
|
44
|
-
}
|
|
45
|
-
try {
|
|
46
|
-
const guardContext = buildGuardRegistryContext(projectRoot);
|
|
47
|
-
const violations = evaluateGuard(essence, {
|
|
48
|
-
...guardContext,
|
|
49
|
-
interaction_issues: interactionIssues
|
|
50
|
-
});
|
|
51
|
-
for (const v of violations) {
|
|
52
|
-
issues.push({
|
|
53
|
-
type: v.severity === "error" ? "error" : "warning",
|
|
54
|
-
rule: v.rule,
|
|
55
|
-
message: v.message,
|
|
56
|
-
suggestion: v.suggestion
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
|
-
} catch {
|
|
60
|
-
}
|
|
61
|
-
if (issues.length === 0) {
|
|
62
|
-
console.log(`${GREEN}No issues found. Project is healthy.${RESET}`);
|
|
63
|
-
await maybeSendTelemetry(projectRoot, essence, issues, options);
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
console.log(`Found ${issues.length} issue(s):
|
|
67
|
-
`);
|
|
68
|
-
for (const issue of issues) {
|
|
69
|
-
const icon = issue.type === "error" ? `${RED}x${RESET}` : `${YELLOW}!${RESET}`;
|
|
70
|
-
console.log(`${icon} [${issue.rule}] ${issue.message}`);
|
|
71
|
-
if (issue.suggestion) {
|
|
72
|
-
console.log(` ${DIM}Suggestion: ${issue.suggestion}${RESET}`);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
console.log(`
|
|
76
|
-
${YELLOW}Manual fixes required. Review the issues above.${RESET}`);
|
|
77
|
-
const hasError = issues.some((i) => i.type === "error");
|
|
78
|
-
if (hasError) {
|
|
79
|
-
process.exitCode = 1;
|
|
80
|
-
}
|
|
81
|
-
await maybeSendTelemetry(projectRoot, essence, issues, options);
|
|
82
|
-
}
|
|
83
|
-
async function maybeSendTelemetry(projectRoot, essence, issues, options) {
|
|
84
|
-
if (options.telemetry && !isOptedIn(projectRoot)) {
|
|
85
|
-
optIn(projectRoot);
|
|
86
|
-
console.log(
|
|
87
|
-
`
|
|
88
|
-
${CYAN}Telemetry enabled.${RESET} Anonymous guard metrics will be sent on future checks.`
|
|
89
|
-
);
|
|
90
|
-
console.log(`${DIM}Set "telemetry": false in .decantr/project.json to opt out.${RESET}`);
|
|
91
|
-
}
|
|
92
|
-
if (isOptedIn(projectRoot)) {
|
|
93
|
-
const metrics = collectMetrics(essence, issues);
|
|
94
|
-
sendGuardMetrics(metrics);
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
export {
|
|
98
|
-
cmdHeal
|
|
99
|
-
};
|