@decantr/cli 1.0.0-beta.8 → 1.1.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,10 +1,18 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ RegistryClient,
4
+ syncRegistry
5
+ } from "./chunk-K6MIDPQH.js";
6
+ import {
7
+ __require
8
+ } from "./chunk-PDX44BCA.js";
2
9
 
3
10
  // src/index.ts
4
- import { readFileSync as readFileSync4, existsSync as existsSync4 } from "fs";
5
- import { join as join4 } from "path";
11
+ import { readFileSync as readFileSync6, existsSync as existsSync7 } from "fs";
12
+ import { join as join7, dirname as dirname2 } from "path";
13
+ import { fileURLToPath as fileURLToPath2 } from "url";
6
14
  import { validateEssence, evaluateGuard } from "@decantr/essence-spec";
7
- import { createResolver, createRegistryClient } from "@decantr/registry";
15
+ import { RegistryAPIClient as RegistryAPIClient2 } from "@decantr/registry";
8
16
 
9
17
  // src/detect.ts
10
18
  import { existsSync, readFileSync } from "fs";
@@ -324,6 +332,38 @@ function mergeWithDefaults(flags, detected) {
324
332
  existing: flags.existing || detected.existingEssence
325
333
  };
326
334
  }
335
+ async function runSimplifiedInit(blueprints) {
336
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
337
+ const question = (q) => new Promise((resolve) => rl.question(q, resolve));
338
+ console.log("\n? What blueprint would you like to scaffold?\n");
339
+ console.log(" 1. Decantr default (recommended)");
340
+ console.log(" 2. Search registry...\n");
341
+ const choice = await question("Enter choice (1 or 2): ");
342
+ if (choice === "1" || choice === "") {
343
+ rl.close();
344
+ return { choice: "default" };
345
+ }
346
+ const searchQuery = await question("Search: ");
347
+ const matches = blueprints.filter(
348
+ (b) => b.id.toLowerCase().includes(searchQuery.toLowerCase()) || b.name?.toLowerCase().includes(searchQuery.toLowerCase()) || b.description?.toLowerCase().includes(searchQuery.toLowerCase())
349
+ ).slice(0, 10);
350
+ if (matches.length === 0) {
351
+ console.log("\nNo matches found. Using Decantr default.");
352
+ rl.close();
353
+ return { choice: "default" };
354
+ }
355
+ console.log("\nResults:");
356
+ matches.forEach((b, i) => {
357
+ console.log(` ${i + 1}. ${b.id} \u2014 ${b.description || b.name || ""}`);
358
+ });
359
+ const selection = await question("\nSelect (number): ");
360
+ const idx = parseInt(selection, 10) - 1;
361
+ rl.close();
362
+ if (idx >= 0 && idx < matches.length) {
363
+ return { choice: "search", selectedBlueprint: matches[idx].id };
364
+ }
365
+ return { choice: "default" };
366
+ }
327
367
 
328
368
  // src/scaffold.ts
329
369
  import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync, appendFileSync } from "fs";
@@ -331,6 +371,182 @@ import { join as join2, dirname } from "path";
331
371
  import { fileURLToPath } from "url";
332
372
  var __dirname = dirname(fileURLToPath(import.meta.url));
333
373
  var CLI_VERSION = "1.0.0";
374
+ function generateTokensCSS(themeData, mode) {
375
+ if (!themeData) {
376
+ return `/* No theme data available */
377
+ :root {
378
+ --d-primary: #6366f1;
379
+ --d-secondary: #a1a1aa;
380
+ --d-accent: #f59e0b;
381
+ --d-bg: #18181b;
382
+ --d-surface: #1f1f23;
383
+ --d-surface-raised: #27272a;
384
+ --d-border: #3f3f46;
385
+ --d-text: #fafafa;
386
+ --d-text-muted: #a1a1aa;
387
+ }
388
+ `;
389
+ }
390
+ const seed = themeData.seed || {};
391
+ const palette = themeData.palette || {};
392
+ const tokens = {
393
+ // Seed colors
394
+ "--d-primary": seed.primary || "#6366f1",
395
+ "--d-secondary": seed.secondary || "#a1a1aa",
396
+ "--d-accent": seed.accent || "#f59e0b",
397
+ // Palette colors (mode-aware)
398
+ "--d-bg": palette.background?.[mode] || "#18181b",
399
+ "--d-surface": palette.surface?.[mode] || "#1f1f23",
400
+ "--d-surface-raised": palette["surface-raised"]?.[mode] || "#27272a",
401
+ "--d-border": palette.border?.[mode] || "#3f3f46",
402
+ "--d-text": palette.text?.[mode] || "#fafafa",
403
+ "--d-text-muted": palette["text-muted"]?.[mode] || "#a1a1aa",
404
+ "--d-primary-hover": palette["primary-hover"]?.[mode] || seed.primary || "#6366f1",
405
+ // Spacing scale
406
+ "--d-gap-1": "0.25rem",
407
+ "--d-gap-2": "0.5rem",
408
+ "--d-gap-3": "0.75rem",
409
+ "--d-gap-4": "1rem",
410
+ "--d-gap-6": "1.5rem",
411
+ "--d-gap-8": "2rem",
412
+ "--d-gap-12": "3rem",
413
+ // Radii
414
+ "--d-radius": "0.5rem",
415
+ "--d-radius-sm": "0.25rem",
416
+ "--d-radius-lg": "0.75rem",
417
+ "--d-radius-xl": "1rem",
418
+ "--d-radius-full": "9999px",
419
+ // Shadows
420
+ "--d-shadow-sm": "0 1px 2px rgba(0,0,0,0.05)",
421
+ "--d-shadow": "0 1px 3px rgba(0,0,0,0.1)",
422
+ "--d-shadow-md": "0 4px 6px rgba(0,0,0,0.1)",
423
+ "--d-shadow-lg": "0 10px 15px rgba(0,0,0,0.1)",
424
+ // Status colors
425
+ "--d-success": themeData.tokens?.base?.success || "#22c55e",
426
+ "--d-error": themeData.tokens?.base?.danger || "#ef4444",
427
+ "--d-warning": themeData.tokens?.base?.warning || "#f59e0b",
428
+ "--d-info": "#3b82f6"
429
+ };
430
+ const lines = Object.entries(tokens).map(([key, value]) => ` ${key}: ${value};`).join("\n");
431
+ return `/* Generated by @decantr/cli */
432
+ :root {
433
+ ${lines}
434
+ }
435
+ `;
436
+ }
437
+ function generateDecoratorsCSS(recipeData, themeName) {
438
+ if (!recipeData?.decorators) {
439
+ return `/* No recipe decorators available */`;
440
+ }
441
+ const decorators = recipeData.decorators;
442
+ const css = [
443
+ `/* Generated by @decantr/cli from recipe: ${themeName} */`,
444
+ ""
445
+ ];
446
+ for (const [name, description] of Object.entries(decorators)) {
447
+ css.push(generateDecoratorRule(name, description));
448
+ css.push("");
449
+ }
450
+ css.push(`/* Animation keyframes */
451
+ @keyframes decantr-fade-in {
452
+ from { opacity: 0; transform: translateY(8px); }
453
+ to { opacity: 1; transform: translateY(0); }
454
+ }
455
+
456
+ @keyframes decantr-pulse {
457
+ 0%, 100% { opacity: 1; }
458
+ 50% { opacity: 0.5; }
459
+ }
460
+ `);
461
+ return css.join("\n");
462
+ }
463
+ function generateDecoratorRule(name, description) {
464
+ const rules = [];
465
+ const descLower = description.toLowerCase();
466
+ if (descLower.includes("surface background") || descLower.includes("surface elevation")) {
467
+ rules.push("background: var(--d-surface)");
468
+ } else if (descLower.includes("background") && descLower.includes("theme")) {
469
+ rules.push("background: var(--d-bg)");
470
+ } else if (descLower.includes("primary-tinted") || descLower.includes("primary background")) {
471
+ rules.push("background: color-mix(in srgb, var(--d-primary) 15%, var(--d-surface))");
472
+ }
473
+ if (descLower.includes("1px border") || descLower.includes("subtle border")) {
474
+ rules.push("border: 1px solid var(--d-border)");
475
+ } else if (descLower.includes("border") && !descLower.includes("radius")) {
476
+ rules.push("border: 1px solid var(--d-border)");
477
+ }
478
+ const radiusMatch = descLower.match(/(\d+)px radius/);
479
+ if (radiusMatch) {
480
+ rules.push(`border-radius: ${radiusMatch[1]}px`);
481
+ } else if (descLower.includes("radius") || descLower.includes("rounded")) {
482
+ rules.push("border-radius: var(--d-radius)");
483
+ }
484
+ if (descLower.includes("hover shadow") || descLower.includes("shadow transition")) {
485
+ rules.push("transition: box-shadow 0.15s ease");
486
+ }
487
+ if (descLower.includes("elevation") || descLower.includes("shadow")) {
488
+ rules.push("box-shadow: var(--d-shadow)");
489
+ }
490
+ if (descLower.includes("entrance animation") || descLower.includes("fade")) {
491
+ rules.push("animation: decantr-fade-in 0.2s ease-out");
492
+ }
493
+ if (descLower.includes("pulse animation") || descLower.includes("skeleton")) {
494
+ rules.push("animation: decantr-pulse 1.5s ease-in-out infinite");
495
+ }
496
+ if (descLower.includes("blur") || descLower.includes("glass")) {
497
+ rules.push("backdrop-filter: blur(8px)");
498
+ }
499
+ if (descLower.includes("right-aligned")) {
500
+ rules.push("margin-left: auto");
501
+ } else if (descLower.includes("left-aligned")) {
502
+ rules.push("margin-right: auto");
503
+ }
504
+ if (descLower.includes("message bubble") || descLower.includes("bubble")) {
505
+ rules.push("padding: var(--d-gap-3) var(--d-gap-4)");
506
+ rules.push("border-radius: var(--d-radius-lg)");
507
+ rules.push("max-width: 80%");
508
+ }
509
+ if (rules.length === 0) {
510
+ return `/* .${name}: ${description} */`;
511
+ }
512
+ return `.${name} {
513
+ ${rules.join(";\n ")};
514
+ }`;
515
+ }
516
+ function serializeLayoutItem(item) {
517
+ if (typeof item === "string") {
518
+ return item;
519
+ }
520
+ if (typeof item === "object" && item !== null) {
521
+ const obj = item;
522
+ if (typeof obj.pattern === "string") {
523
+ const preset = obj.preset ? ` (${obj.preset})` : "";
524
+ const alias = obj.as ? ` as ${obj.as}` : "";
525
+ return `${obj.pattern}${preset}${alias}`;
526
+ }
527
+ if (Array.isArray(obj.cols)) {
528
+ const cols = obj.cols.map(serializeLayoutItem).join(" | ");
529
+ const breakpoint = obj.at ? ` @${obj.at}` : "";
530
+ return `[${cols}]${breakpoint}`;
531
+ }
532
+ }
533
+ return "custom";
534
+ }
535
+ function extractPatternNames(item) {
536
+ if (typeof item === "string") {
537
+ return [item];
538
+ }
539
+ if (typeof item === "object" && item !== null) {
540
+ const obj = item;
541
+ if (typeof obj.pattern === "string") {
542
+ return [obj.pattern];
543
+ }
544
+ if (Array.isArray(obj.cols)) {
545
+ return obj.cols.flatMap(extractPatternNames);
546
+ }
547
+ }
548
+ return [];
549
+ }
334
550
  function loadTemplate(name) {
335
551
  const fromDist = join2(__dirname, "..", "src", "templates", name);
336
552
  if (existsSync2(fromDist)) {
@@ -349,30 +565,56 @@ function renderTemplate(template, vars) {
349
565
  }
350
566
  return result;
351
567
  }
352
- function buildEssence(options, blueprint) {
568
+ function resolvePatternAlias(item, patterns) {
569
+ if (!patterns) return item;
570
+ if (typeof item === "string") {
571
+ const patternDef = patterns.find((p) => p.as === item);
572
+ if (patternDef) {
573
+ if (patternDef.preset) {
574
+ return { pattern: patternDef.pattern, preset: patternDef.preset };
575
+ }
576
+ return patternDef.pattern;
577
+ }
578
+ return item;
579
+ }
580
+ if (typeof item === "object" && item !== null) {
581
+ const obj = item;
582
+ if (Array.isArray(obj.cols)) {
583
+ return {
584
+ ...obj,
585
+ cols: obj.cols.map((col) => resolvePatternAlias(col, patterns))
586
+ };
587
+ }
588
+ }
589
+ return item;
590
+ }
591
+ function buildEssence(options, archetypeData) {
353
592
  let structure = [
354
- { id: "home", shell: options.shell, layout: [] }
593
+ { id: "home", shell: options.shell, layout: ["hero"] }
355
594
  ];
356
595
  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
- }));
596
+ if (archetypeData?.pages) {
597
+ structure = archetypeData.pages.map((p) => {
598
+ const resolvedLayout = (p.default_layout?.length ? p.default_layout : ["hero"]).map((item) => resolvePatternAlias(item, p.patterns));
599
+ return {
600
+ id: p.id,
601
+ shell: p.shell || options.shell,
602
+ layout: resolvedLayout
603
+ };
604
+ });
363
605
  }
