@decantr/cli 1.0.0-beta.10

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/index.js ADDED
@@ -0,0 +1,1353 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { readFileSync as readFileSync4, existsSync as existsSync4 } from "fs";
5
+ import { join as join4 } from "path";
6
+ import { validateEssence, evaluateGuard } from "@decantr/essence-spec";
7
+ import { createResolver, createRegistryClient } from "@decantr/registry";
8
+
9
+ // src/detect.ts
10
+ import { existsSync, readFileSync } from "fs";
11
+ import { join } from "path";
12
+ var RULE_FILES = [
13
+ "CLAUDE.md",
14
+ ".cursorrules",
15
+ ".cursor/rules",
16
+ "AGENTS.md",
17
+ "GEMINI.md",
18
+ "copilot-instructions.md"
19
+ ];
20
+ function detectProject(projectRoot = process.cwd()) {
21
+ const result = {
22
+ framework: "unknown",
23
+ packageManager: "unknown",
24
+ hasTypeScript: false,
25
+ hasTailwind: false,
26
+ existingRuleFiles: [],
27
+ existingEssence: false,
28
+ projectRoot
29
+ };
30
+ result.existingEssence = existsSync(join(projectRoot, "decantr.essence.json"));
31
+ for (const ruleFile of RULE_FILES) {
32
+ if (existsSync(join(projectRoot, ruleFile))) {
33
+ result.existingRuleFiles.push(ruleFile);
34
+ }
35
+ }
36
+ if (existsSync(join(projectRoot, "pnpm-lock.yaml"))) {
37
+ result.packageManager = "pnpm";
38
+ } else if (existsSync(join(projectRoot, "yarn.lock"))) {
39
+ result.packageManager = "yarn";
40
+ } else if (existsSync(join(projectRoot, "bun.lockb"))) {
41
+ result.packageManager = "bun";
42
+ } else if (existsSync(join(projectRoot, "package-lock.json"))) {
43
+ result.packageManager = "npm";
44
+ }
45
+ result.hasTypeScript = existsSync(join(projectRoot, "tsconfig.json"));
46
+ result.hasTailwind = existsSync(join(projectRoot, "tailwind.config.js")) || existsSync(join(projectRoot, "tailwind.config.ts")) || existsSync(join(projectRoot, "tailwind.config.mjs")) || existsSync(join(projectRoot, "tailwind.config.cjs"));
47
+ if (existsSync(join(projectRoot, "next.config.js")) || existsSync(join(projectRoot, "next.config.ts")) || existsSync(join(projectRoot, "next.config.mjs"))) {
48
+ result.framework = "nextjs";
49
+ result.version = getPackageVersion(projectRoot, "next");
50
+ return result;
51
+ }
52
+ if (existsSync(join(projectRoot, "nuxt.config.js")) || existsSync(join(projectRoot, "nuxt.config.ts"))) {
53
+ result.framework = "nuxt";
54
+ result.version = getPackageVersion(projectRoot, "nuxt");
55
+ return result;
56
+ }
57
+ if (existsSync(join(projectRoot, "astro.config.mjs")) || existsSync(join(projectRoot, "astro.config.ts"))) {
58
+ result.framework = "astro";
59
+ result.version = getPackageVersion(projectRoot, "astro");
60
+ return result;
61
+ }
62
+ if (existsSync(join(projectRoot, "svelte.config.js")) || existsSync(join(projectRoot, "svelte.config.ts"))) {
63
+ result.framework = "svelte";
64
+ result.version = getPackageVersion(projectRoot, "svelte");
65
+ return result;
66
+ }
67
+ if (existsSync(join(projectRoot, "angular.json"))) {
68
+ result.framework = "angular";
69
+ result.version = getPackageVersion(projectRoot, "@angular/core");
70
+ return result;
71
+ }
72
+ const packageJsonPath = join(projectRoot, "package.json");
73
+ if (existsSync(packageJsonPath)) {
74
+ try {
75
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
76
+ const deps = { ...packageJson.dependencies, ...packageJson.devDependencies };
77
+ if (deps.next) {
78
+ result.framework = "nextjs";
79
+ result.version = deps.next.replace(/^\^|~/, "");
80
+ } else if (deps.nuxt) {
81
+ result.framework = "nuxt";
82
+ result.version = deps.nuxt.replace(/^\^|~/, "");
83
+ } else if (deps.astro) {
84
+ result.framework = "astro";
85
+ result.version = deps.astro.replace(/^\^|~/, "");
86
+ } else if (deps.svelte) {
87
+ result.framework = "svelte";
88
+ result.version = deps.svelte.replace(/^\^|~/, "");
89
+ } else if (deps["@angular/core"]) {
90
+ result.framework = "angular";
91
+ result.version = deps["@angular/core"].replace(/^\^|~/, "");
92
+ } else if (deps.vue) {
93
+ result.framework = "vue";
94
+ result.version = deps.vue.replace(/^\^|~/, "");
95
+ } else if (deps.react) {
96
+ result.framework = "react";
97
+ result.version = deps.react.replace(/^\^|~/, "");
98
+ }
99
+ } catch {
100
+ }
101
+ }
102
+ if (result.framework === "unknown" && !existsSync(packageJsonPath)) {
103
+ if (existsSync(join(projectRoot, "index.html"))) {
104
+ result.framework = "html";
105
+ }
106
+ }
107
+ return result;
108
+ }
109
+ function getPackageVersion(projectRoot, packageName) {
110
+ const packageJsonPath = join(projectRoot, "package.json");
111
+ if (!existsSync(packageJsonPath)) return void 0;
112
+ try {
113
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
114
+ const deps = { ...packageJson.dependencies, ...packageJson.devDependencies };
115
+ const version = deps[packageName];
116
+ return version?.replace(/^\^|~/, "");
117
+ } catch {
118
+ return void 0;
119
+ }
120
+ }
121
+
122
+ // src/prompts.ts
123
+ import { createInterface } from "readline";
124
+ var BOLD = "\x1B[1m";
125
+ var DIM = "\x1B[2m";
126
+ var RESET = "\x1B[0m";
127
+ var GREEN = "\x1B[32m";
128
+ var YELLOW = "\x1B[33m";
129
+ var CYAN = "\x1B[36m";
130
+ function ask(question, defaultValue) {
131
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
132
+ const prompt = defaultValue ? `${question} ${DIM}(${defaultValue})${RESET}: ` : `${question}: `;
133
+ return new Promise((resolve) => {
134
+ rl.question(prompt, (answer) => {
135
+ rl.close();
136
+ resolve(answer.trim() || defaultValue || "");
137
+ });
138
+ });
139
+ }
140
+ async function select(question, options, defaultIdx = 0, allowOther = false) {
141
+ console.log(`
142
+ ${BOLD}${question}${RESET}`);
143
+ for (let i = 0; i < options.length; i++) {
144
+ const marker = i === defaultIdx ? `${GREEN}>${RESET}` : " ";
145
+ const desc = options[i].description ? ` ${DIM}\u2014 ${options[i].description}${RESET}` : "";
146
+ console.log(` ${marker} ${i + 1}. ${options[i].label}${desc}`);
147
+ }
148
+ if (allowOther) {
149
+ console.log(` ${options.length + 1}. ${DIM}other (enter custom value)${RESET}`);
150
+ }
151
+ const maxIdx = allowOther ? options.length + 1 : options.length;
152
+ const answer = await ask(`Choose (1-${maxIdx})`, String(defaultIdx + 1));
153
+ const idx = parseInt(answer, 10) - 1;
154
+ if (allowOther && idx === options.length) {
155
+ const custom = await ask("Enter custom value");
156
+ return custom;
157
+ }
158
+ const validIdx = Math.max(0, Math.min(idx, options.length - 1));
159
+ return options[validIdx].value;
160
+ }
161
+ async function confirm(question, defaultYes = true) {
162
+ const hint = defaultYes ? "Y/n" : "y/N";
163
+ const answer = await ask(`${question} [${hint}]`, defaultYes ? "y" : "n");
164
+ return answer.toLowerCase() === "y" || answer.toLowerCase() === "yes";
165
+ }
166
+ function warn(message) {
167
+ console.log(`
168
+ ${YELLOW} Warning: ${message}${RESET}`);
169
+ }
170
+ function showDetection(detected) {
171
+ console.log(`
172
+ ${CYAN}Detected project configuration:${RESET}`);
173
+ if (detected.framework !== "unknown") {
174
+ const version = detected.version ? ` ${detected.version}` : "";
175
+ console.log(` Framework: ${detected.framework}${version}`);
176
+ }
177
+ if (detected.packageManager !== "unknown") {
178
+ console.log(` Package manager: ${detected.packageManager}`);
179
+ }
180
+ if (detected.hasTypeScript) {
181
+ console.log(` TypeScript: ${GREEN}yes${RESET}`);
182
+ }
183
+ if (detected.hasTailwind) {
184
+ console.log(` Tailwind CSS: ${GREEN}yes${RESET}`);
185
+ }
186
+ if (detected.existingEssence) {
187
+ console.log(` Existing essence: ${YELLOW}yes${RESET}`);
188
+ }
189
+ }
190
+ async function runInteractivePrompts(detected, archetypes, blueprints, themes) {
191
+ showDetection(detected);
192
+ const blueprintOptions = [
193
+ { value: "none", label: "none", description: "Start from scratch (blank canvas)" },
194
+ ...blueprints.map((b) => ({
195
+ value: b.id,
196
+ label: b.id,
197
+ description: b.description
198
+ }))
199
+ ];
200
+ const blueprint = await select("What are you building?", blueprintOptions, 0, true);
201
+ const isBlank = blueprint === "none";
202
+ const themeOptions = themes.map((t) => ({
203
+ value: t.id,
204
+ label: t.id,
205
+ description: t.description
206
+ }));
207
+ const defaultThemeIdx = themeOptions.findIndex((t) => t.value === "luminarum") || 0;
208
+ const theme = await select("Choose a theme", themeOptions, Math.max(0, defaultThemeIdx), true);
209
+ const mode = await select(
210
+ "Color mode",
211
+ [
212
+ { value: "dark", label: "dark", description: "Dark background" },
213
+ { value: "light", label: "light", description: "Light background" },
214
+ { value: "auto", label: "auto", description: "Follow system preference" }
215
+ ],
216
+ 0
217
+ );
218
+ const shape = await select(
219
+ "Border shape",
220
+ [
221
+ { value: "pill", label: "pill", description: "Fully rounded corners" },
222
+ { value: "rounded", label: "rounded", description: "Moderately rounded" },
223
+ { value: "sharp", label: "sharp", description: "No border radius" }
224
+ ],
225
+ 0,
226
+ true
227
+ );
228
+ const frameworkOptions = [
229
+ { value: "react", label: "react", description: "React / Create React App" },
230
+ { value: "nextjs", label: "nextjs", description: "Next.js" },
231
+ { value: "vue", label: "vue", description: "Vue.js" },
232
+ { value: "nuxt", label: "nuxt", description: "Nuxt" },
233
+ { value: "svelte", label: "svelte", description: "Svelte / SvelteKit" },
234
+ { value: "astro", label: "astro", description: "Astro" },
235
+ { value: "angular", label: "angular", description: "Angular" },
236
+ { value: "html", label: "html", description: "Plain HTML/CSS/JS" }
237
+ ];
238
+ let defaultFrameworkIdx = frameworkOptions.findIndex((f) => f.value === detected.framework);
239
+ if (defaultFrameworkIdx < 0) defaultFrameworkIdx = 0;
240
+ const target = await select("Target framework", frameworkOptions, defaultFrameworkIdx, true);
241
+ if (detected.framework !== "unknown" && target !== detected.framework) {
242
+ warn(`This project appears to be ${detected.framework} but you selected ${target}.`);
243
+ const proceed = await confirm("Continue anyway?", false);
244
+ if (!proceed) {
245
+ console.log(`${DIM}Using detected framework: ${detected.framework}${RESET}`);
246
+ }
247
+ }
248
+ const guardMode = await select(
249
+ "Guard enforcement level",
250
+ [
251
+ { value: "creative", label: "creative", description: "Advisory only (new projects)" },
252
+ { value: "guided", label: "guided", description: "Style, structure, recipe enforced" },
253
+ { value: "strict", label: "strict", description: "All 5 rules enforced exactly" }
254
+ ],
255
+ detected.existingEssence ? 1 : 2
256
+ // Default to guided for existing, strict for new
257
+ );
258
+ const density = await select(
259
+ "Spacing density",
260
+ [
261
+ { value: "compact", label: "compact", description: "Dense UI, minimal spacing" },
262
+ { value: "comfortable", label: "comfortable", description: "Balanced spacing" },
263
+ { value: "spacious", label: "spacious", description: "Generous whitespace" }
264
+ ],
265
+ 1
266
+ );
267
+ const shellOptions = [
268
+ { value: "sidebar-main", label: "sidebar-main", description: "Collapsible sidebar with main content" },
269
+ { value: "top-nav-main", label: "top-nav-main", description: "Horizontal nav with full-width content" },
270
+ { value: "centered", label: "centered", description: "Centered card (auth flows)" },
271
+ { value: "full-bleed", label: "full-bleed", description: "No persistent nav (landing pages)" },
272
+ { value: "minimal-header", label: "minimal-header", description: "Slim header with centered content" }
273
+ ];
274
+ let defaultShellIdx = 0;
275
+ if (["nextjs", "nuxt", "astro"].includes(target)) {
276
+ defaultShellIdx = shellOptions.findIndex((s) => s.value === "top-nav-main");
277
+ }
278
+ const shell = await select("Default page shell (layout)", shellOptions, Math.max(0, defaultShellIdx), true);
279
+ return {
280
+ blueprint: isBlank ? void 0 : blueprint,
281
+ archetype: void 0,
282
+ // Will be derived from blueprint
283
+ theme,
284
+ mode,
285
+ shape,
286
+ target,
287
+ guard: guardMode,
288
+ density,
289
+ shell,
290
+ personality: ["professional"],
291
+ features: [],
292
+ existing: detected.existingEssence
293
+ };
294
+ }
295
+ function parseFlags(args, detected) {
296
+ const options = {};
297
+ if (typeof args.blueprint === "string") options.blueprint = args.blueprint;
298
+ if (typeof args.archetype === "string") options.archetype = args.archetype;
299
+ if (typeof args.theme === "string") options.theme = args.theme;
300
+ if (args.mode === "dark" || args.mode === "light" || args.mode === "auto") options.mode = args.mode;
301
+ if (typeof args.shape === "string") options.shape = args.shape;
302
+ if (typeof args.target === "string") options.target = args.target;
303
+ if (args.guard === "creative" || args.guard === "guided" || args.guard === "strict") options.guard = args.guard;
304
+ if (args.density === "compact" || args.density === "comfortable" || args.density === "spacious") options.density = args.density;
305
+ if (typeof args.shell === "string") options.shell = args.shell;
306
+ if (typeof args.personality === "string") options.personality = args.personality.split(",").map((s) => s.trim());
307
+ if (typeof args.features === "string") options.features = args.features.split(",").map((s) => s.trim());
308
+ if (args.existing === true) options.existing = true;
309
+ return options;
310
+ }
311
+ function mergeWithDefaults(flags, detected) {
312
+ return {
313
+ blueprint: flags.blueprint,
314
+ archetype: flags.archetype,
315
+ theme: flags.theme || "luminarum",
316
+ mode: flags.mode || "dark",
317
+ shape: flags.shape || "rounded",
318
+ target: flags.target || (detected.framework !== "unknown" ? detected.framework : "react"),
319
+ guard: flags.guard || (detected.existingEssence ? "guided" : "strict"),
320
+ density: flags.density || "comfortable",
321
+ shell: flags.shell || "sidebar-main",
322
+ personality: flags.personality || ["professional"],
323
+ features: flags.features || [],
324
+ existing: flags.existing || detected.existingEssence
325
+ };
326
+ }
327
+
328
+ // src/scaffold.ts
329
+ import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync, appendFileSync } from "fs";
330
+ import { join as join2, dirname } from "path";
331
+ import { fileURLToPath } from "url";
332
+ var __dirname = dirname(fileURLToPath(import.meta.url));
333
+ var CLI_VERSION = "1.0.0";
334
+ function loadTemplate(name) {
335
+ const fromDist = join2(__dirname, "..", "src", "templates", name);
336
+ if (existsSync2(fromDist)) {
337
+ return readFileSync2(fromDist, "utf-8");
338
+ }
339
+ const fromSrc = join2(__dirname, "templates", name);
340
+ if (existsSync2(fromSrc)) {
341
+ return readFileSync2(fromSrc, "utf-8");
342
+ }
343
+ throw new Error(`Template not found: ${name}`);
344
+ }
345
+ function renderTemplate(template, vars) {
346
+ let result = template;
347
+ for (const [key, value] of Object.entries(vars)) {
348
+ result = result.replace(new RegExp(`\\{\\{${key}\\}\\}`, "g"), value);
349
+ }
350
+ return result;
351
+ }
352
+ function buildEssence(options, archetypeData) {
353
+ let structure = [
354
+ { id: "home", shell: options.shell, layout: ["hero"] }
355
+ ];
356
+ let features = options.features;
357
+ if (archetypeData?.pages) {
358
+ structure = archetypeData.pages.map((p) => ({
359
+ id: p.id,
360
+ shell: p.shell || options.shell,
361
+ // Ensure layout has at least one item (schema requires minItems: 1)
362
+ layout: p.default_layout?.length ? p.default_layout : ["hero"]
363
+ }));
364
+ }
365
+ if (archetypeData?.features) {
366
+ features = [.../* @__PURE__ */ new Set([...features, ...archetypeData.features])];
367
+ }
368
+ const contentGapMap = {
369
+ compact: "_gap2",
370
+ comfortable: "_gap4",
371
+ spacious: "_gap6"
372
+ };
373
+ const archetype = options.archetype || "custom";
374
+ return {
375
+ version: "2.0.0",
376
+ archetype,
377
+ theme: {
378
+ style: options.theme,
379
+ mode: options.mode,
380
+ recipe: options.theme,
381
+ // Recipe defaults to theme
382
+ shape: options.shape
383
+ },
384
+ personality: options.personality,
385
+ platform: {
386
+ type: "spa",
387
+ routing: "hash"
388
+ },
389
+ structure,
390
+ features,
391
+ guard: {
392
+ enforce_style: true,
393
+ enforce_recipe: true,
394
+ mode: options.guard
395
+ },
396
+ density: {
397
+ level: options.density,
398
+ content_gap: contentGapMap[options.density] || "_gap4"
399
+ },
400
+ target: options.target
401
+ };
402
+ }
403
+ function generateDecantrMd(essence, detected) {
404
+ const template = loadTemplate("DECANTR.md.template");
405
+ const pagesTable = essence.structure.map(
406
+ (p) => `| ${p.id} | ${p.shell} | ${p.layout.join(", ") || "none"} |`
407
+ ).join("\n");
408
+ const allPatterns = [...new Set(essence.structure.flatMap((p) => p.layout))];
409
+ const patternsList = allPatterns.length > 0 ? allPatterns.map((p) => `- \`${p}\``).join("\n") : "- No patterns specified yet";
410
+ const projectSummary = [
411
+ `**Archetype:** ${essence.archetype || "custom"}`,
412
+ `**Target:** ${essence.target}`,
413
+ `**Theme:** ${essence.theme.style} (${essence.theme.mode} mode)`,
414
+ `**Guard Mode:** ${essence.guard.mode}`,
415
+ `**Pages:** ${essence.structure.map((s) => s.id).join(", ")}`
416
+ ].join("\n");
417
+ const shellStructures = {
418
+ "sidebar-main": "nav (left) | header (top) | body (scrollable)",
419
+ "top-nav-main": "header (full width) | body (scrollable)",
420
+ "centered": "body (centered card)",
421
+ "full-bleed": "header (floating) | body (full page sections)",
422
+ "minimal-header": "header (slim) | body (centered)"
423
+ };
424
+ const defaultShell = essence.structure[0]?.shell || "sidebar-main";
425
+ const shellStructure = shellStructures[defaultShell] || "Custom shell layout";
426
+ const vars = {
427
+ GUARD_MODE: essence.guard.mode,
428
+ PROJECT_SUMMARY: projectSummary,
429
+ THEME_STYLE: essence.theme.style,
430
+ THEME_MODE: essence.theme.mode,
431
+ THEME_RECIPE: essence.theme.recipe,
432
+ TARGET: essence.target,
433
+ PAGES_TABLE: `| Page | Shell | Layout |
434
+ |------|-------|--------|
435
+ ${pagesTable}`,
436
+ PATTERNS_LIST: patternsList,
437
+ DEFAULT_SHELL: defaultShell,
438
+ SHELL_STRUCTURE: shellStructure,
439
+ PERSONALITY: essence.personality.join(", "),
440
+ DENSITY: essence.density.level,
441
+ AVAILABLE_PATTERNS: "(See registry or .decantr/cache/patterns/)",
442
+ AVAILABLE_THEMES: "(See registry or .decantr/cache/themes/)",
443
+ AVAILABLE_SHELLS: "sidebar-main, top-nav-main, centered, full-bleed, minimal-header",
444
+ VERSION: CLI_VERSION
445
+ };
446
+ return renderTemplate(template, vars);
447
+ }
448
+ function generateProjectJson(detected, options, registrySource) {
449
+ const now = (/* @__PURE__ */ new Date()).toISOString();
450
+ const data = {
451
+ detected: {
452
+ framework: detected.framework,
453
+ version: detected.version || null,
454
+ packageManager: detected.packageManager,
455
+ hasTypeScript: detected.hasTypeScript,
456
+ hasTailwind: detected.hasTailwind,
457
+ existingRuleFiles: detected.existingRuleFiles
458
+ },
459
+ overrides: {
460
+ framework: options.target !== detected.framework ? options.target : null
461
+ },
462
+ sync: {
463
+ status: registrySource === "api" ? "synced" : "needs-sync",
464
+ lastSync: now,
465
+ registrySource,
466
+ cachedContent: {
467
+ archetypes: [],
468
+ patterns: [],
469
+ themes: [],
470
+ recipes: []
471
+ }
472
+ },
473
+ initialized: {
474
+ at: now,
475
+ via: "cli",
476
+ version: CLI_VERSION,
477
+ flags: buildFlagsString(options)
478
+ }
479
+ };
480
+ return JSON.stringify(data, null, 2);
481
+ }
482
+ function buildFlagsString(options) {
483
+ const flags = [];
484
+ if (options.blueprint) flags.push(`--blueprint=${options.blueprint}`);
485
+ if (options.theme) flags.push(`--theme=${options.theme}`);
486
+ if (options.mode) flags.push(`--mode=${options.mode}`);
487
+ if (options.guard) flags.push(`--guard=${options.guard}`);
488
+ return flags.join(" ");
489
+ }
490
+ function generateTaskContext(templateName, essence) {
491
+ const template = loadTemplate(templateName);
492
+ const defaultShell = essence.structure[0]?.shell || "sidebar-main";
493
+ const layout = essence.structure[0]?.layout.join(", ") || "none";
494
+ const scaffoldStructure = essence.structure.map((p) => {
495
+ const patterns = p.layout.length > 0 ? `
496
+ - Patterns: ${p.layout.join(", ")}` : "";
497
+ return `- **${p.id}** (${p.shell})${patterns}`;
498
+ }).join("\n");
499
+ const vars = {
500
+ TARGET: essence.target,
501
+ THEME_STYLE: essence.theme.style,
502
+ THEME_MODE: essence.theme.mode,
503
+ THEME_RECIPE: essence.theme.recipe,
504
+ DEFAULT_SHELL: defaultShell,
505
+ GUARD_MODE: essence.guard.mode,
506
+ LAYOUT: layout,
507
+ DENSITY: essence.density.level,
508
+ CONTENT_GAP: essence.density.content_gap,
509
+ SCAFFOLD_STRUCTURE: scaffoldStructure
510
+ };
511
+ return renderTemplate(template, vars);
512
+ }
513
+ function generateEssenceSummary(essence) {
514
+ const template = loadTemplate("essence-summary.md.template");
515
+ const pagesTable = `| Page | Shell | Layout |
516
+ |------|-------|--------|
517
+ ${essence.structure.map((p) => `| ${p.id} | ${p.shell} | ${p.layout.join(", ") || "none"} |`).join("\n")}`;
518
+ const featuresList = essence.features.length > 0 ? essence.features.map((f) => `- ${f}`).join("\n") : "- No features specified";
519
+ const vars = {
520
+ ARCHETYPE: essence.archetype || "custom",
521
+ BLUEPRINT: essence.blueprint || "none",
522
+ PERSONALITY: essence.personality.join(", "),
523
+ TARGET: essence.target,
524
+ THEME_STYLE: essence.theme.style,
525
+ THEME_MODE: essence.theme.mode,
526
+ THEME_RECIPE: essence.theme.recipe,
527
+ SHAPE: essence.theme.shape,
528
+ PAGES_TABLE: pagesTable,
529
+ FEATURES_LIST: featuresList,
530
+ GUARD_MODE: essence.guard.mode,
531
+ ENFORCE_STYLE: String(essence.guard.enforce_style),
532
+ ENFORCE_RECIPE: String(essence.guard.enforce_recipe),
533
+ DENSITY: essence.density.level,
534
+ CONTENT_GAP: essence.density.content_gap,
535
+ LAST_UPDATED: (/* @__PURE__ */ new Date()).toISOString()
536
+ };
537
+ return renderTemplate(template, vars);
538
+ }
539
+ function updateGitignore(projectRoot) {
540
+ const gitignorePath = join2(projectRoot, ".gitignore");
541
+ const cacheEntry = ".decantr/cache/";
542
+ if (existsSync2(gitignorePath)) {
543
+ const content = readFileSync2(gitignorePath, "utf-8");
544
+ if (!content.includes(cacheEntry)) {
545
+ appendFileSync(gitignorePath, `
546
+ # Decantr cache
547
+ ${cacheEntry}
548
+ `);
549
+ return true;
550
+ }
551
+ return false;
552
+ } else {
553
+ writeFileSync(gitignorePath, `# Decantr cache
554
+ ${cacheEntry}
555
+ `);
556
+ return true;
557
+ }
558
+ }
559
+ function scaffoldProject(projectRoot, options, detected, archetypeData, registrySource = "bundled") {
560
+ const essence = buildEssence(options, archetypeData);
561
+ const decantrDir = join2(projectRoot, ".decantr");
562
+ const contextDir = join2(decantrDir, "context");
563
+ const cacheDir = join2(decantrDir, "cache");
564
+ mkdirSync(contextDir, { recursive: true });
565
+ mkdirSync(cacheDir, { recursive: true });
566
+ const essencePath = join2(projectRoot, "decantr.essence.json");
567
+ writeFileSync(essencePath, JSON.stringify(essence, null, 2) + "\n");
568
+ const decantrMdPath = join2(projectRoot, "DECANTR.md");
569
+ writeFileSync(decantrMdPath, generateDecantrMd(essence, detected));
570
+ const projectJsonPath = join2(decantrDir, "project.json");
571
+ writeFileSync(projectJsonPath, generateProjectJson(detected, options, registrySource));
572
+ const contextFiles = [];
573
+ const scaffoldPath = join2(contextDir, "task-scaffold.md");
574
+ writeFileSync(scaffoldPath, generateTaskContext("task-scaffold.md.template", essence));
575
+ contextFiles.push(scaffoldPath);
576
+ const addPagePath = join2(contextDir, "task-add-page.md");
577
+ writeFileSync(addPagePath, generateTaskContext("task-add-page.md.template", essence));
578
+ contextFiles.push(addPagePath);
579
+ const modifyPath = join2(contextDir, "task-modify.md");
580
+ writeFileSync(modifyPath, generateTaskContext("task-modify.md.template", essence));
581
+ contextFiles.push(modifyPath);
582
+ const summaryPath = join2(contextDir, "essence-summary.md");
583
+ writeFileSync(summaryPath, generateEssenceSummary(essence));
584
+ contextFiles.push(summaryPath);
585
+ const gitignoreUpdated = updateGitignore(projectRoot);
586
+ return {
587
+ essencePath,
588
+ decantrMdPath,
589
+ projectJsonPath,
590
+ contextFiles,
591
+ gitignoreUpdated
592
+ };
593
+ }
594
+
595
+ // src/registry.ts
596
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2, readdirSync } from "fs";
597
+ import { join as join3, dirname as dirname2 } from "path";
598
+ import { fileURLToPath as fileURLToPath2 } from "url";
599
+ var __dirname2 = dirname2(fileURLToPath2(import.meta.url));
600
+ var DEFAULT_API_URL = "https://decantr-registry.fly.dev/v1";
601
+ function getBundledContentRoot() {
602
+ const bundled = join3(__dirname2, "..", "..", "..", "content");
603
+ if (existsSync3(bundled)) return bundled;
604
+ const distBundled = join3(__dirname2, "..", "..", "..", "..", "content");
605
+ if (existsSync3(distBundled)) return distBundled;
606
+ return bundled;
607
+ }
608
+ async function fetchWithTimeout(url, timeoutMs = 5e3) {
609
+ const controller = new AbortController();
610
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
611
+ try {
612
+ const response = await fetch(url, { signal: controller.signal });
613
+ return response;
614
+ } finally {
615
+ clearTimeout(timeout);
616
+ }
617
+ }
618
+ async function tryApi(endpoint, apiUrl = DEFAULT_API_URL) {
619
+ try {
620
+ const url = `${apiUrl}/${endpoint}`;
621
+ const response = await fetchWithTimeout(url);
622
+ if (!response.ok) return null;
623
+ const data = await response.json();
624
+ return {
625
+ data,
626
+ source: { type: "api", url: apiUrl }
627
+ };
628
+ } catch {
629
+ return null;
630
+ }
631
+ }
632
+ function loadFromCache(cacheDir, contentType, id) {
633
+ const cachePath = id ? join3(cacheDir, contentType, `${id}.json`) : join3(cacheDir, contentType, "index.json");
634
+ if (!existsSync3(cachePath)) return null;
635
+ try {
636
+ const data = JSON.parse(readFileSync3(cachePath, "utf-8"));
637
+ return {
638
+ data,
639
+ source: { type: "cache" }
640
+ };
641
+ } catch {
642
+ return null;
643
+ }
644
+ }
645
+ function loadFromBundled(contentType, id) {
646
+ const contentRoot = getBundledContentRoot();
647
+ if (id) {
648
+ const itemPath = join3(contentRoot, contentType, `${id}.json`);
649
+ if (!existsSync3(itemPath)) return null;
650
+ try {
651
+ const data = JSON.parse(readFileSync3(itemPath, "utf-8"));
652
+ return {
653
+ data,
654
+ source: { type: "bundled" }
655
+ };
656
+ } catch {
657
+ return null;
658
+ }
659
+ } else {
660
+ const dir = join3(contentRoot, contentType);
661
+ if (!existsSync3(dir)) return null;
662
+ try {
663
+ const files = readdirSync(dir).filter((f) => f.endsWith(".json"));
664
+ const items = files.map((f) => {
665
+ const content = JSON.parse(readFileSync3(join3(dir, f), "utf-8"));
666
+ return { id: content.id || f.replace(".json", ""), ...content };
667
+ });
668
+ return {
669
+ data: { items, total: items.length },
670
+ source: { type: "bundled" }
671
+ };
672
+ } catch {
673
+ return null;
674
+ }
675
+ }
676
+ }
677
+ function saveToCache(cacheDir, contentType, id, data) {
678
+ const dir = join3(cacheDir, contentType);
679
+ mkdirSync2(dir, { recursive: true });
680
+ const cachePath = id ? join3(dir, `${id}.json`) : join3(dir, "index.json");
681
+ writeFileSync2(cachePath, JSON.stringify(data, null, 2));
682
+ }
683
+ var RegistryClient = class {
684
+ cacheDir;
685
+ apiUrl;
686
+ offline;
687
+ constructor(options = {}) {
688
+ this.cacheDir = options.cacheDir || join3(process.cwd(), ".decantr", "cache");
689
+ this.apiUrl = options.apiUrl || DEFAULT_API_URL;
690
+ this.offline = options.offline || false;
691
+ }
692
+ /**
693
+ * Fetch archetypes list.
694
+ */
695
+ async fetchArchetypes() {
696
+ if (!this.offline) {
697
+ const apiResult = await tryApi("archetypes", this.apiUrl);
698
+ if (apiResult) {
699
+ saveToCache(this.cacheDir, "archetypes", null, apiResult.data);
700
+ return apiResult;
701
+ }
702
+ }
703
+ const cacheResult = loadFromCache(
704
+ this.cacheDir,
705
+ "archetypes"
706
+ );
707
+ if (cacheResult) return cacheResult;
708
+ const bundledResult = loadFromBundled("archetypes");
709
+ if (bundledResult) return bundledResult;
710
+ return {
711
+ data: { items: [], total: 0 },
712
+ source: { type: "bundled" }
713
+ };
714
+ }
715
+ /**
716
+ * Fetch a single archetype.
717
+ */
718
+ async fetchArchetype(id) {
719
+ if (!this.offline) {
720
+ const apiResult = await tryApi(`archetypes/${id}`, this.apiUrl);
721
+ if (apiResult) {
722
+ saveToCache(this.cacheDir, "archetypes", id, apiResult.data);
723
+ return apiResult;
724
+ }
725
+ }
726
+ const cacheResult = loadFromCache(this.cacheDir, "archetypes", id);
727
+ if (cacheResult) return cacheResult;
728
+ return loadFromBundled("archetypes", id);
729
+ }
730
+ /**
731
+ * Fetch blueprints list.
732
+ */
733
+ async fetchBlueprints() {
734
+ if (!this.offline) {
735
+ const apiResult = await tryApi("blueprints", this.apiUrl);
736
+ if (apiResult) {
737
+ saveToCache(this.cacheDir, "blueprints", null, apiResult.data);
738
+ return apiResult;
739
+ }
740
+ }
741
+ const cacheResult = loadFromCache(
742
+ this.cacheDir,
743
+ "blueprints"
744
+ );
745
+ if (cacheResult) return cacheResult;
746
+ const bundledResult = loadFromBundled("blueprints");
747
+ if (bundledResult) return bundledResult;
748
+ return {
749
+ data: { items: [], total: 0 },
750
+ source: { type: "bundled" }
751
+ };
752
+ }
753
+ /**
754
+ * Fetch a single blueprint.
755
+ */
756
+ async fetchBlueprint(id) {
757
+ if (!this.offline) {
758
+ const apiResult = await tryApi(`blueprints/${id}`, this.apiUrl);
759
+ if (apiResult) {
760
+ saveToCache(this.cacheDir, "blueprints", id, apiResult.data);
761
+ return apiResult;
762
+ }
763
+ }
764
+ const cacheResult = loadFromCache(this.cacheDir, "blueprints", id);
765
+ if (cacheResult) return cacheResult;
766
+ return loadFromBundled("blueprints", id);
767
+ }
768
+ /**
769
+ * Fetch themes list.
770
+ */
771
+ async fetchThemes() {
772
+ if (!this.offline) {
773
+ const apiResult = await tryApi("themes", this.apiUrl);
774
+ if (apiResult) {
775
+ saveToCache(this.cacheDir, "themes", null, apiResult.data);
776
+ return apiResult;
777
+ }
778
+ }
779
+ const cacheResult = loadFromCache(
780
+ this.cacheDir,
781
+ "themes"
782
+ );
783
+ if (cacheResult) return cacheResult;
784
+ const bundledResult = loadFromBundled("themes");
785
+ if (bundledResult) return bundledResult;
786
+ return {
787
+ data: { items: [], total: 0 },
788
+ source: { type: "bundled" }
789
+ };
790
+ }
791
+ /**
792
+ * Fetch patterns list.
793
+ */
794
+ async fetchPatterns() {
795
+ if (!this.offline) {
796
+ const apiResult = await tryApi("patterns", this.apiUrl);
797
+ if (apiResult) {
798
+ saveToCache(this.cacheDir, "patterns", null, apiResult.data);
799
+ return apiResult;
800
+ }
801
+ }
802
+ const cacheResult = loadFromCache(
803
+ this.cacheDir,
804
+ "patterns"
805
+ );
806
+ if (cacheResult) return cacheResult;
807
+ const bundledResult = loadFromBundled("patterns");
808
+ if (bundledResult) return bundledResult;
809
+ return {
810
+ data: { items: [], total: 0 },
811
+ source: { type: "bundled" }
812
+ };
813
+ }
814
+ /**
815
+ * Check if API is available.
816
+ */
817
+ async checkApiAvailability() {
818
+ if (this.offline) return false;
819
+ try {
820
+ const response = await fetchWithTimeout(`${this.apiUrl.replace("/v1", "")}/health`, 3e3);
821
+ return response.ok;
822
+ } catch {
823
+ return false;
824
+ }
825
+ }
826
+ /**
827
+ * Get the source used for the last fetch.
828
+ */
829
+ getSourceType() {
830
+ return this.offline ? "bundled" : "api";
831
+ }
832
+ };
833
+ async function syncRegistry(cacheDir, apiUrl = DEFAULT_API_URL) {
834
+ const client = new RegistryClient({ cacheDir, apiUrl, offline: false });
835
+ const synced = [];
836
+ const failed = [];
837
+ const apiAvailable = await client.checkApiAvailability();
838
+ if (!apiAvailable) {
839
+ return { synced: [], failed: ["API unavailable"], source: "bundled" };
840
+ }
841
+ const types = ["archetypes", "blueprints", "themes", "patterns"];
842
+ for (const type of types) {
843
+ try {
844
+ const fetchMethod = `fetch${type.charAt(0).toUpperCase()}${type.slice(1)}`;
845
+ const result = await client[fetchMethod]();
846
+ if (result.source.type === "api") {
847
+ synced.push(type);
848
+ }
849
+ } catch {
850
+ failed.push(type);
851
+ }
852
+ }
853
+ return {
854
+ synced,
855
+ failed,
856
+ source: synced.length > 0 ? "api" : "bundled"
857
+ };
858
+ }
859
+
860
+ // src/index.ts
861
+ var BOLD2 = "\x1B[1m";
862
+ var DIM2 = "\x1B[2m";
863
+ var RESET2 = "\x1B[0m";
864
+ var RED = "\x1B[31m";
865
+ var GREEN2 = "\x1B[32m";
866
+ var CYAN2 = "\x1B[36m";
867
+ var YELLOW2 = "\x1B[33m";
868
+ function heading(text) {
869
+ return `
870
+ ${BOLD2}${text}${RESET2}
871
+ `;
872
+ }
873
+ function success(text) {
874
+ return `${GREEN2}${text}${RESET2}`;
875
+ }
876
+ function error(text) {
877
+ return `${RED}${text}${RESET2}`;
878
+ }
879
+ function dim(text) {
880
+ return `${DIM2}${text}${RESET2}`;
881
+ }
882
+ function cyan(text) {
883
+ return `${CYAN2}${text}${RESET2}`;
884
+ }
885
+ function getContentRoot() {
886
+ const bundled = join4(import.meta.dirname, "..", "..", "..", "content");
887
+ return process.env.DECANTR_CONTENT_ROOT || bundled;
888
+ }
889
+ function getResolver() {
890
+ return createResolver({ contentRoot: getContentRoot() });
891
+ }
892
+ async function cmdSearch(query, type) {
893
+ const client = createRegistryClient();
894
+ const results = await client.search(query, type);
895
+ if (results.length === 0) {
896
+ console.log(dim(`No results for "${query}"`));
897
+ return;
898
+ }
899
+ console.log(heading(`${results.length} result(s) for "${query}"`));
900
+ for (const r of results) {
901
+ console.log(` ${cyan(r.type.padEnd(12))} ${BOLD2}${r.id}${RESET2}`);
902
+ console.log(` ${dim(r.description || "")}`);
903
+ console.log("");
904
+ }
905
+ }
906
+ async function cmdGet(type, id) {
907
+ const validTypes = ["pattern", "archetype", "recipe", "theme", "blueprint"];
908
+ if (!validTypes.includes(type)) {
909
+ console.error(error(`Invalid type "${type}". Must be one of: ${validTypes.join(", ")}`));
910
+ process.exitCode = 1;
911
+ return;
912
+ }
913
+ const resolver = getResolver();
914
+ let result = await resolver.resolve(type, id);
915
+ if (!result) {
916
+ const apiType = type === "blueprint" ? "blueprints" : `${type}s`;
917
+ try {
918
+ const res = await fetch(`https://decantr-registry.fly.dev/v1/${apiType}/${id}`);
919
+ if (res.ok) {
920
+ const item = await res.json();
921
+ if (!item.error) {
922
+ console.log(JSON.stringify(item, null, 2));
923
+ return;
924
+ }
925
+ }
926
+ } catch {
927
+ }
928
+ console.error(error(`${type} "${id}" not found.`));
929
+ process.exitCode = 1;
930
+ return;
931
+ }
932
+ console.log(JSON.stringify(result.item, null, 2));
933
+ }
934
+ async function cmdValidate(path) {
935
+ const essencePath = path || join4(process.cwd(), "decantr.essence.json");
936
+ let raw;
937
+ try {
938
+ raw = readFileSync4(essencePath, "utf-8");
939
+ } catch {
940
+ console.error(error(`Could not read ${essencePath}`));
941
+ process.exitCode = 1;
942
+ return;
943
+ }
944
+ let essence;
945
+ try {
946
+ essence = JSON.parse(raw);
947
+ } catch (e) {
948
+ console.error(error(`Invalid JSON: ${e.message}`));
949
+ process.exitCode = 1;
950
+ return;
951
+ }
952
+ const result = validateEssence(essence);
953
+ if (result.valid) {
954
+ console.log(success("Essence is valid."));
955
+ } else {
956
+ console.error(error("Validation failed:"));
957
+ for (const err of result.errors) {
958
+ console.error(` ${RED}${err}${RESET2}`);
959
+ }
960
+ process.exitCode = 1;
961
+ }
962
+ try {
963
+ const violations = evaluateGuard(essence, {});
964
+ if (violations.length > 0) {
965
+ console.log(heading("Guard violations:"));
966
+ for (const v of violations) {
967
+ const vr = v;
968
+ console.log(` ${YELLOW2}[${vr.rule}]${RESET2} ${vr.message}`);
969
+ }
970
+ } else if (result.valid) {
971
+ console.log(success("No guard violations."));
972
+ }
973
+ } catch {
974
+ }
975
+ }
976
+ async function cmdList(type) {
977
+ const validTypes = ["patterns", "archetypes", "recipes", "themes", "blueprints"];
978
+ if (!validTypes.includes(type)) {
979
+ console.error(error(`Invalid type "${type}". Must be one of: ${validTypes.join(", ")}`));
980
+ process.exitCode = 1;
981
+ return;
982
+ }
983
+ const { readdirSync: readdirSync2 } = await import("fs");
984
+ const dir = join4(getContentRoot(), type);
985
+ let found = false;
986
+ try {
987
+ const files = readdirSync2(dir).filter((f) => f.endsWith(".json"));
988
+ if (files.length > 0) {
989
+ found = true;
990
+ console.log(heading(`${files.length} ${type}`));
991
+ for (const f of files) {
992
+ const data = JSON.parse(readFileSync4(join4(dir, f), "utf-8"));
993
+ console.log(` ${cyan(data.id || f.replace(".json", ""))} ${dim(data.description || data.name || "")}`);
994
+ }
995
+ }
996
+ } catch {
997
+ }
998
+ if (!found) {
999
+ try {
1000
+ const res = await fetch(`https://decantr-registry.fly.dev/v1/${type}`);
1001
+ if (res.ok) {
1002
+ const data = await res.json();
1003
+ console.log(heading(`${data.total} ${type}`));
1004
+ for (const item of data.items) {
1005
+ console.log(` ${cyan(item.id)} ${dim(item.description || item.name || "")}`);
1006
+ }
1007
+ return;
1008
+ }
1009
+ } catch {
1010
+ }
1011
+ console.log(dim(`No ${type} found.`));
1012
+ }
1013
+ }
1014
+ async function cmdInit(args) {
1015
+ const projectRoot = process.cwd();
1016
+ console.log(heading("Decantr Project Setup"));
1017
+ const detected = detectProject(projectRoot);
1018
+ if (detected.existingEssence && !args.existing) {
1019
+ console.log(`${YELLOW2}Warning: decantr.essence.json already exists.${RESET2}`);
1020
+ const overwrite = await confirm("Overwrite existing configuration?", false);
1021
+ if (!overwrite) {
1022
+ console.log(dim("Cancelled."));
1023
+ return;
1024
+ }
1025
+ }
1026
+ const registryClient = new RegistryClient({
1027
+ cacheDir: join4(projectRoot, ".decantr", "cache"),
1028
+ apiUrl: args.registry,
1029
+ offline: args.offline
1030
+ });
1031
+ console.log(dim("Fetching registry content..."));
1032
+ const [archetypesResult, blueprintsResult, themesResult] = await Promise.all([
1033
+ registryClient.fetchArchetypes(),
1034
+ registryClient.fetchBlueprints(),
1035
+ registryClient.fetchThemes()
1036
+ ]);
1037
+ const registrySource = archetypesResult.source.type;
1038
+ if (registrySource === "bundled") {
1039
+ console.log(dim("Using bundled content (API unavailable)"));
1040
+ }
1041
+ const archetypes = archetypesResult.data.items;
1042
+ const blueprints = blueprintsResult.data.items;
1043
+ const themes = themesResult.data.items;
1044
+ let options;
1045
+ if (args.yes) {
1046
+ const flags = parseFlags(args, detected);
1047
+ options = mergeWithDefaults(flags, detected);
1048
+ } else {
1049
+ options = await runInteractivePrompts(detected, archetypes, blueprints, themes);
1050
+ }
1051
+ let archetypeData;
1052
+ if (options.blueprint) {
1053
+ const blueprintResult = await registryClient.fetchBlueprint(options.blueprint);
1054
+ if (blueprintResult) {
1055
+ const blueprint = blueprintResult.data;
1056
+ const primaryArchetype = blueprint.compose?.[0];
1057
+ if (primaryArchetype) {
1058
+ const archetypeResult = await registryClient.fetchArchetype(primaryArchetype);
1059
+ if (archetypeResult) {
1060
+ archetypeData = archetypeResult.data;
1061
+ options.archetype = primaryArchetype;
1062
+ }
1063
+ }
1064
+ }
1065
+ } else if (options.archetype) {
1066
+ const archetypeResult = await registryClient.fetchArchetype(options.archetype);
1067
+ if (archetypeResult) {
1068
+ archetypeData = archetypeResult.data;
1069
+ }
1070
+ }
1071
+ console.log(heading("Scaffolding project..."));
1072
+ const result = scaffoldProject(
1073
+ projectRoot,
1074
+ options,
1075
+ detected,
1076
+ archetypeData,
1077
+ registrySource
1078
+ );
1079
+ console.log(success("\nProject scaffolded successfully!"));
1080
+ console.log("");
1081
+ console.log(` ${cyan("decantr.essence.json")} Design specification`);
1082
+ console.log(` ${cyan("DECANTR.md")} LLM instructions`);
1083
+ console.log(` ${cyan(".decantr/project.json")} Project state`);
1084
+ console.log(` ${cyan(".decantr/context/")} Task-specific guides`);
1085
+ if (result.gitignoreUpdated) {
1086
+ console.log(` ${dim(".gitignore updated to exclude .decantr/cache/")}`);
1087
+ }
1088
+ const essenceContent = readFileSync4(result.essencePath, "utf-8");
1089
+ const essence = JSON.parse(essenceContent);
1090
+ const validation = validateEssence(essence);
1091
+ if (validation.valid) {
1092
+ console.log(success("\nValidation passed."));
1093
+ } else {
1094
+ console.log(error(`
1095
+ Validation warnings: ${validation.errors.join(", ")}`));
1096
+ }
1097
+ console.log(heading("Next steps"));
1098
+ console.log("1. Review DECANTR.md to understand the methodology");
1099
+ console.log("2. Share DECANTR.md with your AI assistant");
1100
+ console.log("3. Start building! The AI will follow the essence spec.");
1101
+ console.log("");
1102
+ if (registrySource === "bundled") {
1103
+ console.log(dim('Run "decantr sync" when online to get the latest registry content.'));
1104
+ }
1105
+ }
1106
+ async function cmdStatus() {
1107
+ const projectRoot = process.cwd();
1108
+ const essencePath = join4(projectRoot, "decantr.essence.json");
1109
+ const projectJsonPath = join4(projectRoot, ".decantr", "project.json");
1110
+ console.log(heading("Decantr Project Status"));
1111
+ if (!existsSync4(essencePath)) {
1112
+ console.log(`${RED}No decantr.essence.json found.${RESET2}`);
1113
+ console.log(dim('Run "decantr init" to create one.'));
1114
+ return;
1115
+ }
1116
+ try {
1117
+ const essence = JSON.parse(readFileSync4(essencePath, "utf-8"));
1118
+ const validation = validateEssence(essence);
1119
+ console.log(`${BOLD2}Essence:${RESET2}`);
1120
+ if (validation.valid) {
1121
+ console.log(` ${GREEN2}Valid${RESET2}`);
1122
+ } else {
1123
+ console.log(` ${RED}Invalid: ${validation.errors.join(", ")}${RESET2}`);
1124
+ }
1125
+ console.log(` Theme: ${essence.theme?.style || "unknown"} (${essence.theme?.mode || "unknown"})`);
1126
+ console.log(` Guard: ${essence.guard?.mode || "unknown"}`);
1127
+ console.log(` Pages: ${(essence.structure || []).length}`);
1128
+ } catch (e) {
1129
+ console.log(` ${RED}Error reading essence: ${e.message}${RESET2}`);
1130
+ }
1131
+ console.log("");
1132
+ console.log(`${BOLD2}Sync Status:${RESET2}`);
1133
+ if (existsSync4(projectJsonPath)) {
1134
+ try {
1135
+ const projectJson = JSON.parse(readFileSync4(projectJsonPath, "utf-8"));
1136
+ const syncStatus = projectJson.sync?.status || "unknown";
1137
+ const lastSync = projectJson.sync?.lastSync || "never";
1138
+ const source = projectJson.sync?.registrySource || "unknown";
1139
+ const statusColor = syncStatus === "synced" ? GREEN2 : YELLOW2;
1140
+ console.log(` Status: ${statusColor}${syncStatus}${RESET2}`);
1141
+ console.log(` Last sync: ${dim(lastSync)}`);
1142
+ console.log(` Source: ${dim(source)}`);
1143
+ } catch {
1144
+ console.log(` ${YELLOW2}Could not read project.json${RESET2}`);
1145
+ }
1146
+ } else {
1147
+ console.log(` ${YELLOW2}No .decantr/project.json found${RESET2}`);
1148
+ console.log(dim(' Run "decantr init" to create project files.'));
1149
+ }
1150
+ }
1151
+ async function cmdSync() {
1152
+ const projectRoot = process.cwd();
1153
+ const cacheDir = join4(projectRoot, ".decantr", "cache");
1154
+ console.log(heading("Syncing registry content..."));
1155
+ const result = await syncRegistry(cacheDir);
1156
+ if (result.source === "api") {
1157
+ console.log(success("Sync completed successfully."));
1158
+ if (result.synced.length > 0) {
1159
+ console.log(` Synced: ${result.synced.join(", ")}`);
1160
+ }
1161
+ if (result.failed.length > 0) {
1162
+ console.log(` ${YELLOW2}Failed: ${result.failed.join(", ")}${RESET2}`);
1163
+ }
1164
+ } else {
1165
+ console.log(`${YELLOW2}Could not sync: API unavailable${RESET2}`);
1166
+ console.log(dim("Using bundled content."));
1167
+ }
1168
+ }
1169
+ async function cmdAudit() {
1170
+ const projectRoot = process.cwd();
1171
+ const essencePath = join4(projectRoot, "decantr.essence.json");
1172
+ console.log(heading("Auditing project..."));
1173
+ if (!existsSync4(essencePath)) {
1174
+ console.log(`${RED}No decantr.essence.json found.${RESET2}`);
1175
+ process.exitCode = 1;
1176
+ return;
1177
+ }
1178
+ try {
1179
+ const essence = JSON.parse(readFileSync4(essencePath, "utf-8"));
1180
+ const validation = validateEssence(essence);
1181
+ if (!validation.valid) {
1182
+ console.log(`${RED}Essence validation failed:${RESET2}`);
1183
+ for (const err of validation.errors) {
1184
+ console.log(` ${RED}${err}${RESET2}`);
1185
+ }
1186
+ process.exitCode = 1;
1187
+ return;
1188
+ }
1189
+ console.log(success("Essence is valid."));
1190
+ const violations = evaluateGuard(essence, {});
1191
+ if (violations.length > 0) {
1192
+ console.log("");
1193
+ console.log(`${YELLOW2}Guard violations:${RESET2}`);
1194
+ for (const v of violations) {
1195
+ const vr = v;
1196
+ console.log(` ${YELLOW2}[${vr.rule}]${RESET2} ${vr.message}`);
1197
+ }
1198
+ } else {
1199
+ console.log(success("No guard violations."));
1200
+ }
1201
+ console.log("");
1202
+ console.log(`${BOLD2}Summary:${RESET2}`);
1203
+ console.log(` Pages defined: ${essence.structure.length}`);
1204
+ console.log(` Guard mode: ${essence.guard.mode}`);
1205
+ console.log(` Theme: ${essence.theme.style}`);
1206
+ } catch (e) {
1207
+ console.log(`${RED}Error: ${e.message}${RESET2}`);
1208
+ process.exitCode = 1;
1209
+ }
1210
+ }
1211
+ function cmdHelp() {
1212
+ console.log(`
1213
+ ${BOLD2}decantr${RESET2} \u2014 Design intelligence for AI-generated UI
1214
+
1215
+ ${BOLD2}Usage:${RESET2}
1216
+ decantr init [options]
1217
+ decantr status
1218
+ decantr sync
1219
+ decantr audit
1220
+ decantr search <query> [--type <type>]
1221
+ decantr get <type> <id>
1222
+ decantr list <type>
1223
+ decantr validate [path]
1224
+ decantr help
1225
+
1226
+ ${BOLD2}Init Options:${RESET2}
1227
+ --blueprint, -b Blueprint ID
1228
+ --theme Theme ID
1229
+ --mode Color mode: dark | light | auto
1230
+ --shape Border shape: pill | rounded | sharp
1231
+ --target Framework: react | vue | svelte | nextjs | html
1232
+ --guard Guard mode: creative | guided | strict
1233
+ --density Spacing: compact | comfortable | spacious
1234
+ --shell Default shell layout
1235
+ --existing Initialize in existing project
1236
+ --offline Force offline mode
1237
+ --yes, -y Accept defaults, skip confirmations
1238
+ --registry Custom registry URL
1239
+
1240
+ ${BOLD2}Commands:${RESET2}
1241
+ ${cyan("init")} Initialize a new Decantr project with full scaffolding
1242
+ ${cyan("status")} Show project status and sync state
1243
+ ${cyan("sync")} Sync registry content from API
1244
+ ${cyan("audit")} Validate essence and check for drift
1245
+ ${cyan("search")} Search the registry
1246
+ ${cyan("get")} Get full details of a registry item
1247
+ ${cyan("list")} List items by type
1248
+ ${cyan("validate")} Validate essence file
1249
+ ${cyan("help")} Show this help
1250
+
1251
+ ${BOLD2}Examples:${RESET2}
1252
+ decantr init
1253
+ decantr init --blueprint=saas-dashboard --theme=luminarum --yes
1254
+ decantr status
1255
+ decantr sync
1256
+ decantr audit
1257
+ decantr search dashboard
1258
+ decantr list patterns
1259
+ `);
1260
+ }
1261
+ async function main() {
1262
+ const args = process.argv.slice(2);
1263
+ const command = args[0];
1264
+ if (!command || command === "help" || command === "--help" || command === "-h") {
1265
+ cmdHelp();
1266
+ return;
1267
+ }
1268
+ switch (command) {
1269
+ case "init": {
1270
+ const initArgs = {};
1271
+ for (let i = 1; i < args.length; i++) {
1272
+ const arg = args[i];
1273
+ if (arg === "--yes" || arg === "-y") {
1274
+ initArgs.yes = true;
1275
+ } else if (arg === "--offline") {
1276
+ initArgs.offline = true;
1277
+ } else if (arg === "--existing") {
1278
+ initArgs.existing = true;
1279
+ } else if (arg.startsWith("--")) {
1280
+ const [key, value] = arg.slice(2).split("=");
1281
+ if (value) {
1282
+ initArgs[key] = value;
1283
+ } else if (args[i + 1] && !args[i + 1].startsWith("-")) {
1284
+ initArgs[key] = args[++i];
1285
+ }
1286
+ } else if (arg.startsWith("-")) {
1287
+ const key = arg.slice(1);
1288
+ if (key === "b" && args[i + 1]) initArgs.blueprint = args[++i];
1289
+ if (key === "y") initArgs.yes = true;
1290
+ }
1291
+ }
1292
+ await cmdInit(initArgs);
1293
+ break;
1294
+ }
1295
+ case "status": {
1296
+ await cmdStatus();
1297
+ break;
1298
+ }
1299
+ case "sync": {
1300
+ await cmdSync();
1301
+ break;
1302
+ }
1303
+ case "audit": {
1304
+ await cmdAudit();
1305
+ break;
1306
+ }
1307
+ case "search": {
1308
+ const query = args[1];
1309
+ if (!query) {
1310
+ console.error(error("Usage: decantr search <query> [--type <type>]"));
1311
+ process.exitCode = 1;
1312
+ return;
1313
+ }
1314
+ const typeIdx = args.indexOf("--type");
1315
+ const type = typeIdx !== -1 ? args[typeIdx + 1] : void 0;
1316
+ await cmdSearch(query, type);
1317
+ break;
1318
+ }
1319
+ case "get": {
1320
+ const type = args[1];
1321
+ const id = args[2];
1322
+ if (!type || !id) {
1323
+ console.error(error("Usage: decantr get <type> <id>"));
1324
+ process.exitCode = 1;
1325
+ return;
1326
+ }
1327
+ await cmdGet(type, id);
1328
+ break;
1329
+ }
1330
+ case "list": {
1331
+ const type = args[1];
1332
+ if (!type) {
1333
+ console.error(error("Usage: decantr list <type>"));
1334
+ process.exitCode = 1;
1335
+ return;
1336
+ }
1337
+ await cmdList(type);
1338
+ break;
1339
+ }
1340
+ case "validate": {
1341
+ await cmdValidate(args[1]);
1342
+ break;
1343
+ }
1344
+ default:
1345
+ console.error(error(`Unknown command: ${command}`));
1346
+ cmdHelp();
1347
+ process.exitCode = 1;
1348
+ }
1349
+ }
1350
+ main().catch((e) => {
1351
+ console.error(error(e.message));
1352
+ process.exitCode = 1;
1353
+ });