@decantr/cli 1.0.0-beta.8 → 1.0.0

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,4 +1,11 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ RegistryClient,
4
+ syncRegistry
5
+ } from "./chunk-PWTUBGGJ.js";
6
+ import {
7
+ __require
8
+ } from "./chunk-PDX44BCA.js";
2
9
 
3
10
  // src/index.ts
4
11
  import { readFileSync as readFileSync4, existsSync as existsSync4 } from "fs";
@@ -324,6 +331,38 @@ function mergeWithDefaults(flags, detected) {
324
331
  existing: flags.existing || detected.existingEssence
325
332
  };
326
333
  }
334
+ async function runSimplifiedInit(blueprints) {
335
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
336
+ const question = (q) => new Promise((resolve) => rl.question(q, resolve));
337
+ console.log("\n? What blueprint would you like to scaffold?\n");
338
+ console.log(" 1. Decantr default (recommended)");
339
+ console.log(" 2. Search registry...\n");
340
+ const choice = await question("Enter choice (1 or 2): ");
341
+ if (choice === "1" || choice === "") {
342
+ rl.close();
343
+ return { choice: "default" };
344
+ }
345
+ const searchQuery = await question("Search: ");
346
+ const matches = blueprints.filter(
347
+ (b) => b.id.toLowerCase().includes(searchQuery.toLowerCase()) || b.name?.toLowerCase().includes(searchQuery.toLowerCase()) || b.description?.toLowerCase().includes(searchQuery.toLowerCase())
348
+ ).slice(0, 10);
349
+ if (matches.length === 0) {
350
+ console.log("\nNo matches found. Using Decantr default.");
351
+ rl.close();
352
+ return { choice: "default" };
353
+ }
354
+ console.log("\nResults:");
355
+ matches.forEach((b, i) => {
356
+ console.log(` ${i + 1}. ${b.id} \u2014 ${b.description || b.name || ""}`);
357
+ });
358
+ const selection = await question("\nSelect (number): ");
359
+ const idx = parseInt(selection, 10) - 1;
360
+ rl.close();
361
+ if (idx >= 0 && idx < matches.length) {
362
+ return { choice: "search", selectedBlueprint: matches[idx].id };
363
+ }
364
+ return { choice: "default" };
365
+ }
327
366
 
328
367
  // src/scaffold.ts
329
368
  import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync, appendFileSync } from "fs";
@@ -331,6 +370,40 @@ import { join as join2, dirname } from "path";
331
370
  import { fileURLToPath } from "url";
332
371
  var __dirname = dirname(fileURLToPath(import.meta.url));
333
372
  var CLI_VERSION = "1.0.0";
373
+ function serializeLayoutItem(item) {
374
+ if (typeof item === "string") {
375
+ return item;
376
+ }
377
+ if (typeof item === "object" && item !== null) {
378
+ const obj = item;
379
+ if (typeof obj.pattern === "string") {
380
+ const preset = obj.preset ? ` (${obj.preset})` : "";
381
+ const alias = obj.as ? ` as ${obj.as}` : "";
382
+ return `${obj.pattern}${preset}${alias}`;
383
+ }
384
+ if (Array.isArray(obj.cols)) {
385
+ const cols = obj.cols.map(serializeLayoutItem).join(" | ");
386
+ const breakpoint = obj.at ? ` @${obj.at}` : "";
387
+ return `[${cols}]${breakpoint}`;
388
+ }
389
+ }
390
+ return "custom";
391
+ }
392
+ function extractPatternNames(item) {
393
+ if (typeof item === "string") {
394
+ return [item];
395
+ }
396
+ if (typeof item === "object" && item !== null) {
397
+ const obj = item;
398
+ if (typeof obj.pattern === "string") {
399
+ return [obj.pattern];
400
+ }
401
+ if (Array.isArray(obj.cols)) {
402
+ return obj.cols.flatMap(extractPatternNames);
403
+ }
404
+ }
405
+ return [];
406
+ }
334
407
  function loadTemplate(name) {
335
408
  const fromDist = join2(__dirname, "..", "src", "templates", name);
336
409
  if (existsSync2(fromDist)) {
@@ -349,30 +422,56 @@ function renderTemplate(template, vars) {
349
422
  }
350
423
  return result;
351
424
  }
352
- function buildEssence(options, blueprint) {
425
+ function resolvePatternAlias(item, patterns) {
426
+ if (!patterns) return item;
427
+ if (typeof item === "string") {
428
+ const patternDef = patterns.find((p) => p.as === item);
429
+ if (patternDef) {
430
+ if (patternDef.preset) {
431
+ return { pattern: patternDef.pattern, preset: patternDef.preset };
432
+ }
433
+ return patternDef.pattern;
434
+ }
435
+ return item;
436
+ }
437
+ if (typeof item === "object" && item !== null) {
438
+ const obj = item;
439
+ if (Array.isArray(obj.cols)) {
440
+ return {
441
+ ...obj,
442
+ cols: obj.cols.map((col) => resolvePatternAlias(col, patterns))
443
+ };
444
+ }
445
+ }
446
+ return item;
447
+ }
448
+ function buildEssence(options, archetypeData) {
353
449
  let structure = [
354
- { id: "home", shell: options.shell, layout: [] }
450
+ { id: "home", shell: options.shell, layout: ["hero"] }
355
451
  ];
356
452
  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
- }));
453
+ if (archetypeData?.pages) {
454
+ structure = archetypeData.pages.map((p) => {
455
+ const resolvedLayout = (p.default_layout?.length ? p.default_layout : ["hero"]).map((item) => resolvePatternAlias(item, p.patterns));
456
+ return {
457
+ id: p.id,
458
+ shell: p.shell || options.shell,
459
+ layout: resolvedLayout
460
+ };
461
+ });
363
462
  }