364
- if (blueprint?.features) {
365
- features = [.../* @__PURE__ */ new Set([...features, ...blueprint.features])];
606
+ if (archetypeData?.features) {
607
+ features = [.../* @__PURE__ */ new Set([...features, ...archetypeData.features])];
366
608
  }
367
609
  const contentGapMap = {
368
610
  compact: "_gap2",
369
611
  comfortable: "_gap4",
370
612
  spacious: "_gap6"
371
613
  };
372
- return {
614
+ const archetype = options.archetype || "custom";
615
+ const essence = {
373
616
  version: "2.0.0",
374
- archetype: options.archetype,
375
- blueprint: options.blueprint,
617
+ archetype,
376
618
  theme: {
377
619
  style: options.theme,
378
620
  mode: options.mode,
@@ -398,14 +640,105 @@ function buildEssence(options, blueprint) {
398
640
  },
399
641
  target: options.target
400
642
  };
643
+ if (options.accessibility) {
644
+ essence.accessibility = options.accessibility;
645
+ }
646
+ return essence;
647
+ }
648
+ function generateAccessibilitySection(essence, themeData) {
649
+ const accessibility = essence.accessibility;
650
+ if (!accessibility?.wcag_level || accessibility.wcag_level === "none") {
651
+ return "";
652
+ }
653
+ const wcagLevel = accessibility.wcag_level;
654
+ const cvdPreference = accessibility.cvd_preference || "none";
655
+ const cvdSupport = themeData?.cvd_support || [];
656
+ let section = `---
657
+
658
+ ## Accessibility
659
+
660
+ **WCAG Level:** ${wcagLevel}
661
+ `;
662
+ if (cvdSupport.length > 0) {
663
+ section += `**CVD Support:** Theme supports ${cvdSupport.join(", ")}
664
+ **CVD Preference:** ${cvdPreference}
665
+ `;
666
+ }
667
+ section += `
668
+ ### What This Means
669
+
670
+ This project requires WCAG 2.1 Level ${wcagLevel} compliance. You already know these rules \u2014 apply them:
671
+
672
+ - Semantic HTML structure
673
+ - Sufficient color contrast (4.5:1 for normal text, 3:1 for large text)
674
+ - Keyboard navigability for all interactive elements
675
+ - Visible focus indicators
676
+ - Meaningful alt text for images
677
+ - Proper heading hierarchy
678
+ `;
679
+ if (cvdSupport.length > 0) {
680
+ section += `
681
+ ### CVD Implementation
682
+
683
+ The theme provides these data attributes:
684
+
685
+ \`\`\`html
686
+ <html data-theme="${essence.theme.style}" data-mode="${essence.theme.mode}" data-cvd="none">
687
+ \`\`\`
688
+
689
+ Valid \`data-cvd\` values for this theme: \`none\`, ${cvdSupport.map((m) => `\`${m}\``).join(", ")}
690
+ `;
691
+ if (cvdPreference === "auto") {
692
+ section += `
693
+ Detect user preference via \`prefers-contrast\` or user settings and apply accordingly.
694
+ `;
695
+ }
696
+ }
697
+ section += `
698
+ ---
699
+ `;
700
+ return section;
401
701
  }
