@decantr/cli 1.0.0-beta.4 → 1.0.0-beta.7

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.
Files changed (2) hide show
  1. package/dist/index.js +1130 -36
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,36 +1,888 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { readFileSync } from "fs";
5
- import { join } from "path";
4
+ import { readFileSync as readFileSync4, existsSync as existsSync4 } from "fs";
5
+ import { join as join4 } from "path";
6
6
  import { validateEssence, evaluateGuard } from "@decantr/essence-spec";
7
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";
8
124
  var BOLD = "\x1B[1m";
9
125
  var DIM = "\x1B[2m";
10
126
  var RESET = "\x1B[0m";
11
- var RED = "\x1B[31m";
12
127
  var GREEN = "\x1B[32m";
13
- var CYAN = "\x1B[36m";
14
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 templatePath = join2(__dirname, "templates", name);
336
+ if (existsSync2(templatePath)) {
337
+ return readFileSync2(templatePath, "utf-8");
338
+ }
339
+ const srcPath = join2(__dirname, "..", "src", "templates", name);
340
+ if (existsSync2(srcPath)) {
341
+ return readFileSync2(srcPath, "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, blueprint) {
353
+ let structure = [
354
+ { id: "home", shell: options.shell, layout: [] }
355
+ ];
356
+ let features = options.features;
357
+ if (blueprint?.pages) {
358
+ structure = blueprint.pages.map((p) => ({
359
+ id: p.id,
360
+ shell: p.shell || options.shell,
361
+ layout: p.default_layout || []
362
+ }));
363
+ }
364
+ if (blueprint?.features) {
365
+ features = [.../* @__PURE__ */ new Set([...features, ...blueprint.features])];
366
+ }
367
+ const contentGapMap = {
368
+ compact: "_gap2",
369
+ comfortable: "_gap4",
370
+ spacious: "_gap6"
371
+ };
372
+ return {
373
+ version: "2.0.0",
374
+ archetype: options.archetype,
375
+ blueprint: options.blueprint,
376
+ theme: {
377
+ style: options.theme,
378
+ mode: options.mode,
379
+ recipe: options.theme,
380
+ // Recipe defaults to theme
381
+ shape: options.shape
382
+ },
383
+ personality: options.personality,
384
+ platform: {
385
+ type: "spa",
386
+ routing: "hash"
387
+ },
388
+ structure,
389
+ features,
390
+ guard: {
391
+ enforce_style: true,
392
+ enforce_recipe: true,
393
+ mode: options.guard
394
+ },
395
+ density: {
396
+ level: options.density,
397
+ content_gap: contentGapMap[options.density] || "_gap4"
398
+ },
399
+ target: options.target
400
+ };
401
+ }
402
+ function generateDecantrMd(essence, detected) {
403
+ const template = loadTemplate("DECANTR.md.template");
404
+ const pagesTable = essence.structure.map(
405
+ (p) => `| ${p.id} | ${p.shell} | ${p.layout.join(", ") || "none"} |`
406
+ ).join("\n");
407
+ const allPatterns = [...new Set(essence.structure.flatMap((p) => p.layout))];
408
+ const patternsList = allPatterns.length > 0 ? allPatterns.map((p) => `- \`${p}\``).join("\n") : "- No patterns specified yet";
409
+ const projectSummary = [
410
+ `**Archetype:** ${essence.archetype || "custom"}`,
411
+ `**Target:** ${essence.target}`,
412
+ `**Theme:** ${essence.theme.style} (${essence.theme.mode} mode)`,
413
+ `**Guard Mode:** ${essence.guard.mode}`,
414
+ `**Pages:** ${essence.structure.map((s) => s.id).join(", ")}`
415
+ ].join("\n");
416
+ const shellStructures = {
417
+ "sidebar-main": "nav (left) | header (top) | body (scrollable)",
418
+ "top-nav-main": "header (full width) | body (scrollable)",
419
+ "centered": "body (centered card)",
420
+ "full-bleed": "header (floating) | body (full page sections)",
421
+ "minimal-header": "header (slim) | body (centered)"
422
+ };
423
+ const defaultShell = essence.structure[0]?.shell || "sidebar-main";
424
+ const shellStructure = shellStructures[defaultShell] || "Custom shell layout";
425
+ const vars = {
426
+ GUARD_MODE: essence.guard.mode,
427
+ PROJECT_SUMMARY: projectSummary,
428
+ THEME_STYLE: essence.theme.style,
429
+ THEME_MODE: essence.theme.mode,
430
+ THEME_RECIPE: essence.theme.recipe,
431
+ TARGET: essence.target,
432
+ PAGES_TABLE: `| Page | Shell | Layout |
433
+ |------|-------|--------|
434
+ ${pagesTable}`,
435
+ PATTERNS_LIST: patternsList,
436
+ DEFAULT_SHELL: defaultShell,
437
+ SHELL_STRUCTURE: shellStructure,
438
+ PERSONALITY: essence.personality.join(", "),
439
+ DENSITY: essence.density.level,
440
+ AVAILABLE_PATTERNS: "(See registry or .decantr/cache/patterns/)",
441
+ AVAILABLE_THEMES: "(See registry or .decantr/cache/themes/)",
442
+ AVAILABLE_SHELLS: "sidebar-main, top-nav-main, centered, full-bleed, minimal-header",
443
+ VERSION: CLI_VERSION
444
+ };
445
+ return renderTemplate(template, vars);
446
+ }
447
+ function generateProjectJson(detected, options, registrySource) {
448
+ const now = (/* @__PURE__ */ new Date()).toISOString();
449
+ const data = {
450
+ detected: {
451
+ framework: detected.framework,
452
+ version: detected.version || null,
453
+ packageManager: detected.packageManager,
454
+ hasTypeScript: detected.hasTypeScript,
455
+ hasTailwind: detected.hasTailwind,
456
+ existingRuleFiles: detected.existingRuleFiles
457
+ },
458
+ overrides: {
459
+ framework: options.target !== detected.framework ? options.target : null
460
+ },
461
+ sync: {
462
+ status: registrySource === "api" ? "synced" : "needs-sync",
463
+ lastSync: now,
464
+ registrySource,
465
+ cachedContent: {
466
+ archetypes: [],
467
+ patterns: [],
468
+ themes: [],
469
+ recipes: []
470
+ }
471
+ },
472
+ initialized: {
473
+ at: now,
474
+ via: "cli",
475
+ version: CLI_VERSION,
476
+ flags: buildFlagsString(options)
477
+ }
478
+ };
479
+ return JSON.stringify(data, null, 2);
480
+ }
481
+ function buildFlagsString(options) {
482
+ const flags = [];
483
+ if (options.blueprint) flags.push(`--blueprint=${options.blueprint}`);
484
+ if (options.theme) flags.push(`--theme=${options.theme}`);
485
+ if (options.mode) flags.push(`--mode=${options.mode}`);
486
+ if (options.guard) flags.push(`--guard=${options.guard}`);
487
+ return flags.join(" ");
488
+ }
489
+ function generateTaskContext(templateName, essence) {
490
+ const template = loadTemplate(templateName);
491
+ const defaultShell = essence.structure[0]?.shell || "sidebar-main";
492
+ const layout = essence.structure[0]?.layout.join(", ") || "none";
493
+ const scaffoldStructure = essence.structure.map((p) => {
494
+ const patterns = p.layout.length > 0 ? `
495
+ - Patterns: ${p.layout.join(", ")}` : "";
496
+ return `- **${p.id}** (${p.shell})${patterns}`;
497
+ }).join("\n");
498
+ const vars = {
499
+ TARGET: essence.target,
500
+ THEME_STYLE: essence.theme.style,
501
+ THEME_MODE: essence.theme.mode,
502
+ THEME_RECIPE: essence.theme.recipe,
503
+ DEFAULT_SHELL: defaultShell,
504
+ GUARD_MODE: essence.guard.mode,
505
+ LAYOUT: layout,
506
+ DENSITY: essence.density.level,
507
+ CONTENT_GAP: essence.density.content_gap,
508
+ SCAFFOLD_STRUCTURE: scaffoldStructure
509
+ };
510
+ return renderTemplate(template, vars);
511
+ }
512
+ function generateEssenceSummary(essence) {
513
+ const template = loadTemplate("essence-summary.md.template");
514
+ const pagesTable = `| Page | Shell | Layout |
515
+ |------|-------|--------|
516
+ ${essence.structure.map((p) => `| ${p.id} | ${p.shell} | ${p.layout.join(", ") || "none"} |`).join("\n")}`;
517
+ const featuresList = essence.features.length > 0 ? essence.features.map((f) => `- ${f}`).join("\n") : "- No features specified";
518
+ const vars = {
519
+ ARCHETYPE: essence.archetype || "custom",
520
+ BLUEPRINT: essence.blueprint || "none",
521
+ PERSONALITY: essence.personality.join(", "),
522
+ TARGET: essence.target,
523
+ THEME_STYLE: essence.theme.style,
524
+ THEME_MODE: essence.theme.mode,
525
+ THEME_RECIPE: essence.theme.recipe,
526
+ SHAPE: essence.theme.shape,
527
+ PAGES_TABLE: pagesTable,
528
+ FEATURES_LIST: featuresList,
529
+ GUARD_MODE: essence.guard.mode,
530
+ ENFORCE_STYLE: String(essence.guard.enforce_style),
531
+ ENFORCE_RECIPE: String(essence.guard.enforce_recipe),
532
+ DENSITY: essence.density.level,
533
+ CONTENT_GAP: essence.density.content_gap,
534
+ LAST_UPDATED: (/* @__PURE__ */ new Date()).toISOString()
535
+ };
536
+ return renderTemplate(template, vars);
537
+ }
538
+ function updateGitignore(projectRoot) {
539
+ const gitignorePath = join2(projectRoot, ".gitignore");
540
+ const cacheEntry = ".decantr/cache/";
541
+ if (existsSync2(gitignorePath)) {
542
+ const content = readFileSync2(gitignorePath, "utf-8");
543
+ if (!content.includes(cacheEntry)) {
544
+ appendFileSync(gitignorePath, `
545
+ # Decantr cache
546
+ ${cacheEntry}
547
+ `);
548
+ return true;
549
+ }
550
+ return false;
551
+ } else {
552
+ writeFileSync(gitignorePath, `# Decantr cache
553
+ ${cacheEntry}
554
+ `);
555
+ return true;
556
+ }
557
+ }
558
+ function scaffoldProject(projectRoot, options, detected, blueprint, registrySource = "bundled") {
559
+ const essence = buildEssence(options, blueprint);
560
+ const decantrDir = join2(projectRoot, ".decantr");
561
+ const contextDir = join2(decantrDir, "context");
562
+ const cacheDir = join2(decantrDir, "cache");
563
+ mkdirSync(contextDir, { recursive: true });
564
+ mkdirSync(cacheDir, { recursive: true });
565
+ const essencePath = join2(projectRoot, "decantr.essence.json");
566
+ writeFileSync(essencePath, JSON.stringify(essence, null, 2) + "\n");
567
+ const decantrMdPath = join2(projectRoot, "DECANTR.md");
568
+ writeFileSync(decantrMdPath, generateDecantrMd(essence, detected));
569
+ const projectJsonPath = join2(decantrDir, "project.json");
570
+ writeFileSync(projectJsonPath, generateProjectJson(detected, options, registrySource));
571
+ const contextFiles = [];
572
+ const scaffoldPath = join2(contextDir, "task-scaffold.md");
573
+ writeFileSync(scaffoldPath, generateTaskContext("task-scaffold.md.template", essence));
574
+ contextFiles.push(scaffoldPath);
575
+ const addPagePath = join2(contextDir, "task-add-page.md");
576
+ writeFileSync(addPagePath, generateTaskContext("task-add-page.md.template", essence));
577
+ contextFiles.push(addPagePath);
578
+ const modifyPath = join2(contextDir, "task-modify.md");
579
+ writeFileSync(modifyPath, generateTaskContext("task-modify.md.template", essence));
580
+ contextFiles.push(modifyPath);
581
+ const summaryPath = join2(contextDir, "essence-summary.md");
582
+ writeFileSync(summaryPath, generateEssenceSummary(essence));
583
+ contextFiles.push(summaryPath);
584
+ const gitignoreUpdated = updateGitignore(projectRoot);
585
+ return {
586
+ essencePath,
587
+ decantrMdPath,
588
+ projectJsonPath,
589
+ contextFiles,
590
+ gitignoreUpdated
591
+ };
592
+ }
593
+
594
+ // src/registry.ts
595
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2, readdirSync } from "fs";
596
+ import { join as join3, dirname as dirname2 } from "path";
597
+ import { fileURLToPath as fileURLToPath2 } from "url";
598
+ var __dirname2 = dirname2(fileURLToPath2(import.meta.url));
599
+ var DEFAULT_API_URL = "https://decantr-registry.fly.dev/v1";
600
+ function getBundledContentRoot() {
601
+ const bundled = join3(__dirname2, "..", "..", "..", "content");
602
+ if (existsSync3(bundled)) return bundled;
603
+ const distBundled = join3(__dirname2, "..", "..", "..", "..", "content");
604
+ if (existsSync3(distBundled)) return distBundled;
605
+ return bundled;
606
+ }
607
+ async function fetchWithTimeout(url, timeoutMs = 5e3) {
608
+ const controller = new AbortController();
609
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
610
+ try {
611
+ const response = await fetch(url, { signal: controller.signal });
612
+ return response;
613
+ } finally {
614
+ clearTimeout(timeout);
615
+ }
616
+ }
617
+ async function tryApi(endpoint, apiUrl = DEFAULT_API_URL) {
618
+ try {
619
+ const url = `${apiUrl}/${endpoint}`;
620
+ const response = await fetchWithTimeout(url);
621
+ if (!response.ok) return null;
622
+ const data = await response.json();
623
+ return {
624
+ data,
625
+ source: { type: "api", url: apiUrl }
626
+ };
627
+ } catch {
628
+ return null;
629
+ }
630
+ }
631
+ function loadFromCache(cacheDir, contentType, id) {
632
+ const cachePath = id ? join3(cacheDir, contentType, `${id}.json`) : join3(cacheDir, contentType, "index.json");
633
+ if (!existsSync3(cachePath)) return null;
634
+ try {
635
+ const data = JSON.parse(readFileSync3(cachePath, "utf-8"));
636
+ return {
637
+ data,
638
+ source: { type: "cache" }
639
+ };
640
+ } catch {
641
+ return null;
642
+ }
643
+ }
644
+ function loadFromBundled(contentType, id) {
645
+ const contentRoot = getBundledContentRoot();
646
+ if (id) {
647
+ const itemPath = join3(contentRoot, contentType, `${id}.json`);
648
+ if (!existsSync3(itemPath)) return null;
649
+ try {
650
+ const data = JSON.parse(readFileSync3(itemPath, "utf-8"));
651
+ return {
652
+ data,
653
+ source: { type: "bundled" }
654
+ };
655
+ } catch {
656
+ return null;
657
+ }
658
+ } else {
659
+ const dir = join3(contentRoot, contentType);
660
+ if (!existsSync3(dir)) return null;
661
+ try {
662
+ const files = readdirSync(dir).filter((f) => f.endsWith(".json"));
663
+ const items = files.map((f) => {
664
+ const content = JSON.parse(readFileSync3(join3(dir, f), "utf-8"));
665
+ return { id: content.id || f.replace(".json", ""), ...content };
666
+ });
667
+ return {
668
+ data: { items, total: items.length },
669
+ source: { type: "bundled" }
670
+ };
671
+ } catch {
672
+ return null;
673
+ }
674
+ }
675
+ }
676
+ function saveToCache(cacheDir, contentType, id, data) {
677
+ const dir = join3(cacheDir, contentType);
678
+ mkdirSync2(dir, { recursive: true });
679
+ const cachePath = id ? join3(dir, `${id}.json`) : join3(dir, "index.json");
680
+ writeFileSync2(cachePath, JSON.stringify(data, null, 2));
681
+ }
682
+ var RegistryClient = class {
683
+ cacheDir;
684
+ apiUrl;
685
+ offline;
686
+ constructor(options = {}) {
687
+ this.cacheDir = options.cacheDir || join3(process.cwd(), ".decantr", "cache");
688
+ this.apiUrl = options.apiUrl || DEFAULT_API_URL;
689
+ this.offline = options.offline || false;
690
+ }
691
+ /**
692
+ * Fetch archetypes list.
693
+ */
694
+ async fetchArchetypes() {
695
+ if (!this.offline) {
696
+ const apiResult = await tryApi("archetypes", this.apiUrl);
697
+ if (apiResult) {
698
+ saveToCache(this.cacheDir, "archetypes", null, apiResult.data);
699
+ return apiResult;
700
+ }
701
+ }
702
+ const cacheResult = loadFromCache(
703
+ this.cacheDir,
704
+ "archetypes"
705
+ );
706
+ if (cacheResult) return cacheResult;
707
+ const bundledResult = loadFromBundled("archetypes");
708
+ if (bundledResult) return bundledResult;
709
+ return {
710
+ data: { items: [], total: 0 },
711
+ source: { type: "bundled" }
712
+ };
713
+ }
714
+ /**
715
+ * Fetch a single archetype.
716
+ */
717
+ async fetchArchetype(id) {
718
+ if (!this.offline) {
719
+ const apiResult = await tryApi(`archetypes/${id}`, this.apiUrl);
720
+ if (apiResult) {
721
+ saveToCache(this.cacheDir, "archetypes", id, apiResult.data);
722
+ return apiResult;
723
+ }
724
+ }
725
+ const cacheResult = loadFromCache(this.cacheDir, "archetypes", id);
726
+ if (cacheResult) return cacheResult;
727
+ return loadFromBundled("archetypes", id);
728
+ }
729
+ /**
730
+ * Fetch blueprints list.
731
+ */
732
+ async fetchBlueprints() {
733
+ if (!this.offline) {
734
+ const apiResult = await tryApi("blueprints", this.apiUrl);
735
+ if (apiResult) {
736
+ saveToCache(this.cacheDir, "blueprints", null, apiResult.data);
737
+ return apiResult;
738
+ }
739
+ }
740
+ const cacheResult = loadFromCache(
741
+ this.cacheDir,
742
+ "blueprints"
743
+ );
744
+ if (cacheResult) return cacheResult;
745
+ const bundledResult = loadFromBundled("blueprints");
746
+ if (bundledResult) return bundledResult;
747
+ return {
748
+ data: { items: [], total: 0 },
749
+ source: { type: "bundled" }
750
+ };
751
+ }
752
+ /**
753
+ * Fetch a single blueprint.
754
+ */
755
+ async fetchBlueprint(id) {
756
+ if (!this.offline) {
757
+ const apiResult = await tryApi(`blueprints/${id}`, this.apiUrl);
758
+ if (apiResult) {
759
+ saveToCache(this.cacheDir, "blueprints", id, apiResult.data);
760
+ return apiResult;
761
+ }
762
+ }
763
+ const cacheResult = loadFromCache(this.cacheDir, "blueprints", id);
764
+ if (cacheResult) return cacheResult;
765
+ return loadFromBundled("blueprints", id);
766
+ }
767
+ /**
768
+ * Fetch themes list.
769
+ */
770
+ async fetchThemes() {
771
+ if (!this.offline) {
772
+ const apiResult = await tryApi("themes", this.apiUrl);
773
+ if (apiResult) {
774
+ saveToCache(this.cacheDir, "themes", null, apiResult.data);
775
+ return apiResult;
776
+ }
777
+ }
778
+ const cacheResult = loadFromCache(
779
+ this.cacheDir,
780
+ "themes"
781
+ );
782
+ if (cacheResult) return cacheResult;
783
+ const bundledResult = loadFromBundled("themes");
784
+ if (bundledResult) return bundledResult;
785
+ return {
786
+ data: { items: [], total: 0 },
787
+ source: { type: "bundled" }
788
+ };
789
+ }
790
+ /**
791
+ * Fetch patterns list.
792
+ */
793
+ async fetchPatterns() {
794
+ if (!this.offline) {
795
+ const apiResult = await tryApi("patterns", this.apiUrl);
796
+ if (apiResult) {
797
+ saveToCache(this.cacheDir, "patterns", null, apiResult.data);
798
+ return apiResult;
799
+ }
800
+ }
801
+ const cacheResult = loadFromCache(
802
+ this.cacheDir,
803
+ "patterns"
804
+ );
805
+ if (cacheResult) return cacheResult;
806
+ const bundledResult = loadFromBundled("patterns");
807
+ if (bundledResult) return bundledResult;
808
+ return {
809
+ data: { items: [], total: 0 },
810
+ source: { type: "bundled" }
811
+ };
812
+ }
813
+ /**
814
+ * Check if API is available.
815
+ */
816
+ async checkApiAvailability() {
817
+ if (this.offline) return false;
818
+ try {
819
+ const response = await fetchWithTimeout(`${this.apiUrl.replace("/v1", "")}/health`, 3e3);
820
+ return response.ok;
821
+ } catch {
822
+ return false;
823
+ }
824
+ }
825
+ /**
826
+ * Get the source used for the last fetch.
827
+ */
828
+ getSourceType() {
829
+ return this.offline ? "bundled" : "api";
830
+ }
831
+ };
832
+ async function syncRegistry(cacheDir, apiUrl = DEFAULT_API_URL) {
833
+ const client = new RegistryClient({ cacheDir, apiUrl, offline: false });
834
+ const synced = [];
835
+ const failed = [];
836
+ const apiAvailable = await client.checkApiAvailability();
837
+ if (!apiAvailable) {
838
+ return { synced: [], failed: ["API unavailable"], source: "bundled" };
839
+ }
840
+ const types = ["archetypes", "blueprints", "themes", "patterns"];
841
+ for (const type of types) {
842
+ try {
843
+ const fetchMethod = `fetch${type.charAt(0).toUpperCase()}${type.slice(1)}`;
844
+ const result = await client[fetchMethod]();
845
+ if (result.source.type === "api") {
846
+ synced.push(type);
847
+ }
848
+ } catch {
849
+ failed.push(type);
850
+ }
851
+ }
852
+ return {
853
+ synced,
854
+ failed,
855
+ source: synced.length > 0 ? "api" : "bundled"
856
+ };
857
+ }
858
+
859
+ // src/index.ts
860
+ var BOLD2 = "\x1B[1m";
861
+ var DIM2 = "\x1B[2m";
862
+ var RESET2 = "\x1B[0m";
863
+ var RED = "\x1B[31m";
864
+ var GREEN2 = "\x1B[32m";
865
+ var CYAN2 = "\x1B[36m";
866
+ var YELLOW2 = "\x1B[33m";
15
867
  function heading(text) {
16
868
  return `
17
- ${BOLD}${text}${RESET}
869
+ ${BOLD2}${text}${RESET2}
18
870
  `;
19
871
  }
