@decantr/cli 1.7.25 → 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/dist/bin.js CHANGED
@@ -1,4 +1,4 @@
1
1
  #!/usr/bin/env node
2
- import "./chunk-7FXMRAC3.js";
3
- import "./chunk-3K6HWLD5.js";
4
- import "./chunk-QRQCPD3C.js";
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-3K6HWLD5.js";
17
+ } from "./chunk-GCDFX7UE.js";
18
18
  import {
19
19
  buildGuardRegistryContext,
20
- scanProjectInteractions
21
- } from "./chunk-QRQCPD3C.js";
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";
@@ -40,29 +41,63 @@ import {
40
41
  } from "@decantr/verifier";
41
42
 
42
43
  // src/auth.ts
43
- import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
44
+ import {
45
+ chmodSync,
46
+ existsSync,
47
+ mkdirSync,
48
+ readFileSync,
49
+ rmSync,
50
+ statSync,
51
+ writeFileSync
52
+ } from "fs";
44
53
  import { homedir } from "os";
45
54
  import { join } from "path";
46
- var CONFIG_DIR = join(homedir(), ".config", "decantr");
47
- var AUTH_FILE = join(CONFIG_DIR, "auth.json");
55
+ var CONFIG_DIR_MODE = 448;
56
+ var AUTH_FILE_MODE = 384;
57
+ function getConfigDir() {
58
+ return process.env.DECANTR_CONFIG_DIR || join(homedir(), ".config", "decantr");
59
+ }
60
+ function getAuthFile() {
61
+ return join(getConfigDir(), "auth.json");
62
+ }
63
+ function chmodIfNeeded(path, mode) {
64
+ try {
65
+ if ((statSync(path).mode & 511) !== mode) {
66
+ chmodSync(path, mode);
67
+ }
68
+ } catch {
69
+ }
70
+ }
71
+ function ensureConfigDir() {
72
+ const configDir = getConfigDir();
73
+ mkdirSync(configDir, { recursive: true, mode: CONFIG_DIR_MODE });
74
+ chmodIfNeeded(configDir, CONFIG_DIR_MODE);
75
+ }
48
76
  function getCredentials() {
49
- if (!existsSync(AUTH_FILE)) return null;
77
+ const authFile = getAuthFile();
78
+ if (!existsSync(authFile)) return null;
50
79
  try {
51
- return JSON.parse(readFileSync(AUTH_FILE, "utf-8"));
80
+ chmodIfNeeded(authFile, AUTH_FILE_MODE);
81
+ return JSON.parse(readFileSync(authFile, "utf-8"));
52
82
  } catch {
53
83
  return null;
54
84
  }
55
85
  }
56
86
  function saveCredentials(creds) {
57
- mkdirSync(CONFIG_DIR, { recursive: true });
58
- writeFileSync(AUTH_FILE, JSON.stringify(creds, null, 2));
87
+ ensureConfigDir();
88
+ const authFile = getAuthFile();
89
+ writeFileSync(authFile, JSON.stringify(creds, null, 2), { mode: AUTH_FILE_MODE });
90
+ chmodIfNeeded(authFile, AUTH_FILE_MODE);
59
91
  }