402
- function generateDecantrMd(essence, detected) {
702
+ function generateSeoSection(essence, archetypeData) {
703
+ const seoHints = archetypeData?.seo_hints;
704
+ if (!seoHints) {
705
+ return "";
706
+ }
707
+ const schemaOrg = seoHints.schema_org || [];
708
+ const metaPriorities = seoHints.meta_priorities || [];
709
+ if (schemaOrg.length === 0 && metaPriorities.length === 0) {
710
+ return "";
711
+ }
712
+ let section = `---
713
+
714
+ ## SEO Guidance
715
+
716
+ This archetype (\`${essence.archetype}\`) typically benefits from:
717
+
718
+ `;
719
+ if (schemaOrg.length > 0) {
720
+ section += `- **Schema.org:** ${schemaOrg.join(", ")}
721
+ `;
722
+ }
723
+ if (metaPriorities.length > 0) {
724
+ section += `- **Meta priorities:** ${metaPriorities.join(", ")}
725
+ `;
726
+ }
727
+ section += `
728
+ These are suggestions, not requirements. Apply where appropriate for the page content.
729
+
730
+ ---
731
+ `;
732
+ return section;
733
+ }
734
+ function generateDecantrMd(essence, detected, themeData, recipeData, archetypeData) {
403
735
  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";
736
+ const pagesTable = essence.structure.map((p) => {
737
+ const layoutStr = p.layout.map(serializeLayoutItem).join(", ") || "none";
738
+ return `| ${p.id} | ${p.shell} | ${layoutStr} |`;
739
+ }).join("\n");
740
+ const allPatternNames = [...new Set(essence.structure.flatMap((p) => p.layout.flatMap(extractPatternNames)))];
741
+ const patternsList = allPatternNames.length > 0 ? allPatternNames.map((p) => `- \`${p}\``).join("\n") : "- No patterns specified yet";
409
742
  const projectSummary = [
410
743
  `**Archetype:** ${essence.archetype || "custom"}`,
411
744
  `**Target:** ${essence.target}`,
@@ -422,6 +755,29 @@ function generateDecantrMd(essence, detected) {
422
755
  };
423
756
  const defaultShell = essence.structure[0]?.shell || "sidebar-main";
424
757
  const shellStructure = shellStructures[defaultShell] || "Custom shell layout";
758
+ let themeQuickRef = "";
759
+ if (themeData?.seed) {
760
+ const colors = Object.entries(themeData.seed).map(([name, hex]) => `- **${name}:** \`${hex}\``).join("\n");
761
+ themeQuickRef = `**Seed Colors:**
762
+ ${colors}`;
763
+ }
764
+ if (recipeData?.decorators) {
765
+ const decorators = Object.entries(recipeData.decorators).slice(0, 5).map(([name, desc]) => `- \`${name}\` \u2014 ${desc}`).join("\n");
766
+ if (themeQuickRef) {
767
+ themeQuickRef += `
768
+
769
+ **Key Decorators:**
770
+ ${decorators}`;
771
+ } else {
772
+ themeQuickRef = `**Key Decorators:**
773
+ ${decorators}`;
774
+ }
775
+ }
776
+ if (!themeQuickRef) {
777
+ themeQuickRef = `See \`decantr get theme ${essence.theme.style}\` for details.`;
778
+ }
779
+ const accessibilitySection = generateAccessibilitySection(essence, themeData);
780
+ const seoSection = generateSeoSection(essence, archetypeData);
425
781
  const vars = {
426
782
  GUARD_MODE: essence.guard.mode,
427
783
  PROJECT_SUMMARY: projectSummary,
@@ -440,7 +796,10 @@ ${pagesTable}`,
440
796
  AVAILABLE_PATTERNS: "(See registry or .decantr/cache/patterns/)",
441
797
  AVAILABLE_THEMES: "(See registry or .decantr/cache/themes/)",
442
798
  AVAILABLE_SHELLS: "sidebar-main, top-nav-main, centered, full-bleed, minimal-header",
443
- VERSION: CLI_VERSION
799
+ VERSION: CLI_VERSION,
800
+ THEME_QUICK_REFERENCE: themeQuickRef,
801
+ ACCESSIBILITY_SECTION: accessibilitySection,
802
+ SEO_SECTION: seoSection
444
803
  };
445
804
  return renderTemplate(template, vars);
446
805
  }
@@ -489,10 +848,10 @@ function buildFlagsString(options) {
489
848
  function generateTaskContext(templateName, essence) {
490
849
  const template = loadTemplate(templateName);
491
850
  const defaultShell = essence.structure[0]?.shell || "sidebar-main";
492
- const layout = essence.structure[0]?.layout.join(", ") || "none";
851
+ const layout = essence.structure[0]?.layout.map(serializeLayoutItem).join(", ") || "none";
493
852
  const scaffoldStructure = essence.structure.map((p) => {
494
853
  const patterns = p.layout.length > 0 ? `
495
- - Patterns: ${p.layout.join(", ")}` : "";
854
+ - Patterns: ${p.layout.map(serializeLayoutItem).join(", ")}` : "";
496
855
  return `- **${p.id}** (${p.shell})${patterns}`;
497
856
  }).join("\n");
498
857
  const vars = {
@@ -513,7 +872,7 @@ function generateEssenceSummary(essence) {
513
872
  const template = loadTemplate("essence-summary.md.template");
514
873
  const pagesTable = `| Page | Shell | Layout |
515
874
  |------|-------|--------|
516
- ${essence.structure.map((p) => `| ${p.id} | ${p.shell} | ${p.layout.join(", ") || "none"} |`).join("\n")}`;
875
+ ${essence.structure.map((p) => `| ${p.id} | ${p.shell} | ${p.layout.map(serializeLayoutItem).join(", ") || "none"} |`).join("\n")}`;
517
876
  const featuresList = essence.features.length > 0 ? essence.features.map((f) => `- ${f}`).join("\n") : "- No features specified";
518
877
  const vars = {
519
878
  ARCHETYPE: essence.archetype || "custom",
@@ -555,8 +914,8 @@ ${cacheEntry}
555
914
  return true;
556
915
  }
557
916
  }
558
- function scaffoldProject(projectRoot, options, detected, blueprint, registrySource = "bundled") {
559
- const essence = buildEssence(options, blueprint);
917
+ function scaffoldProject(projectRoot, options, detected, archetypeData, registrySource = "cache", themeData, recipeData) {
918
+ const essence = buildEssence(options, archetypeData);
560
919
  const decantrDir = join2(projectRoot, ".decantr");
561
920
  const contextDir = join2(decantrDir, "context");
562
921
  const cacheDir = join2(decantrDir, "cache");
@@ -565,7 +924,7 @@ function scaffoldProject(projectRoot, options, detected, blueprint, registrySour
565
924
  const essencePath = join2(projectRoot, "decantr.essence.json");
566
925
  writeFileSync(essencePath, JSON.stringify(essence, null, 2) + "\n");
567
926
  const decantrMdPath = join2(projectRoot, "DECANTR.md");
568
- writeFileSync(decantrMdPath, generateDecantrMd(essence, detected));
927
+ writeFileSync(decantrMdPath, generateDecantrMd(essence, detected, themeData, recipeData, archetypeData));
569
928
  const projectJsonPath = join2(decantrDir, "project.json");
570
929
  writeFileSync(projectJsonPath, generateProjectJson(detected, options, registrySource));
571
930
  const contextFiles = [];
@@ -581,281 +940,520 @@ function scaffoldProject(projectRoot, options, detected, blueprint, registrySour
581
940
  const summaryPath = join2(contextDir, "essence-summary.md");
582
941
  writeFileSync(summaryPath, generateEssenceSummary(essence));
583
942
  contextFiles.push(summaryPath);
943
+ const stylesDir = join2(projectRoot, "src", "styles");
944
+ mkdirSync(stylesDir, { recursive: true });
945
+ const tokensPath = join2(stylesDir, "tokens.css");
946
+ writeFileSync(tokensPath, generateTokensCSS(themeData, essence.theme.mode));
947
+ const decoratorsPath = join2(stylesDir, "decorators.css");
948
+ writeFileSync(decoratorsPath, generateDecoratorsCSS(recipeData, essence.theme.style));
584
949
  const gitignoreUpdated = updateGitignore(projectRoot);
585
950
  return {
586
951
  essencePath,
587
952
  decantrMdPath,
588
953
  projectJsonPath,
589
954
  contextFiles,
955
+ cssFiles: [tokensPath, decoratorsPath],
590
956
  gitignoreUpdated
591
957
  };
592
958
  }
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);
959
+ function scaffoldMinimal(projectRoot) {
960
+ const decantrDir = join2(projectRoot, ".decantr");
961
+ const customDir = join2(decantrDir, "custom");
962
+ const contentTypes = ["patterns", "recipes", "themes", "blueprints", "archetypes", "shells"];
963
+ for (const type of contentTypes) {
964
+ mkdirSync(join2(customDir, type), { recursive: true });
615
965
  }
966
+ const essence = {
967
+ version: "2.0.0",
968
+ archetype: "custom",
969
+ theme: {
970
+ style: "default",
971
+ mode: "dark",
972
+ recipe: "default",
973
+ shape: "rounded"
974
+ },
975
+ personality: ["clean", "modern"],
976
+ platform: {
977
+ type: "spa",
978
+ routing: "hash"
979
+ },
980
+ structure: [
981
+ { id: "home", shell: "sidebar-main", layout: ["hero"] }
982
+ ],
983
+ features: [],
984
+ guard: {
985
+ enforce_style: true,
986
+ enforce_recipe: true,
987
+ mode: "guided"
988
+ },
989
+ density: {
990
+ level: "comfortable",
991
+ content_gap: "_gap4"
992
+ },
993
+ target: "react"
994
+ };
995
+ const essencePath = join2(projectRoot, "decantr.essence.json");
996
+ writeFileSync(essencePath, JSON.stringify(essence, null, 2) + "\n");
997
+ const now = (/* @__PURE__ */ new Date()).toISOString();
998
+ const projectJson = {
999
+ detected: {
1000
+ framework: "unknown",
1001
+ version: null,
1002
+ packageManager: "npm",
1003
+ hasTypeScript: false,
1004
+ hasTailwind: false,
1005
+ existingRuleFiles: []
1006
+ },
1007
+ overrides: {
1008
+ framework: null
1009
+ },
1010
+ sync: {
1011
+ status: "needs-sync",
1012
+ lastSync: now,
1013
+ registrySource: "cache",
1014
+ cachedContent: {
1015
+ archetypes: [],
1016
+ patterns: [],
1017
+ themes: [],
1018
+ recipes: []
1019
+ }
1020
+ },
1021
+ initialized: {
1022
+ at: now,
1023
+ via: "cli",
1024
+ version: CLI_VERSION,
1025
+ flags: "--offline --minimal"
1026
+ }
1027
+ };
1028
+ const projectJsonPath = join2(decantrDir, "project.json");
1029
+ writeFileSync(projectJsonPath, JSON.stringify(projectJson, null, 2));
1030
+ const decantrMdPath = join2(projectRoot, "DECANTR.md");
1031
+ const decantrMdContent = `# DECANTR.md
1032
+
1033
+ > This file was generated by \`decantr init\` in offline/minimal mode.
1034
+ > Run \`decantr upgrade\` when online to pull full registry content.
1035
+
1036
+ ## Guard Mode: guided
1037
+
1038
+ ## Project Summary
1039
+
1040
+ **Archetype:** custom
1041
+ **Target:** react
1042
+ **Theme:** default (dark mode)
1043
+ **Guard Mode:** guided
1044
+ **Pages:** home
1045
+
1046
+ ## Pages
1047
+
1048
+ | Page | Shell | Layout |
1049
+ |------|-------|--------|
1050
+ | home | sidebar-main | hero |
1051
+
1052
+ ## Quick Start
1053
+
1054
+ 1. Edit \`decantr.essence.json\` to define your project structure.
1055
+ 2. Run \`decantr sync\` when online to fetch registry content.
1056
+ 3. Use \`decantr create <type> <name>\` to create custom content.
1057
+ 4. Use \`decantr search <query>\` to find patterns and themes.
1058
+ 5. Use \`decantr validate\` to check your essence file.
1059
+
1060
+ ## Commands
1061
+
1062
+ - \`decantr status\` \u2014 Project health
1063
+ - \`decantr search\` \u2014 Search the registry
1064
+ - \`decantr get <type> <id>\` \u2014 Fetch content details
1065
+ - \`decantr validate\` \u2014 Validate essence file
1066
+ - \`decantr sync\` \u2014 Sync registry content
1067
+ - \`decantr create <type> <name>\` \u2014 Create custom content
1068
+ - \`decantr publish <type> <name>\` \u2014 Publish to community registry
1069
+
1070
+ ---
1071
+
1072
+ *Generated by @decantr/cli v${CLI_VERSION}*
1073
+ `;
1074
+ writeFileSync(decantrMdPath, decantrMdContent);
1075
+ const gitignoreUpdated = updateGitignore(projectRoot);
1076
+ return {
1077
+ essencePath,
1078
+ decantrMdPath,
1079
+ projectJsonPath,
1080
+ contextFiles: [],
1081
+ cssFiles: [],
1082
+ gitignoreUpdated
1083
+ };
616
1084
  }
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
- }
1085
+
1086
+ // src/theme-commands.ts
1087
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2, readdirSync, rmSync } from "fs";
1088
+ import { join as join3 } from "path";
1089
+
1090
+ // src/theme-templates.ts
1091
+ function getThemeSkeleton(id, name) {
1092
+ return {
1093
+ $schema: "https://decantr.ai/schemas/style-metadata.v1.json",
1094
+ id,
1095
+ name,
1096
+ description: "",
1097
+ tags: [],
1098
+ seed: {
1099
+ primary: "#6366F1",
1100
+ secondary: "#8B5CF6",
1101
+ accent: "#EC4899",
1102
+ background: "#0F172A"
1103
+ },
1104
+ palette: {},
1105
+ modes: ["dark"],
1106
+ shapes: ["rounded"],
1107
+ decantr_compat: ">=1.0.0",
1108
+ source: "custom"
1109
+ };
630
1110
  }
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;
1111
+ function getHowToThemeDoc() {
1112
+ return `# Custom Themes
1113
+
1114
+ Create custom themes for your Decantr project.
1115
+
1116
+ ## Quick Start
1117
+
1118
+ \`\`\`bash
1119
+ decantr theme create mytheme
1120
+ \`\`\`
1121
+
1122
+ ## Theme Structure
1123
+
1124
+ | Field | Required | Description |
1125
+ |-------|----------|-------------|
1126
+ | id | Yes | Unique identifier (matches filename) |
1127
+ | name | Yes | Display name |
1128
+ | description | No | Brief description |
1129
+ | tags | No | Searchable tags |
1130
+ | seed | Yes | Core colors: primary, secondary, accent, background |
1131
+ | palette | No | Extended color palette |
1132
+ | modes | Yes | Supported modes: ["light"], ["dark"], or both |
1133
+ | shapes | Yes | Supported shapes: sharp, rounded, pill |
1134
+ | decantr_compat | Yes | Version compatibility (e.g., ">=1.0.0") |
1135
+ | source | Yes | Must be "custom" |
1136
+
1137
+ ## Using Your Theme
1138
+
1139
+ In \`decantr.essence.json\`:
1140
+
1141
+ \`\`\`json
1142
+ {
1143
+ "theme": {
1144
+ "style": "custom:mytheme",
1145
+ "mode": "dark"
642
1146
  }
643
1147
  }
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;
1148
+ \`\`\`
1149
+
1150
+ ## Validation
1151
+
1152
+ \`\`\`bash
1153
+ decantr theme validate mytheme
1154
+ \`\`\`
1155
+
1156
+ ## Reference
1157
+
1158
+ See registry themes for examples:
1159
+
1160
+ \`\`\`bash
1161
+ decantr get theme auradecantism
1162
+ \`\`\`
1163
+ `;
1164
+ }
1165
+
1166
+ // src/theme-commands.ts
1167
+ var REQUIRED_FIELDS = ["id", "name", "seed", "modes", "shapes", "decantr_compat", "source"];
1168
+ var REQUIRED_SEED = ["primary", "secondary", "accent", "background"];
1169
+ var VALID_MODES = ["light", "dark"];
1170
+ var VALID_SHAPES = ["sharp", "rounded", "pill"];
1171
+ function validateCustomTheme(theme) {
1172
+ const errors = [];
1173
+ for (const field of REQUIRED_FIELDS) {
1174
+ if (!(field in theme)) {
1175
+ errors.push(`Missing required field: ${field}`);
657
1176
  }
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;
1177
+ }
1178
+ if (theme.seed && typeof theme.seed === "object") {
1179
+ const seed = theme.seed;
1180
+ for (const color of REQUIRED_SEED) {
1181
+ if (!(color in seed)) {
1182
+ errors.push(`Missing seed color: ${color}`);
700
1183
  }
701
1184
  }
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
1185
  }
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;
1186
+ if (Array.isArray(theme.modes)) {
1187
+ for (const mode of theme.modes) {
1188
+ if (!VALID_MODES.includes(mode)) {
1189
+ errors.push(`Invalid mode "${mode}" - must be "light" or "dark"`);
723
1190
  }
724
1191
  }
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;
1192
+ }
1193
+ if (Array.isArray(theme.shapes)) {
1194
+ for (const shape of theme.shapes) {
1195
+ if (!VALID_SHAPES.includes(shape)) {
1196
+ errors.push(`Invalid shape "${shape}" - use: sharp, rounded, pill`);
738
1197
  }
739
1198
  }
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;
1199
+ }
1200
+ return {
1201
+ valid: errors.length === 0,
1202
+ errors
1203
+ };
1204
+ }
1205
+ function createTheme(projectRoot, id, name) {
1206
+ const customThemesDir = join3(projectRoot, ".decantr", "custom", "themes");
1207
+ const themePath = join3(customThemesDir, `${id}.json`);
1208
+ const howToPath = join3(customThemesDir, "how-to-theme.md");
1209
+ mkdirSync2(customThemesDir, { recursive: true });
1210
+ if (existsSync3(themePath)) {
747
1211
  return {
748
- data: { items: [], total: 0 },
749
- source: { type: "bundled" }
1212
+ success: false,
1213
+ error: `Theme "${id}" already exists at ${themePath}`
750
1214
  };
751
1215
  }
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;
1216
+ const skeleton = getThemeSkeleton(id, name);
1217
+ writeFileSync2(themePath, JSON.stringify(skeleton, null, 2));
1218
+ if (!existsSync3(howToPath)) {
1219
+ writeFileSync2(howToPath, getHowToThemeDoc());
1220
+ }
1221
+ return {
1222
+ success: true,
1223
+ path: themePath
1224
+ };
1225
+ }
1226
+ function listCustomThemes(projectRoot) {
1227
+ const customThemesDir = join3(projectRoot, ".decantr", "custom", "themes");
1228
+ if (!existsSync3(customThemesDir)) {
1229
+ return [];
1230
+ }
1231
+ const themes = [];
1232
+ try {
1233
+ const files = readdirSync(customThemesDir).filter((f) => f.endsWith(".json"));
1234
+ for (const file of files) {
1235
+ const filePath = join3(customThemesDir, file);
1236
+ try {
1237
+ const data = JSON.parse(readFileSync3(filePath, "utf-8"));
1238
+ themes.push({
1239
+ id: data.id || file.replace(".json", ""),
1240
+ name: data.name || data.id,
1241
+ description: data.description,
1242
+ path: filePath
1243
+ });
1244
+ } catch {
776
1245
  }
777
1246
  }
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;
1247
+ } catch {
1248
+ }
1249
+ return themes;
1250
+ }
1251
+ function deleteTheme(projectRoot, id) {
1252
+ const themePath = join3(projectRoot, ".decantr", "custom", "themes", `${id}.json`);
1253
+ if (!existsSync3(themePath)) {
785
1254
  return {
786
- data: { items: [], total: 0 },
787
- source: { type: "bundled" }
1255
+ success: false,
1256
+ error: `Theme "${id}" not found at ${themePath}`
788
1257
  };
789
1258
  }
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;
1259
+ try {
1260
+ rmSync(themePath);
1261
+ return { success: true };
1262
+ } catch (e) {
808
1263
  return {
809
- data: { items: [], total: 0 },
810
- source: { type: "bundled" }
1264
+ success: false,
1265
+ error: `Failed to delete: ${e.message}`
811
1266
  };
812
1267
  }
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
- }
1268
+ }
1269
+ function importTheme(projectRoot, sourcePath) {
1270
+ if (!existsSync3(sourcePath)) {
1271
+ return {
1272
+ success: false,
1273
+ errors: [`Source file not found: ${sourcePath}`]
1274
+ };
824
1275
  }
825
- /**
826
- * Get the source used for the last fetch.
827
- */
828
- getSourceType() {
829
- return this.offline ? "bundled" : "api";
1276
+ let theme;
1277
+ try {
1278
+ theme = JSON.parse(readFileSync3(sourcePath, "utf-8"));
1279
+ } catch (e) {
1280
+ return {
1281
+ success: false,
1282
+ errors: [`Invalid JSON: ${e.message}`]
1283
+ };
830
1284
  }
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
- }
1285
+ const validation = validateCustomTheme(theme);
1286
+ if (!validation.valid) {
1287
+ return {
1288
+ success: false,
1289
+ errors: validation.errors
1290
+ };
851
1291
  }
1292
+ theme.source = "custom";
1293
+ const id = theme.id;
1294
+ const customThemesDir = join3(projectRoot, ".decantr", "custom", "themes");
1295
+ const destPath = join3(customThemesDir, `${id}.json`);
1296
+ mkdirSync2(customThemesDir, { recursive: true });
1297
+ const howToPath = join3(customThemesDir, "how-to-theme.md");
1298
+ if (!existsSync3(howToPath)) {
1299
+ writeFileSync2(howToPath, getHowToThemeDoc());
1300
+ }
1301
+ writeFileSync2(destPath, JSON.stringify(theme, null, 2));
852
1302
  return {
853
- synced,
854
- failed,
855
- source: synced.length > 0 ? "api" : "bundled"
1303
+ success: true,
1304
+ path: destPath
856
1305
  };
857
1306
  }