364
- if (blueprint?.features) {
365
- features = [.../* @__PURE__ */ new Set([...features, ...blueprint.features])];
463
+ if (archetypeData?.features) {
464
+ features = [.../* @__PURE__ */ new Set([...features, ...archetypeData.features])];
366
465
  }
367
466
  const contentGapMap = {
368
467
  compact: "_gap2",
369
468
  comfortable: "_gap4",
370
469
  spacious: "_gap6"
371
470
  };
372
- return {
471
+ const archetype = options.archetype || "custom";
472
+ const essence = {
373
473
  version: "2.0.0",
374
- archetype: options.archetype,
375
- blueprint: options.blueprint,
474
+ archetype,
376
475
  theme: {
377
476
  style: options.theme,
378
477
  mode: options.mode,
@@ -398,14 +497,105 @@ function buildEssence(options, blueprint) {
398
497
  },
399
498
  target: options.target
400
499
  };
500
+ if (options.accessibility) {
501
+ essence.accessibility = options.accessibility;
502
+ }
503
+ return essence;
401
504
  }
402
- function generateDecantrMd(essence, detected) {
505
+ function generateAccessibilitySection(essence, themeData) {
506
+ const accessibility = essence.accessibility;
507
+ if (!accessibility?.wcag_level || accessibility.wcag_level === "none") {
508
+ return "";
509
+ }
510
+ const wcagLevel = accessibility.wcag_level;
511
+ const cvdPreference = accessibility.cvd_preference || "none";
512
+ const cvdSupport = themeData?.cvd_support || [];
513
+ let section = `---
514
+
515
+ ## Accessibility
516
+
517
+ **WCAG Level:** ${wcagLevel}
518
+ `;
519
+ if (cvdSupport.length > 0) {
520
+ section += `**CVD Support:** Theme supports ${cvdSupport.join(", ")}
521
+ **CVD Preference:** ${cvdPreference}
522
+ `;
523
+ }
524
+ section += `
525
+ ### What This Means
526
+
527
+ This project requires WCAG 2.1 Level ${wcagLevel} compliance. You already know these rules \u2014 apply them:
528
+
529
+ - Semantic HTML structure
530
+ - Sufficient color contrast (4.5:1 for normal text, 3:1 for large text)
531
+ - Keyboard navigability for all interactive elements
532
+ - Visible focus indicators
533
+ - Meaningful alt text for images
534
+ - Proper heading hierarchy
535
+ `;
536
+ if (cvdSupport.length > 0) {
537
+ section += `
538
+ ### CVD Implementation
539
+
540
+ The theme provides these data attributes:
541
+
542
+ \`\`\`html
543
+ <html data-theme="${essence.theme.style}" data-mode="${essence.theme.mode}" data-cvd="none">
544
+ \`\`\`
545
+
546
+ Valid \`data-cvd\` values for this theme: \`none\`, ${cvdSupport.map((m) => `\`${m}\``).join(", ")}
547
+ `;
548
+ if (cvdPreference === "auto") {
549
+ section += `
550
+ Detect user preference via \`prefers-contrast\` or user settings and apply accordingly.
551
+ `;
552
+ }
553
+ }
554
+ section += `
555
+ ---
556
+ `;
557
+ return section;
558
+ }
559
+ function generateSeoSection(essence, archetypeData) {
560
+ const seoHints = archetypeData?.seo_hints;
561
+ if (!seoHints) {
562
+ return "";
563
+ }
564
+ const schemaOrg = seoHints.schema_org || [];
565
+ const metaPriorities = seoHints.meta_priorities || [];
566
+ if (schemaOrg.length === 0 && metaPriorities.length === 0) {
567
+ return "";
568
+ }
569
+ let section = `---
570
+
571
+ ## SEO Guidance
572
+
573
+ This archetype (\`${essence.archetype}\`) typically benefits from:
574
+
575
+ `;
576
+ if (schemaOrg.length > 0) {
577
+ section += `- **Schema.org:** ${schemaOrg.join(", ")}
578
+ `;
579
+ }
580
+ if (metaPriorities.length > 0) {
581
+ section += `- **Meta priorities:** ${metaPriorities.join(", ")}
582
+ `;
583
+ }
584
+ section += `
585
+ These are suggestions, not requirements. Apply where appropriate for the page content.
586
+
587
+ ---
588
+ `;
589
+ return section;
590
+ }
591
+ function generateDecantrMd(essence, detected, themeData, recipeData, archetypeData) {
403
592
  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";
593
+ const pagesTable = essence.structure.map((p) => {
594
+ const layoutStr = p.layout.map(serializeLayoutItem).join(", ") || "none";
595
+ return `| ${p.id} | ${p.shell} | ${layoutStr} |`;
596
+ }).join("\n");
597
+ const allPatternNames = [...new Set(essence.structure.flatMap((p) => p.layout.flatMap(extractPatternNames)))];
598
+ const patternsList = allPatternNames.length > 0 ? allPatternNames.map((p) => `- \`${p}\``).join("\n") : "- No patterns specified yet";
409
599
  const projectSummary = [
410
600
  `**Archetype:** ${essence.archetype || "custom"}`,
411
601
  `**Target:** ${essence.target}`,
@@ -422,6 +612,29 @@ function generateDecantrMd(essence, detected) {
422
612
  };
423
613
  const defaultShell = essence.structure[0]?.shell || "sidebar-main";
424
614
  const shellStructure = shellStructures[defaultShell] || "Custom shell layout";
615
+ let themeQuickRef = "";
616
+ if (themeData?.seed) {
617
+ const colors = Object.entries(themeData.seed).map(([name, hex]) => `- **${name}:** \`${hex}\``).join("\n");
618
+ themeQuickRef = `**Seed Colors:**
619
+ ${colors}`;
620
+ }
621
+ if (recipeData?.decorators) {
622
+ const decorators = Object.entries(recipeData.decorators).slice(0, 5).map(([name, desc]) => `- \`${name}\` \u2014 ${desc}`).join("\n");
623
+ if (themeQuickRef) {
624
+ themeQuickRef += `
625
+
626
+ **Key Decorators:**
627
+ ${decorators}`;
628
+ } else {
629
+ themeQuickRef = `**Key Decorators:**
630
+ ${decorators}`;
631
+ }
632
+ }
633
+ if (!themeQuickRef) {
634
+ themeQuickRef = `See \`decantr get theme ${essence.theme.style}\` for details.`;
635
+ }
636
+ const accessibilitySection = generateAccessibilitySection(essence, themeData);
637
+ const seoSection = generateSeoSection(essence, archetypeData);
425
638
  const vars = {
426
639
  GUARD_MODE: essence.guard.mode,
427
640
  PROJECT_SUMMARY: projectSummary,
@@ -440,7 +653,10 @@ ${pagesTable}`,
440
653
  AVAILABLE_PATTERNS: "(See registry or .decantr/cache/patterns/)",
441
654
  AVAILABLE_THEMES: "(See registry or .decantr/cache/themes/)",
442
655
  AVAILABLE_SHELLS: "sidebar-main, top-nav-main, centered, full-bleed, minimal-header",
443
- VERSION: CLI_VERSION
656
+ VERSION: CLI_VERSION,
657
+ THEME_QUICK_REFERENCE: themeQuickRef,
658
+ ACCESSIBILITY_SECTION: accessibilitySection,
659
+ SEO_SECTION: seoSection
444
660
  };
445
661
  return renderTemplate(template, vars);
446
662
  }
@@ -489,10 +705,10 @@ function buildFlagsString(options) {
489
705
  function generateTaskContext(templateName, essence) {
490
706
  const template = loadTemplate(templateName);
491
707
  const defaultShell = essence.structure[0]?.shell || "sidebar-main";
492
- const layout = essence.structure[0]?.layout.join(", ") || "none";
708
+ const layout = essence.structure[0]?.layout.map(serializeLayoutItem).join(", ") || "none";
493
709
  const scaffoldStructure = essence.structure.map((p) => {
494
710
  const patterns = p.layout.length > 0 ? `
495
- - Patterns: ${p.layout.join(", ")}` : "";
711
+ - Patterns: ${p.layout.map(serializeLayoutItem).join(", ")}` : "";
496
712
  return `- **${p.id}** (${p.shell})${patterns}`;
497
713
  }).join("\n");
498
714
  const vars = {
@@ -513,7 +729,7 @@ function generateEssenceSummary(essence) {
513
729
  const template = loadTemplate("essence-summary.md.template");
514
730
  const pagesTable = `| Page | Shell | Layout |
515
731
  |------|-------|--------|
516
- ${essence.structure.map((p) => `| ${p.id} | ${p.shell} | ${p.layout.join(", ") || "none"} |`).join("\n")}`;
732
+ ${essence.structure.map((p) => `| ${p.id} | ${p.shell} | ${p.layout.map(serializeLayoutItem).join(", ") || "none"} |`).join("\n")}`;
517
733
  const featuresList = essence.features.length > 0 ? essence.features.map((f) => `- ${f}`).join("\n") : "- No features specified";
518
734
  const vars = {
519
735
  ARCHETYPE: essence.archetype || "custom",
@@ -555,8 +771,8 @@ ${cacheEntry}
555
771
  return true;
556
772
  }
557
773
  }
558
- function scaffoldProject(projectRoot, options, detected, blueprint, registrySource = "bundled") {
559
- const essence = buildEssence(options, blueprint);
774
+ function scaffoldProject(projectRoot, options, detected, archetypeData, registrySource = "bundled", themeData, recipeData) {
775
+ const essence = buildEssence(options, archetypeData);
560
776
  const decantrDir = join2(projectRoot, ".decantr");
561
777
  const contextDir = join2(decantrDir, "context");
562
778
  const cacheDir = join2(decantrDir, "cache");
@@ -565,7 +781,7 @@ function scaffoldProject(projectRoot, options, detected, blueprint, registrySour
565
781
  const essencePath = join2(projectRoot, "decantr.essence.json");
566
782
  writeFileSync(essencePath, JSON.stringify(essence, null, 2) + "\n");
567
783
  const decantrMdPath = join2(projectRoot, "DECANTR.md");
568
- writeFileSync(decantrMdPath, generateDecantrMd(essence, detected));
784
+ writeFileSync(decantrMdPath, generateDecantrMd(essence, detected, themeData, recipeData, archetypeData));
569
785
  const projectJsonPath = join2(decantrDir, "project.json");
570
786
  writeFileSync(projectJsonPath, generateProjectJson(detected, options, registrySource));
571
787
  const contextFiles = [];
@@ -591,268 +807,225 @@ function scaffoldProject(projectRoot, options, detected, blueprint, registrySour
591
807
  };
592
808
  }
593
809
 
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
- }
810
+ // src/theme-commands.ts
811
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2, readdirSync, rmSync } from "fs";
812
+ import { join as join3 } from "path";
813
+
814
+ // src/theme-templates.ts
815
+ function getThemeSkeleton(id, name) {
816
+ return {
817
+ $schema: "https://decantr.ai/schemas/style-metadata.v1.json",
818
+ id,
819
+ name,
820
+ description: "",
821
+ tags: [],
822
+ seed: {
823
+ primary: "#6366F1",
824
+ secondary: "#8B5CF6",
825
+ accent: "#EC4899",
826
+ background: "#0F172A"
827
+ },
828
+ palette: {},
829
+ modes: ["dark"],
830
+ shapes: ["rounded"],
831
+ decantr_compat: ">=1.0.0",
832
+ source: "custom"
833
+ };
616
834
  }
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;
835
+ function getHowToThemeDoc() {
836
+ return `# Custom Themes
837
+
838
+ Create custom themes for your Decantr project.
839
+
840
+ ## Quick Start
841
+
842
+ \`\`\`bash
843
+ decantr theme create mytheme
844
+ \`\`\`
845
+
846
+ ## Theme Structure
847
+
848
+ | Field | Required | Description |
849
+ |-------|----------|-------------|
850
+ | id | Yes | Unique identifier (matches filename) |
851
+ | name | Yes | Display name |
852
+ | description | No | Brief description |
853
+ | tags | No | Searchable tags |
854
+ | seed | Yes | Core colors: primary, secondary, accent, background |
855
+ | palette | No | Extended color palette |
856
+ | modes | Yes | Supported modes: ["light"], ["dark"], or both |
857
+ | shapes | Yes | Supported shapes: sharp, rounded, pill |
858
+ | decantr_compat | Yes | Version compatibility (e.g., ">=1.0.0") |
859
+ | source | Yes | Must be "custom" |
860
+
861
+ ## Using Your Theme
862
+
863
+ In \`decantr.essence.json\`:
864
+
865
+ \`\`\`json
866
+ {
867
+ "theme": {
868
+ "style": "custom:mytheme",
869
+ "mode": "dark"
629
870
  }
630
871
  }
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
- }
872
+ \`\`\`
873
+
874
+ ## Validation
875
+
876
+ \`\`\`bash
877
+ decantr theme validate mytheme
878
+ \`\`\`
879
+
880
+ ## Reference
881
+
882
+ See registry themes for examples:
883
+
884
+ \`\`\`bash
885
+ decantr get theme auradecantism
886
+ \`\`\`
887
+ `;
643
888
  }
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;
889
+
890
+ // src/theme-commands.ts
891
+ var REQUIRED_FIELDS = ["id", "name", "seed", "modes", "shapes", "decantr_compat", "source"];
892
+ var REQUIRED_SEED = ["primary", "secondary", "accent", "background"];
893
+ var VALID_MODES = ["light", "dark"];
894
+ var VALID_SHAPES = ["sharp", "rounded", "pill"];
895
+ function validateCustomTheme(theme) {
896
+ const errors = [];
897
+ for (const field of REQUIRED_FIELDS) {
898
+ if (!(field in theme)) {
899
+ errors.push(`Missing required field: ${field}`);
673
900
  }
674
901
  }
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;
902
+ if (theme.seed && typeof theme.seed === "object") {
903
+ const seed = theme.seed;
904
+ for (const color of REQUIRED_SEED) {
905
+ if (!(color in seed)) {
906
+ errors.push(`Missing seed color: ${color}`);
700
907
  }
701
908
  }
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
909
  }
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;
910
+ if (Array.isArray(theme.modes)) {
911
+ for (const mode of theme.modes) {
912
+ if (!VALID_MODES.includes(mode)) {
913
+ errors.push(`Invalid mode "${mode}" - must be "light" or "dark"`);
723
914
  }
724
915
  }
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;
916
+ }
917
+ if (Array.isArray(theme.shapes)) {
918
+ for (const shape of theme.shapes) {
919
+ if (!VALID_SHAPES.includes(shape)) {
920
+ errors.push(`Invalid shape "${shape}" - use: sharp, rounded, pill`);
738
921
  }
739
922
  }
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;
923
+ }
924
+ return {
925
+ valid: errors.length === 0,
926
+ errors
927
+ };
928
+ }
929
+ function createTheme(projectRoot, id, name) {
930
+ const customThemesDir = join3(projectRoot, ".decantr", "custom", "themes");
931
+ const themePath = join3(customThemesDir, `${id}.json`);
932
+ const howToPath = join3(customThemesDir, "how-to-theme.md");
933
+ mkdirSync2(customThemesDir, { recursive: true });
934
+ if (existsSync3(themePath)) {
747
935
  return {
748
- data: { items: [], total: 0 },
749
- source: { type: "bundled" }
936
+ success: false,
937
+ error: `Theme "${id}" already exists at ${themePath}`
750
938
  };
751
939
  }
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;
940
+ const skeleton = getThemeSkeleton(id, name);
941
+ writeFileSync2(themePath, JSON.stringify(skeleton, null, 2));
942
+ if (!existsSync3(howToPath)) {
943
+ writeFileSync2(howToPath, getHowToThemeDoc());
944
+ }
945
+ return {
946
+ success: true,
947
+ path: themePath
948
+ };
949
+ }
950
+ function listCustomThemes(projectRoot) {
951
+ const customThemesDir = join3(projectRoot, ".decantr", "custom", "themes");
952
+ if (!existsSync3(customThemesDir)) {
953
+ return [];
954
+ }
955
+ const themes = [];
956
+ try {
957
+ const files = readdirSync(customThemesDir).filter((f) => f.endsWith(".json"));
958
+ for (const file of files) {
959
+ const filePath = join3(customThemesDir, file);
960
+ try {
961
+ const data = JSON.parse(readFileSync3(filePath, "utf-8"));
962
+ themes.push({
963
+ id: data.id || file.replace(".json", ""),
964
+ name: data.name || data.id,
965
+ description: data.description,
966
+ path: filePath
967
+ });
968
+ } catch {
776
969
  }
777
970
  }
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;
971
+ } catch {
972
+ }
973
+ return themes;
974
+ }
975
+ function deleteTheme(projectRoot, id) {
976
+ const themePath = join3(projectRoot, ".decantr", "custom", "themes", `${id}.json`);
977
+ if (!existsSync3(themePath)) {
785
978
  return {
786
- data: { items: [], total: 0 },
787
- source: { type: "bundled" }
979
+ success: false,
980
+ error: `Theme "${id}" not found at ${themePath}`
788
981
  };
789
982
  }
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;
983
+ try {
984
+ rmSync(themePath);
985
+ return { success: true };
986
+ } catch (e) {
808
987
  return {
809
- data: { items: [], total: 0 },
810
- source: { type: "bundled" }
988
+ success: false,
989
+ error: `Failed to delete: ${e.message}`
811
990
  };
812
991
  }
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
- }
992
+ }
993
+ function importTheme(projectRoot, sourcePath) {
994
+ if (!existsSync3(sourcePath)) {
995
+ return {
996
+ success: false,
997
+ errors: [`Source file not found: ${sourcePath}`]
998
+ };
824
999
  }
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
- }
1000
+ let theme;
1001
+ try {
1002
+ theme = JSON.parse(readFileSync3(sourcePath, "utf-8"));
1003
+ } catch (e) {
1004
+ return {
1005
+ success: false,
1006
+ errors: [`Invalid JSON: ${e.message}`]
1007
+ };
1008
+ }
1009
+ const validation = validateCustomTheme(theme);
1010
+ if (!validation.valid) {
1011
+ return {
1012
+ success: false,
1013
+ errors: validation.errors
1014
+ };
1015
+ }
1016
+ theme.source = "custom";
1017
+ const id = theme.id;
1018
+ const customThemesDir = join3(projectRoot, ".decantr", "custom", "themes");
1019
+ const destPath = join3(customThemesDir, `${id}.json`);
1020
+ mkdirSync2(customThemesDir, { recursive: true });
1021
+ const howToPath = join3(customThemesDir, "how-to-theme.md");
1022
+ if (!existsSync3(howToPath)) {
1023
+ writeFileSync2(howToPath, getHowToThemeDoc());
851
1024
  }
1025
+ writeFileSync2(destPath, JSON.stringify(theme, null, 2));
852
1026
  return {
853
- synced,
854
- failed,
855
- source: synced.length > 0 ? "api" : "bundled"
1027
+ success: true,
1028
+ path: destPath
856
1029
  };
857
1030
  }