20
872
  function success(text) {
21
- return `${GREEN}${text}${RESET}`;
873
+ return `${GREEN2}${text}${RESET2}`;
22
874
  }
23
875
  function error(text) {
24
- return `${RED}${text}${RESET}`;
876
+ return `${RED}${text}${RESET2}`;
25
877
  }
26
878
  function dim(text) {
27
- return `${DIM}${text}${RESET}`;
879
+ return `${DIM2}${text}${RESET2}`;
28
880
  }
29
881
  function cyan(text) {
30
- return `${CYAN}${text}${RESET}`;
882
+ return `${CYAN2}${text}${RESET2}`;
31
883
  }
32
884
  function getContentRoot() {
33
- const bundled = join(import.meta.dirname, "..", "..", "..", "content");
885
+ const bundled = join4(import.meta.dirname, "..", "..", "..", "content");
34
886
  return process.env.DECANTR_CONTENT_ROOT || bundled;
35
887
  }
36
888
  function getResolver() {
@@ -45,7 +897,7 @@ async function cmdSearch(query, type) {
45
897
  }
46
898
  console.log(heading(`${results.length} result(s) for "${query}"`));
47
899
  for (const r of results) {
48
- console.log(` ${cyan(r.type.padEnd(12))} ${BOLD}${r.id}${RESET}`);
900
+ console.log(` ${cyan(r.type.padEnd(12))} ${BOLD2}${r.id}${RESET2}`);
49
901
  console.log(` ${dim(r.description || "")}`);
50
902
  console.log("");
51
903
  }
@@ -79,10 +931,10 @@ async function cmdGet(type, id) {
79
931
  console.log(JSON.stringify(result.item, null, 2));
80
932
  }
81
933
  async function cmdValidate(path) {
82
- const essencePath = path || join(process.cwd(), "decantr.essence.json");
934
+ const essencePath = path || join4(process.cwd(), "decantr.essence.json");
83
935
  let raw;
84
936
  try {
85
- raw = readFileSync(essencePath, "utf-8");
937
+ raw = readFileSync4(essencePath, "utf-8");
86
938
  } catch {
87
939
  console.error(error(`Could not read ${essencePath}`));
88
940
  process.exitCode = 1;
@@ -102,7 +954,7 @@ async function cmdValidate(path) {
102
954
  } else {
103
955
  console.error(error("Validation failed:"));
104
956
  for (const err of result.errors) {
105
- console.error(` ${RED}${err}${RESET}`);
957
+ console.error(` ${RED}${err}${RESET2}`);
106
958
  }
107
959
  process.exitCode = 1;
108
960
  }
@@ -112,7 +964,7 @@ async function cmdValidate(path) {
112
964
  console.log(heading("Guard violations:"));
113
965
  for (const v of violations) {
114
966
  const vr = v;
115
- console.log(` ${YELLOW}[${vr.rule}]${RESET} ${vr.message}`);
967
+ console.log(` ${YELLOW2}[${vr.rule}]${RESET2} ${vr.message}`);
116
968
  }
117
969
  } else if (result.valid) {
118
970
  console.log(success("No guard violations."));
@@ -127,16 +979,16 @@ async function cmdList(type) {
127
979
  process.exitCode = 1;
128
980
  return;
129
981
  }
130
- const { readdirSync } = await import("fs");
131
- const dir = join(getContentRoot(), type);
982
+ const { readdirSync: readdirSync2 } = await import("fs");
983
+ const dir = join4(getContentRoot(), type);
132
984
  let found = false;
133
985
  try {
134
- const files = readdirSync(dir).filter((f) => f.endsWith(".json"));
986
+ const files = readdirSync2(dir).filter((f) => f.endsWith(".json"));
135
987
  if (files.length > 0) {
136
988
  found = true;
137
989
  console.log(heading(`${files.length} ${type}`));
138
990
  for (const f of files) {
139
- const data = JSON.parse(readFileSync(join(dir, f), "utf-8"));
991
+ const data = JSON.parse(readFileSync4(join4(dir, f), "utf-8"));
140
992
  console.log(` ${cyan(data.id || f.replace(".json", ""))} ${dim(data.description || data.name || "")}`);
141
993
  }
142
994
  }
@@ -158,34 +1010,238 @@ async function cmdList(type) {
158
1010
  console.log(dim(`No ${type} found.`));
159
1011
  }
160
1012
  }
1013
+ async function cmdInit(args) {
1014
+ const projectRoot = process.cwd();
1015
+ console.log(heading("Decantr Project Setup"));
1016
+ const detected = detectProject(projectRoot);
1017
+ if (detected.existingEssence && !args.existing) {
1018
+ console.log(`${YELLOW2}Warning: decantr.essence.json already exists.${RESET2}`);
1019
+ const overwrite = await confirm("Overwrite existing configuration?", false);
1020
+ if (!overwrite) {
1021
+ console.log(dim("Cancelled."));
1022
+ return;
1023
+ }
1024
+ }
1025
+ const registryClient = new RegistryClient({
1026
+ cacheDir: join4(projectRoot, ".decantr", "cache"),
1027
+ apiUrl: args.registry,
1028
+ offline: args.offline
1029
+ });
1030
+ console.log(dim("Fetching registry content..."));
1031
+ const [archetypesResult, blueprintsResult, themesResult] = await Promise.all([
1032
+ registryClient.fetchArchetypes(),
1033
+ registryClient.fetchBlueprints(),
1034
+ registryClient.fetchThemes()
1035
+ ]);
1036
+ const registrySource = archetypesResult.source.type;
1037
+ if (registrySource === "bundled") {
1038
+ console.log(dim("Using bundled content (API unavailable)"));
1039
+ }
1040
+ const archetypes = archetypesResult.data.items;
1041
+ const blueprints = blueprintsResult.data.items;
1042
+ const themes = themesResult.data.items;
1043
+ let options;
1044
+ if (args.yes) {
1045
+ const flags = parseFlags(args, detected);
1046
+ options = mergeWithDefaults(flags, detected);
1047
+ } else {
1048
+ options = await runInteractivePrompts(detected, archetypes, blueprints, themes);
1049
+ }
1050
+ let blueprintData;
1051
+ if (options.blueprint) {
1052
+ const result2 = await registryClient.fetchBlueprint(options.blueprint);
1053
+ if (result2) {
1054
+ blueprintData = result2.data;
1055
+ }
1056
+ }
1057
+ console.log(heading("Scaffolding project..."));
1058
+ const result = scaffoldProject(
1059
+ projectRoot,
1060
+ options,
1061
+ detected,
1062
+ blueprintData,
1063
+ registrySource
1064
+ );
1065
+ console.log(success("\nProject scaffolded successfully!"));
1066
+ console.log("");
1067
+ console.log(` ${cyan("decantr.essence.json")} Design specification`);
1068
+ console.log(` ${cyan("DECANTR.md")} LLM instructions`);
1069
+ console.log(` ${cyan(".decantr/project.json")} Project state`);
1070
+ console.log(` ${cyan(".decantr/context/")} Task-specific guides`);
1071
+ if (result.gitignoreUpdated) {
1072
+ console.log(` ${dim(".gitignore updated to exclude .decantr/cache/")}`);
1073
+ }
1074
+ const essenceContent = readFileSync4(result.essencePath, "utf-8");
1075
+ const essence = JSON.parse(essenceContent);
1076
+ const validation = validateEssence(essence);
1077
+ if (validation.valid) {
1078
+ console.log(success("\nValidation passed."));
1079
+ } else {
1080
+ console.log(error(`
1081
+ Validation warnings: ${validation.errors.join(", ")}`));
1082
+ }
1083
+ console.log(heading("Next steps"));
1084
+ console.log("1. Review DECANTR.md to understand the methodology");
1085
+ console.log("2. Share DECANTR.md with your AI assistant");
1086
+ console.log("3. Start building! The AI will follow the essence spec.");
1087
+ console.log("");
1088
+ if (registrySource === "bundled") {
1089
+ console.log(dim('Run "decantr sync" when online to get the latest registry content.'));
1090
+ }
1091
+ }
1092
+ async function cmdStatus() {
1093
+ const projectRoot = process.cwd();
1094
+ const essencePath = join4(projectRoot, "decantr.essence.json");
1095
+ const projectJsonPath = join4(projectRoot, ".decantr", "project.json");
1096
+ console.log(heading("Decantr Project Status"));
1097
+ if (!existsSync4(essencePath)) {
1098
+ console.log(`${RED}No decantr.essence.json found.${RESET2}`);
1099
+ console.log(dim('Run "decantr init" to create one.'));
1100
+ return;
1101
+ }
1102
+ try {
1103
+ const essence = JSON.parse(readFileSync4(essencePath, "utf-8"));
1104
+ const validation = validateEssence(essence);
1105
+ console.log(`${BOLD2}Essence:${RESET2}`);
1106
+ if (validation.valid) {
1107
+ console.log(` ${GREEN2}Valid${RESET2}`);
1108
+ } else {
1109
+ console.log(` ${RED}Invalid: ${validation.errors.join(", ")}${RESET2}`);
1110
+ }
1111
+ console.log(` Theme: ${essence.theme?.style || "unknown"} (${essence.theme?.mode || "unknown"})`);
1112
+ console.log(` Guard: ${essence.guard?.mode || "unknown"}`);
1113
+ console.log(` Pages: ${(essence.structure || []).length}`);
1114
+ } catch (e) {
1115
+ console.log(` ${RED}Error reading essence: ${e.message}${RESET2}`);
1116
+ }
1117
+ console.log("");
1118
+ console.log(`${BOLD2}Sync Status:${RESET2}`);
1119
+ if (existsSync4(projectJsonPath)) {
1120
+ try {
1121
+ const projectJson = JSON.parse(readFileSync4(projectJsonPath, "utf-8"));
1122
+ const syncStatus = projectJson.sync?.status || "unknown";
1123
+ const lastSync = projectJson.sync?.lastSync || "never";
1124
+ const source = projectJson.sync?.registrySource || "unknown";
1125
+ const statusColor = syncStatus === "synced" ? GREEN2 : YELLOW2;
1126
+ console.log(` Status: ${statusColor}${syncStatus}${RESET2}`);
1127
+ console.log(` Last sync: ${dim(lastSync)}`);
1128
+ console.log(` Source: ${dim(source)}`);
1129
+ } catch {
1130
+ console.log(` ${YELLOW2}Could not read project.json${RESET2}`);
1131
+ }
1132
+ } else {
1133
+ console.log(` ${YELLOW2}No .decantr/project.json found${RESET2}`);
1134
+ console.log(dim(' Run "decantr init" to create project files.'));
1135
+ }
1136
+ }
1137
+ async function cmdSync() {
1138
+ const projectRoot = process.cwd();
1139
+ const cacheDir = join4(projectRoot, ".decantr", "cache");
1140
+ console.log(heading("Syncing registry content..."));
1141
+ const result = await syncRegistry(cacheDir);
1142
+ if (result.source === "api") {
1143
+ console.log(success("Sync completed successfully."));
1144
+ if (result.synced.length > 0) {
1145
+ console.log(` Synced: ${result.synced.join(", ")}`);
1146
+ }
1147
+ if (result.failed.length > 0) {
1148
+ console.log(` ${YELLOW2}Failed: ${result.failed.join(", ")}${RESET2}`);
1149
+ }
1150
+ } else {
1151
+ console.log(`${YELLOW2}Could not sync: API unavailable${RESET2}`);
1152
+ console.log(dim("Using bundled content."));
1153
+ }
1154
+ }
1155
+ async function cmdAudit() {
1156
+ const projectRoot = process.cwd();
1157
+ const essencePath = join4(projectRoot, "decantr.essence.json");
1158
+ console.log(heading("Auditing project..."));
1159
+ if (!existsSync4(essencePath)) {
1160
+ console.log(`${RED}No decantr.essence.json found.${RESET2}`);
1161
+ process.exitCode = 1;
1162
+ return;
1163
+ }
1164
+ try {
1165
+ const essence = JSON.parse(readFileSync4(essencePath, "utf-8"));
1166
+ const validation = validateEssence(essence);
1167
+ if (!validation.valid) {
1168
+ console.log(`${RED}Essence validation failed:${RESET2}`);
1169
+ for (const err of validation.errors) {
1170
+ console.log(` ${RED}${err}${RESET2}`);
1171
+ }
1172
+ process.exitCode = 1;
1173
+ return;
1174
+ }
1175
+ console.log(success("Essence is valid."));
1176
+ const violations = evaluateGuard(essence, {});
1177
+ if (violations.length > 0) {
1178
+ console.log("");
1179
+ console.log(`${YELLOW2}Guard violations:${RESET2}`);
1180
+ for (const v of violations) {
1181
+ const vr = v;
1182
+ console.log(` ${YELLOW2}[${vr.rule}]${RESET2} ${vr.message}`);
1183
+ }
1184
+ } else {
1185
+ console.log(success("No guard violations."));
1186
+ }
1187
+ console.log("");
1188
+ console.log(`${BOLD2}Summary:${RESET2}`);
1189
+ console.log(` Pages defined: ${essence.structure.length}`);
1190
+ console.log(` Guard mode: ${essence.guard.mode}`);
1191
+ console.log(` Theme: ${essence.theme.style}`);
1192
+ } catch (e) {
1193
+ console.log(`${RED}Error: ${e.message}${RESET2}`);
1194
+ process.exitCode = 1;
1195
+ }
1196
+ }
161
1197
  function cmdHelp() {
162
1198
  console.log(`
163
- ${BOLD}decantr${RESET} \u2014 Design intelligence for AI-generated UI
1199
+ ${BOLD2}decantr${RESET2} \u2014 Design intelligence for AI-generated UI
164
1200
 
165
- ${BOLD}Usage:${RESET}
166
- decantr search <query> [--type pattern|archetype|recipe|theme]
1201
+ ${BOLD2}Usage:${RESET2}
1202
+ decantr init [options]
1203
+ decantr status
1204
+ decantr sync
1205
+ decantr audit
1206
+ decantr search <query> [--type <type>]
167
1207
  decantr get <type> <id>
168
1208
  decantr list <type>
169
1209
  decantr validate [path]
170
1210
  decantr help
171
1211
 
172
- ${BOLD}Commands:${RESET}
173
- ${cyan("search")} Search the registry for patterns, archetypes, recipes, themes
174
- ${cyan("get")} Get full details of a registry item as JSON
175
- ${cyan("list")} List all items of a type (patterns, archetypes, recipes, themes, blueprints)
176
- ${cyan("validate")} Validate a decantr.essence.json file against the schema and guard rules
177
- ${cyan("help")} Show this help message
1212
+ ${BOLD2}Init Options:${RESET2}
1213
+ --blueprint, -b Blueprint ID
1214
+ --theme Theme ID
1215
+ --mode Color mode: dark | light | auto
1216
+ --shape Border shape: pill | rounded | sharp
1217
+ --target Framework: react | vue | svelte | nextjs | html
1218
+ --guard Guard mode: creative | guided | strict
1219
+ --density Spacing: compact | comfortable | spacious
1220
+ --shell Default shell layout
1221
+ --existing Initialize in existing project
1222
+ --offline Force offline mode
1223
+ --yes, -y Accept defaults, skip confirmations
1224
+ --registry Custom registry URL
178
1225
 
179
- ${BOLD}Examples:${RESET}
1226
+ ${BOLD2}Commands:${RESET2}
1227
+ ${cyan("init")} Initialize a new Decantr project with full scaffolding
1228
+ ${cyan("status")} Show project status and sync state
1229
+ ${cyan("sync")} Sync registry content from API
1230
+ ${cyan("audit")} Validate essence and check for drift
1231
+ ${cyan("search")} Search the registry
1232
+ ${cyan("get")} Get full details of a registry item
1233
+ ${cyan("list")} List items by type
1234
+ ${cyan("validate")} Validate essence file
1235
+ ${cyan("help")} Show this help
1236
+
1237
+ ${BOLD2}Examples:${RESET2}
1238
+ decantr init
1239
+ decantr init --blueprint=saas-dashboard --theme=luminarum --yes
1240
+ decantr status
1241
+ decantr sync
1242
+ decantr audit
180
1243
  decantr search dashboard
181
- decantr search kpi --type pattern
182
- decantr get pattern kpi-grid
183
- decantr get recipe luminarum
184
- decantr get theme luminarum
185
1244
  decantr list patterns
186
- decantr list themes
187
- decantr validate
188
- decantr validate ./my-project/decantr.essence.json
189
1245
  `);
190
1246
  }
191
1247
  async function main() {
@@ -196,6 +1252,44 @@ async function main() {
196
1252
  return;
197
1253
  }
198
1254
  switch (command) {
1255
+ case "init": {
1256
+ const initArgs = {};
1257
+ for (let i = 1; i < args.length; i++) {
1258
+ const arg = args[i];
1259
+ if (arg === "--yes" || arg === "-y") {
1260
+ initArgs.yes = true;
1261
+ } else if (arg === "--offline") {
1262
+ initArgs.offline = true;
1263
+ } else if (arg === "--existing") {
1264
+ initArgs.existing = true;
1265
+ } else if (arg.startsWith("--")) {
1266
+ const [key, value] = arg.slice(2).split("=");
1267
+ if (value) {
1268
+ initArgs[key] = value;
1269
+ } else if (args[i + 1] && !args[i + 1].startsWith("-")) {
1270
+ initArgs[key] = args[++i];
1271
+ }
1272
+ } else if (arg.startsWith("-")) {
1273
+ const key = arg.slice(1);
1274
+ if (key === "b" && args[i + 1]) initArgs.blueprint = args[++i];
1275
+ if (key === "y") initArgs.yes = true;
1276
+ }
1277
+ }
1278
+ await cmdInit(initArgs);
1279
+ break;
1280
+ }
1281
+ case "status": {
1282
+ await cmdStatus();
1283
+ break;
1284
+ }
1285
+ case "sync": {
1286
+ await cmdSync();
1287
+ break;
1288
+ }
1289
+ case "audit": {
1290
+ await cmdAudit();
1291
+ break;
1292
+ }
199
1293
  case "search": {
200
1294
  const query = args[1];
201
1295
  if (!query) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decantr/cli",
3
- "version": "1.0.0-beta.4",
3
+ "version": "1.0.0-beta.7",
4
4
  "description": "Decantr CLI — search the registry, validate essence files, and access design intelligence from the terminal",
5
5
  "license": "MIT",
6
6
  "repository": {