858
1307
 
1308
+ // src/auth.ts
1309
+ import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync3, rmSync as rmSync2 } from "fs";
1310
+ import { join as join4 } from "path";
1311
+ import { homedir } from "os";
1312
+ var CONFIG_DIR = join4(homedir(), ".config", "decantr");
1313
+ var AUTH_FILE = join4(CONFIG_DIR, "auth.json");
1314
+ function getCredentials() {
1315
+ if (!existsSync4(AUTH_FILE)) return null;
1316
+ try {
1317
+ return JSON.parse(readFileSync4(AUTH_FILE, "utf-8"));
1318
+ } catch {
1319
+ return null;
1320
+ }
1321
+ }
1322
+ function saveCredentials(creds) {
1323
+ mkdirSync3(CONFIG_DIR, { recursive: true });
1324
+ writeFileSync3(AUTH_FILE, JSON.stringify(creds, null, 2));
1325
+ }
1326
+ function clearCredentials() {
1327
+ if (existsSync4(AUTH_FILE)) {
1328
+ rmSync2(AUTH_FILE);
1329
+ }
1330
+ }
1331
+ function getApiKeyOrToken() {
1332
+ const creds = getCredentials();
1333
+ if (!creds) return null;
1334
+ return creds.api_key || creds.access_token || null;
1335
+ }
1336
+
1337
+ // src/commands/publish.ts
1338
+ import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
1339
+ import { join as join5 } from "path";
1340
+ import { RegistryAPIClient } from "@decantr/registry";
1341
+ var PLURAL_TO_SINGULAR = {
1342
+ patterns: "pattern",
1343
+ recipes: "recipe",
1344
+ themes: "theme",
1345
+ blueprints: "blueprint",
1346
+ archetypes: "archetype",
1347
+ shells: "shell"
1348
+ };
1349
+ var SINGULAR_TO_PLURAL = {
1350
+ pattern: "patterns",
1351
+ recipe: "recipes",
1352
+ theme: "themes",
1353
+ blueprint: "blueprints",
1354
+ archetype: "archetypes",
1355
+ shell: "shells"
1356
+ };
1357
+ async function cmdPublish(type, name, projectRoot = process.cwd()) {
1358
+ const token = getApiKeyOrToken();
1359
+ if (!token) {
1360
+ console.error("Not authenticated. Run `decantr login` first.");
1361
+ process.exitCode = 1;
1362
+ return;
1363
+ }
1364
+ const singularType = PLURAL_TO_SINGULAR[type] || type;
1365
+ const pluralType = SINGULAR_TO_PLURAL[type] || SINGULAR_TO_PLURAL[singularType] || `${type}s`;
1366
+ const customPath = join5(projectRoot, ".decantr", "custom", pluralType, `${name}.json`);
1367
+ if (!existsSync5(customPath)) {
1368
+ console.error(`Custom ${singularType} "${name}" not found at ${customPath}`);
1369
+ console.error(`Create one first: decantr create ${singularType} ${name}`);
1370
+ process.exitCode = 1;
1371
+ return;
1372
+ }
1373
+ let data;
1374
+ try {
1375
+ data = JSON.parse(readFileSync5(customPath, "utf-8"));
1376
+ } catch {
1377
+ console.error(`Failed to parse ${customPath}`);
1378
+ process.exitCode = 1;
1379
+ return;
1380
+ }
1381
+ const client = new RegistryAPIClient({
1382
+ apiKey: token
1383
+ });
1384
+ try {
1385
+ const result = await client.publishContent({
1386
+ type: pluralType,
1387
+ slug: name,
1388
+ version: data.version || "1.0.0",
1389
+ data,
1390
+ namespace: "@community",
1391
+ visibility: "public"
1392
+ });
1393
+ console.log(`Published ${singularType}/${name} to @community`);
1394
+ console.log(`Status: ${result.status}`);
1395
+ } catch (err) {
1396
+ console.error(`Failed to publish: ${err.message}`);
1397
+ process.exitCode = 1;
1398
+ }
1399
+ }
1400
+
1401
+ // src/commands/create.ts
1402
+ import { existsSync as existsSync6, mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "fs";
1403
+ import { join as join6 } from "path";
1404
+ var CONTENT_TYPES = ["pattern", "recipe", "theme", "blueprint", "archetype", "shell"];
1405
+ var PLURAL = {
1406
+ pattern: "patterns",
1407
+ recipe: "recipes",
1408
+ theme: "themes",
1409
+ blueprint: "blueprints",
1410
+ archetype: "archetypes",
1411
+ shell: "shells"
1412
+ };
1413
+ function getSkeleton(type, id, name) {
1414
+ const base = {
1415
+ id,
1416
+ name,
1417
+ description: "",
1418
+ version: "1.0.0",
1419
+ source: "custom"
1420
+ };
1421
+ switch (type) {
1422
+ case "pattern":
1423
+ return { ...base, components: [], presets: {}, layout: {} };
1424
+ case "recipe":
1425
+ return { ...base, shell: {}, spatial: {}, effects: {} };
1426
+ case "theme":
1427
+ return { ...base, seed: { primary: "#6500C6", secondary: "#0AF3EB", accent: "#F58882", background: "#0D0D1A" }, modes: ["dark"], shapes: ["rounded"] };
1428
+ case "blueprint":
1429
+ return { ...base, compose: [], theme: {}, personality: [] };
1430
+ case "archetype":
1431
+ return { ...base, pages: [], features: [], suggested_theme: "" };
1432
+ case "shell":
1433
+ return { ...base, regions: [], layout: "sidebar-main" };
1434
+ }
1435
+ }
1436
+ function cmdCreate(type, name, projectRoot = process.cwd()) {
1437
+ if (!CONTENT_TYPES.includes(type)) {
1438
+ console.error(`Invalid type "${type}". Must be one of: ${CONTENT_TYPES.join(", ")}`);
1439
+ process.exitCode = 1;
1440
+ return;
1441
+ }
1442
+ const plural = PLURAL[type];
1443
+ const customDir = join6(projectRoot, ".decantr", "custom", plural);
1444
+ const filePath = join6(customDir, `${name}.json`);
1445
+ if (existsSync6(filePath)) {
1446
+ console.error(`${type} "${name}" already exists at ${filePath}`);
1447
+ process.exitCode = 1;
1448
+ return;
1449
+ }
1450
+ mkdirSync4(customDir, { recursive: true });
1451
+ const skeleton = getSkeleton(type, name, name.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()));
1452
+ writeFileSync4(filePath, JSON.stringify(skeleton, null, 2));
1453
+ console.log(`Created ${type} "${name}" at ${filePath}`);
1454
+ console.log(`Edit it, then publish with: decantr publish ${type} ${name}`);
1455
+ }
1456
+
859
1457
  // src/index.ts