858
1031
 
@@ -881,6 +1054,61 @@ function dim(text) {
881
1054
  function cyan(text) {
882
1055
  return `${CYAN2}${text}${RESET2}`;
883
1056
  }
1057
+ function extractPatternName(item) {
1058
+ if (typeof item === "string") return item;
1059
+ if (typeof item === "object" && item !== null) {
1060
+ const obj = item;
1061
+ if (typeof obj.pattern === "string") return obj.pattern;
1062
+ if (Array.isArray(obj.cols)) {
1063
+ return obj.cols.map(extractPatternName).join(" | ");
1064
+ }
1065
+ }
1066
+ return "custom";
1067
+ }
1068
+ function generateCuratedPrompt(ctx) {
1069
+ const lines = [];
1070
+ lines.push(`I'm building a ${ctx.archetype} application using ${ctx.target}.`);
1071
+ lines.push("");
1072
+ if (ctx.blueprint) {
1073
+ lines.push(`Blueprint: ${ctx.blueprint}`);
1074
+ }
1075
+ lines.push(`Theme: ${ctx.theme} (${ctx.mode} mode)`);
1076
+ lines.push(`Personality: ${ctx.personality.join(", ")}`);
1077
+ lines.push(`Guard mode: ${ctx.guard}`);
1078
+ lines.push("");
1079
+ lines.push("Pages to build:");
1080
+ for (const page of ctx.pages) {
1081
+ const patternNames = page.layout.map(extractPatternName);
1082
+ const patterns = patternNames.length > 0 ? patternNames.join(", ") : "custom";
1083
+ lines.push(` - ${page.id}: ${page.shell} shell with ${patterns}`);
1084
+ }
1085
+ if (ctx.features.length > 0) {
1086
+ lines.push("");
1087
+ lines.push(`Features: ${ctx.features.join(", ")}`);
1088
+ }
1089
+ lines.push("");
1090
+ lines.push("Please read DECANTR.md for the full design spec and methodology.");
1091
+ lines.push("Follow the guard rules and use the patterns from decantr.essence.json.");
1092
+ return lines.join("\n");
1093
+ }
1094
+ function boxedPrompt(content, title) {
1095
+ const lines = content.split("\n");
1096
+ const maxLen = Math.max(...lines.map((l) => l.length), title.length + 4);
1097
+ const width = maxLen + 4;
1098
+ const top = `\u250C${"\u2500".repeat(width - 2)}\u2510`;
1099
+ const titleLine = `\u2502 ${BOLD2}${title}${RESET2}${" ".repeat(width - title.length - 4)} \u2502`;
1100
+ const sep = `\u251C${"\u2500".repeat(width - 2)}\u2524`;
1101
+ const bottom = `\u2514${"\u2500".repeat(width - 2)}\u2518`;
1102
+ const body = lines.map((line) => {
1103
+ const padding = " ".repeat(width - line.length - 4);
1104
+ return `\u2502 ${line}${padding} \u2502`;
1105
+ }).join("\n");
1106
+ return `${top}
1107
+ ${titleLine}
1108
+ ${sep}
1109
+ ${body}
1110
+ ${bottom}`;
1111
+ }
884
1112
  function getContentRoot() {
885
1113
  const bundled = join4(import.meta.dirname, "..", "..", "..", "content");
886
1114
  return process.env.DECANTR_CONTENT_ROOT || bundled;
@@ -902,13 +1130,58 @@ async function cmdSearch(query, type) {
902
1130
  console.log("");
903
1131
  }
904
1132
  }
1133
+ async function cmdSuggest(query, type) {
1134
+ const client = createRegistryClient();
1135
+ const searchType = type || "pattern";
1136
+ const results = await client.search(query, searchType);
1137
+ if (results.length === 0) {
1138
+ console.log(dim(`No suggestions for "${query}"`));
1139
+ console.log("");
1140
+ console.log("Try:");
1141
+ console.log(` ${cyan("decantr list patterns")} - see all patterns`);
1142
+ console.log(` ${cyan("decantr search <broader-term>")} - broaden your search`);
1143
+ return;
1144
+ }
1145
+ console.log(heading(`Suggestions for "${query}"`));
1146
+ const queryLower = query.toLowerCase();
1147
+ const exact = results.filter((r) => r.id.toLowerCase().includes(queryLower));
1148
+ const related = results.filter((r) => !r.id.toLowerCase().includes(queryLower));
1149
+ if (exact.length > 0) {
1150
+ console.log(`${BOLD2}Direct matches:${RESET2}`);
1151
+ for (const r of exact.slice(0, 3)) {
1152
+ console.log(` ${cyan(r.id)} - ${r.description || ""}`);
1153
+ }
1154
+ console.log("");
1155
+ }
1156
+ if (related.length > 0) {
1157
+ console.log(`${BOLD2}Related:${RESET2}`);
1158
+ for (const r of related.slice(0, 5)) {
1159
+ console.log(` ${cyan(r.id)} - ${r.description || ""}`);
1160
+ }
1161
+ console.log("");
1162
+ }
1163
+ console.log(dim(`Use "decantr get pattern <id>" for full details`));
1164
+ }
905
1165
  async function cmdGet(type, id) {
906
- const validTypes = ["pattern", "archetype", "recipe", "theme", "blueprint"];
1166
+ const validTypes = ["pattern", "archetype", "recipe", "theme", "blueprint", "shell"];
907
1167
  if (!validTypes.includes(type)) {
908
1168
  console.error(error(`Invalid type "${type}". Must be one of: ${validTypes.join(", ")}`));
909
1169
  process.exitCode = 1;
910
1170
  return;
911
1171
  }
1172
+ if (type === "shell") {
1173
+ const registryClient = new RegistryClient({
1174
+ cacheDir: join4(process.cwd(), ".decantr", "cache")
1175
+ });
1176
+ const shellResult = await registryClient.fetchShell(id);
1177
+ if (shellResult) {
1178
+ console.log(JSON.stringify(shellResult.data, null, 2));
1179
+ return;
1180
+ }
1181
+ console.error(error(`shell "${id}" not found.`));
1182
+ process.exitCode = 1;
1183
+ return;
1184
+ }
912
1185
  const resolver = getResolver();
913
1186
  let result = await resolver.resolve(type, id);
914
1187
  if (!result) {
@@ -930,6 +1203,53 @@ async function cmdGet(type, id) {
930
1203
  }
931
1204
  console.log(JSON.stringify(result.item, null, 2));
932
1205
  }
1206
+ function buildRegistryContext() {
1207
+ const { readdirSync: readdirSync2 } = __require("fs");
1208
+ const themeRegistry = /* @__PURE__ */ new Map();
1209
+ const patternRegistry = /* @__PURE__ */ new Map();
1210
+ const contentRoot = getContentRoot();
1211
+ const themeDirs = [join4(contentRoot, "themes"), join4(contentRoot, "core", "themes")];
1212
+ for (const dir of themeDirs) {
1213
+ try {
1214
+ if (existsSync4(dir)) {
1215
+ for (const f of readdirSync2(dir).filter((f2) => f2.endsWith(".json"))) {
1216
+ const data = JSON.parse(readFileSync4(join4(dir, f), "utf-8"));
1217
+ if (data.id && !themeRegistry.has(data.id)) {
1218
+ themeRegistry.set(data.id, { modes: data.modes || ["light", "dark"] });
1219
+ }
1220
+ }
1221
+ }
1222
+ } catch {
1223
+ }
1224
+ }
1225
+ const customThemesDir = join4(process.cwd(), ".decantr", "custom", "themes");
1226
+ try {
1227
+ if (existsSync4(customThemesDir)) {
1228
+ for (const f of readdirSync2(customThemesDir).filter((f2) => f2.endsWith(".json"))) {
1229
+ const data = JSON.parse(readFileSync4(join4(customThemesDir, f), "utf-8"));
1230
+ if (data.id) {
1231
+ themeRegistry.set(`custom:${data.id}`, { modes: data.modes || ["light", "dark"] });
1232
+ }
1233
+ }
1234
+ }
1235
+ } catch {
1236
+ }
1237
+ const patternDirs = [join4(contentRoot, "patterns"), join4(contentRoot, "core", "patterns")];
1238
+ for (const dir of patternDirs) {
1239
+ try {
1240
+ if (existsSync4(dir)) {
1241
+ for (const f of readdirSync2(dir).filter((f2) => f2.endsWith(".json"))) {
1242
+ const data = JSON.parse(readFileSync4(join4(dir, f), "utf-8"));
1243
+ if (data.id && !patternRegistry.has(data.id)) {
1244
+ patternRegistry.set(data.id, data);
1245
+ }
1246
+ }
1247
+ }
1248
+ } catch {
1249
+ }
1250
+ }
1251
+ return { themeRegistry, patternRegistry };
1252
+ }
933
1253
  async function cmdValidate(path) {
934
1254
  const essencePath = path || join4(process.cwd(), "decantr.essence.json");
935
1255
  let raw;
@@ -959,12 +1279,16 @@ async function cmdValidate(path) {
959
1279
  process.exitCode = 1;
960
1280
  }
961
1281
  try {
962
- const violations = evaluateGuard(essence, {});
1282
+ const { themeRegistry, patternRegistry } = buildRegistryContext();
1283
+ const violations = evaluateGuard(essence, { themeRegistry, patternRegistry });
963
1284
  if (violations.length > 0) {
964
1285
  console.log(heading("Guard violations:"));
965
1286
  for (const v of violations) {
966
1287
  const vr = v;
967
1288
  console.log(` ${YELLOW2}[${vr.rule}]${RESET2} ${vr.message}`);
1289
+ if (vr.suggestion) {
1290
+ console.log(` ${DIM2}Suggestion: ${vr.suggestion}${RESET2}`);
1291
+ }
968
1292
  }
969
1293
  } else if (result.valid) {
970
1294
  console.log(success("No guard violations."));
@@ -973,42 +1297,116 @@ async function cmdValidate(path) {
973
1297
  }
974
1298
  }
975
1299
  async function cmdList(type) {
976
- const validTypes = ["patterns", "archetypes", "recipes", "themes", "blueprints"];
1300
+ const validTypes = ["patterns", "archetypes", "recipes", "themes", "blueprints", "shells"];
977
1301
  if (!validTypes.includes(type)) {
978
1302
  console.error(error(`Invalid type "${type}". Must be one of: ${validTypes.join(", ")}`));
979
1303
  process.exitCode = 1;
980
1304
  return;
981
1305
  }
982
- const { readdirSync: readdirSync2 } = await import("fs");
983
- const dir = join4(getContentRoot(), type);
984
- let found = false;
1306
+ if (type === "shells") {
1307
+ const registryClient = new RegistryClient({
1308
+ cacheDir: join4(process.cwd(), ".decantr", "cache")
1309
+ });
1310
+ const shellsResult = await registryClient.fetchShells();
1311
+ for (const item of shellsResult.data.items) {
1312
+ console.log(` ${item.id}${item.description ? ` \u2014 ${item.description}` : ""}`);
1313
+ }
1314
+ console.log(`
1315
+ ${shellsResult.data.total} shells found`);
1316
+ return;
1317
+ }
1318
+ const { readdirSync: readdirSync2, existsSync: existsSync5 } = await import("fs");
1319
+ const contentRoot = getContentRoot();
1320
+ const mainDir = join4(contentRoot, type);
1321
+ const coreDir = join4(contentRoot, "core", type);
1322
+ const items = [];
1323
+ try {
1324
+ if (existsSync5(mainDir)) {
1325
+ const files = readdirSync2(mainDir).filter((f) => f.endsWith(".json"));
1326
+ for (const f of files) {
1327
+ const data = JSON.parse(readFileSync4(join4(mainDir, f), "utf-8"));
1328
+ items.push({ id: data.id || f.replace(".json", ""), description: data.description, name: data.name });
1329
+ }
1330
+ }
1331
+ } catch {
1332
+ }
985
1333
  try {
986
- const files = readdirSync2(dir).filter((f) => f.endsWith(".json"));
987
- if (files.length > 0) {
988
- found = true;
989
- console.log(heading(`${files.length} ${type}`));
1334
+ if (existsSync5(coreDir)) {
1335
+ const files = readdirSync2(coreDir).filter((f) => f.endsWith(".json"));
1336
+ const existingIds = new Set(items.map((i) => i.id));
990
1337
  for (const f of files) {
991
- const data = JSON.parse(readFileSync4(join4(dir, f), "utf-8"));
992
- console.log(` ${cyan(data.id || f.replace(".json", ""))} ${dim(data.description || data.name || "")}`);
1338
+ const data = JSON.parse(readFileSync4(join4(coreDir, f), "utf-8"));
1339
+ const itemId = data.id || f.replace(".json", "");
1340
+ if (!existingIds.has(itemId)) {
1341
+ items.push({ id: itemId, description: data.description, name: data.name });
1342
+ }
993
1343
  }
994
1344
  }
995
1345
  } catch {
996
1346
  }
997
- if (!found) {
1347
+ const customItems = [];
1348
+ if (type === "themes") {
998
1349
  try {
999
- const res = await fetch(`https://decantr-registry.fly.dev/v1/${type}`);
1000
- if (res.ok) {
1001
- const data = await res.json();
1350
+ const custom = listCustomThemes(process.cwd());
1351
+ for (const theme of custom) {
1352
+ customItems.push({
1353
+ id: `custom:${theme.id}`,
1354
+ description: theme.description,
1355
+ name: theme.name,
1356
+ source: "custom"
1357
+ });
1358
+ }
1359
+ } catch {
1360
+ }
1361
+ }
1362
+ if (items.length > 0 || customItems.length > 0) {
1363
+ if (type === "themes") {
1364
+ console.log(heading(`Registry themes (${items.length}):`));
1365
+ for (const item of items) {
1366
+ console.log(` ${cyan(item.id)} ${dim(item.description || item.name || "")}`);
1367
+ }
1368
+ if (customItems.length > 0) {
1369
+ console.log("");
1370
+ console.log(heading(`Custom themes (${customItems.length}):`));
1371
+ for (const item of customItems) {
1372
+ console.log(` ${cyan(item.id)} ${dim(item.description || item.name || "")}`);
1373
+ }
1374
+ } else {
1375
+ console.log("");
1376
+ console.log(dim("Custom themes (0):"));
1377
+ console.log(dim(' Run "decantr theme create <name>" to create a custom theme.'));
1378
+ }
1379
+ } else {
1380
+ console.log(heading(`${items.length} ${type}`));
1381
+ for (const item of items) {
1382
+ console.log(` ${cyan(item.id)} ${dim(item.description || item.name || "")}`);
1383
+ }
1384
+ }
1385
+ return;
1386
+ }
1387
+ try {
1388
+ const res = await fetch(`https://decantr-registry.fly.dev/v1/${type}`);
1389
+ if (res.ok) {
1390
+ const data = await res.json();
1391
+ if (type === "themes") {
1392
+ console.log(heading(`Registry themes (${data.total}):`));
1393
+ for (const item of data.items) {
1394
+ console.log(` ${cyan(item.id)} ${dim(item.description || item.name || "")}`);
1395
+ }
1396
+ console.log("");
1397
+ console.log(dim("Custom themes (0):"));
1398
+ console.log(dim(' Run "decantr theme create <name>" to create a custom theme.'));
1399
+ } else {
1002
1400
  console.log(heading(`${data.total} ${type}`));
1003
1401
  for (const item of data.items) {
1004
1402
  console.log(` ${cyan(item.id)} ${dim(item.description || item.name || "")}`);
1005
1403
  }
1006
- return;
1007
1404
  }
1008
- } catch {
1405
+ return;
1009
1406
  }
1010
- console.log(dim(`No ${type} found.`));
1407
+ } catch {
1011
1408
  }
1409
+ console.log(dim(`No ${type} found.`));
1012
1410
  }
1013
1411
  async function cmdInit(args) {
1014
1412
  const projectRoot = process.cwd();
@@ -1027,31 +1425,73 @@ async function cmdInit(args) {
1027
1425
  apiUrl: args.registry,
1028
1426
  offline: args.offline
1029
1427
  });
1030
- console.log(dim("Fetching registry content..."));
1428
+ const apiAvailable = await registryClient.checkApiAvailability();
1429
+ let selectedBlueprint = "default";
1430
+ let registrySource = "bundled";
1431
+ if (args.yes) {
1432
+ selectedBlueprint = args.blueprint || "default";
1433
+ } else if (!apiAvailable) {
1434
+ console.log(`
1435
+ ${YELLOW2}You're offline. Scaffolding Decantr default.${RESET2}`);
1436
+ console.log(dim("Run `decantr upgrade` when online, or visit decantr.ai/registry\n"));
1437
+ selectedBlueprint = "default";
1438
+ } else {
1439
+ console.log(dim("Fetching registry content..."));
1440
+ const blueprintsResult2 = await registryClient.fetchBlueprints();
1441
+ registrySource = blueprintsResult2.source.type === "api" ? "api" : "bundled";
1442
+ const { selectedBlueprint: selected } = await runSimplifiedInit(
1443
+ blueprintsResult2.data.items
1444
+ );
1445
+ selectedBlueprint = selected || "default";
1446
+ }
1031
1447
  const [archetypesResult, blueprintsResult, themesResult] = await Promise.all([
1032
1448
  registryClient.fetchArchetypes(),
1033
1449
  registryClient.fetchBlueprints(),
1034
1450
  registryClient.fetchThemes()
1035
1451
  ]);
1036
- const registrySource = archetypesResult.source.type;
1037
- if (registrySource === "bundled") {
1038
- console.log(dim("Using bundled content (API unavailable)"));
1452
+ if (archetypesResult.source.type === "api") {
1453
+ registrySource = "api";
1039
1454
  }
1040
1455
  const archetypes = archetypesResult.data.items;
1041
1456
  const blueprints = blueprintsResult.data.items;
1042
1457
  const themes = themesResult.data.items;
1043
1458
  let options;
1044
- if (args.yes) {
1459
+ if (args.yes || selectedBlueprint !== "default") {
1045
1460
  const flags = parseFlags(args, detected);
1461
+ flags.blueprint = selectedBlueprint !== "default" ? selectedBlueprint : flags.blueprint;
1046
1462
  options = mergeWithDefaults(flags, detected);
1047
1463
  } else {
1048
1464
  options = await runInteractivePrompts(detected, archetypes, blueprints, themes);
1049
1465
  }
1050
- let blueprintData;
1466
+ let archetypeData;
1051
1467
  if (options.blueprint) {
1052
- const result2 = await registryClient.fetchBlueprint(options.blueprint);
1053
- if (result2) {
1054
- blueprintData = result2.data;
1468
+ const blueprintResult = await registryClient.fetchBlueprint(options.blueprint);
1469
+ if (blueprintResult) {
1470
+ const blueprint = blueprintResult.data;
1471
+ const primaryArchetype = blueprint.compose?.[0];
1472
+ if (primaryArchetype) {
1473
+ const archetypeResult = await registryClient.fetchArchetype(primaryArchetype);
1474
+ if (archetypeResult) {
1475
+ archetypeData = archetypeResult.data;
1476
+ options.archetype = primaryArchetype;
1477
+ }
1478
+ }
1479
+ }
1480
+ } else if (options.archetype) {
1481
+ const archetypeResult = await registryClient.fetchArchetype(options.archetype);
1482
+ if (archetypeResult) {
1483
+ archetypeData = archetypeResult.data;
1484
+ }
1485
+ }
1486
+ let themeData;
1487
+ let recipeData;
1488
+ if (options.theme) {
1489
+ const themeResult = await registryClient.fetchTheme(options.theme);
1490
+ if (themeResult) {
1491
+ const theme = themeResult.data;
1492
+ if (theme.seed) {
1493
+ themeData = { seed: theme.seed };
1494
+ }
1055
1495
  }
1056
1496
  }
1057
1497
  console.log(heading("Scaffolding project..."));
@@ -1059,31 +1499,52 @@ async function cmdInit(args) {
1059
1499
  projectRoot,
1060
1500
  options,
1061
1501
  detected,
1062
- blueprintData,
1063
- registrySource
1502
+ archetypeData,
1503
+ registrySource,
1504
+ themeData,
1505
+ recipeData
1064
1506
  );
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`);
1507
+ console.log(success("\nProject scaffolded!\n"));
1508
+ console.log(" Files created:");
1509
+ console.log(` ${cyan("decantr.essence.json")} Design specification`);
1510
+ console.log(` ${cyan("DECANTR.md")} LLM instructions`);
1511
+ console.log(` ${cyan(".decantr/")} Project state & cache`);
1071
1512
  if (result.gitignoreUpdated) {
1072
- console.log(` ${dim(".gitignore updated to exclude .decantr/cache/")}`);
1513
+ console.log(` ${dim(".gitignore updated")}`);
1073
1514
  }
1515
+ console.log("");
1516
+ console.log(" Next steps:");
1517
+ console.log(" 1. Review DECANTR.md for methodology");
1518
+ console.log(" 2. Explore more at decantr.ai/registry");
1519
+ console.log("");
1520
+ console.log(" Commands:");
1521
+ console.log(` ${cyan("decantr status")} Project health`);
1522
+ console.log(` ${cyan("decantr search")} Search registry`);
1523
+ console.log(` ${cyan("decantr get")} Fetch content details`);
1524
+ console.log(` ${cyan("decantr validate")} Check essence file`);
1525
+ console.log(` ${cyan("decantr upgrade")} Update to latest patterns`);
1526
+ console.log(` ${cyan("decantr heal")} Fix drift issues`);
1074
1527
  const essenceContent = readFileSync4(result.essencePath, "utf-8");
1075
1528
  const essence = JSON.parse(essenceContent);
1076
1529
  const validation = validateEssence(essence);
1077
- if (validation.valid) {
1078
- console.log(success("\nValidation passed."));
1079
- } else {
1530
+ if (!validation.valid) {
1080
1531
  console.log(error(`
1081
1532
  Validation warnings: ${validation.errors.join(", ")}`));
1082
1533
  }
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.");
1534
+ console.log("");
1535
+ const promptCtx = {
1536
+ archetype: options.archetype || "custom",
1537
+ blueprint: options.blueprint,
1538
+ theme: options.theme,
1539
+ mode: options.mode,
1540
+ target: options.target,
1541
+ pages: essence.structure || [{ id: "home", shell: options.shell, layout: ["hero"] }],
1542
+ personality: options.personality,
1543
+ features: options.features,
1544
+ guard: options.guard
1545
+ };
1546
+ const curatedPrompt = generateCuratedPrompt(promptCtx);
1547
+ console.log(boxedPrompt(curatedPrompt, "Copy this prompt for your AI assistant"));
1087
1548
  console.log("");
1088
1549
  if (registrySource === "bundled") {
1089
1550
  console.log(dim('Run "decantr sync" when online to get the latest registry content.'));
@@ -1173,13 +1634,17 @@ async function cmdAudit() {
1173
1634
  return;
1174
1635
  }
1175
1636
  console.log(success("Essence is valid."));
1176
- const violations = evaluateGuard(essence, {});
1637
+ const { themeRegistry, patternRegistry } = buildRegistryContext();
1638
+ const violations = evaluateGuard(essence, { themeRegistry, patternRegistry });
1177
1639
  if (violations.length > 0) {
1178
1640
  console.log("");
1179
1641
  console.log(`${YELLOW2}Guard violations:${RESET2}`);
1180
1642
  for (const v of violations) {
1181
1643
  const vr = v;
1182
1644
  console.log(` ${YELLOW2}[${vr.rule}]${RESET2} ${vr.message}`);
1645
+ if (vr.suggestion) {
1646
+ console.log(` ${DIM2}Suggestion: ${vr.suggestion}${RESET2}`);
1647
+ }
1183
1648
  }
1184
1649
  } else {
1185
1650
  console.log(success("No guard violations."));
@@ -1194,6 +1659,135 @@ async function cmdAudit() {
1194
1659
  process.exitCode = 1;
1195
1660
  }
1196
1661
  }
1662
+ async function cmdTheme(args) {
1663
+ const subcommand = args[0];
1664
+ const projectRoot = process.cwd();
1665
+ if (!subcommand || subcommand === "help") {
1666
+ console.log(`
1667
+ ${BOLD2}decantr theme${RESET2} \u2014 Manage custom themes
1668
+
1669
+ ${BOLD2}Commands:${RESET2}
1670
+ ${cyan("create")} <name> Create a new custom theme
1671
+ ${cyan("create")} <name> --guided Interactive theme creation
1672
+ ${cyan("list")} List custom themes
1673
+ ${cyan("validate")} <name> Validate a custom theme
1674
+ ${cyan("delete")} <name> Delete a custom theme
1675
+ ${cyan("import")} <path> Import theme from JSON file
1676
+
1677
+ ${BOLD2}Examples:${RESET2}
1678
+ decantr theme create mytheme
1679
+ decantr theme list
1680
+ decantr theme validate mytheme
1681
+ decantr theme import ./external-theme.json
1682
+ `);
1683
+ return;
1684
+ }
1685
+ switch (subcommand) {
1686
+ case "create": {
1687
+ const name = args[1];
1688
+ if (!name) {
1689
+ console.error(error("Usage: decantr theme create <name>"));
1690
+ process.exitCode = 1;
1691
+ return;
1692
+ }
1693
+ const displayName = name.charAt(0).toUpperCase() + name.slice(1).replace(/-/g, " ");
1694
+ const result = createTheme(projectRoot, name, displayName);
1695
+ if (result.success) {
1696
+ console.log(success(`Created custom theme "${name}"`));
1697
+ console.log(dim(` Path: ${result.path}`));
1698
+ console.log("");
1699
+ console.log(`Use in essence: ${cyan(`"style": "custom:${name}"`)}`);
1700
+ } else {
1701
+ console.error(error(result.error || "Failed to create theme"));
1702
+ process.exitCode = 1;
1703
+ }
1704
+ break;
1705
+ }
1706
+ case "list": {
1707
+ const themes = listCustomThemes(projectRoot);
1708
+ if (themes.length === 0) {
1709
+ console.log(dim("No custom themes found."));
1710
+ console.log(dim('Run "decantr theme create <name>" to create one.'));
1711
+ } else {
1712
+ console.log(heading(`${themes.length} custom theme(s)`));
1713
+ for (const theme of themes) {
1714
+ console.log(` ${cyan(`custom:${theme.id}`)} ${dim(theme.description || theme.name)}`);
1715
+ }
1716
+ }
1717
+ break;
1718
+ }
1719
+ case "validate": {
1720
+ const name = args[1];
1721
+ if (!name) {
1722
+ console.error(error("Usage: decantr theme validate <name>"));
1723
+ process.exitCode = 1;
1724
+ return;
1725
+ }
1726
+ const themePath = join4(projectRoot, ".decantr", "custom", "themes", `${name}.json`);
1727
+ if (!existsSync4(themePath)) {
1728
+ console.error(error(`Theme "${name}" not found at ${themePath}`));
1729
+ process.exitCode = 1;
1730
+ return;
1731
+ }
1732
+ try {
1733
+ const theme = JSON.parse(readFileSync4(themePath, "utf-8"));
1734
+ const result = validateCustomTheme(theme);
1735
+ if (result.valid) {
1736
+ console.log(success(`Custom theme "${name}" is valid`));
1737
+ } else {
1738
+ console.error(error("Validation failed:"));
1739
+ for (const err of result.errors) {
1740
+ console.error(` ${RED}${err}${RESET2}`);
1741
+ }
1742
+ process.exitCode = 1;
1743
+ }
1744
+ } catch (e) {
1745
+ console.error(error(`Invalid JSON: ${e.message}`));
1746
+ process.exitCode = 1;
1747
+ }
1748
+ break;
1749
+ }
1750
+ case "delete": {
1751
+ const name = args[1];
1752
+ if (!name) {
1753
+ console.error(error("Usage: decantr theme delete <name>"));
1754
+ process.exitCode = 1;
1755
+ return;
1756
+ }
1757
+ const result = deleteTheme(projectRoot, name);
1758
+ if (result.success) {
1759
+ console.log(success(`Deleted custom theme "${name}"`));
1760
+ } else {
1761
+ console.error(error(result.error || "Failed to delete theme"));
1762
+ process.exitCode = 1;
1763
+ }
1764
+ break;
1765
+ }
1766
+ case "import": {
1767
+ const sourcePath = args[1];
1768
+ if (!sourcePath) {
1769
+ console.error(error("Usage: decantr theme import <path>"));
1770
+ process.exitCode = 1;
1771
+ return;
1772
+ }
1773
+ const result = importTheme(projectRoot, sourcePath);
1774
+ if (result.success) {
1775
+ console.log(success("Theme imported successfully"));
1776
+ console.log(dim(` Path: ${result.path}`));
1777
+ } else {
1778
+ console.error(error("Import failed:"));
1779
+ for (const err of result.errors || []) {
1780
+ console.error(` ${RED}${err}${RESET2}`);
1781
+ }
1782
+ process.exitCode = 1;
1783
+ }
1784
+ break;
1785
+ }
1786
+ default:
1787
+ console.error(error(`Unknown theme command: ${subcommand}`));
1788
+ process.exitCode = 1;
1789
+ }
1790
+ }
1197
1791
  function cmdHelp() {
1198
1792
  console.log(`
1199
1793
  ${BOLD2}decantr${RESET2} \u2014 Design intelligence for AI-generated UI
@@ -1204,9 +1798,11 @@ ${BOLD2}Usage:${RESET2}
1204
1798
  decantr sync
1205
1799
  decantr audit
1206
1800
  decantr search <query> [--type <type>]
1801
+ decantr suggest <query> [--type <type>]
1207
1802
  decantr get <type> <id>
1208
1803
  decantr list <type>
1209
1804
  decantr validate [path]
1805
+ decantr theme <subcommand>
1210
1806
  decantr help
1211
1807
 
1212
1808
  ${BOLD2}Init Options:${RESET2}
@@ -1229,9 +1825,11 @@ ${BOLD2}Commands:${RESET2}
1229
1825
  ${cyan("sync")} Sync registry content from API
1230
1826
  ${cyan("audit")} Validate essence and check for drift
1231
1827
  ${cyan("search")} Search the registry
1828
+ ${cyan("suggest")} Suggest patterns or alternatives for a query
1232
1829
  ${cyan("get")} Get full details of a registry item
1233
1830
  ${cyan("list")} List items by type
1234
1831
  ${cyan("validate")} Validate essence file
1832
+ ${cyan("theme")} Manage custom themes (create, list, validate, delete, import)
1235
1833
  ${cyan("help")} Show this help
1236
1834
 
1237
1835
  ${BOLD2}Examples:${RESET2}
@@ -1241,6 +1839,8 @@ ${BOLD2}Examples:${RESET2}
1241
1839
  decantr sync
1242
1840
  decantr audit
1243
1841
  decantr search dashboard
1842
+ decantr suggest leaderboard
1843
+ decantr suggest ranking --type pattern
1244
1844
  decantr list patterns
1245
1845
  `);
1246
1846
  }
@@ -1286,6 +1886,16 @@ async function main() {
1286
1886
  await cmdSync();
1287
1887
  break;
1288
1888
  }
1889
+ case "upgrade": {
1890
+ const { cmdUpgrade } = await import("./upgrade-FWICWIQW.js");
1891
+ await cmdUpgrade(process.cwd());
1892
+ break;
1893
+ }
1894
+ case "heal": {
1895
+ const { cmdHeal } = await import("./heal-2OPN63OT.js");
1896
+ await cmdHeal(process.cwd());
1897
+ break;
1898
+ }
1289
1899
  case "audit": {
1290
1900
  await cmdAudit();
1291
1901
  break;
@@ -1302,6 +1912,18 @@ async function main() {
1302
1912
  await cmdSearch(query, type);
1303
1913
  break;
1304
1914
  }
1915
+ case "suggest": {
1916
+ const query = args[1];
1917
+ if (!query) {
1918
+ console.error(error("Usage: decantr suggest <query> [--type <type>]"));
1919
+ process.exitCode = 1;
1920
+ return;
1921
+ }
1922
+ const typeIdx = args.indexOf("--type");
1923
+ const type = typeIdx !== -1 ? args[typeIdx + 1] : void 0;
1924
+ await cmdSuggest(query, type);
1925
+ break;
1926
+ }
1305
1927
  case "get": {
1306
1928
  const type = args[1];
1307
1929
  const id = args[2];
@@ -1327,6 +1949,10 @@ async function main() {
1327
1949
  await cmdValidate(args[1]);
1328
1950
  break;
1329
1951
  }
1952
+ case "theme": {
1953
+ await cmdTheme(args.slice(1));
1954
+ break;
1955
+ }
1330
1956
  default:
1331
1957
  console.error(error(`Unknown command: ${command}`));
1332
1958
  cmdHelp();