60
92
  function clearCredentials() {
61
- if (existsSync(AUTH_FILE)) {
62
- rmSync(AUTH_FILE);
93
+ const authFile = getAuthFile();
94
+ if (existsSync(authFile)) {
95
+ rmSync(authFile);
63
96
  }
64
97
  }
65
98
  function getApiKeyOrToken() {
99
+ const envKey = process.env.DECANTR_API_KEY?.trim();
100
+ if (envKey) return envKey;
66
101
  const creds = getCredentials();
67
102
  if (!creds) return null;
68
103
  return creds.api_key || creds.access_token || null;
@@ -341,7 +376,7 @@ import { existsSync as existsSync12, mkdirSync as mkdirSync3, writeFileSync as w
341
376
  import { join as join12 } from "path";
342
377
 
343
378
  // src/analyzers/components.ts
344
- import { existsSync as existsSync4, readdirSync, statSync } from "fs";
379
+ import { existsSync as existsSync4, readdirSync, statSync as statSync2 } from "fs";
345
380
  import { join as join4 } from "path";
346
381
  var PAGE_EXTENSIONS = /* @__PURE__ */ new Set([".tsx", ".ts", ".jsx", ".js"]);
347
382
  var ROOT_COMPONENT_CANDIDATES = [
@@ -366,7 +401,7 @@ function countFilesRecursive(dir, extensions) {
366
401
  if (entry.startsWith(".") || entry === "node_modules") continue;
367
402
  const fullPath = join4(dir, entry);
368
403
  try {
369
- const stat = statSync(fullPath);
404
+ const stat = statSync2(fullPath);
370
405
  if (stat.isDirectory()) {
371
406
  count += countFilesRecursive(fullPath, extensions);
372
407
  } else if (stat.isFile()) {
@@ -392,7 +427,7 @@ function countPageFiles(dir) {
392
427
  if (entry.startsWith(".") || entry === "node_modules") continue;
393
428
  const fullPath = join4(dir, entry);
394
429
  try {
395
- const stat = statSync(fullPath);
430
+ const stat = statSync2(fullPath);
396
431
  if (stat.isDirectory()) {
397
432
  count += countPageFiles(fullPath);
398
433
  } else if (stat.isFile()) {
@@ -632,7 +667,7 @@ function scanDependencies(projectRoot) {
632
667
  }
633
668
 
634
669
  // src/analyzers/features.ts
635
- import { existsSync as existsSync6, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
670
+ import { existsSync as existsSync6, readdirSync as readdirSync2, statSync as statSync3 } from "fs";
636
671
  import { join as join6 } from "path";
637
672
  var FEATURE_PATTERNS = {
638
673
  auth: [
@@ -690,7 +725,7 @@ function collectPaths(dir, baseDir, depth = 0) {
690
725
  const relPath = fullPath.slice(baseDir.length + 1);
691
726
  paths.push(relPath);
692
727
  try {
693
- if (statSync2(fullPath).isDirectory()) {
728
+ if (statSync3(fullPath).isDirectory()) {
694
729
  paths.push(...collectPaths(fullPath, baseDir, depth + 1));
695
730
  }
696
731
  } catch {
@@ -861,7 +896,7 @@ function scanLayout(projectRoot) {
861
896
  }
862
897
 
863
898
  // src/analyzers/routes.ts
864
- import { existsSync as existsSync8, readdirSync as readdirSync4, readFileSync as readFileSync6, statSync as statSync3 } from "fs";
899
+ import { existsSync as existsSync8, readdirSync as readdirSync4, readFileSync as readFileSync6, statSync as statSync4 } from "fs";
865
900
  import { join as join8, relative } from "path";
866
901
  var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".next", ".git", "api", "_app", "_document"]);
867
902
  function shouldSkipDir(name) {
@@ -910,7 +945,7 @@ function walkAppDir(dir, baseDir, segments) {
910
945
  if (shouldSkipDir(entry)) continue;
911
946
  const fullPath = join8(dir, entry);
912
947
  try {
913
- if (!statSync3(fullPath).isDirectory()) continue;
948
+ if (!statSync4(fullPath).isDirectory()) continue;
914
949
  } catch {
915
950
  continue;
916
951
  }
@@ -932,7 +967,7 @@ function walkPagesDir(dir, baseDir, segments) {
932
967
  if (shouldSkipDir(entry)) continue;
933
968
  const fullPath = join8(dir, entry);
934
969
  try {
935
- const stat = statSync3(fullPath);
970
+ const stat = statSync4(fullPath);
936
971
  if (stat.isDirectory()) {
937
972
  const routeSegment = segmentToRoute(entry);
938
973
  const nextSegments = routeSegment === null ? [...segments] : [...segments, routeSegment];
@@ -968,7 +1003,7 @@ function collectRouteCandidateFiles(dir, files, depth = 0) {
968
1003
  if (entry.startsWith(".") || entry === "node_modules") continue;
969
1004
  const fullPath = join8(dir, entry);
970
1005
  try {
971
- const stat = statSync3(fullPath);
1006
+ const stat = statSync4(fullPath);
972
1007
  if (stat.isDirectory()) {
973
1008
  collectRouteCandidateFiles(fullPath, files, depth + 1);
974
1009
  } else if (stat.isFile()) {
@@ -2668,7 +2703,7 @@ async function cmdMigrate(projectRoot = process.cwd()) {
2668
2703
  }
2669
2704
 
2670
2705
  // src/commands/new-project.ts
2671
- import { execSync } from "child_process";
2706
+ import { spawnSync } from "child_process";
2672
2707
  import { existsSync as existsSync18, mkdirSync as mkdirSync8 } from "fs";
2673
2708
  import { join as join19, resolve as resolve2 } from "path";
2674
2709
  import { fileURLToPath } from "url";
@@ -3118,6 +3153,72 @@ function dim2(text) {
3118
3153
  function cyan2(text) {
3119
3154
  return `${CYAN3}${text}${RESET6}`;
3120
3155
  }
3156
+ function validatePassThroughFlagValue(flag, value) {
3157
+ if (value.length === 0) {
3158
+ throw new Error(`--${flag} cannot be empty.`);
3159
+ }
3160
+ if (value.length > 512) {
3161
+ throw new Error(`--${flag} is too long.`);
3162
+ }
3163
+ if (/[\u0000-\u001f\u007f]/.test(value)) {
3164
+ throw new Error(`--${flag} contains unsupported control characters.`);
3165
+ }
3166
+ return value;
3167
+ }
3168
+ function pushPassThroughFlag(flags, flag, value) {
3169
+ if (value == null) return;
3170
+ flags.push(`--${flag}=${validatePassThroughFlagValue(flag, value)}`);
3171
+ }
3172
+ function buildNewProjectInitArgs(options, inferredAdoption) {
3173
+ const initFlags = [
3174
+ "--yes",
3175
+ "--workflow=greenfield",
3176
+ `--adoption=${validatePassThroughFlagValue("mode", inferredAdoption)}`
3177
+ ];
3178
+ pushPassThroughFlag(initFlags, "blueprint", options.blueprint);
3179
+ pushPassThroughFlag(initFlags, "archetype", options.archetype);
3180
+ pushPassThroughFlag(initFlags, "theme", options.theme);
3181
+ pushPassThroughFlag(initFlags, "mode", options.mode);
3182
+ pushPassThroughFlag(initFlags, "shape", options.shape);
3183
+ pushPassThroughFlag(initFlags, "target", options.target);
3184
+ if (options.offline) initFlags.push("--offline");
3185
+ pushPassThroughFlag(initFlags, "registry", options.registry);
3186
+ pushPassThroughFlag(initFlags, "assistant-bridge", options.assistantBridge);
3187
+ return initFlags;
3188
+ }
3189
+ function commandForPlatform(command) {
3190
+ if (process.platform !== "win32") {
3191
+ return command;
3192
+ }
3193
+ return /^(?:npm|pnpm|yarn|bun|npx)$/.test(command) ? `${command}.cmd` : command;
3194
+ }
3195
+ function runArgvCommand(command, args, cwd) {
3196
+ const result = spawnSync(commandForPlatform(command), args, {
3197
+ cwd,
3198
+ stdio: "inherit",
3199
+ shell: false
3200
+ });
3201
+ if (result.error) {
3202
+ throw result.error;
3203
+ }
3204
+ if (result.status !== 0) {
3205
+ throw new Error(`Command failed: ${command} ${args.join(" ")}`);
3206
+ }
3207
+ }
3208
+ function resolveInitCommand(initFlags) {
3209
+ const bundledCliEntrypoint = fileURLToPath(new URL("./bin.js", import.meta.url));
3210
+ const cliEntrypoint = existsSync18(bundledCliEntrypoint) ? bundledCliEntrypoint : process.argv[1] && existsSync18(process.argv[1]) ? process.argv[1] : null;
3211
+ if (cliEntrypoint) {
3212
+ return {
3213
+ command: process.execPath,
3214
+ args: [cliEntrypoint, "init", ...initFlags]
3215
+ };
3216
+ }
3217
+ return {
3218
+ command: "npx",
3219
+ args: ["decantr", "init", ...initFlags]
3220
+ };
3221
+ }
3121
3222
  async function cmdNewProject(projectName, options) {
3122
3223
  const workspaceRoot = process.cwd();
3123
3224
  const projectDir = resolve2(workspaceRoot, projectName);
@@ -3138,6 +3239,14 @@ async function cmdNewProject(projectName, options) {
3138
3239
  process.exitCode = 1;
3139
3240
  return;
3140
3241
  }
3242
+ let initFlags;
3243
+ try {
3244
+ initFlags = buildNewProjectInitArgs(options, inferredAdoption);
3245
+ } catch (err) {
3246
+ console.error(error2(err.message));
3247
+ process.exitCode = 1;
3248
+ return;
3249
+ }
3141
3250
  console.log(heading(`Creating ${projectName}...`));
3142
3251
  mkdirSync8(projectDir, { recursive: true });
3143
3252
  console.log(dim2(` Created ${projectName}/`));
@@ -3165,7 +3274,7 @@ async function cmdNewProject(projectName, options) {
3165
3274
  if (shouldBootstrapRuntime) {
3166
3275
  console.log(heading("Installing dependencies..."));
3167
3276
  try {
3168
- execSync(`${packageManager} install`, { cwd: projectDir, stdio: "inherit" });
3277
+ runArgvCommand(packageManager, ["install"], projectDir);
3169
3278
  } catch {
3170
3279
  console.log(
3171
3280
  `
@@ -3202,21 +3311,9 @@ ${YELLOW4}Dependency install failed. Run \`${packageManager} install\` manually.
3202
3311
  return;
3203
3312
  }
3204
3313
  console.log(heading("Initializing Decantr..."));
3205
- const initFlags = ["--yes", "--workflow=greenfield", `--adoption=${inferredAdoption}`];
3206
- if (options.blueprint) initFlags.push(`--blueprint=${options.blueprint}`);
3207
- if (options.archetype) initFlags.push(`--archetype=${options.archetype}`);
3208
- if (options.theme) initFlags.push(`--theme=${options.theme}`);
3209
- if (options.mode) initFlags.push(`--mode=${options.mode}`);
3210
- if (options.shape) initFlags.push(`--shape=${options.shape}`);
3211
- if (options.target) initFlags.push(`--target=${options.target}`);
3212
- if (options.offline) initFlags.push("--offline");
3213
- if (options.registry) initFlags.push(`--registry=${options.registry}`);
3214
- if (options.assistantBridge) initFlags.push(`--assistant-bridge=${options.assistantBridge}`);
3215
3314
  try {
3216
- const bundledCliEntrypoint = fileURLToPath(new URL("./bin.js", import.meta.url));
3217
- const cliEntrypoint = existsSync18(bundledCliEntrypoint) ? bundledCliEntrypoint : process.argv[1] && existsSync18(process.argv[1]) ? process.argv[1] : null;
3218
- const cliPath = cliEntrypoint ? `"${process.execPath}" "${cliEntrypoint}"` : "npx decantr";
3219
- execSync(`${cliPath} init ${initFlags.join(" ")}`, { cwd: projectDir, stdio: "inherit" });
3315
+ const initCommand = resolveInitCommand(initFlags);
3316
+ runArgvCommand(initCommand.command, initCommand.args, projectDir);
3220
3317
  if (shouldBootstrapRuntime && bootstrapAdapter) {
3221
3318
  bootstrapAdapter.writeProjectFiles(projectDir, title, detectRoutingMode(projectDir));
3222
3319
  }
@@ -6578,7 +6675,7 @@ async function main() {
6578
6675
  break;
6579
6676
  }
6580
6677
  case "upgrade": {
6581
- const { cmdUpgrade } = await import("./upgrade-KG42WK5C.js");
6678
+ const { cmdUpgrade } = await import("./upgrade-XMY6LIPS.js");
6582
6679
  const applyFlag = args.includes("--apply");
6583
6680
  await cmdUpgrade(process.cwd(), { apply: applyFlag });
6584
6681
  break;
@@ -6590,7 +6687,7 @@ async function main() {
6590
6687
  `${YELLOW9}Note: \`decantr heal\` is deprecated. Use \`decantr check\` instead.${RESET13}`
6591
6688
  );
6592
6689
  }
6593
- const { cmdHeal } = await import("./heal-EMT5LYVZ.js");
6690
+ const { cmdHeal } = await import("./heal-JPW3BXS3.js");
6594
6691
  const telemetryFlag = args.includes("--telemetry");
6595
6692
  await cmdHeal(process.cwd(), { telemetry: telemetryFlag });
6596
6693
  break;
@@ -7076,8 +7173,23 @@ async function main() {
7076
7173
  process.exitCode = 1;
7077
7174
  }
7078
7175
  }
7079
- main().catch((e) => {
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) => {
7080
7186
  console.error(error3(e.message));
7081
7187
  if (e.stack) console.error(e.stack);
7082
7188
  process.exitCode = 1;
7189
+ await sendCliCommandTelemetry({
7190
+ args: cliArgs,
7191
+ durationMs: Date.now() - cliStartedAt,
7192
+ projectRoot: process.cwd(),
7193
+ success: false
7194
+ });
7083
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": palette.secondary?.[tokenMode] || pickFb("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-7FXMRAC3.js";
2
- import "./chunk-3K6HWLD5.js";
3
- import "./chunk-QRQCPD3C.js";
1
+ import "./chunk-DSVBYVG5.js";
2
+ import "./chunk-GCDFX7UE.js";
3
+ import "./chunk-RSIOCKZF.js";
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  RegistryClient,
3
3
  refreshDerivedFiles
4
- } from "./chunk-3K6HWLD5.js";
4
+ } from "./chunk-GCDFX7UE.js";
5
5
 
6
6
  // src/commands/upgrade.ts
7
7
  import { existsSync, readFileSync, writeFileSync } from "fs";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decantr/cli",
3
- "version": "1.7.25",
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",
@@ -23,14 +23,18 @@
23
23
  "src/templates",
24
24
  "src/bundled"
25
25
  ],
26
+ "engines": {
27
+ "node": ">=20"
28
+ },
26
29
  "publishConfig": {
27
30
  "access": "public"
28
31
  },
29
32
  "dependencies": {
30
- "@decantr/core": "1.0.5",
31
- "@decantr/essence-spec": "1.0.5",
32
- "@decantr/registry": "1.0.3",
33
- "@decantr/verifier": "1.0.4"
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"
34
38
  },
35
39
  "scripts": {
36
40
  "build": "tsup",
@@ -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
- };
@@ -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
- };