@decantr/cli 1.0.0-beta.5 → 1.0.0-beta.8

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 CHANGED
@@ -1,37 +1,888 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { readFileSync, writeFileSync, existsSync } from "fs";
5
- import { join } from "path";
6
- import { createInterface } from "readline";
4
+ import { readFileSync as readFileSync4, existsSync as existsSync4 } from "fs";
5
+ import { join as join4 } from "path";
7
6
  import { validateEssence, evaluateGuard } from "@decantr/essence-spec";
8
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";
9
124
  var BOLD = "\x1B[1m";
10
125
  var DIM = "\x1B[2m";
11
126
  var RESET = "\x1B[0m";
12
- var RED = "\x1B[31m";
13
127
  var GREEN = "\x1B[32m";
14
- var CYAN = "\x1B[36m";
15
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, 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";
16
867
  function heading(text) {
17
868
  return `
18
- ${BOLD}${text}${RESET}
869
+ ${BOLD2}${text}${RESET2}
19
870
  `;
20
871
  }
21
872
  function success(text) {
22
- return `${GREEN}${text}${RESET}`;
873
+ return `${GREEN2}${text}${RESET2}`;
23
874
  }
24
875
  function error(text) {
25
- return `${RED}${text}${RESET}`;
876
+ return `${RED}${text}${RESET2}`;
26
877
  }
27
878
  function dim(text) {
28
- return `${DIM}${text}${RESET}`;
879
+ return `${DIM2}${text}${RESET2}`;
29
880
  }
30
881
  function cyan(text) {
31
- return `${CYAN}${text}${RESET}`;
882
+ return `${CYAN2}${text}${RESET2}`;
32
883
  }
33
884
  function getContentRoot() {
34
- const bundled = join(import.meta.dirname, "..", "..", "..", "content");
885
+ const bundled = join4(import.meta.dirname, "..", "..", "..", "content");
35
886
  return process.env.DECANTR_CONTENT_ROOT || bundled;
36
887
  }
