@decantr/cli 1.7.26 → 1.7.27
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/LICENSE +21 -0
- package/dist/bin.js +3 -3
- package/dist/{chunk-RAAUNHXD.js → chunk-DSVBYVG5.js} +22 -6
- package/dist/{chunk-3K6HWLD5.js → chunk-GCDFX7UE.js} +30 -4
- package/dist/chunk-RSIOCKZF.js +391 -0
- package/dist/heal-JPW3BXS3.js +99 -0
- package/dist/index.js +3 -3
- package/dist/{upgrade-KG42WK5C.js → upgrade-XMY6LIPS.js} +1 -1
- package/package.json +9 -9
- package/dist/chunk-QRQCPD3C.js +0 -135
- package/dist/heal-EMT5LYVZ.js +0 -175
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Decantr AI
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/bin.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import "./chunk-
|
|
3
|
-
import "./chunk-
|
|
4
|
-
import "./chunk-
|
|
2
|
+
import "./chunk-DSVBYVG5.js";
|
|
3
|
+
import "./chunk-GCDFX7UE.js";
|
|
4
|
+
import "./chunk-RSIOCKZF.js";
|
|
@@ -14,11 +14,12 @@ import {
|
|
|
14
14
|
scaffoldProject,
|
|
15
15
|
syncRegistry,
|
|
16
16
|
writeExecutionPackBundleArtifacts
|
|
17
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-GCDFX7UE.js";
|
|
18
18
|
import {
|
|
19
19
|
buildGuardRegistryContext,
|
|
20
|
-
scanProjectInteractions
|
|
21
|
-
|
|
20
|
+
scanProjectInteractions,
|
|
21
|
+
sendCliCommandTelemetry
|
|
22
|
+
} from "./chunk-RSIOCKZF.js";
|
|
22
23
|
|
|
23
24
|
// src/index.ts
|
|
24
25
|
import { existsSync as existsSync27, mkdirSync as mkdirSync11, readdirSync as readdirSync7, readFileSync as readFileSync20, writeFileSync as writeFileSync14 } from "fs";
|
|
@@ -6674,7 +6675,7 @@ async function main() {
|
|
|
6674
6675
|
break;
|
|
6675
6676
|
}
|
|
6676
6677
|
case "upgrade": {
|
|
6677
|
-
const { cmdUpgrade } = await import("./upgrade-
|
|
6678
|
+
const { cmdUpgrade } = await import("./upgrade-XMY6LIPS.js");
|
|
6678
6679
|
const applyFlag = args.includes("--apply");
|
|
6679
6680
|
await cmdUpgrade(process.cwd(), { apply: applyFlag });
|
|
6680
6681
|
break;
|
|
@@ -6686,7 +6687,7 @@ async function main() {
|
|
|
6686
6687
|
`${YELLOW9}Note: \`decantr heal\` is deprecated. Use \`decantr check\` instead.${RESET13}`
|
|
6687
6688
|
);
|
|
6688
6689
|
}
|
|
6689
|
-
const { cmdHeal } = await import("./heal-
|
|
6690
|
+
const { cmdHeal } = await import("./heal-JPW3BXS3.js");
|
|
6690
6691
|
const telemetryFlag = args.includes("--telemetry");
|
|
6691
6692
|
await cmdHeal(process.cwd(), { telemetry: telemetryFlag });
|
|
6692
6693
|
break;
|
|
@@ -7172,8 +7173,23 @@ async function main() {
|
|
|
7172
7173
|
process.exitCode = 1;
|
|
7173
7174
|
}
|
|
7174
7175
|
}
|
|
7175
|
-
|
|
7176
|
+
var cliStartedAt = Date.now();
|
|
7177
|
+
var cliArgs = process.argv.slice(2);
|
|
7178
|
+
main().then(async () => {
|
|
7179
|
+
await sendCliCommandTelemetry({
|
|
7180
|
+
args: cliArgs,
|
|
7181
|
+
durationMs: Date.now() - cliStartedAt,
|
|
7182
|
+
projectRoot: process.cwd(),
|
|
7183
|
+
success: !process.exitCode || process.exitCode === 0
|
|
7184
|
+
});
|
|
7185
|
+
}).catch(async (e) => {
|
|
7176
7186
|
console.error(error3(e.message));
|
|
7177
7187
|
if (e.stack) console.error(e.stack);
|
|
7178
7188
|
process.exitCode = 1;
|
|
7189
|
+
await sendCliCommandTelemetry({
|
|
7190
|
+
args: cliArgs,
|
|
7191
|
+
durationMs: Date.now() - cliStartedAt,
|
|
7192
|
+
projectRoot: process.cwd(),
|
|
7193
|
+
success: false
|
|
7194
|
+
});
|
|
7179
7195
|
});
|
|
@@ -2422,6 +2422,18 @@ function generateTokensCSS(themeData, mode, spatialTokens, options) {
|
|
|
2422
2422
|
}
|
|
2423
2423
|
const seed = themeData.seed || {};
|
|
2424
2424
|
const palette = themeData.palette || {};
|
|
2425
|
+
const CORE_PALETTE_KEYS = /* @__PURE__ */ new Set([
|
|
2426
|
+
"background",
|
|
2427
|
+
"surface",
|
|
2428
|
+
"surface-raised",
|
|
2429
|
+
"border",
|
|
2430
|
+
"text",
|
|
2431
|
+
"text-muted",
|
|
2432
|
+
"primary",
|
|
2433
|
+
"primary-hover",
|
|
2434
|
+
"secondary",
|
|
2435
|
+
"accent"
|
|
2436
|
+
]);
|
|
2425
2437
|
const resolvedMode = mode === "auto" ? "dark" : mode;
|
|
2426
2438
|
const FALLBACKS = {
|
|
2427
2439
|
bg: { light: "#ffffff", dark: "#18181b" },
|
|
@@ -2439,11 +2451,19 @@ function generateTokensCSS(themeData, mode, spatialTokens, options) {
|
|
|
2439
2451
|
if (!fallbacks) return tokenModeKey === "light" ? "#ffffff" : "#18181b";
|
|
2440
2452
|
return fallbacks[tokenModeKey];
|
|
2441
2453
|
};
|
|
2454
|
+
const pickPalette = (key) => {
|
|
2455
|
+
const entry = palette[key];
|
|
2456
|
+
return entry?.[tokenMode] || entry?.[tokenModeKey] || entry?.dark || entry?.light;
|
|
2457
|
+
};
|
|
2458
|
+
const extendedPaletteTokens = Object.fromEntries(
|
|
2459
|
+
Object.keys(palette).filter((key) => !CORE_PALETTE_KEYS.has(key)).map((key) => [`--d-${key.replace(/[^a-zA-Z0-9-]/g, "-")}`, pickPalette(key)]).filter((entry) => typeof entry[1] === "string" && entry[1].length > 0)
|
|
2460
|
+
);
|
|
2442
2461
|
return {
|
|
2443
2462
|
// Seed colors
|
|
2444
|
-
"--d-primary": seed.primary || "#6366f1",
|
|
2445
|
-
"--d-secondary":
|
|
2446
|
-
"--d-accent": seed.accent || "#f59e0b",
|
|
2463
|
+
"--d-primary": pickPalette("primary") || seed.primary || "#6366f1",
|
|
2464
|
+
"--d-secondary": pickPalette("secondary") || seed.secondary || pickFb("secondary"),
|
|
2465
|
+
"--d-accent": pickPalette("accent") || seed.accent || "#f59e0b",
|
|
2466
|
+
...extendedPaletteTokens,
|
|
2447
2467
|
// Palette colors (mode-aware with mode-aware fallbacks)
|
|
2448
2468
|
"--d-bg": palette.background?.[tokenMode] || pickFb("bg"),
|
|
2449
2469
|
"--d-surface": palette.surface?.[tokenMode] || pickFb("surface"),
|
|
@@ -2513,12 +2533,17 @@ function generateTokensCSS(themeData, mode, spatialTokens, options) {
|
|
|
2513
2533
|
"--d-motion-ease-out": "cubic-bezier(0, 0, 0.2, 1)",
|
|
2514
2534
|
"--d-motion-ease-in": "cubic-bezier(0.4, 0, 1, 1)",
|
|
2515
2535
|
"--d-motion-ease-spring": "cubic-bezier(0.34, 1.56, 0.64, 1)",
|
|
2536
|
+
"--d-duration-hover": "var(--d-motion-fast)",
|
|
2537
|
+
"--d-duration-entrance": "var(--d-motion-base)",
|
|
2538
|
+
"--d-easing": "var(--d-motion-ease-out)",
|
|
2539
|
+
"--d-accent-glow": "color-mix(in srgb, var(--d-accent) 24%, transparent)",
|
|
2516
2540
|
// Typography scale (v2.1 Tier B2). Canonical sizes + weights +
|
|
2517
2541
|
// tracking + leading. d-display, d-headline, d-title, d-prose,
|
|
2518
2542
|
// d-caption, d-eyebrow read these. Themes override via
|
|
2519
2543
|
// theme.typography.* below.
|
|
2520
2544
|
"--d-font-body": "ui-sans-serif, system-ui, -apple-system, sans-serif",
|
|
2521
2545
|
"--d-font-display": "ui-sans-serif, system-ui, -apple-system, sans-serif",
|
|
2546
|
+
"--d-font-mono": "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace",
|
|
2522
2547
|
"--d-weight-regular": "400",
|
|
2523
2548
|
"--d-weight-medium": "500",
|
|
2524
2549
|
"--d-weight-semibold": "600",
|
|
@@ -2631,7 +2656,8 @@ ${lines}${spatialLines}
|
|
|
2631
2656
|
"--d-elevation-2",
|
|
2632
2657
|
"--d-elevation-3",
|
|
2633
2658
|
"--d-elevation-4",
|
|
2634
|
-
"--d-elevation-5"
|
|
2659
|
+
"--d-elevation-5",
|
|
2660
|
+
...Object.keys(palette).filter((key) => !CORE_PALETTE_KEYS.has(key)).map((key) => `--d-${key.replace(/[^a-zA-Z0-9-]/g, "-")}`)
|
|
2635
2661
|
];
|
|
2636
2662
|
if (mode === "auto") {
|
|
2637
2663
|
const lightTokens = buildTokens("light");
|
|
@@ -0,0 +1,391 @@
|
|
|
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
|
+
} from "@decantr/telemetry";
|
|
142
|
+
var TELEMETRY_ENDPOINT = "https://api.decantr.ai/v1/telemetry/guard";
|
|
143
|
+
var DEFAULT_TELEMETRY_EVENTS_ENDPOINT = "https://api.decantr.ai/v1/telemetry/events";
|
|
144
|
+
var TELEMETRY_TIMEOUT_MS = 3e3;
|
|
145
|
+
var DNA_RULES = /* @__PURE__ */ new Set(["theme", "style", "density", "accessibility", "theme-mode"]);
|
|
146
|
+
async function sendGuardMetrics(metrics) {
|
|
147
|
+
try {
|
|
148
|
+
const controller = new AbortController();
|
|
149
|
+
const timer = setTimeout(() => controller.abort(), TELEMETRY_TIMEOUT_MS);
|
|
150
|
+
await fetch(TELEMETRY_ENDPOINT, {
|
|
151
|
+
method: "POST",
|
|
152
|
+
headers: { "Content-Type": "application/json" },
|
|
153
|
+
body: JSON.stringify(metrics),
|
|
154
|
+
signal: controller.signal
|
|
155
|
+
});
|
|
156
|
+
clearTimeout(timer);
|
|
157
|
+
} catch {
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
function isOptedIn(projectRoot) {
|
|
161
|
+
const projectJsonPath = join3(projectRoot, ".decantr", "project.json");
|
|
162
|
+
if (!existsSync3(projectJsonPath)) return false;
|
|
163
|
+
try {
|
|
164
|
+
const data = JSON.parse(readFileSync3(projectJsonPath, "utf-8"));
|
|
165
|
+
return data.telemetry === true;
|
|
166
|
+
} catch {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
function optIn(projectRoot) {
|
|
171
|
+
const projectJsonPath = join3(projectRoot, ".decantr", "project.json");
|
|
172
|
+
let data = {};
|
|
173
|
+
if (existsSync3(projectJsonPath)) {
|
|
174
|
+
try {
|
|
175
|
+
data = JSON.parse(readFileSync3(projectJsonPath, "utf-8"));
|
|
176
|
+
} catch {
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
data.telemetry = true;
|
|
180
|
+
mkdirSync(dirname(projectJsonPath), { recursive: true });
|
|
181
|
+
writeFileSync(projectJsonPath, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
182
|
+
}
|
|
183
|
+
async function sendCliCommandTelemetry(input) {
|
|
184
|
+
const projectRoot = input.projectRoot ?? process.cwd();
|
|
185
|
+
const command = normalizeCommand(input.args[0]);
|
|
186
|
+
if (!isOptedIn(projectRoot) || !command || command === "help" || command === "version") {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
const identities = ensureTelemetryIdentities(projectRoot);
|
|
190
|
+
if (!identities) {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
const client = createTelemetryClient({
|
|
194
|
+
sink: createFetchTelemetrySink({
|
|
195
|
+
endpoint: getTelemetryEventsEndpoint(),
|
|
196
|
+
timeoutMs: TELEMETRY_TIMEOUT_MS
|
|
197
|
+
})
|
|
198
|
+
});
|
|
199
|
+
const event = {
|
|
200
|
+
name: "cli.command.completed",
|
|
201
|
+
context: {
|
|
202
|
+
source: "cli",
|
|
203
|
+
environment: "production",
|
|
204
|
+
decantrVersion: getCliVersion(),
|
|
205
|
+
installId: identities.installId,
|
|
206
|
+
projectId: identities.projectId,
|
|
207
|
+
registrySource: inferRegistrySource(input.args)
|
|
208
|
+
},
|
|
209
|
+
properties: {
|
|
210
|
+
command,
|
|
211
|
+
success: input.success,
|
|
212
|
+
durationMs: input.durationMs,
|
|
213
|
+
adoptionMode: inferAdoptionMode(input.args),
|
|
214
|
+
errorCode: input.success ? void 0 : "cli_command_failed",
|
|
215
|
+
offline: input.args.includes("--offline"),
|
|
216
|
+
projectScope: inferProjectScope(projectRoot),
|
|
217
|
+
registrySource: inferRegistrySource(input.args),
|
|
218
|
+
targetFramework: inferFlagValue(input.args, "--target"),
|
|
219
|
+
workflowMode: inferWorkflowMode(input.args)
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
try {
|
|
223
|
+
await client.capture(event);
|
|
224
|
+
} catch {
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
function collectMetrics(essence, issues) {
|
|
228
|
+
const dna = essence.dna ?? {};
|
|
229
|
+
const blueprint = essence.blueprint ?? {};
|
|
230
|
+
const meta = essence.meta ?? {};
|
|
231
|
+
const guard = meta.guard ?? {};
|
|
232
|
+
const theme = dna.theme ?? {};
|
|
233
|
+
const sections = blueprint.sections ?? [];
|
|
234
|
+
const routes = blueprint.routes ?? {};
|
|
235
|
+
const byRule = {};
|
|
236
|
+
let dnaCount = 0;
|
|
237
|
+
let blueprintCount = 0;
|
|
238
|
+
for (const issue of issues) {
|
|
239
|
+
byRule[issue.rule] = (byRule[issue.rule] ?? 0) + 1;
|
|
240
|
+
if (DNA_RULES.has(issue.rule)) {
|
|
241
|
+
dnaCount++;
|
|
242
|
+
} else {
|
|
243
|
+
blueprintCount++;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return {
|
|
247
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
248
|
+
cli_version: getCliVersion(),
|
|
249
|
+
essence_version: essence.version ?? "unknown",
|
|
250
|
+
guard_mode: guard.mode ?? "unknown",
|
|
251
|
+
violations: {
|
|
252
|
+
dna: dnaCount,
|
|
253
|
+
blueprint: blueprintCount,
|
|
254
|
+
by_rule: byRule
|
|
255
|
+
},
|
|
256
|
+
resolution_rate: 0,
|
|
257
|
+
sections_count: sections.length,
|
|
258
|
+
routes_count: Object.keys(routes).length,
|
|
259
|
+
theme: theme.id ?? "unknown"
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
function ensureTelemetryIdentities(projectRoot) {
|
|
263
|
+
const installId = getOrCreateInstallId();
|
|
264
|
+
const projectJsonPath = join3(projectRoot, ".decantr", "project.json");
|
|
265
|
+
if (!existsSync3(projectJsonPath)) {
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
try {
|
|
269
|
+
const data = JSON.parse(readFileSync3(projectJsonPath, "utf-8"));
|
|
270
|
+
let projectId = typeof data.telemetryProjectId === "string" ? data.telemetryProjectId : void 0;
|
|
271
|
+
if (!projectId) {
|
|
272
|
+
projectId = `project_${randomUUID()}`;
|
|
273
|
+
data.telemetryProjectId = projectId;
|
|
274
|
+
writeFileSync(projectJsonPath, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
275
|
+
}
|
|
276
|
+
return { installId, projectId };
|
|
277
|
+
} catch {
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
function getOrCreateInstallId() {
|
|
282
|
+
const configDir = getConfigDir();
|
|
283
|
+
const configPath = join3(configDir, "config.json");
|
|
284
|
+
try {
|
|
285
|
+
if (existsSync3(configPath)) {
|
|
286
|
+
const data = JSON.parse(readFileSync3(configPath, "utf-8"));
|
|
287
|
+
if (typeof data.telemetryInstallId === "string") {
|
|
288
|
+
return data.telemetryInstallId;
|
|
289
|
+
}
|
|
290
|
+
const installId2 = `install_${randomUUID()}`;
|
|
291
|
+
data.telemetryInstallId = installId2;
|
|
292
|
+
writeFileSync(configPath, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
293
|
+
return installId2;
|
|
294
|
+
}
|
|
295
|
+
mkdirSync(configDir, { recursive: true });
|
|
296
|
+
const installId = `install_${randomUUID()}`;
|
|
297
|
+
writeFileSync(
|
|
298
|
+
configPath,
|
|
299
|
+
JSON.stringify({ telemetryInstallId: installId }, null, 2) + "\n",
|
|
300
|
+
"utf-8"
|
|
301
|
+
);
|
|
302
|
+
return installId;
|
|
303
|
+
} catch {
|
|
304
|
+
return `install_${randomUUID()}`;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
function getConfigDir() {
|
|
308
|
+
return process.env.DECANTR_CONFIG_DIR || join3(homedir(), ".config", "decantr");
|
|
309
|
+
}
|
|
310
|
+
function getTelemetryEventsEndpoint() {
|
|
311
|
+
return process.env.DECANTR_TELEMETRY_ENDPOINT || DEFAULT_TELEMETRY_EVENTS_ENDPOINT;
|
|
312
|
+
}
|
|
313
|
+
function normalizeCommand(command) {
|
|
314
|
+
if (!command) return null;
|
|
315
|
+
if (command === "--help" || command === "-h") return "help";
|
|
316
|
+
if (command === "--version" || command === "-v") return "version";
|
|
317
|
+
return command;
|
|
318
|
+
}
|
|
319
|
+
function inferFlagValue(args, flag) {
|
|
320
|
+
const equalsPrefix = `${flag}=`;
|
|
321
|
+
const inline = args.find((arg) => arg.startsWith(equalsPrefix));
|
|
322
|
+
if (inline) {
|
|
323
|
+
return inline.slice(equalsPrefix.length) || void 0;
|
|
324
|
+
}
|
|
325
|
+
const index = args.indexOf(flag);
|
|
326
|
+
if (index !== -1 && args[index + 1] && !args[index + 1].startsWith("-")) {
|
|
327
|
+
return args[index + 1];
|
|
328
|
+
}
|
|
329
|
+
return void 0;
|
|
330
|
+
}
|
|
331
|
+
function inferAdoptionMode(args) {
|
|
332
|
+
const value = inferFlagValue(args, "--adoption");
|
|
333
|
+
if (value === "contract-only" || value === "decantr-css" || value === "style-bridge") {
|
|
334
|
+
return value;
|
|
335
|
+
}
|
|
336
|
+
return void 0;
|
|
337
|
+
}
|
|
338
|
+
function inferWorkflowMode(args) {
|
|
339
|
+
const value = inferFlagValue(args, "--workflow");
|
|
340
|
+
if (value === "greenfield" || value === "greenfield-scaffold") {
|
|
341
|
+
return "greenfield-scaffold";
|
|
342
|
+
}
|
|
343
|
+
if (value === "contract" || value === "greenfield-contract-only") {
|
|
344
|
+
return "greenfield-contract-only";
|
|
345
|
+
}
|
|
346
|
+
if (value === "brownfield" || value === "brownfield-attach") {
|
|
347
|
+
return "brownfield-attach";
|
|
348
|
+
}
|
|
349
|
+
if (value === "hybrid" || value === "hybrid-compose") {
|
|
350
|
+
return "hybrid-compose";
|
|
351
|
+
}
|
|
352
|
+
return void 0;
|
|
353
|
+
}
|
|
354
|
+
function inferRegistrySource(args) {
|
|
355
|
+
if (args.includes("--offline")) {
|
|
356
|
+
return "cache";
|
|
357
|
+
}
|
|
358
|
+
if (args.some((arg) => arg === "--registry" || arg.startsWith("--registry="))) {
|
|
359
|
+
return "custom";
|
|
360
|
+
}
|
|
361
|
+
return "official";
|
|
362
|
+
}
|
|
363
|
+
function inferProjectScope(projectRoot) {
|
|
364
|
+
return existsSync3(join3(projectRoot, "pnpm-workspace.yaml")) || existsSync3(join3(projectRoot, "turbo.json")) || existsSync3(join3(projectRoot, "lerna.json")) ? "workspace-app" : "single-app";
|
|
365
|
+
}
|
|
366
|
+
function getCliVersion() {
|
|
367
|
+
try {
|
|
368
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
369
|
+
const candidates = [join3(here, "..", "package.json"), join3(here, "..", "..", "package.json")];
|
|
370
|
+
for (const candidate of candidates) {
|
|
371
|
+
if (existsSync3(candidate)) {
|
|
372
|
+
const pkg = JSON.parse(readFileSync3(candidate, "utf-8"));
|
|
373
|
+
if (pkg.version) {
|
|
374
|
+
return pkg.version;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
} catch {
|
|
379
|
+
}
|
|
380
|
+
return "unknown";
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
export {
|
|
384
|
+
scanProjectInteractions,
|
|
385
|
+
buildGuardRegistryContext,
|
|
386
|
+
sendGuardMetrics,
|
|
387
|
+
isOptedIn,
|
|
388
|
+
optIn,
|
|
389
|
+
sendCliCommandTelemetry,
|
|
390
|
+
collectMetrics
|
|
391
|
+
};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildGuardRegistryContext,
|
|
3
|
+
collectMetrics,
|
|
4
|
+
isOptedIn,
|
|
5
|
+
optIn,
|
|
6
|
+
scanProjectInteractions,
|
|
7
|
+
sendGuardMetrics
|
|
8
|
+
} from "./chunk-RSIOCKZF.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
|
+
};
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import "./chunk-
|
|
2
|
-
import "./chunk-
|
|
3
|
-
import "./chunk-
|
|
1
|
+
import "./chunk-DSVBYVG5.js";
|
|
2
|
+
import "./chunk-GCDFX7UE.js";
|
|
3
|
+
import "./chunk-RSIOCKZF.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decantr/cli",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.27",
|
|
4
4
|
"description": "Decantr CLI — scaffold, audit, and maintain Decantr projects from the terminal",
|
|
5
5
|
"author": "Decantr AI",
|
|
6
6
|
"license": "MIT",
|
|
@@ -29,19 +29,19 @@
|
|
|
29
29
|
"publishConfig": {
|
|
30
30
|
"access": "public"
|
|
31
31
|
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@decantr/core": "1.0.6",
|
|
34
|
+
"@decantr/essence-spec": "1.0.6",
|
|
35
|
+
"@decantr/telemetry": "0.1.0",
|
|
36
|
+
"@decantr/verifier": "1.0.5",
|
|
37
|
+
"@decantr/registry": "1.0.4"
|
|
38
|
+
},
|
|
32
39
|
"scripts": {
|
|
33
40
|
"build": "tsup",
|
|
34
41
|
"certify:blueprints": "pnpm build && node scripts/certify-blueprints.mjs",
|
|
35
42
|
"certify:workflows": "pnpm build && node scripts/certify-workflows.mjs",
|
|
36
43
|
"test": "vitest run",
|
|
37
44
|
"test:watch": "vitest",
|
|
38
|
-
"prepublishOnly": "pnpm build",
|
|
39
45
|
"preversion": "pnpm build && pnpm test"
|
|
40
|
-
},
|
|
41
|
-
"dependencies": {
|
|
42
|
-
"@decantr/core": "workspace:*",
|
|
43
|
-
"@decantr/essence-spec": "workspace:*",
|
|
44
|
-
"@decantr/registry": "workspace:*",
|
|
45
|
-
"@decantr/verifier": "workspace:*"
|
|
46
46
|
}
|
|
47
|
-
}
|
|
47
|
+
}
|
package/dist/chunk-QRQCPD3C.js
DELETED
|
@@ -1,135 +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
|
-
export {
|
|
133
|
-
scanProjectInteractions,
|
|
134
|
-
buildGuardRegistryContext
|
|
135
|
-
};
|
package/dist/heal-EMT5LYVZ.js
DELETED
|
@@ -1,175 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
buildGuardRegistryContext,
|
|
3
|
-
scanProjectInteractions
|
|
4
|
-
} from "./chunk-QRQCPD3C.js";
|
|
5
|
-
|
|
6
|
-
// src/commands/heal.ts
|
|
7
|
-
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
8
|
-
import { join as join2 } from "path";
|
|
9
|
-
import { evaluateGuard, validateEssence } from "@decantr/essence-spec";
|
|
10
|
-
|
|
11
|
-
// src/telemetry.ts
|
|
12
|
-
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
13
|
-
import { join } from "path";
|
|
14
|
-
var TELEMETRY_ENDPOINT = "https://api.decantr.ai/v1/telemetry/guard";
|
|
15
|
-
var TELEMETRY_TIMEOUT_MS = 3e3;
|
|
16
|
-
var DNA_RULES = /* @__PURE__ */ new Set(["theme", "style", "density", "accessibility", "theme-mode"]);
|
|
17
|
-
async function sendGuardMetrics(metrics) {
|
|
18
|
-
try {
|
|
19
|
-
const controller = new AbortController();
|
|
20
|
-
const timer = setTimeout(() => controller.abort(), TELEMETRY_TIMEOUT_MS);
|
|
21
|
-
await fetch(TELEMETRY_ENDPOINT, {
|
|
22
|
-
method: "POST",
|
|
23
|
-
headers: { "Content-Type": "application/json" },
|
|
24
|
-
body: JSON.stringify(metrics),
|
|
25
|
-
signal: controller.signal
|
|
26
|
-
});
|
|
27
|
-
clearTimeout(timer);
|
|
28
|
-
} catch {
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
function isOptedIn(projectRoot) {
|
|
32
|
-
const projectJsonPath = join(projectRoot, ".decantr", "project.json");
|
|
33
|
-
if (!existsSync(projectJsonPath)) return false;
|
|
34
|
-
try {
|
|
35
|
-
const data = JSON.parse(readFileSync(projectJsonPath, "utf-8"));
|
|
36
|
-
return data.telemetry === true;
|
|
37
|
-
} catch {
|
|
38
|
-
return false;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
function optIn(projectRoot) {
|
|
42
|
-
const projectJsonPath = join(projectRoot, ".decantr", "project.json");
|
|
43
|
-
let data = {};
|
|
44
|
-
if (existsSync(projectJsonPath)) {
|
|
45
|
-
try {
|
|
46
|
-
data = JSON.parse(readFileSync(projectJsonPath, "utf-8"));
|
|
47
|
-
} catch {
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
data.telemetry = true;
|
|
51
|
-
writeFileSync(projectJsonPath, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
52
|
-
}
|
|
53
|
-
function collectMetrics(essence, issues) {
|
|
54
|
-
const dna = essence.dna ?? {};
|
|
55
|
-
const blueprint = essence.blueprint ?? {};
|
|
56
|
-
const meta = essence.meta ?? {};
|
|
57
|
-
const guard = meta.guard ?? {};
|
|
58
|
-
const theme = dna.theme ?? {};
|
|
59
|
-
const sections = blueprint.sections ?? [];
|
|
60
|
-
const routes = blueprint.routes ?? {};
|
|
61
|
-
const byRule = {};
|
|
62
|
-
let dnaCount = 0;
|
|
63
|
-
let blueprintCount = 0;
|
|
64
|
-
for (const issue of issues) {
|
|
65
|
-
byRule[issue.rule] = (byRule[issue.rule] ?? 0) + 1;
|
|
66
|
-
if (DNA_RULES.has(issue.rule)) {
|
|
67
|
-
dnaCount++;
|
|
68
|
-
} else {
|
|
69
|
-
blueprintCount++;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
return {
|
|
73
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
74
|
-
cli_version: "1.5.1",
|
|
75
|
-
essence_version: essence.version ?? "unknown",
|
|
76
|
-
guard_mode: guard.mode ?? "unknown",
|
|
77
|
-
violations: {
|
|
78
|
-
dna: dnaCount,
|
|
79
|
-
blueprint: blueprintCount,
|
|
80
|
-
by_rule: byRule
|
|
81
|
-
},
|
|
82
|
-
resolution_rate: 0,
|
|
83
|
-
sections_count: sections.length,
|
|
84
|
-
routes_count: Object.keys(routes).length,
|
|
85
|
-
theme: theme.id ?? "unknown"
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// src/commands/heal.ts
|
|
90
|
-
var GREEN = "\x1B[32m";
|
|
91
|
-
var RED = "\x1B[31m";
|
|
92
|
-
var YELLOW = "\x1B[33m";
|
|
93
|
-
var CYAN = "\x1B[36m";
|
|
94
|
-
var RESET = "\x1B[0m";
|
|
95
|
-
var DIM = "\x1B[2m";
|
|
96
|
-
async function cmdHeal(projectRoot = process.cwd(), options = {}) {
|
|
97
|
-
const essencePath = join2(projectRoot, "decantr.essence.json");
|
|
98
|
-
if (!existsSync2(essencePath)) {
|
|
99
|
-
console.error("No decantr.essence.json found. Run `decantr init` first.");
|
|
100
|
-
process.exitCode = 1;
|
|
101
|
-
return;
|
|
102
|
-
}
|
|
103
|
-
const essence = JSON.parse(readFileSync2(essencePath, "utf-8"));
|
|
104
|
-
console.log("Scanning for issues...\n");
|
|
105
|
-
const issues = [];
|
|
106
|
-
const validation = validateEssence(essence);
|
|
107
|
-
if (!validation.valid) {
|
|
108
|
-
for (const err of validation.errors) {
|
|
109
|
-
issues.push({
|
|
110
|
-
type: "error",
|
|
111
|
-
rule: "schema",
|
|
112
|
-
message: err
|
|
113
|
-
});
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
let interactionIssues = [];
|
|
117
|
-
try {
|
|
118
|
-
interactionIssues = scanProjectInteractions(projectRoot);
|
|
119
|
-
} catch {
|
|
120
|
-
}
|
|
121
|
-
try {
|
|
122
|
-
const guardContext = buildGuardRegistryContext(projectRoot);
|
|
123
|
-
const violations = evaluateGuard(essence, {
|
|
124
|
-
...guardContext,
|
|
125
|
-
interaction_issues: interactionIssues
|
|
126
|
-
});
|
|
127
|
-
for (const v of violations) {
|
|
128
|
-
issues.push({
|
|
129
|
-
type: v.severity === "error" ? "error" : "warning",
|
|
130
|
-
rule: v.rule,
|
|
131
|
-
message: v.message,
|
|
132
|
-
suggestion: v.suggestion
|
|
133
|
-
});
|
|
134
|
-
}
|
|
135
|
-
} catch {
|
|
136
|
-
}
|
|
137
|
-
if (issues.length === 0) {
|
|
138
|
-
console.log(`${GREEN}No issues found. Project is healthy.${RESET}`);
|
|
139
|
-
await maybeSendTelemetry(projectRoot, essence, issues, options);
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
142
|
-
console.log(`Found ${issues.length} issue(s):
|
|
143
|
-
`);
|
|
144
|
-
for (const issue of issues) {
|
|
145
|
-
const icon = issue.type === "error" ? `${RED}x${RESET}` : `${YELLOW}!${RESET}`;
|
|
146
|
-
console.log(`${icon} [${issue.rule}] ${issue.message}`);
|
|
147
|
-
if (issue.suggestion) {
|
|
148
|
-
console.log(` ${DIM}Suggestion: ${issue.suggestion}${RESET}`);
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
console.log(`
|
|
152
|
-
${YELLOW}Manual fixes required. Review the issues above.${RESET}`);
|
|
153
|
-
const hasError = issues.some((i) => i.type === "error");
|
|
154
|
-
if (hasError) {
|
|
155
|
-
process.exitCode = 1;
|
|
156
|
-
}
|
|
157
|
-
await maybeSendTelemetry(projectRoot, essence, issues, options);
|
|
158
|
-
}
|
|
159
|
-
async function maybeSendTelemetry(projectRoot, essence, issues, options) {
|
|
160
|
-
if (options.telemetry && !isOptedIn(projectRoot)) {
|
|
161
|
-
optIn(projectRoot);
|
|
162
|
-
console.log(
|
|
163
|
-
`
|
|
164
|
-
${CYAN}Telemetry enabled.${RESET} Anonymous guard metrics will be sent on future checks.`
|
|
165
|
-
);
|
|
166
|
-
console.log(`${DIM}Set "telemetry": false in .decantr/project.json to opt out.${RESET}`);
|
|
167
|
-
}
|
|
168
|
-
if (isOptedIn(projectRoot)) {
|
|
169
|
-
const metrics = collectMetrics(essence, issues);
|
|
170
|
-
sendGuardMetrics(metrics);
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
export {
|
|
174
|
-
cmdHeal
|
|
175
|
-
};
|