860
1458
  var BOLD2 = "\x1B[1m";
861
1459
  var DIM2 = "\x1B[2m";
@@ -881,60 +1479,210 @@ function dim(text) {
881
1479
  function cyan(text) {
882
1480
  return `${CYAN2}${text}${RESET2}`;
883
1481
  }
884
- function getContentRoot() {
885
- const bundled = join4(import.meta.dirname, "..", "..", "..", "content");
886
- return process.env.DECANTR_CONTENT_ROOT || bundled;
1482
+ function extractPatternName(item) {
1483
+ if (typeof item === "string") return item;
1484
+ if (typeof item === "object" && item !== null) {
1485
+ const obj = item;
1486
+ if (typeof obj.pattern === "string") return obj.pattern;
1487
+ if (Array.isArray(obj.cols)) {
1488
+ return obj.cols.map(extractPatternName).join(" | ");
1489
+ }
1490
+ }
1491
+ return "custom";
887
1492
  }
888
- function getResolver() {
889
- return createResolver({ contentRoot: getContentRoot() });
1493
+ function generateCuratedPrompt(ctx) {
1494
+ const lines = [];
1495
+ lines.push(`I'm building a ${ctx.archetype} application using ${ctx.target}.`);
1496
+ lines.push("");
1497
+ if (ctx.blueprint) {
1498
+ lines.push(`Blueprint: ${ctx.blueprint}`);
1499
+ }
1500
+ lines.push(`Theme: ${ctx.theme} (${ctx.mode} mode)`);
1501
+ lines.push(`Personality: ${ctx.personality.join(", ")}`);
1502
+ lines.push(`Guard mode: ${ctx.guard}`);
1503
+ lines.push("");
1504
+ lines.push("Pages to build:");
1505
+ for (const page of ctx.pages) {
1506
+ const patternNames = page.layout.map(extractPatternName);
1507
+ const patterns = patternNames.length > 0 ? patternNames.join(", ") : "custom";
1508
+ lines.push(` - ${page.id}: ${page.shell} shell with ${patterns}`);
1509
+ }
1510
+ if (ctx.features.length > 0) {
1511
+ lines.push("");
1512
+ lines.push(`Features: ${ctx.features.join(", ")}`);
1513
+ }
1514
+ lines.push("");
1515
+ lines.push("Please read DECANTR.md for the full design spec and methodology.");
1516
+ lines.push("Follow the guard rules and use the patterns from decantr.essence.json.");
1517
+ return lines.join("\n");
1518
+ }
1519
+ function boxedPrompt(content, title) {
1520
+ const lines = content.split("\n");
1521
+ const maxLen = Math.max(...lines.map((l) => l.length), title.length + 4);
1522
+ const width = maxLen + 4;
1523
+ const top = `\u250C${"\u2500".repeat(width - 2)}\u2510`;
1524
+ const titleLine = `\u2502 ${BOLD2}${title}${RESET2}${" ".repeat(width - title.length - 4)} \u2502`;
1525
+ const sep = `\u251C${"\u2500".repeat(width - 2)}\u2524`;
1526
+ const bottom = `\u2514${"\u2500".repeat(width - 2)}\u2518`;
1527
+ const body = lines.map((line) => {
1528
+ const padding = " ".repeat(width - line.length - 4);
1529
+ return `\u2502 ${line}${padding} \u2502`;
1530
+ }).join("\n");
1531
+ return `${top}
1532
+ ${titleLine}
1533
+ ${sep}
1534
+ ${body}
1535
+ ${bottom}`;
1536
+ }
1537
+ function getAPIClient() {
1538
+ return new RegistryAPIClient2({
1539
+ baseUrl: process.env.DECANTR_API_URL || void 0,
1540
+ apiKey: process.env.DECANTR_API_KEY || void 0
1541
+ });
890
1542
  }
891
1543
  async function cmdSearch(query, type) {
892
- const client = createRegistryClient();
893
- const results = await client.search(query, type);
894
- if (results.length === 0) {
895
- console.log(dim(`No results for "${query}"`));
896
- return;
1544
+ const apiClient = getAPIClient();
1545
+ try {
1546
+ const response = await apiClient.search({ q: query, type });
1547
+ const results = response.results;
1548
+ if (results.length === 0) {
1549
+ console.log(dim(`No results for "${query}"`));
1550
+ return;
1551
+ }
1552
+ console.log(heading(`${results.length} result(s) for "${query}"`));
1553
+ for (const r of results) {
1554
+ console.log(` ${cyan(r.type.padEnd(12))} ${BOLD2}${r.slug}${RESET2}`);
1555
+ console.log(` ${dim(r.description || "")}`);
1556
+ console.log("");
1557
+ }
1558
+ } catch {
1559
+ console.log(dim(`Search failed. API may be unavailable.`));
897
1560
  }
898
- console.log(heading(`${results.length} result(s) for "${query}"`));
899
- for (const r of results) {
900
- console.log(` ${cyan(r.type.padEnd(12))} ${BOLD2}${r.id}${RESET2}`);
901
- console.log(` ${dim(r.description || "")}`);
902
- console.log("");
1561
+ }
1562
+ async function cmdSuggest(query, type) {
1563
+ const apiClient = getAPIClient();
1564
+ const searchType = type || "pattern";
1565
+ try {
1566
+ const response = await apiClient.search({ q: query, type: searchType });
1567
+ const results = response.results;
1568
+ if (results.length === 0) {
1569
+ console.log(dim(`No suggestions for "${query}"`));
1570
+ console.log("");
1571
+ console.log("Try:");
1572
+ console.log(` ${cyan("decantr list patterns")} - see all patterns`);
1573
+ console.log(` ${cyan("decantr search <broader-term>")} - broaden your search`);
1574
+ return;
1575
+ }
1576
+ console.log(heading(`Suggestions for "${query}"`));
1577
+ const queryLower = query.toLowerCase();
1578
+ const exact = results.filter((r) => r.slug.toLowerCase().includes(queryLower));
1579
+ const related = results.filter((r) => !r.slug.toLowerCase().includes(queryLower));
1580
+ if (exact.length > 0) {
1581
+ console.log(`${BOLD2}Direct matches:${RESET2}`);
1582
+ for (const r of exact.slice(0, 3)) {
1583
+ console.log(` ${cyan(r.slug)} - ${r.description || ""}`);
1584
+ }
1585
+ console.log("");
1586
+ }
1587
+ if (related.length > 0) {
1588
+ console.log(`${BOLD2}Related:${RESET2}`);
1589
+ for (const r of related.slice(0, 5)) {
1590
+ console.log(` ${cyan(r.slug)} - ${r.description || ""}`);
1591
+ }
1592
+ console.log("");
1593
+ }
1594
+ console.log(dim(`Use "decantr get pattern <id>" for full details`));
1595
+ } catch {
1596
+ console.log(dim(`Suggestion search failed. API may be unavailable.`));
903
1597
  }
904
1598
  }
905
1599
  async function cmdGet(type, id) {
906
- const validTypes = ["pattern", "archetype", "recipe", "theme", "blueprint"];
1600
+ const validTypes = ["pattern", "archetype", "recipe", "theme", "blueprint", "shell"];
907
1601
  if (!validTypes.includes(type)) {
908
1602
  console.error(error(`Invalid type "${type}". Must be one of: ${validTypes.join(", ")}`));
909
1603
  process.exitCode = 1;
910
1604
  return;
911
1605
  }
912
- const resolver = getResolver();
913
- let result = await resolver.resolve(type, id);
914
- if (!result) {
915
- const apiType = type === "blueprint" ? "blueprints" : `${type}s`;
916
- try {
917
- const res = await fetch(`https://decantr-registry.fly.dev/v1/${apiType}/${id}`);
918
- if (res.ok) {
919
- const item = await res.json();
920
- if (!item.error) {
921
- console.log(JSON.stringify(item, null, 2));
922
- return;
1606
+ const typeMap = {
1607
+ pattern: "patterns",
1608
+ archetype: "archetypes",
1609
+ recipe: "recipes",
1610
+ theme: "themes",
1611
+ blueprint: "blueprints",
1612
+ shell: "shells"
1613
+ };
1614
+ const apiType = typeMap[type];
1615
+ const registryClient = new RegistryClient({
1616
+ cacheDir: join7(process.cwd(), ".decantr", "cache")
1617
+ });
1618
+ const result = await registryClient.fetchContentItem(apiType, id);
1619
+ if (result) {
1620
+ console.log(JSON.stringify(result.data, null, 2));
1621
+ return;
1622
+ }
1623
+ const currentDir = dirname2(fileURLToPath2(import.meta.url));
1624
+ const bundledFromDist = join7(currentDir, "..", "src", "bundled", apiType, `${id}.json`);
1625
+ const bundledFromSrc = join7(currentDir, "bundled", apiType, `${id}.json`);
1626
+ const bundledPath = existsSync7(bundledFromDist) ? bundledFromDist : existsSync7(bundledFromSrc) ? bundledFromSrc : null;
1627
+ if (bundledPath) {
1628
+ const data = JSON.parse(readFileSync6(bundledPath, "utf-8"));
1629
+ console.log(JSON.stringify(data, null, 2));
1630
+ return;
1631
+ }
1632
+ console.error(error(`${type} "${id}" not found.`));
1633
+ process.exitCode = 1;
1634
+ return;
1635
+ }
1636
+ function buildRegistryContext() {
1637
+ const { readdirSync: readdirSync2 } = __require("fs");
1638
+ const themeRegistry = /* @__PURE__ */ new Map();
1639
+ const patternRegistry = /* @__PURE__ */ new Map();
1640
+ const projectRoot = process.cwd();
1641
+ const cacheDir = join7(projectRoot, ".decantr", "cache");
1642
+ const customDir = join7(projectRoot, ".decantr", "custom");
1643
+ const cachedThemesDir = join7(cacheDir, "@official", "themes");
1644
+ try {
1645
+ if (existsSync7(cachedThemesDir)) {
1646
+ for (const f of readdirSync2(cachedThemesDir).filter((f2) => f2.endsWith(".json") && f2 !== "index.json")) {
1647
+ const data = JSON.parse(readFileSync6(join7(cachedThemesDir, f), "utf-8"));
1648
+ if (data.id && !themeRegistry.has(data.id)) {
1649
+ themeRegistry.set(data.id, { modes: data.modes || ["light", "dark"] });
923
1650
  }
924
1651
  }
925
- } catch {
926
1652
  }
927
- console.error(error(`${type} "${id}" not found.`));
928
- process.exitCode = 1;
929
- return;
1653
+ } catch {
1654
+ }
1655
+ const customThemesDir = join7(customDir, "themes");
1656
+ try {
1657
+ if (existsSync7(customThemesDir)) {
1658
+ for (const f of readdirSync2(customThemesDir).filter((f2) => f2.endsWith(".json"))) {
1659
+ const data = JSON.parse(readFileSync6(join7(customThemesDir, f), "utf-8"));
1660
+ if (data.id) {
1661
+ themeRegistry.set(`custom:${data.id}`, { modes: data.modes || ["light", "dark"] });
1662
+ }
1663
+ }
1664
+ }
1665
+ } catch {
930
1666
  }
931
- console.log(JSON.stringify(result.item, null, 2));
1667
+ const cachedPatternsDir = join7(cacheDir, "@official", "patterns");
1668
+ try {
1669
+ if (existsSync7(cachedPatternsDir)) {
1670
+ for (const f of readdirSync2(cachedPatternsDir).filter((f2) => f2.endsWith(".json") && f2 !== "index.json")) {
1671
+ const data = JSON.parse(readFileSync6(join7(cachedPatternsDir, f), "utf-8"));
1672
+ if (data.id && !patternRegistry.has(data.id)) {
1673
+ patternRegistry.set(data.id, data);
1674
+ }
1675
+ }
1676
+ }
1677
+ } catch {
1678
+ }
1679
+ return { themeRegistry, patternRegistry };
932
1680
  }
933
1681
  async function cmdValidate(path) {
934
- const essencePath = path || join4(process.cwd(), "decantr.essence.json");
1682
+ const essencePath = path || join7(process.cwd(), "decantr.essence.json");
935
1683
  let raw;
936
1684
  try {
937
- raw = readFileSync4(essencePath, "utf-8");
1685
+ raw = readFileSync6(essencePath, "utf-8");
938
1686
  } catch {
939
1687
  console.error(error(`Could not read ${essencePath}`));
940
1688
  process.exitCode = 1;
@@ -959,12 +1707,16 @@ async function cmdValidate(path) {
959
1707
  process.exitCode = 1;
960
1708
  }
961
1709
  try {
962
- const violations = evaluateGuard(essence, {});
1710
+ const { themeRegistry, patternRegistry } = buildRegistryContext();
1711
+ const violations = evaluateGuard(essence, { themeRegistry, patternRegistry });
963
1712
  if (violations.length > 0) {
964
1713
  console.log(heading("Guard violations:"));
965
1714
  for (const v of violations) {
966
1715
  const vr = v;
967
1716
  console.log(` ${YELLOW2}[${vr.rule}]${RESET2} ${vr.message}`);
1717
+ if (vr.suggestion) {
1718
+ console.log(` ${DIM2}Suggestion: ${vr.suggestion}${RESET2}`);
1719
+ }
968
1720
  }
969
1721
  } else if (result.valid) {
970
1722
  console.log(success("No guard violations."));
@@ -973,41 +1725,45 @@ async function cmdValidate(path) {
973
1725
  }
974
1726
  }
975
1727
  async function cmdList(type) {
976
- const validTypes = ["patterns", "archetypes", "recipes", "themes", "blueprints"];
1728
+ const validTypes = ["patterns", "archetypes", "recipes", "themes", "blueprints", "shells"];
977
1729
  if (!validTypes.includes(type)) {
978
1730
  console.error(error(`Invalid type "${type}". Must be one of: ${validTypes.join(", ")}`));
979
1731
  process.exitCode = 1;
980
1732
  return;
981
1733
  }
982
- const { readdirSync: readdirSync2 } = await import("fs");
983
- const dir = join4(getContentRoot(), type);
984
- let found = false;
985
- 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}`));
990
- 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 || "")}`);
993
- }
994
- }
995
- } catch {
1734
+ const registryClient = new RegistryClient({
1735
+ cacheDir: join7(process.cwd(), ".decantr", "cache")
1736
+ });
1737
+ const result = await registryClient.fetchContentList(type);
1738
+ const items = result.data.items;
1739
+ if (items.length === 0) {
1740
+ console.log(dim(`No ${type} found.`));
1741
+ return;
996
1742
  }
997
- if (!found) {
998
- try {
999
- const res = await fetch(`https://decantr-registry.fly.dev/v1/${type}`);
1000
- if (res.ok) {
1001
- const data = await res.json();
1002
- console.log(heading(`${data.total} ${type}`));
1003
- for (const item of data.items) {
1004
- console.log(` ${cyan(item.id)} ${dim(item.description || item.name || "")}`);
1005
- }
1006
- return;
1743
+ if (type === "themes") {
1744
+ const customItems = registryClient.listCustomContent("themes");
1745
+ const customIds = new Set(customItems.map((c) => c.id));
1746
+ const registryItems = items.filter((i) => !customIds.has(i.id));
1747
+ console.log(heading(`Registry themes (${registryItems.length}):`));
1748
+ for (const item of registryItems) {
1749
+ console.log(` ${cyan(item.id)} ${dim(item.description || item.name || "")}`);
1750
+ }
1751
+ if (customItems.length > 0) {
1752
+ console.log("");
1753
+ console.log(heading(`Custom themes (${customItems.length}):`));
1754
+ for (const item of customItems) {
1755
+ console.log(` ${cyan(`custom:${item.id}`)} ${dim(item.description || item.name || "")}`);
1007
1756
  }
1008
- } catch {
1757
+ } else {
1758
+ console.log("");
1759
+ console.log(dim("Custom themes (0):"));
1760
+ console.log(dim(' Run "decantr theme create <name>" to create a custom theme.'));
1761
+ }
1762
+ } else {
1763
+ console.log(heading(`${items.length} ${type}`));
1764
+ for (const item of items) {
1765
+ console.log(` ${cyan(item.id)} ${dim(item.description || item.name || "")}`);
1009
1766
  }
1010
- console.log(dim(`No ${type} found.`));
1011
1767
  }
1012
1768
  }
1013
1769
  async function cmdInit(args) {
@@ -1023,35 +1779,113 @@ async function cmdInit(args) {
1023
1779
  }
1024
1780
  }
1025
1781
  const registryClient = new RegistryClient({
1026
- cacheDir: join4(projectRoot, ".decantr", "cache"),
1782
+ cacheDir: join7(projectRoot, ".decantr", "cache"),
1027
1783
  apiUrl: args.registry,
1028
1784
  offline: args.offline
1029
1785
  });
1030
- console.log(dim("Fetching registry content..."));
1786
+ const apiAvailable = await registryClient.checkApiAvailability();
1787
+ let selectedBlueprint = "default";
1788
+ let registrySource = "cache";
1789
+ if (args.yes) {
1790
+ selectedBlueprint = args.blueprint || "default";
1791
+ } else if (!apiAvailable) {
1792
+ if (!args.blueprint) {
1793
+ console.log(`
1794
+ ${YELLOW2}You're offline. Scaffolding minimal Decantr project.${RESET2}`);
1795
+ console.log(dim("Run `decantr sync` or `decantr upgrade` when online to pull full registry content.\n"));
1796
+ const result2 = scaffoldMinimal(projectRoot);
1797
+ console.log(success("\nProject scaffolded (minimal/offline)!\n"));
1798
+ console.log(" Files created:");
1799
+ console.log(` ${cyan("decantr.essence.json")} Design specification`);
1800
+ console.log(` ${cyan("DECANTR.md")} LLM instructions`);
1801
+ console.log(` ${cyan(".decantr/")} Project state & custom content dirs`);
1802
+ if (result2.gitignoreUpdated) {
1803
+ console.log(` ${dim(".gitignore updated")}`);
1804
+ }
1805
+ console.log("");
1806
+ console.log(" Next steps:");
1807
+ console.log(` 1. Run ${cyan("decantr sync")} when online`);
1808
+ console.log(` 2. Use ${cyan("decantr create <type> <name>")} to create custom content`);
1809
+ console.log(` 3. Review DECANTR.md for methodology`);
1810
+ return;
1811
+ }
1812
+ console.log(`
1813
+ ${YELLOW2}You're offline. Scaffolding Decantr default.${RESET2}`);
1814
+ console.log(dim("Run `decantr upgrade` when online, or visit decantr.ai/registry\n"));
1815
+ selectedBlueprint = "default";
1816
+ } else {
1817
+ console.log(dim("Fetching registry content..."));
1818
+ const blueprintsResult2 = await registryClient.fetchBlueprints();
1819
+ registrySource = blueprintsResult2.source.type === "api" ? "api" : "cache";
1820
+ const { selectedBlueprint: selected } = await runSimplifiedInit(
1821
+ blueprintsResult2.data.items
1822
+ );
1823
+ selectedBlueprint = selected || "default";
1824
+ }
1031
1825
  const [archetypesResult, blueprintsResult, themesResult] = await Promise.all([
1032
1826
  registryClient.fetchArchetypes(),
1033
1827
  registryClient.fetchBlueprints(),
1034
1828
  registryClient.fetchThemes()
1035
1829
  ]);
1036
- const registrySource = archetypesResult.source.type;
1037
- if (registrySource === "bundled") {
1038
- console.log(dim("Using bundled content (API unavailable)"));
1830
+ if (archetypesResult.source.type === "api") {
1831
+ registrySource = "api";
1039
1832
  }
1040
1833
  const archetypes = archetypesResult.data.items;
1041
1834
  const blueprints = blueprintsResult.data.items;
1042
1835
  const themes = themesResult.data.items;
1043
1836
  let options;
1044
- if (args.yes) {
1837
+ if (args.yes || selectedBlueprint !== "default") {
1045
1838
  const flags = parseFlags(args, detected);
1839
+ flags.blueprint = selectedBlueprint !== "default" ? selectedBlueprint : flags.blueprint;
1046
1840
  options = mergeWithDefaults(flags, detected);
1047
1841
  } else {
1048
1842
  options = await runInteractivePrompts(detected, archetypes, blueprints, themes);
1049
1843
  }
1050
- let blueprintData;
1844
+ let archetypeData;
1051
1845
  if (options.blueprint) {
1052
- const result2 = await registryClient.fetchBlueprint(options.blueprint);
1053
- if (result2) {
1054
- blueprintData = result2.data;
1846
+ const blueprintResult = await registryClient.fetchBlueprint(options.blueprint);
1847
+ if (blueprintResult) {
1848
+ const blueprint = blueprintResult.data;
1849
+ if (blueprint.theme) {
1850
+ if (blueprint.theme.style && options.theme === "luminarum") {
1851
+ options.theme = blueprint.theme.style;
1852
+ }
1853
+ if (blueprint.theme.mode && options.mode === "dark") {
1854
+ options.mode = blueprint.theme.mode;
1855
+ }
1856
+ if (blueprint.theme.shape && options.shape === "rounded") {
1857
+ options.shape = blueprint.theme.shape;
1858
+ }
1859
+ }
1860
+ const primaryArchetype = blueprint.compose?.[0];
1861
+ if (primaryArchetype) {
1862
+ const archetypeResult = await registryClient.fetchArchetype(primaryArchetype);
1863
+ if (archetypeResult) {
1864
+ archetypeData = archetypeResult.data;
1865
+ options.archetype = primaryArchetype;
1866
+ }
1867
+ }
1868
+ }
1869
+ } else if (options.archetype) {
1870
+ const archetypeResult = await registryClient.fetchArchetype(options.archetype);
1871
+ if (archetypeResult) {
1872
+ archetypeData = archetypeResult.data;
1873
+ }
1874
+ }
1875
+ let themeData;
1876
+ let recipeData;
1877
+ if (options.theme) {
1878
+ const themeResult = await registryClient.fetchTheme(options.theme);
1879
+ if (themeResult) {
1880
+ const theme = themeResult.data;
1881
+ themeData = {
1882
+ seed: theme.seed,
1883
+ palette: theme.palette,
1884
+ tokens: theme.tokens
1885
+ };
1886
+ if (theme.decorators) {
1887
+ recipeData = { decorators: theme.decorators };
1888
+ }
1055
1889
  }
1056
1890
  }
1057
1891
  console.log(heading("Scaffolding project..."));
@@ -1059,48 +1893,69 @@ async function cmdInit(args) {
1059
1893
  projectRoot,
1060
1894
  options,
1061
1895
  detected,
1062
- blueprintData,
1063
- registrySource
1896
+ archetypeData,
1897
+ registrySource,
1898
+ themeData,
1899
+ recipeData
1064
1900
  );
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`);
1901
+ console.log(success("\nProject scaffolded!\n"));
1902
+ console.log(" Files created:");
1903
+ console.log(` ${cyan("decantr.essence.json")} Design specification`);
1904
+ console.log(` ${cyan("DECANTR.md")} LLM instructions`);
1905
+ console.log(` ${cyan(".decantr/")} Project state & cache`);
1071
1906
  if (result.gitignoreUpdated) {
1072
- console.log(` ${dim(".gitignore updated to exclude .decantr/cache/")}`);
1907
+ console.log(` ${dim(".gitignore updated")}`);
1073
1908
  }
1074
- const essenceContent = readFileSync4(result.essencePath, "utf-8");
1909
+ console.log("");
1910
+ console.log(" Next steps:");
1911
+ console.log(" 1. Review DECANTR.md for methodology");
1912
+ console.log(" 2. Explore more at decantr.ai/registry");
1913
+ console.log("");
1914
+ console.log(" Commands:");
1915
+ console.log(` ${cyan("decantr status")} Project health`);
1916
+ console.log(` ${cyan("decantr search")} Search registry`);
1917
+ console.log(` ${cyan("decantr get")} Fetch content details`);
1918
+ console.log(` ${cyan("decantr validate")} Check essence file`);
1919
+ console.log(` ${cyan("decantr upgrade")} Update to latest patterns`);
1920
+ console.log(` ${cyan("decantr heal")} Fix drift issues`);
1921
+ const essenceContent = readFileSync6(result.essencePath, "utf-8");
1075
1922
  const essence = JSON.parse(essenceContent);
1076
1923
  const validation = validateEssence(essence);
1077
- if (validation.valid) {
1078
- console.log(success("\nValidation passed."));
1079
- } else {
1924
+ if (!validation.valid) {
1080
1925
  console.log(error(`
1081
1926
  Validation warnings: ${validation.errors.join(", ")}`));
1082
1927
  }
1083
- console.log(heading("Next steps"));
1084
- console.log("1. Review DECANTR.md to understand the methodology");
1085
- console.log("2. Share DECANTR.md with your AI assistant");
1086
- console.log("3. Start building! The AI will follow the essence spec.");
1087
1928
  console.log("");
1088
- if (registrySource === "bundled") {
1929
+ const promptCtx = {
1930
+ archetype: options.archetype || "custom",
1931
+ blueprint: options.blueprint,
1932
+ theme: options.theme,
1933
+ mode: options.mode,
1934
+ target: options.target,
1935
+ pages: essence.structure || [{ id: "home", shell: options.shell, layout: ["hero"] }],
1936
+ personality: options.personality,
1937
+ features: options.features,
1938
+ guard: options.guard
1939
+ };
1940
+ const curatedPrompt = generateCuratedPrompt(promptCtx);
1941
+ console.log(boxedPrompt(curatedPrompt, "Copy this prompt for your AI assistant"));
1942
+ console.log("");
1943
+ if (registrySource === "cache") {
1089
1944
  console.log(dim('Run "decantr sync" when online to get the latest registry content.'));
1090
1945
  }
1091
1946
  }
1092
1947
  async function cmdStatus() {
1093
1948
  const projectRoot = process.cwd();
1094
- const essencePath = join4(projectRoot, "decantr.essence.json");
1095
- const projectJsonPath = join4(projectRoot, ".decantr", "project.json");
1949
+ const essencePath = join7(projectRoot, "decantr.essence.json");
1950
+ const projectJsonPath = join7(projectRoot, ".decantr", "project.json");
1096
1951
  console.log(heading("Decantr Project Status"));
1097
- if (!existsSync4(essencePath)) {
1952
+ if (!existsSync7(essencePath)) {
1098
1953
  console.log(`${RED}No decantr.essence.json found.${RESET2}`);
1099
1954
  console.log(dim('Run "decantr init" to create one.'));
1100
1955
  return;
1101
1956
  }
1102
1957
  try {
1103
- const essence = JSON.parse(readFileSync4(essencePath, "utf-8"));
1958
+ const essence = JSON.parse(readFileSync6(essencePath, "utf-8"));
1104
1959
  const validation = validateEssence(essence);
1105
1960
  console.log(`${BOLD2}Essence:${RESET2}`);
1106
1961
  if (validation.valid) {
@@ -1116,9 +1971,9 @@ async function cmdStatus() {
1116
1971
  }
1117
1972
  console.log("");
1118
1973
  console.log(`${BOLD2}Sync Status:${RESET2}`);
1119
- if (existsSync4(projectJsonPath)) {
1974
+ if (existsSync7(projectJsonPath)) {
1120
1975
  try {
1121
- const projectJson = JSON.parse(readFileSync4(projectJsonPath, "utf-8"));
1976
+ const projectJson = JSON.parse(readFileSync6(projectJsonPath, "utf-8"));
1122
1977
  const syncStatus = projectJson.sync?.status || "unknown";
1123
1978
  const lastSync = projectJson.sync?.lastSync || "never";
1124
1979
  const source = projectJson.sync?.registrySource || "unknown";
@@ -1136,33 +1991,33 @@ async function cmdStatus() {
1136
1991
  }
1137
1992
  async function cmdSync() {
1138
1993
  const projectRoot = process.cwd();
1139
- const cacheDir = join4(projectRoot, ".decantr", "cache");
1994
+ const cacheDir = join7(projectRoot, ".decantr", "cache");
1140
1995
  console.log(heading("Syncing registry content..."));
1141
1996
  const result = await syncRegistry(cacheDir);
1142
- if (result.source === "api") {
1997
+ if (result.synced.length > 0) {
1143
1998
  console.log(success("Sync completed successfully."));
1144
- if (result.synced.length > 0) {
1145
- console.log(` Synced: ${result.synced.join(", ")}`);
1146
- }
1999
+ console.log(` Synced: ${result.synced.join(", ")}`);
1147
2000
  if (result.failed.length > 0) {
1148
2001
  console.log(` ${YELLOW2}Failed: ${result.failed.join(", ")}${RESET2}`);
1149
2002
  }
1150
2003
  } else {
1151
2004
  console.log(`${YELLOW2}Could not sync: API unavailable${RESET2}`);
1152
- console.log(dim("Using bundled content."));
2005
+ if (result.failed.length > 0) {
2006
+ console.log(` ${YELLOW2}Failed: ${result.failed.join(", ")}${RESET2}`);
2007
+ }
1153
2008
  }
1154
2009
  }
1155
2010
  async function cmdAudit() {
1156
2011
  const projectRoot = process.cwd();
1157
- const essencePath = join4(projectRoot, "decantr.essence.json");
2012
+ const essencePath = join7(projectRoot, "decantr.essence.json");
1158
2013
  console.log(heading("Auditing project..."));
1159
- if (!existsSync4(essencePath)) {
2014
+ if (!existsSync7(essencePath)) {
1160
2015
  console.log(`${RED}No decantr.essence.json found.${RESET2}`);
1161
2016
  process.exitCode = 1;
1162
2017
  return;
1163
2018
  }
1164
2019
  try {
1165
- const essence = JSON.parse(readFileSync4(essencePath, "utf-8"));
2020
+ const essence = JSON.parse(readFileSync6(essencePath, "utf-8"));
1166
2021
  const validation = validateEssence(essence);
1167
2022
  if (!validation.valid) {
1168
2023
  console.log(`${RED}Essence validation failed:${RESET2}`);
@@ -1173,13 +2028,17 @@ async function cmdAudit() {
1173
2028
  return;
1174
2029
  }
1175
2030
  console.log(success("Essence is valid."));
1176
- const violations = evaluateGuard(essence, {});
2031
+ const { themeRegistry, patternRegistry } = buildRegistryContext();
2032
+ const violations = evaluateGuard(essence, { themeRegistry, patternRegistry });
1177
2033
  if (violations.length > 0) {
1178
2034
  console.log("");
1179
2035
  console.log(`${YELLOW2}Guard violations:${RESET2}`);
1180
2036
  for (const v of violations) {
1181
2037
  const vr = v;
1182
2038
  console.log(` ${YELLOW2}[${vr.rule}]${RESET2} ${vr.message}`);
2039
+ if (vr.suggestion) {
2040
+ console.log(` ${DIM2}Suggestion: ${vr.suggestion}${RESET2}`);
2041
+ }
1183
2042
  }
1184
2043
  } else {
1185
2044
  console.log(success("No guard violations."));
@@ -1194,6 +2053,135 @@ async function cmdAudit() {
1194
2053
  process.exitCode = 1;
1195
2054
  }
1196
2055
  }
2056
+ async function cmdTheme(args) {
2057
+ const subcommand = args[0];
2058
+ const projectRoot = process.cwd();
2059
+ if (!subcommand || subcommand === "help") {
2060
+ console.log(`
2061
+ ${BOLD2}decantr theme${RESET2} \u2014 Manage custom themes
2062
+
2063
+ ${BOLD2}Commands:${RESET2}
2064
+ ${cyan("create")} <name> Create a new custom theme
2065
+ ${cyan("create")} <name> --guided Interactive theme creation
2066
+ ${cyan("list")} List custom themes
2067
+ ${cyan("validate")} <name> Validate a custom theme
2068
+ ${cyan("delete")} <name> Delete a custom theme
2069
+ ${cyan("import")} <path> Import theme from JSON file
2070
+
2071
+ ${BOLD2}Examples:${RESET2}
2072
+ decantr theme create mytheme
2073
+ decantr theme list
2074
+ decantr theme validate mytheme
2075
+ decantr theme import ./external-theme.json
2076
+ `);
2077
+ return;
2078
+ }
2079
+ switch (subcommand) {
2080
+ case "create": {
2081
+ const name = args[1];
2082
+ if (!name) {
2083
+ console.error(error("Usage: decantr theme create <name>"));
2084
+ process.exitCode = 1;
2085
+ return;
2086
+ }
2087
+ const displayName = name.charAt(0).toUpperCase() + name.slice(1).replace(/-/g, " ");
2088
+ const result = createTheme(projectRoot, name, displayName);
2089
+ if (result.success) {
2090
+ console.log(success(`Created custom theme "${name}"`));
2091
+ console.log(dim(` Path: ${result.path}`));
2092
+ console.log("");
2093
+ console.log(`Use in essence: ${cyan(`"style": "custom:${name}"`)}`);
2094
+ } else {
2095
+ console.error(error(result.error || "Failed to create theme"));
2096
+ process.exitCode = 1;
2097
+ }
2098
+ break;
2099
+ }
2100
+ case "list": {
2101
+ const themes = listCustomThemes(projectRoot);
2102
+ if (themes.length === 0) {
2103
+ console.log(dim("No custom themes found."));
2104
+ console.log(dim('Run "decantr theme create <name>" to create one.'));
2105
+ } else {
2106
+ console.log(heading(`${themes.length} custom theme(s)`));
2107
+ for (const theme of themes) {
2108
+ console.log(` ${cyan(`custom:${theme.id}`)} ${dim(theme.description || theme.name)}`);
2109
+ }
2110
+ }
2111
+ break;
2112
+ }
2113
+ case "validate": {
2114
+ const name = args[1];
2115
+ if (!name) {
2116
+ console.error(error("Usage: decantr theme validate <name>"));
2117
+ process.exitCode = 1;
2118
+ return;
2119
+ }
2120
+ const themePath = join7(projectRoot, ".decantr", "custom", "themes", `${name}.json`);
2121
+ if (!existsSync7(themePath)) {
2122
+ console.error(error(`Theme "${name}" not found at ${themePath}`));
2123
+ process.exitCode = 1;
2124
+ return;
2125
+ }
2126
+ try {
2127
+ const theme = JSON.parse(readFileSync6(themePath, "utf-8"));
2128
+ const result = validateCustomTheme(theme);
2129
+ if (result.valid) {
2130
+ console.log(success(`Custom theme "${name}" is valid`));
2131
+ } else {
2132
+ console.error(error("Validation failed:"));
2133
+ for (const err of result.errors) {
2134
+ console.error(` ${RED}${err}${RESET2}`);
2135
+ }
2136
+ process.exitCode = 1;
2137
+ }
2138
+ } catch (e) {
2139
+ console.error(error(`Invalid JSON: ${e.message}`));
2140
+ process.exitCode = 1;
2141
+ }
2142
+ break;
2143
+ }
2144
+ case "delete": {
2145
+ const name = args[1];
2146
+ if (!name) {
2147
+ console.error(error("Usage: decantr theme delete <name>"));
2148
+ process.exitCode = 1;
2149
+ return;
2150
+ }
2151
+ const result = deleteTheme(projectRoot, name);
2152
+ if (result.success) {
2153
+ console.log(success(`Deleted custom theme "${name}"`));
2154
+ } else {
2155
+ console.error(error(result.error || "Failed to delete theme"));
2156
+ process.exitCode = 1;
2157
+ }
2158
+ break;
2159
+ }
2160
+ case "import": {
2161
+ const sourcePath = args[1];
2162
+ if (!sourcePath) {
2163
+ console.error(error("Usage: decantr theme import <path>"));
2164
+ process.exitCode = 1;
2165
+ return;
2166
+ }
2167
+ const result = importTheme(projectRoot, sourcePath);
2168
+ if (result.success) {
2169
+ console.log(success("Theme imported successfully"));
2170
+ console.log(dim(` Path: ${result.path}`));
2171
+ } else {
2172
+ console.error(error("Import failed:"));
2173
+ for (const err of result.errors || []) {
2174
+ console.error(` ${RED}${err}${RESET2}`);
2175
+ }
2176
+ process.exitCode = 1;
2177
+ }
2178
+ break;
2179
+ }
2180
+ default:
2181
+ console.error(error(`Unknown theme command: ${subcommand}`));
2182
+ process.exitCode = 1;
2183
+ }
2184
+ }
1197
2185
  function cmdHelp() {
1198
2186
  console.log(`
1199
2187
  ${BOLD2}decantr${RESET2} \u2014 Design intelligence for AI-generated UI
@@ -1204,9 +2192,15 @@ ${BOLD2}Usage:${RESET2}
1204
2192
  decantr sync
1205
2193
  decantr audit
1206
2194
  decantr search <query> [--type <type>]
2195
+ decantr suggest <query> [--type <type>]
1207
2196
  decantr get <type> <id>
1208
2197
  decantr list <type>
1209
2198
  decantr validate [path]
2199
+ decantr theme <subcommand>
2200
+ decantr create <type> <name>
2201
+ decantr publish <type> <name>
2202
+ decantr login
2203
+ decantr logout
1210
2204
  decantr help
1211
2205
 
1212
2206
  ${BOLD2}Init Options:${RESET2}
@@ -1229,9 +2223,15 @@ ${BOLD2}Commands:${RESET2}
1229
2223
  ${cyan("sync")} Sync registry content from API
1230
2224
  ${cyan("audit")} Validate essence and check for drift
1231
2225
  ${cyan("search")} Search the registry
2226
+ ${cyan("suggest")} Suggest patterns or alternatives for a query
1232
2227
  ${cyan("get")} Get full details of a registry item
1233
2228
  ${cyan("list")} List items by type
1234
2229
  ${cyan("validate")} Validate essence file
2230
+ ${cyan("theme")} Manage custom themes (create, list, validate, delete, import)
2231
+ ${cyan("create")} Create a custom content item (pattern, recipe, theme, etc.)
2232
+ ${cyan("publish")} Publish a custom content item to the community registry
2233
+ ${cyan("login")} Authenticate with the Decantr registry
2234
+ ${cyan("logout")} Remove stored credentials
1235
2235
  ${cyan("help")} Show this help
1236
2236
 
1237
2237
  ${BOLD2}Examples:${RESET2}
@@ -1241,7 +2241,11 @@ ${BOLD2}Examples:${RESET2}
1241
2241
  decantr sync
1242
2242
  decantr audit
1243
2243
  decantr search dashboard
2244
+ decantr suggest leaderboard
2245
+ decantr suggest ranking --type pattern
1244
2246
  decantr list patterns
2247
+ decantr create pattern my-card
2248
+ decantr publish pattern my-card
1245
2249
  `);
1246
2250
  }
1247
2251
  async function main() {
@@ -1286,6 +2290,16 @@ async function main() {
1286
2290
  await cmdSync();
1287
2291
  break;
1288
2292
  }
2293
+ case "upgrade": {
2294
+ const { cmdUpgrade } = await import("./upgrade-KRFCKUMR.js");
2295
+ await cmdUpgrade(process.cwd());
2296
+ break;
2297
+ }
2298
+ case "heal": {
2299
+ const { cmdHeal } = await import("./heal-2OPN63OT.js");
2300
+ await cmdHeal(process.cwd());
2301
+ break;
2302
+ }
1289
2303
  case "audit": {
1290
2304
  await cmdAudit();
1291
2305
  break;
@@ -1302,6 +2316,18 @@ async function main() {
1302
2316
  await cmdSearch(query, type);
1303
2317
  break;
1304
2318
  }
2319
+ case "suggest": {
2320
+ const query = args[1];
2321
+ if (!query) {
2322
+ console.error(error("Usage: decantr suggest <query> [--type <type>]"));
2323
+ process.exitCode = 1;
2324
+ return;
2325
+ }
2326
+ const typeIdx = args.indexOf("--type");
2327
+ const type = typeIdx !== -1 ? args[typeIdx + 1] : void 0;
2328
+ await cmdSuggest(query, type);
2329
+ break;
2330
+ }
1305
2331
  case "get": {
1306
2332
  const type = args[1];
1307
2333
  const id = args[2];
@@ -1327,6 +2353,64 @@ async function main() {
1327
2353
  await cmdValidate(args[1]);
1328
2354
  break;
1329
2355
  }
2356
+ case "theme": {
2357
+ await cmdTheme(args.slice(1));
2358
+ break;
2359
+ }
2360
+ case "login": {
2361
+ const apiKeyArg = args[1];
2362
+ if (apiKeyArg && apiKeyArg.startsWith("--api-key=")) {
2363
+ const key = apiKeyArg.split("=")[1];
2364
+ saveCredentials({ access_token: key, api_key: key });
2365
+ console.log(success("API key saved."));
2366
+ } else {
2367
+ console.log(heading("Decantr Login"));
2368
+ console.log(" To authenticate, get your API key from the Decantr dashboard:");
2369
+ console.log("");
2370
+ console.log(` ${cyan("https://decantr.ai/dashboard/api-keys")}`);
2371
+ console.log("");
2372
+ console.log(" Then run:");
2373
+ console.log(` ${cyan("decantr login --api-key=<your-key>")}`);
2374
+ console.log("");
2375
+ console.log(" Or set the environment variable:");
2376
+ console.log(` ${cyan("export DECANTR_API_KEY=<your-key>")}`);
2377
+ const existingCreds = getCredentials();
2378
+ if (existingCreds) {
2379
+ console.log("");
2380
+ console.log(dim("You are currently authenticated."));
2381
+ }
2382
+ }
2383
+ break;
2384
+ }
2385
+ case "logout": {
2386
+ clearCredentials();
2387
+ console.log(success("Logged out. Credentials removed."));
2388
+ break;
2389
+ }
2390
+ case "create": {
2391
+ const type = args[1];
2392
+ const name = args[2];
2393
+ if (!type || !name) {
2394
+ console.error(error("Usage: decantr create <type> <name>"));
2395
+ console.error(dim("Types: pattern, recipe, theme, blueprint, archetype, shell"));
2396
+ process.exitCode = 1;
2397
+ break;
2398
+ }
2399
+ cmdCreate(type, name);
2400
+ break;
2401
+ }
2402
+ case "publish": {
2403
+ const type = args[1];
2404
+ const name = args[2];
2405
+ if (!type || !name) {
2406
+ console.error(error("Usage: decantr publish <type> <name>"));
2407
+ console.error(dim("Types: pattern, recipe, theme, blueprint, archetype, shell"));
2408
+ process.exitCode = 1;
2409
+ break;
2410
+ }
2411
+ await cmdPublish(type, name);
2412
+ break;
2413
+ }
1330
2414
  default:
1331
2415
  console.error(error(`Unknown command: ${command}`));
1332
2416
  cmdHelp();