37
888
  function getResolver() {
@@ -46,7 +897,7 @@ async function cmdSearch(query, type) {
46
897
  }
47
898
  console.log(heading(`${results.length} result(s) for "${query}"`));
48
899
  for (const r of results) {
49
- console.log(` ${cyan(r.type.padEnd(12))} ${BOLD}${r.id}${RESET}`);
900
+ console.log(` ${cyan(r.type.padEnd(12))} ${BOLD2}${r.id}${RESET2}`);
50
901
  console.log(` ${dim(r.description || "")}`);
51
902
  console.log("");
52
903
  }
@@ -80,10 +931,10 @@ async function cmdGet(type, id) {
80
931
  console.log(JSON.stringify(result.item, null, 2));
81
932
  }
82
933
  async function cmdValidate(path) {
83
- const essencePath = path || join(process.cwd(), "decantr.essence.json");
934
+ const essencePath = path || join4(process.cwd(), "decantr.essence.json");
84
935
  let raw;
85
936
  try {
86
- raw = readFileSync(essencePath, "utf-8");
937
+ raw = readFileSync4(essencePath, "utf-8");
87
938
  } catch {
88
939
  console.error(error(`Could not read ${essencePath}`));
89
940
  process.exitCode = 1;
@@ -103,7 +954,7 @@ async function cmdValidate(path) {
103
954
  } else {
104
955
  console.error(error("Validation failed:"));
105
956
  for (const err of result.errors) {
106
- console.error(` ${RED}${err}${RESET}`);
957
+ console.error(` ${RED}${err}${RESET2}`);
107
958
  }
108
959
  process.exitCode = 1;
109
960
  }
@@ -113,7 +964,7 @@ async function cmdValidate(path) {
113
964
  console.log(heading("Guard violations:"));
114
965
  for (const v of violations) {
115
966
  const vr = v;
116
- console.log(` ${YELLOW}[${vr.rule}]${RESET} ${vr.message}`);
967
+ console.log(` ${YELLOW2}[${vr.rule}]${RESET2} ${vr.message}`);
117
968
  }
118
969
  } else if (result.valid) {
119
970
  console.log(success("No guard violations."));
@@ -128,16 +979,16 @@ async function cmdList(type) {
128
979
  process.exitCode = 1;
129
980
  return;
130
981
  }
131
- const { readdirSync } = await import("fs");
132
- const dir = join(getContentRoot(), type);
982
+ const { readdirSync: readdirSync2 } = await import("fs");
983
+ const dir = join4(getContentRoot(), type);
133
984
  let found = false;
134
985
  try {
135
- const files = readdirSync(dir).filter((f) => f.endsWith(".json"));
986
+ const files = readdirSync2(dir).filter((f) => f.endsWith(".json"));
136
987
  if (files.length > 0) {
137
988
  found = true;
138
989
  console.log(heading(`${files.length} ${type}`));
139
990
  for (const f of files) {
140
- const data = JSON.parse(readFileSync(join(dir, f), "utf-8"));
991
+ const data = JSON.parse(readFileSync4(join4(dir, f), "utf-8"));
141
992
  console.log(` ${cyan(data.id || f.replace(".json", ""))} ${dim(data.description || data.name || "")}`);
142
993
  }
143
994
  }
@@ -159,169 +1010,238 @@ async function cmdList(type) {
159
1010
  console.log(dim(`No ${type} found.`));
160
1011
  }
161
1012
  }
162
- function ask(question, defaultValue) {
163
- const rl = createInterface({ input: process.stdin, output: process.stdout });
164
- const prompt = defaultValue ? `${question} ${dim(`(${defaultValue})`)}: ` : `${question}: `;
165
- return new Promise((resolve) => {
166
- rl.question(prompt, (answer) => {
167
- rl.close();
168
- resolve(answer.trim() || defaultValue || "");
169
- });
170
- });
171
- }
172
- async function select(question, options, defaultIdx = 0) {
173
- console.log(`
174
- ${BOLD}${question}${RESET}`);
175
- for (let i = 0; i < options.length; i++) {
176
- const marker = i === defaultIdx ? `${GREEN}>${RESET}` : " ";
177
- console.log(` ${marker} ${i + 1}. ${options[i]}`);
178
- }
179
- const answer = await ask(`Choose (1-${options.length})`, String(defaultIdx + 1));
180
- const idx = parseInt(answer, 10) - 1;
181
- return options[Math.max(0, Math.min(idx, options.length - 1))];
182
- }
183
- async function cmdInit() {
184
- console.log(heading("Create a new Decantr project"));
185
- const essencePath = join(process.cwd(), "decantr.essence.json");
186
- if (existsSync(essencePath)) {
187
- const overwrite = await ask("decantr.essence.json already exists. Overwrite?", "n");
188
- if (overwrite.toLowerCase() !== "y") {
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) {
189
1021
  console.log(dim("Cancelled."));
190
1022
  return;
191
1023
  }
192
1024
  }
193
- let archetypes = [];
194
- try {
195
- const res = await fetch("https://decantr-registry.fly.dev/v1/archetypes");
196
- if (res.ok) {
197
- const data = await res.json();
198
- archetypes = data.items;
199
- }
200
- } catch {
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)"));
201
1039
  }
202
- if (archetypes.length === 0) {
203
- archetypes = [
204
- { id: "saas-dashboard", description: "Analytics dashboard with KPIs and data tables" },
205
- { id: "ecommerce", description: "Online store with product catalog" },
206
- { id: "portfolio", description: "Personal or agency portfolio site" },
207
- { id: "marketing-landing", description: "Product marketing landing page" },
208
- { id: "gaming-platform", description: "Gaming community hub" },
209
- { id: "content-site", description: "Blog or content site" }
210
- ];
211
- }
212
- const archetypeOptions = archetypes.map((a) => `${a.id} ${dim(`\u2014 ${a.description || ""}`)}`);
213
- const selectedArchetype = await select("What are you building?", archetypeOptions);
214
- const archetypeId = selectedArchetype.split(" ")[0];
215
- let themes = [];
216
- try {
217
- const res = await fetch("https://decantr-registry.fly.dev/v1/themes");
218
- if (res.ok) {
219
- const data = await res.json();
220
- themes = data.items;
221
- }
222
- } catch {
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);
223
1049
  }
224
- if (themes.length === 0) {
225
- themes = [
226
- { id: "luminarum", description: "Dark geometric canvas with vibrant accents" },
227
- { id: "clean", description: "Professional, minimal, universal" },
228
- { id: "glassmorphism", description: "Frosted glass aesthetic" }
229
- ];
230
- }
231
- const themeOptions = themes.map((t) => `${t.id} ${dim(`\u2014 ${t.description || ""}`)}`);
232
- const selectedTheme = await select("Choose a theme", themeOptions);
233
- const themeId = selectedTheme.split(" ")[0];
234
- const mode = await select("Mode", ["dark", "light"], 0);
235
- const shape = await select("Shape", ["pill", "rounded", "sharp"], 0);
236
- const target = await select("Target framework", ["react", "vue", "svelte", "html"], 0);
237
- let pages = [];
238
- try {
239
- const res = await fetch(`https://decantr-registry.fly.dev/v1/archetypes/${archetypeId}`);
240
- if (res.ok) {
241
- const data = await res.json();
242
- if (data.pages) pages = data.pages;
1050
+ let blueprintData;
1051
+ if (options.blueprint) {
1052
+ const result2 = await registryClient.fetchBlueprint(options.blueprint);
1053
+ if (result2) {
1054
+ blueprintData = result2.data;
243
1055
  }
244
- } catch {
245
1056
  }
246
- const structure = pages.length > 0 ? pages.map((p) => ({
247
- id: p.id,
248
- shell: p.shell || "sidebar-main",
249
- layout: p.default_layout || []
250
- })) : [{ id: "home", shell: "full-bleed", layout: ["hero-split"] }];
251
- let features = [];
252
- try {
253
- const res = await fetch(`https://decantr-registry.fly.dev/v1/archetypes/${archetypeId}`);
254
- if (res.ok) {
255
- const data = await res.json();
256
- if (data.features) features = data.features;
257
- }
258
- } catch {
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/")}`);
259
1073
  }
260
- const essence = {
261
- version: "2.0.0",
262
- archetype: archetypeId,
263
- theme: {
264
- style: themeId,
265
- mode,
266
- recipe: themeId,
267
- shape
268
- },
269
- personality: ["professional"],
270
- platform: { type: "spa", routing: "hash" },
271
- structure,
272
- features,
273
- guard: { enforce_style: true, enforce_recipe: true, mode: "strict" },
274
- density: { level: "comfortable", content_gap: "_gap4" },
275
- target
276
- };
277
- writeFileSync(essencePath, JSON.stringify(essence, null, 2) + "\n");
278
- console.log(success(`
279
- Created decantr.essence.json`));
280
- console.log(dim(` Archetype: ${archetypeId}`));
281
- console.log(dim(` Theme: ${themeId} (${mode})`));
282
- console.log(dim(` Pages: ${structure.map((s) => s.id).join(", ")}`));
283
- console.log(dim(` Target: ${target}`));
1074
+ const essenceContent = readFileSync4(result.essencePath, "utf-8");
1075
+ const essence = JSON.parse(essenceContent);
284
1076
  const validation = validateEssence(essence);
285
1077
  if (validation.valid) {
286
- console.log(success(" Validation: passed"));
1078
+ console.log(success("\nValidation passed."));
287
1079
  } else {
288
- console.log(error(` Validation: ${validation.errors.join(", ")}`));
1080
+ console.log(error(`
1081
+ Validation warnings: ${validation.errors.join(", ")}`));
289
1082
  }
290
1083
  console.log(heading("Next steps"));
291
- console.log(` 1. Open your AI assistant (Claude, Cursor, etc.)`);
292
- console.log(` 2. Tell it to read ${cyan("decantr.essence.json")} before generating code`);
293
- console.log(` 3. The essence file defines your theme, pages, and patterns`);
294
- console.log(` 4. Run ${cyan("decantr validate")} after changes to check for drift
295
- `);
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
+ }
296
1196
  }
297
1197
  function cmdHelp() {
298
1198
  console.log(`
299
- ${BOLD}decantr${RESET} \u2014 Design intelligence for AI-generated UI
1199
+ ${BOLD2}decantr${RESET2} \u2014 Design intelligence for AI-generated UI
300
1200
 
301
- ${BOLD}Usage:${RESET}
302
- decantr init
303
- 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>]
304
1207
  decantr get <type> <id>
305
1208
  decantr list <type>
306
1209
  decantr validate [path]
307
1210
  decantr help
308
1211
 
309
- ${BOLD}Commands:${RESET}
310
- ${cyan("init")} Create a new decantr.essence.json \u2014 pick an archetype, theme, and target
311
- ${cyan("search")} Search the registry for patterns, archetypes, recipes, themes
312
- ${cyan("get")} Get full details of a registry item as JSON
313
- ${cyan("list")} List all items of a type (patterns, archetypes, recipes, themes, blueprints)
314
- ${cyan("validate")} Validate a decantr.essence.json file against the schema and guard rules
315
- ${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
316
1225
 
317
- ${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}
318
1238
  decantr init
1239
+ decantr init --blueprint=saas-dashboard --theme=luminarum --yes
1240
+ decantr status
1241
+ decantr sync
1242
+ decantr audit
319
1243
  decantr search dashboard
320
- decantr search kpi --type pattern
321
- decantr get pattern kpi-grid
322
- decantr get recipe luminarum
323
1244
  decantr list patterns
324
- decantr validate
325
1245
  `);
326
1246
  }
327
1247
  async function main() {
@@ -333,7 +1253,41 @@ async function main() {
333
1253
  }
334
1254
  switch (command) {
335
1255
  case "init": {
336
- await cmdInit();
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();
337
1291
  break;
338
1292
  }
339
1293
  case "search": {