@decantr/cli 1.0.0-beta.10 → 1.0.0-beta.11

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,8 +1,14 @@
1
1
  #!/usr/bin/env node
2
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
3
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
4
+ }) : x)(function(x) {
5
+ if (typeof require !== "undefined") return require.apply(this, arguments);
6
+ throw Error('Dynamic require of "' + x + '" is not supported');
7
+ });
2
8
 
3
9
  // src/index.ts
4
- import { readFileSync as readFileSync4, existsSync as existsSync4 } from "fs";
5
- import { join as join4 } from "path";
10
+ import { readFileSync as readFileSync5, existsSync as existsSync5 } from "fs";
11
+ import { join as join5 } from "path";
6
12
  import { validateEssence, evaluateGuard } from "@decantr/essence-spec";
7
13
  import { createResolver, createRegistryClient } from "@decantr/registry";
8
14
 
@@ -331,6 +337,40 @@ import { join as join2, dirname } from "path";
331
337
  import { fileURLToPath } from "url";
332
338
  var __dirname = dirname(fileURLToPath(import.meta.url));
333
339
  var CLI_VERSION = "1.0.0";
340
+ function serializeLayoutItem(item) {
341
+ if (typeof item === "string") {
342
+ return item;
343
+ }
344
+ if (typeof item === "object" && item !== null) {
345
+ const obj = item;
346
+ if (typeof obj.pattern === "string") {
347
+ const preset = obj.preset ? ` (${obj.preset})` : "";
348
+ const alias = obj.as ? ` as ${obj.as}` : "";
349
+ return `${obj.pattern}${preset}${alias}`;
350
+ }
351
+ if (Array.isArray(obj.cols)) {
352
+ const cols = obj.cols.map(serializeLayoutItem).join(" | ");
353
+ const breakpoint = obj.at ? ` @${obj.at}` : "";
354
+ return `[${cols}]${breakpoint}`;
355
+ }
356
+ }
357
+ return "custom";
358
+ }
359
+ function extractPatternNames(item) {
360
+ if (typeof item === "string") {
361
+ return [item];
362
+ }
363
+ if (typeof item === "object" && item !== null) {
364
+ const obj = item;
365
+ if (typeof obj.pattern === "string") {
366
+ return [obj.pattern];
367
+ }
368
+ if (Array.isArray(obj.cols)) {
369
+ return obj.cols.flatMap(extractPatternNames);
370
+ }
371
+ }
372
+ return [];
373
+ }
334
374
  function loadTemplate(name) {
335
375
  const fromDist = join2(__dirname, "..", "src", "templates", name);
336
376
  if (existsSync2(fromDist)) {
@@ -371,7 +411,7 @@ function buildEssence(options, archetypeData) {
371
411
  spacious: "_gap6"
372
412
  };
373
413
  const archetype = options.archetype || "custom";
374
- return {
414
+ const essence = {
375
415
  version: "2.0.0",
376
416
  archetype,
377
417
  theme: {
@@ -399,14 +439,105 @@ function buildEssence(options, archetypeData) {
399
439
  },
400
440
  target: options.target
401
441
  };
442
+ if (options.accessibility) {
443
+ essence.accessibility = options.accessibility;
444
+ }
445
+ return essence;
446
+ }
447
+ function generateAccessibilitySection(essence, themeData) {
448
+ const accessibility = essence.accessibility;
449
+ if (!accessibility?.wcag_level || accessibility.wcag_level === "none") {
450
+ return "";
451
+ }
452
+ const wcagLevel = accessibility.wcag_level;
453
+ const cvdPreference = accessibility.cvd_preference || "none";
454
+ const cvdSupport = themeData?.cvd_support || [];
455
+ let section = `---
456
+
457
+ ## Accessibility
458
+
459
+ **WCAG Level:** ${wcagLevel}
460
+ `;
461
+ if (cvdSupport.length > 0) {
462
+ section += `**CVD Support:** Theme supports ${cvdSupport.join(", ")}
463
+ **CVD Preference:** ${cvdPreference}
464
+ `;
465
+ }
466
+ section += `
467
+ ### What This Means
468
+
469
+ This project requires WCAG 2.1 Level ${wcagLevel} compliance. You already know these rules \u2014 apply them:
470
+
471
+ - Semantic HTML structure
472
+ - Sufficient color contrast (4.5:1 for normal text, 3:1 for large text)
473
+ - Keyboard navigability for all interactive elements
474
+ - Visible focus indicators
475
+ - Meaningful alt text for images
476
+ - Proper heading hierarchy
477
+ `;
478
+ if (cvdSupport.length > 0) {
479
+ section += `
480
+ ### CVD Implementation
481
+
482
+ The theme provides these data attributes:
483
+
484
+ \`\`\`html
485
+ <html data-theme="${essence.theme.style}" data-mode="${essence.theme.mode}" data-cvd="none">
486
+ \`\`\`
487
+
488
+ Valid \`data-cvd\` values for this theme: \`none\`, ${cvdSupport.map((m) => `\`${m}\``).join(", ")}
489
+ `;
490
+ if (cvdPreference === "auto") {
491
+ section += `
492
+ Detect user preference via \`prefers-contrast\` or user settings and apply accordingly.
493
+ `;
494
+ }
495
+ }
496
+ section += `
497
+ ---
498
+ `;
499
+ return section;
500
+ }
501
+ function generateSeoSection(essence, archetypeData) {
502
+ const seoHints = archetypeData?.seo_hints;
503
+ if (!seoHints) {
504
+ return "";
505
+ }
506
+ const schemaOrg = seoHints.schema_org || [];
507
+ const metaPriorities = seoHints.meta_priorities || [];
508
+ if (schemaOrg.length === 0 && metaPriorities.length === 0) {
509
+ return "";
510
+ }
511
+ let section = `---
512
+
513
+ ## SEO Guidance
514
+
515
+ This archetype (\`${essence.archetype}\`) typically benefits from:
516
+
517
+ `;
518
+ if (schemaOrg.length > 0) {
519
+ section += `- **Schema.org:** ${schemaOrg.join(", ")}
520
+ `;
521
+ }
522
+ if (metaPriorities.length > 0) {
523
+ section += `- **Meta priorities:** ${metaPriorities.join(", ")}
524
+ `;
525
+ }
526
+ section += `
527
+ These are suggestions, not requirements. Apply where appropriate for the page content.
528
+
529
+ ---
530
+ `;
531
+ return section;
402
532
  }
403
- function generateDecantrMd(essence, detected) {
533
+ function generateDecantrMd(essence, detected, themeData, recipeData, archetypeData) {
404
534
  const template = loadTemplate("DECANTR.md.template");
405
- const pagesTable = essence.structure.map(
406
- (p) => `| ${p.id} | ${p.shell} | ${p.layout.join(", ") || "none"} |`
407
- ).join("\n");
408
- const allPatterns = [...new Set(essence.structure.flatMap((p) => p.layout))];
409
- const patternsList = allPatterns.length > 0 ? allPatterns.map((p) => `- \`${p}\``).join("\n") : "- No patterns specified yet";
535
+ const pagesTable = essence.structure.map((p) => {
536
+ const layoutStr = p.layout.map(serializeLayoutItem).join(", ") || "none";
537
+ return `| ${p.id} | ${p.shell} | ${layoutStr} |`;
538
+ }).join("\n");
539
+ const allPatternNames = [...new Set(essence.structure.flatMap((p) => p.layout.flatMap(extractPatternNames)))];
540
+ const patternsList = allPatternNames.length > 0 ? allPatternNames.map((p) => `- \`${p}\``).join("\n") : "- No patterns specified yet";
410
541
  const projectSummary = [
411
542
  `**Archetype:** ${essence.archetype || "custom"}`,
412
543
  `**Target:** ${essence.target}`,
@@ -423,6 +554,29 @@ function generateDecantrMd(essence, detected) {
423
554
  };
424
555
  const defaultShell = essence.structure[0]?.shell || "sidebar-main";
425
556
  const shellStructure = shellStructures[defaultShell] || "Custom shell layout";
557
+ let themeQuickRef = "";
558
+ if (themeData?.seed) {
559
+ const colors = Object.entries(themeData.seed).map(([name, hex]) => `- **${name}:** \`${hex}\``).join("\n");
560
+ themeQuickRef = `**Seed Colors:**
561
+ ${colors}`;
562
+ }
563
+ if (recipeData?.decorators) {
564
+ const decorators = Object.entries(recipeData.decorators).slice(0, 5).map(([name, desc]) => `- \`${name}\` \u2014 ${desc}`).join("\n");
565
+ if (themeQuickRef) {
566
+ themeQuickRef += `
567
+
568
+ **Key Decorators:**
569
+ ${decorators}`;
570
+ } else {
571
+ themeQuickRef = `**Key Decorators:**
572
+ ${decorators}`;
573
+ }
574
+ }
575
+ if (!themeQuickRef) {
576
+ themeQuickRef = `See \`decantr get theme ${essence.theme.style}\` for details.`;
577
+ }
578
+ const accessibilitySection = generateAccessibilitySection(essence, themeData);
579
+ const seoSection = generateSeoSection(essence, archetypeData);
426
580
  const vars = {
427
581
  GUARD_MODE: essence.guard.mode,
428
582
  PROJECT_SUMMARY: projectSummary,
@@ -441,7 +595,10 @@ ${pagesTable}`,
441
595
  AVAILABLE_PATTERNS: "(See registry or .decantr/cache/patterns/)",
442
596
  AVAILABLE_THEMES: "(See registry or .decantr/cache/themes/)",
443
597
  AVAILABLE_SHELLS: "sidebar-main, top-nav-main, centered, full-bleed, minimal-header",
444
- VERSION: CLI_VERSION
598
+ VERSION: CLI_VERSION,
599
+ THEME_QUICK_REFERENCE: themeQuickRef,
600
+ ACCESSIBILITY_SECTION: accessibilitySection,
601
+ SEO_SECTION: seoSection
445
602
  };
446
603
  return renderTemplate(template, vars);
447
604
  }
@@ -490,10 +647,10 @@ function buildFlagsString(options) {
490
647
  function generateTaskContext(templateName, essence) {
491
648
  const template = loadTemplate(templateName);
492
649
  const defaultShell = essence.structure[0]?.shell || "sidebar-main";
493
- const layout = essence.structure[0]?.layout.join(", ") || "none";
650
+ const layout = essence.structure[0]?.layout.map(serializeLayoutItem).join(", ") || "none";
494
651
  const scaffoldStructure = essence.structure.map((p) => {
495
652
  const patterns = p.layout.length > 0 ? `
496
- - Patterns: ${p.layout.join(", ")}` : "";
653
+ - Patterns: ${p.layout.map(serializeLayoutItem).join(", ")}` : "";
497
654
  return `- **${p.id}** (${p.shell})${patterns}`;
498
655
  }).join("\n");
499
656
  const vars = {
@@ -514,7 +671,7 @@ function generateEssenceSummary(essence) {
514
671
  const template = loadTemplate("essence-summary.md.template");
515
672
  const pagesTable = `| Page | Shell | Layout |
516
673
  |------|-------|--------|
517
- ${essence.structure.map((p) => `| ${p.id} | ${p.shell} | ${p.layout.join(", ") || "none"} |`).join("\n")}`;
674
+ ${essence.structure.map((p) => `| ${p.id} | ${p.shell} | ${p.layout.map(serializeLayoutItem).join(", ") || "none"} |`).join("\n")}`;
518
675
  const featuresList = essence.features.length > 0 ? essence.features.map((f) => `- ${f}`).join("\n") : "- No features specified";
519
676
  const vars = {
520
677
  ARCHETYPE: essence.archetype || "custom",
@@ -556,7 +713,7 @@ ${cacheEntry}
556
713
  return true;
557
714
  }
558
715
  }
559
- function scaffoldProject(projectRoot, options, detected, archetypeData, registrySource = "bundled") {
716
+ function scaffoldProject(projectRoot, options, detected, archetypeData, registrySource = "bundled", themeData, recipeData) {
560
717
  const essence = buildEssence(options, archetypeData);
561
718
  const decantrDir = join2(projectRoot, ".decantr");
562
719
  const contextDir = join2(decantrDir, "context");
@@ -566,7 +723,7 @@ function scaffoldProject(projectRoot, options, detected, archetypeData, registry
566
723
  const essencePath = join2(projectRoot, "decantr.essence.json");
567
724
  writeFileSync(essencePath, JSON.stringify(essence, null, 2) + "\n");
568
725
  const decantrMdPath = join2(projectRoot, "DECANTR.md");
569
- writeFileSync(decantrMdPath, generateDecantrMd(essence, detected));
726
+ writeFileSync(decantrMdPath, generateDecantrMd(essence, detected, themeData, recipeData, archetypeData));
570
727
  const projectJsonPath = join2(decantrDir, "project.json");
571
728
  writeFileSync(projectJsonPath, generateProjectJson(detected, options, registrySource));
572
729
  const contextFiles = [];
@@ -645,33 +802,56 @@ function loadFromCache(cacheDir, contentType, id) {
645
802
  function loadFromBundled(contentType, id) {
646
803
  const contentRoot = getBundledContentRoot();
647
804
  if (id) {
648
- const itemPath = join3(contentRoot, contentType, `${id}.json`);
649
- if (!existsSync3(itemPath)) return null;
650
- try {
651
- const data = JSON.parse(readFileSync3(itemPath, "utf-8"));
652
- return {
653
- data,
654
- source: { type: "bundled" }
655
- };
656
- } catch {
657
- return null;
805
+ const mainPath = join3(contentRoot, contentType, `${id}.json`);
806
+ if (existsSync3(mainPath)) {
807
+ try {
808
+ const data = JSON.parse(readFileSync3(mainPath, "utf-8"));
809
+ return { data, source: { type: "bundled" } };
810
+ } catch {
811
+ }
812
+ }
813
+ const corePath = join3(contentRoot, "core", contentType, `${id}.json`);
814
+ if (existsSync3(corePath)) {
815
+ try {
816
+ const data = JSON.parse(readFileSync3(corePath, "utf-8"));
817
+ return { data, source: { type: "bundled" } };
818
+ } catch {
819
+ }
658
820
  }
821
+ return null;
659
822
  } else {
660
- const dir = join3(contentRoot, contentType);
661
- if (!existsSync3(dir)) return null;
662
- try {
663
- const files = readdirSync(dir).filter((f) => f.endsWith(".json"));
664
- const items = files.map((f) => {
665
- const content = JSON.parse(readFileSync3(join3(dir, f), "utf-8"));
666
- return { id: content.id || f.replace(".json", ""), ...content };
667
- });
668
- return {
669
- data: { items, total: items.length },
670
- source: { type: "bundled" }
671
- };
672
- } catch {
673
- return null;
823
+ const mainDir = join3(contentRoot, contentType);
824
+ const coreDir = join3(contentRoot, "core", contentType);
825
+ const items = [];
826
+ if (existsSync3(mainDir)) {
827
+ try {
828
+ const files = readdirSync(mainDir).filter((f) => f.endsWith(".json"));
829
+ for (const f of files) {
830
+ const content = JSON.parse(readFileSync3(join3(mainDir, f), "utf-8"));
831
+ items.push({ id: content.id || f.replace(".json", ""), ...content });
832
+ }
833
+ } catch {
834
+ }
835
+ }
836
+ if (existsSync3(coreDir)) {
837
+ try {
838
+ const files = readdirSync(coreDir).filter((f) => f.endsWith(".json"));
839
+ const existingIds = new Set(items.map((i) => i.id));
840
+ for (const f of files) {
841
+ const content = JSON.parse(readFileSync3(join3(coreDir, f), "utf-8"));
842
+ const itemId = content.id || f.replace(".json", "");
843
+ if (!existingIds.has(itemId)) {
844
+ items.push({ id: itemId, ...content });
845
+ }
846
+ }
847
+ } catch {
848
+ }
674
849
  }
850
+ if (items.length === 0) return null;
851
+ return {
852
+ data: { items, total: items.length },
853
+ source: { type: "bundled" }
854
+ };
675
855
  }
676
856
  }
677
857
  function saveToCache(cacheDir, contentType, id, data) {
@@ -684,11 +864,37 @@ var RegistryClient = class {
684
864
  cacheDir;
685
865
  apiUrl;
686
866
  offline;
867
+ projectRoot;
687
868
  constructor(options = {}) {
688
- this.cacheDir = options.cacheDir || join3(process.cwd(), ".decantr", "cache");
869
+ this.projectRoot = options.projectRoot || process.cwd();
870
+ this.cacheDir = options.cacheDir || join3(this.projectRoot, ".decantr", "cache");
689
871
  this.apiUrl = options.apiUrl || DEFAULT_API_URL;
690
872
  this.offline = options.offline || false;
691
873
  }
874
+ /**
875
+ * Load content from .decantr/custom/{contentType}/{id}.json
876
+ */
877
+ loadCustomContent(contentType, id) {
878
+ const customPath = join3(
879
+ this.projectRoot,
880
+ ".decantr",
881
+ "custom",
882
+ contentType,
883
+ `${id}.json`
884
+ );
885
+ if (!existsSync3(customPath)) {
886
+ return null;
887
+ }
888
+ try {
889
+ const data = JSON.parse(readFileSync3(customPath, "utf-8"));
890
+ return {
891
+ data,
892
+ source: { type: "custom", path: customPath }
893
+ };
894
+ } catch {
895
+ return null;
896
+ }
897
+ }
692
898
  /**
693
899
  * Fetch archetypes list.
694
900
  */
@@ -788,6 +994,24 @@ var RegistryClient = class {
788
994
  source: { type: "bundled" }
789
995
  };
790
996
  }
997
+ /**
998
+ * Fetch a single theme.
999
+ */
1000
+ async fetchTheme(id) {
1001
+ if (id.startsWith("custom:")) {
1002
+ return this.loadCustomContent("themes", id.slice(7));
1003
+ }
1004
+ if (!this.offline) {
1005
+ const apiResult = await tryApi(`themes/${id}`, this.apiUrl);
1006
+ if (apiResult) {
1007
+ saveToCache(this.cacheDir, "themes", id, apiResult.data);
1008
+ return apiResult;
1009
+ }
1010
+ }
1011
+ const cacheResult = loadFromCache(this.cacheDir, "themes", id);
1012
+ if (cacheResult) return cacheResult;
1013
+ return loadFromBundled("themes", id);
1014
+ }
791
1015
  /**
792
1016
  * Fetch patterns list.
793
1017
  */
@@ -857,6 +1081,228 @@ async function syncRegistry(cacheDir, apiUrl = DEFAULT_API_URL) {
857
1081
  };
858
1082
  }
859
1083
 
1084
+ // src/theme-commands.ts
1085
+ import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync3, readdirSync as readdirSync2, rmSync } from "fs";
1086
+ import { join as join4 } from "path";
1087
+
1088
+ // src/theme-templates.ts
1089
+ function getThemeSkeleton(id, name) {
1090
+ return {
1091
+ $schema: "https://decantr.ai/schemas/style-metadata.v1.json",
1092
+ id,
1093
+ name,
1094
+ description: "",
1095
+ tags: [],
1096
+ seed: {
1097
+ primary: "#6366F1",
1098
+ secondary: "#8B5CF6",
1099
+ accent: "#EC4899",
1100
+ background: "#0F172A"
1101
+ },
1102
+ palette: {},
1103
+ modes: ["dark"],
1104
+ shapes: ["rounded"],
1105
+ decantr_compat: ">=1.0.0",
1106
+ source: "custom"
1107
+ };
1108
+ }
1109
+ function getHowToThemeDoc() {
1110
+ return `# Custom Themes
1111
+
1112
+ Create custom themes for your Decantr project.
1113
+
1114
+ ## Quick Start
1115
+
1116
+ \`\`\`bash
1117
+ decantr theme create mytheme
1118
+ \`\`\`
1119
+
1120
+ ## Theme Structure
1121
+
1122
+ | Field | Required | Description |
1123
+ |-------|----------|-------------|
1124
+ | id | Yes | Unique identifier (matches filename) |
1125
+ | name | Yes | Display name |
1126
+ | description | No | Brief description |
1127
+ | tags | No | Searchable tags |
1128
+ | seed | Yes | Core colors: primary, secondary, accent, background |
1129
+ | palette | No | Extended color palette |
1130
+ | modes | Yes | Supported modes: ["light"], ["dark"], or both |
1131
+ | shapes | Yes | Supported shapes: sharp, rounded, pill |
1132
+ | decantr_compat | Yes | Version compatibility (e.g., ">=1.0.0") |
1133
+ | source | Yes | Must be "custom" |
1134
+
1135
+ ## Using Your Theme
1136
+
1137
+ In \`decantr.essence.json\`:
1138
+
1139
+ \`\`\`json
1140
+ {
1141
+ "theme": {
1142
+ "style": "custom:mytheme",
1143
+ "mode": "dark"
1144
+ }
1145
+ }
1146
+ \`\`\`
1147
+
1148
+ ## Validation
1149
+
1150
+ \`\`\`bash
1151
+ decantr theme validate mytheme
1152
+ \`\`\`
1153
+
1154
+ ## Reference
1155
+
1156
+ See registry themes for examples:
1157
+
1158
+ \`\`\`bash
1159
+ decantr get theme auradecantism
1160
+ \`\`\`
1161
+ `;
1162
+ }
1163
+
1164
+ // src/theme-commands.ts
1165
+ var REQUIRED_FIELDS = ["id", "name", "seed", "modes", "shapes", "decantr_compat", "source"];
1166
+ var REQUIRED_SEED = ["primary", "secondary", "accent", "background"];
1167
+ var VALID_MODES = ["light", "dark"];
1168
+ var VALID_SHAPES = ["sharp", "rounded", "pill"];
1169
+ function validateCustomTheme(theme) {
1170
+ const errors = [];
1171
+ for (const field of REQUIRED_FIELDS) {
1172
+ if (!(field in theme)) {
1173
+ errors.push(`Missing required field: ${field}`);
1174
+ }
1175
+ }
1176
+ if (theme.seed && typeof theme.seed === "object") {
1177
+ const seed = theme.seed;
1178
+ for (const color of REQUIRED_SEED) {
1179
+ if (!(color in seed)) {
1180
+ errors.push(`Missing seed color: ${color}`);
1181
+ }
1182
+ }
1183
+ }
1184
+ if (Array.isArray(theme.modes)) {
1185
+ for (const mode of theme.modes) {
1186
+ if (!VALID_MODES.includes(mode)) {
1187
+ errors.push(`Invalid mode "${mode}" - must be "light" or "dark"`);
1188
+ }
1189
+ }
1190
+ }
1191
+ if (Array.isArray(theme.shapes)) {
1192
+ for (const shape of theme.shapes) {
1193
+ if (!VALID_SHAPES.includes(shape)) {
1194
+ errors.push(`Invalid shape "${shape}" - use: sharp, rounded, pill`);
1195
+ }
1196
+ }
1197
+ }
1198
+ return {
1199
+ valid: errors.length === 0,
1200
+ errors
1201
+ };
1202
+ }
1203
+ function createTheme(projectRoot, id, name) {
1204
+ const customThemesDir = join4(projectRoot, ".decantr", "custom", "themes");
1205
+ const themePath = join4(customThemesDir, `${id}.json`);
1206
+ const howToPath = join4(customThemesDir, "how-to-theme.md");
1207
+ mkdirSync3(customThemesDir, { recursive: true });
1208
+ if (existsSync4(themePath)) {
1209
+ return {
1210
+ success: false,
1211
+ error: `Theme "${id}" already exists at ${themePath}`
1212
+ };
1213
+ }
1214
+ const skeleton = getThemeSkeleton(id, name);
1215
+ writeFileSync3(themePath, JSON.stringify(skeleton, null, 2));
1216
+ if (!existsSync4(howToPath)) {
1217
+ writeFileSync3(howToPath, getHowToThemeDoc());
1218
+ }
1219
+ return {
1220
+ success: true,
1221
+ path: themePath
1222
+ };
1223
+ }
1224
+ function listCustomThemes(projectRoot) {
1225
+ const customThemesDir = join4(projectRoot, ".decantr", "custom", "themes");
1226
+ if (!existsSync4(customThemesDir)) {
1227
+ return [];
1228
+ }
1229
+ const themes = [];
1230
+ try {
1231
+ const files = readdirSync2(customThemesDir).filter((f) => f.endsWith(".json"));
1232
+ for (const file of files) {
1233
+ const filePath = join4(customThemesDir, file);
1234
+ try {
1235
+ const data = JSON.parse(readFileSync4(filePath, "utf-8"));
1236
+ themes.push({
1237
+ id: data.id || file.replace(".json", ""),
1238
+ name: data.name || data.id,
1239
+ description: data.description,
1240
+ path: filePath
1241
+ });
1242
+ } catch {
1243
+ }
1244
+ }
1245
+ } catch {
1246
+ }
1247
+ return themes;
1248
+ }
1249
+ function deleteTheme(projectRoot, id) {
1250
+ const themePath = join4(projectRoot, ".decantr", "custom", "themes", `${id}.json`);
1251
+ if (!existsSync4(themePath)) {
1252
+ return {
1253
+ success: false,
1254
+ error: `Theme "${id}" not found at ${themePath}`
1255
+ };
1256
+ }
1257
+ try {
1258
+ rmSync(themePath);
1259
+ return { success: true };
1260
+ } catch (e) {
1261
+ return {
1262
+ success: false,
1263
+ error: `Failed to delete: ${e.message}`
1264
+ };
1265
+ }
1266
+ }
1267
+ function importTheme(projectRoot, sourcePath) {
1268
+ if (!existsSync4(sourcePath)) {
1269
+ return {
1270
+ success: false,
1271
+ errors: [`Source file not found: ${sourcePath}`]
1272
+ };
1273
+ }
1274
+ let theme;
1275
+ try {
1276
+ theme = JSON.parse(readFileSync4(sourcePath, "utf-8"));
1277
+ } catch (e) {
1278
+ return {
1279
+ success: false,
1280
+ errors: [`Invalid JSON: ${e.message}`]
1281
+ };
1282
+ }
1283
+ const validation = validateCustomTheme(theme);
1284
+ if (!validation.valid) {
1285
+ return {
1286
+ success: false,
1287
+ errors: validation.errors
1288
+ };
1289
+ }
1290
+ theme.source = "custom";
1291
+ const id = theme.id;
1292
+ const customThemesDir = join4(projectRoot, ".decantr", "custom", "themes");
1293
+ const destPath = join4(customThemesDir, `${id}.json`);
1294
+ mkdirSync3(customThemesDir, { recursive: true });
1295
+ const howToPath = join4(customThemesDir, "how-to-theme.md");
1296
+ if (!existsSync4(howToPath)) {
1297
+ writeFileSync3(howToPath, getHowToThemeDoc());
1298
+ }
1299
+ writeFileSync3(destPath, JSON.stringify(theme, null, 2));
1300
+ return {
1301
+ success: true,
1302
+ path: destPath
1303
+ };
1304
+ }
1305
+
860
1306
  // src/index.ts
861
1307
  var BOLD2 = "\x1B[1m";
862
1308
  var DIM2 = "\x1B[2m";
@@ -882,8 +1328,63 @@ function dim(text) {
882
1328
  function cyan(text) {
883
1329
  return `${CYAN2}${text}${RESET2}`;
884
1330
  }
1331
+ function extractPatternName(item) {
1332
+ if (typeof item === "string") return item;
1333
+ if (typeof item === "object" && item !== null) {
1334
+ const obj = item;
1335
+ if (typeof obj.pattern === "string") return obj.pattern;
1336
+ if (Array.isArray(obj.cols)) {
1337
+ return obj.cols.map(extractPatternName).join(" | ");
1338
+ }
1339
+ }
1340
+ return "custom";
1341
+ }
1342
+ function generateCuratedPrompt(ctx) {
1343
+ const lines = [];
1344
+ lines.push(`I'm building a ${ctx.archetype} application using ${ctx.target}.`);
1345
+ lines.push("");
1346
+ if (ctx.blueprint) {
1347
+ lines.push(`Blueprint: ${ctx.blueprint}`);
1348
+ }
1349
+ lines.push(`Theme: ${ctx.theme} (${ctx.mode} mode)`);
1350
+ lines.push(`Personality: ${ctx.personality.join(", ")}`);
1351
+ lines.push(`Guard mode: ${ctx.guard}`);
1352
+ lines.push("");
1353
+ lines.push("Pages to build:");
1354
+ for (const page of ctx.pages) {
1355
+ const patternNames = page.layout.map(extractPatternName);
1356
+ const patterns = patternNames.length > 0 ? patternNames.join(", ") : "custom";
1357
+ lines.push(` - ${page.id}: ${page.shell} shell with ${patterns}`);
1358
+ }
1359
+ if (ctx.features.length > 0) {
1360
+ lines.push("");
1361
+ lines.push(`Features: ${ctx.features.join(", ")}`);
1362
+ }
1363
+ lines.push("");
1364
+ lines.push("Please read DECANTR.md for the full design spec and methodology.");
1365
+ lines.push("Follow the guard rules and use the patterns from decantr.essence.json.");
1366
+ return lines.join("\n");
1367
+ }
1368
+ function boxedPrompt(content, title) {
1369
+ const lines = content.split("\n");
1370
+ const maxLen = Math.max(...lines.map((l) => l.length), title.length + 4);
1371
+ const width = maxLen + 4;
1372
+ const top = `\u250C${"\u2500".repeat(width - 2)}\u2510`;
1373
+ const titleLine = `\u2502 ${BOLD2}${title}${RESET2}${" ".repeat(width - title.length - 4)} \u2502`;
1374
+ const sep = `\u251C${"\u2500".repeat(width - 2)}\u2524`;
1375
+ const bottom = `\u2514${"\u2500".repeat(width - 2)}\u2518`;
1376
+ const body = lines.map((line) => {
1377
+ const padding = " ".repeat(width - line.length - 4);
1378
+ return `\u2502 ${line}${padding} \u2502`;
1379
+ }).join("\n");
1380
+ return `${top}
1381
+ ${titleLine}
1382
+ ${sep}
1383
+ ${body}
1384
+ ${bottom}`;
1385
+ }
885
1386
  function getContentRoot() {
886
- const bundled = join4(import.meta.dirname, "..", "..", "..", "content");
1387
+ const bundled = join5(import.meta.dirname, "..", "..", "..", "content");
887
1388
  return process.env.DECANTR_CONTENT_ROOT || bundled;
888
1389
  }
889
1390
  function getResolver() {
@@ -903,6 +1404,38 @@ async function cmdSearch(query, type) {
903
1404
  console.log("");
904
1405
  }
905
1406
  }
1407
+ async function cmdSuggest(query, type) {
1408
+ const client = createRegistryClient();
1409
+ const searchType = type || "pattern";
1410
+ const results = await client.search(query, searchType);
1411
+ if (results.length === 0) {
1412
+ console.log(dim(`No suggestions for "${query}"`));
1413
+ console.log("");
1414
+ console.log("Try:");
1415
+ console.log(` ${cyan("decantr list patterns")} - see all patterns`);
1416
+ console.log(` ${cyan("decantr search <broader-term>")} - broaden your search`);
1417
+ return;
1418
+ }
1419
+ console.log(heading(`Suggestions for "${query}"`));
1420
+ const queryLower = query.toLowerCase();
1421
+ const exact = results.filter((r) => r.id.toLowerCase().includes(queryLower));
1422
+ const related = results.filter((r) => !r.id.toLowerCase().includes(queryLower));
1423
+ if (exact.length > 0) {
1424
+ console.log(`${BOLD2}Direct matches:${RESET2}`);
1425
+ for (const r of exact.slice(0, 3)) {
1426
+ console.log(` ${cyan(r.id)} - ${r.description || ""}`);
1427
+ }
1428
+ console.log("");
1429
+ }
1430
+ if (related.length > 0) {
1431
+ console.log(`${BOLD2}Related:${RESET2}`);
1432
+ for (const r of related.slice(0, 5)) {
1433
+ console.log(` ${cyan(r.id)} - ${r.description || ""}`);
1434
+ }
1435
+ console.log("");
1436
+ }
1437
+ console.log(dim(`Use "decantr get pattern <id>" for full details`));
1438
+ }
906
1439
  async function cmdGet(type, id) {
907
1440
  const validTypes = ["pattern", "archetype", "recipe", "theme", "blueprint"];
908
1441
  if (!validTypes.includes(type)) {
@@ -931,11 +1464,58 @@ async function cmdGet(type, id) {
931
1464
  }
932
1465
  console.log(JSON.stringify(result.item, null, 2));
933
1466
  }
1467
+ function buildRegistryContext() {
1468
+ const { readdirSync: readdirSync3 } = __require("fs");
1469
+ const themeRegistry = /* @__PURE__ */ new Map();
1470
+ const patternRegistry = /* @__PURE__ */ new Map();
1471
+ const contentRoot = getContentRoot();
1472
+ const themeDirs = [join5(contentRoot, "themes"), join5(contentRoot, "core", "themes")];
1473
+ for (const dir of themeDirs) {
1474
+ try {
1475
+ if (existsSync5(dir)) {
1476
+ for (const f of readdirSync3(dir).filter((f2) => f2.endsWith(".json"))) {
1477
+ const data = JSON.parse(readFileSync5(join5(dir, f), "utf-8"));
1478
+ if (data.id && !themeRegistry.has(data.id)) {
1479
+ themeRegistry.set(data.id, { modes: data.modes || ["light", "dark"] });
1480
+ }
1481
+ }
1482
+ }
1483
+ } catch {
1484
+ }
1485
+ }
1486
+ const customThemesDir = join5(process.cwd(), ".decantr", "custom", "themes");
1487
+ try {
1488
+ if (existsSync5(customThemesDir)) {
1489
+ for (const f of readdirSync3(customThemesDir).filter((f2) => f2.endsWith(".json"))) {
1490
+ const data = JSON.parse(readFileSync5(join5(customThemesDir, f), "utf-8"));
1491
+ if (data.id) {
1492
+ themeRegistry.set(`custom:${data.id}`, { modes: data.modes || ["light", "dark"] });
1493
+ }
1494
+ }
1495
+ }
1496
+ } catch {
1497
+ }
1498
+ const patternDirs = [join5(contentRoot, "patterns"), join5(contentRoot, "core", "patterns")];
1499
+ for (const dir of patternDirs) {
1500
+ try {
1501
+ if (existsSync5(dir)) {
1502
+ for (const f of readdirSync3(dir).filter((f2) => f2.endsWith(".json"))) {
1503
+ const data = JSON.parse(readFileSync5(join5(dir, f), "utf-8"));
1504
+ if (data.id && !patternRegistry.has(data.id)) {
1505
+ patternRegistry.set(data.id, data);
1506
+ }
1507
+ }
1508
+ }
1509
+ } catch {
1510
+ }
1511
+ }
1512
+ return { themeRegistry, patternRegistry };
1513
+ }
934
1514
  async function cmdValidate(path) {
935
- const essencePath = path || join4(process.cwd(), "decantr.essence.json");
1515
+ const essencePath = path || join5(process.cwd(), "decantr.essence.json");
936
1516
  let raw;
937
1517
  try {
938
- raw = readFileSync4(essencePath, "utf-8");
1518
+ raw = readFileSync5(essencePath, "utf-8");
939
1519
  } catch {
940
1520
  console.error(error(`Could not read ${essencePath}`));
941
1521
  process.exitCode = 1;
@@ -960,12 +1540,16 @@ async function cmdValidate(path) {
960
1540
  process.exitCode = 1;
961
1541
  }
962
1542
  try {
963
- const violations = evaluateGuard(essence, {});
1543
+ const { themeRegistry, patternRegistry } = buildRegistryContext();
1544
+ const violations = evaluateGuard(essence, { themeRegistry, patternRegistry });
964
1545
  if (violations.length > 0) {
965
1546
  console.log(heading("Guard violations:"));
966
1547
  for (const v of violations) {
967
1548
  const vr = v;
968
1549
  console.log(` ${YELLOW2}[${vr.rule}]${RESET2} ${vr.message}`);
1550
+ if (vr.suggestion) {
1551
+ console.log(` ${DIM2}Suggestion: ${vr.suggestion}${RESET2}`);
1552
+ }
969
1553
  }
970
1554
  } else if (result.valid) {
971
1555
  console.log(success("No guard violations."));
@@ -980,36 +1564,98 @@ async function cmdList(type) {
980
1564
  process.exitCode = 1;
981
1565
  return;
982
1566
  }
983
- const { readdirSync: readdirSync2 } = await import("fs");
984
- const dir = join4(getContentRoot(), type);
985
- let found = false;
1567
+ const { readdirSync: readdirSync3, existsSync: existsSync6 } = await import("fs");
1568
+ const contentRoot = getContentRoot();
1569
+ const mainDir = join5(contentRoot, type);
1570
+ const coreDir = join5(contentRoot, "core", type);
1571
+ const items = [];
986
1572
  try {
987
- const files = readdirSync2(dir).filter((f) => f.endsWith(".json"));
988
- if (files.length > 0) {
989
- found = true;
990
- console.log(heading(`${files.length} ${type}`));
1573
+ if (existsSync6(mainDir)) {
1574
+ const files = readdirSync3(mainDir).filter((f) => f.endsWith(".json"));
991
1575
  for (const f of files) {
992
- const data = JSON.parse(readFileSync4(join4(dir, f), "utf-8"));
993
- console.log(` ${cyan(data.id || f.replace(".json", ""))} ${dim(data.description || data.name || "")}`);
1576
+ const data = JSON.parse(readFileSync5(join5(mainDir, f), "utf-8"));
1577
+ items.push({ id: data.id || f.replace(".json", ""), description: data.description, name: data.name });
994
1578
  }
995
1579
  }
996
1580
  } catch {
997
1581
  }
998
- if (!found) {
1582
+ try {
1583
+ if (existsSync6(coreDir)) {
1584
+ const files = readdirSync3(coreDir).filter((f) => f.endsWith(".json"));
1585
+ const existingIds = new Set(items.map((i) => i.id));
1586
+ for (const f of files) {
1587
+ const data = JSON.parse(readFileSync5(join5(coreDir, f), "utf-8"));
1588
+ const itemId = data.id || f.replace(".json", "");
1589
+ if (!existingIds.has(itemId)) {
1590
+ items.push({ id: itemId, description: data.description, name: data.name });
1591
+ }
1592
+ }
1593
+ }
1594
+ } catch {
1595
+ }
1596
+ const customItems = [];
1597
+ if (type === "themes") {
999
1598
  try {
1000
- const res = await fetch(`https://decantr-registry.fly.dev/v1/${type}`);
1001
- if (res.ok) {
1002
- const data = await res.json();
1599
+ const custom = listCustomThemes(process.cwd());
1600
+ for (const theme of custom) {
1601
+ customItems.push({
1602
+ id: `custom:${theme.id}`,
1603
+ description: theme.description,
1604
+ name: theme.name,
1605
+ source: "custom"
1606
+ });
1607
+ }
1608
+ } catch {
1609
+ }
1610
+ }
1611
+ if (items.length > 0 || customItems.length > 0) {
1612
+ if (type === "themes") {
1613
+ console.log(heading(`Registry themes (${items.length}):`));
1614
+ for (const item of items) {
1615
+ console.log(` ${cyan(item.id)} ${dim(item.description || item.name || "")}`);
1616
+ }
1617
+ if (customItems.length > 0) {
1618
+ console.log("");
1619
+ console.log(heading(`Custom themes (${customItems.length}):`));
1620
+ for (const item of customItems) {
1621
+ console.log(` ${cyan(item.id)} ${dim(item.description || item.name || "")}`);
1622
+ }
1623
+ } else {
1624
+ console.log("");
1625
+ console.log(dim("Custom themes (0):"));
1626
+ console.log(dim(' Run "decantr theme create <name>" to create a custom theme.'));
1627
+ }
1628
+ } else {
1629
+ console.log(heading(`${items.length} ${type}`));
1630
+ for (const item of items) {
1631
+ console.log(` ${cyan(item.id)} ${dim(item.description || item.name || "")}`);
1632
+ }
1633
+ }
1634
+ return;
1635
+ }
1636
+ try {
1637
+ const res = await fetch(`https://decantr-registry.fly.dev/v1/${type}`);
1638
+ if (res.ok) {
1639
+ const data = await res.json();
1640
+ if (type === "themes") {
1641
+ console.log(heading(`Registry themes (${data.total}):`));
1642
+ for (const item of data.items) {
1643
+ console.log(` ${cyan(item.id)} ${dim(item.description || item.name || "")}`);
1644
+ }
1645
+ console.log("");
1646
+ console.log(dim("Custom themes (0):"));
1647
+ console.log(dim(' Run "decantr theme create <name>" to create a custom theme.'));
1648
+ } else {
1003
1649
  console.log(heading(`${data.total} ${type}`));
1004
1650
  for (const item of data.items) {
1005
1651
  console.log(` ${cyan(item.id)} ${dim(item.description || item.name || "")}`);
1006
1652
  }
1007
- return;
1008
1653
  }
1009
- } catch {
1654
+ return;
1010
1655
  }
1011
- console.log(dim(`No ${type} found.`));
1656
+ } catch {
1012
1657
  }
1658
+ console.log(dim(`No ${type} found.`));
1013
1659
  }
1014
1660
  async function cmdInit(args) {
1015
1661
  const projectRoot = process.cwd();
@@ -1024,7 +1670,7 @@ async function cmdInit(args) {
1024
1670
  }
1025
1671
  }
1026
1672
  const registryClient = new RegistryClient({
1027
- cacheDir: join4(projectRoot, ".decantr", "cache"),
1673
+ cacheDir: join5(projectRoot, ".decantr", "cache"),
1028
1674
  apiUrl: args.registry,
1029
1675
  offline: args.offline
1030
1676
  });
@@ -1068,13 +1714,26 @@ async function cmdInit(args) {
1068
1714
  archetypeData = archetypeResult.data;
1069
1715
  }
1070
1716
  }
1717
+ let themeData;
1718
+ let recipeData;
1719
+ if (options.theme) {
1720
+ const themeResult = await registryClient.fetchTheme(options.theme);
1721
+ if (themeResult) {
1722
+ const theme = themeResult.data;
1723
+ if (theme.seed) {
1724
+ themeData = { seed: theme.seed };
1725
+ }
1726
+ }
1727
+ }
1071
1728
  console.log(heading("Scaffolding project..."));
1072
1729
  const result = scaffoldProject(
1073
1730
  projectRoot,
1074
1731
  options,
1075
1732
  detected,
1076
1733
  archetypeData,
1077
- registrySource
1734
+ registrySource,
1735
+ themeData,
1736
+ recipeData
1078
1737
  );
1079
1738
  console.log(success("\nProject scaffolded successfully!"));
1080
1739
  console.log("");
@@ -1085,7 +1744,7 @@ async function cmdInit(args) {
1085
1744
  if (result.gitignoreUpdated) {
1086
1745
  console.log(` ${dim(".gitignore updated to exclude .decantr/cache/")}`);
1087
1746
  }
1088
- const essenceContent = readFileSync4(result.essencePath, "utf-8");
1747
+ const essenceContent = readFileSync5(result.essencePath, "utf-8");
1089
1748
  const essence = JSON.parse(essenceContent);
1090
1749
  const validation = validateEssence(essence);
1091
1750
  if (validation.valid) {
@@ -1096,25 +1755,39 @@ Validation warnings: ${validation.errors.join(", ")}`));
1096
1755
  }
1097
1756
  console.log(heading("Next steps"));
1098
1757
  console.log("1. Review DECANTR.md to understand the methodology");
1099
- console.log("2. Share DECANTR.md with your AI assistant");
1758
+ console.log("2. Copy the prompt below and share it with your AI assistant");
1100
1759
  console.log("3. Start building! The AI will follow the essence spec.");
1101
1760
  console.log("");
1761
+ const promptCtx = {
1762
+ archetype: options.archetype || "custom",
1763
+ blueprint: options.blueprint,
1764
+ theme: options.theme,
1765
+ mode: options.mode,
1766
+ target: options.target,
1767
+ pages: essence.structure || [{ id: "home", shell: options.shell, layout: ["hero"] }],
1768
+ personality: options.personality,
1769
+ features: options.features,
1770
+ guard: options.guard
1771
+ };
1772
+ const curatedPrompt = generateCuratedPrompt(promptCtx);
1773
+ console.log(boxedPrompt(curatedPrompt, "Copy this prompt for your AI assistant"));
1774
+ console.log("");
1102
1775
  if (registrySource === "bundled") {
1103
1776
  console.log(dim('Run "decantr sync" when online to get the latest registry content.'));
1104
1777
  }
1105
1778
  }
1106
1779
  async function cmdStatus() {
1107
1780
  const projectRoot = process.cwd();
1108
- const essencePath = join4(projectRoot, "decantr.essence.json");
1109
- const projectJsonPath = join4(projectRoot, ".decantr", "project.json");
1781
+ const essencePath = join5(projectRoot, "decantr.essence.json");
1782
+ const projectJsonPath = join5(projectRoot, ".decantr", "project.json");
1110
1783
  console.log(heading("Decantr Project Status"));
1111
- if (!existsSync4(essencePath)) {
1784
+ if (!existsSync5(essencePath)) {
1112
1785
  console.log(`${RED}No decantr.essence.json found.${RESET2}`);
1113
1786
  console.log(dim('Run "decantr init" to create one.'));
1114
1787
  return;
1115
1788
  }
1116
1789
  try {
1117
- const essence = JSON.parse(readFileSync4(essencePath, "utf-8"));
1790
+ const essence = JSON.parse(readFileSync5(essencePath, "utf-8"));
1118
1791
  const validation = validateEssence(essence);
1119
1792
  console.log(`${BOLD2}Essence:${RESET2}`);
1120
1793
  if (validation.valid) {
@@ -1130,9 +1803,9 @@ async function cmdStatus() {
1130
1803
  }
1131
1804
  console.log("");
1132
1805
  console.log(`${BOLD2}Sync Status:${RESET2}`);
1133
- if (existsSync4(projectJsonPath)) {
1806
+ if (existsSync5(projectJsonPath)) {
1134
1807
  try {
1135
- const projectJson = JSON.parse(readFileSync4(projectJsonPath, "utf-8"));
1808
+ const projectJson = JSON.parse(readFileSync5(projectJsonPath, "utf-8"));
1136
1809
  const syncStatus = projectJson.sync?.status || "unknown";
1137
1810
  const lastSync = projectJson.sync?.lastSync || "never";
1138
1811
  const source = projectJson.sync?.registrySource || "unknown";
@@ -1150,7 +1823,7 @@ async function cmdStatus() {
1150
1823
  }
1151
1824
  async function cmdSync() {
1152
1825
  const projectRoot = process.cwd();
1153
- const cacheDir = join4(projectRoot, ".decantr", "cache");
1826
+ const cacheDir = join5(projectRoot, ".decantr", "cache");
1154
1827
  console.log(heading("Syncing registry content..."));
1155
1828
  const result = await syncRegistry(cacheDir);
1156
1829
  if (result.source === "api") {
@@ -1168,15 +1841,15 @@ async function cmdSync() {
1168
1841
  }
1169
1842
  async function cmdAudit() {
1170
1843
  const projectRoot = process.cwd();
1171
- const essencePath = join4(projectRoot, "decantr.essence.json");
1844
+ const essencePath = join5(projectRoot, "decantr.essence.json");
1172
1845
  console.log(heading("Auditing project..."));
1173
- if (!existsSync4(essencePath)) {
1846
+ if (!existsSync5(essencePath)) {
1174
1847
  console.log(`${RED}No decantr.essence.json found.${RESET2}`);
1175
1848
  process.exitCode = 1;
1176
1849
  return;
1177
1850
  }
1178
1851
  try {
1179
- const essence = JSON.parse(readFileSync4(essencePath, "utf-8"));
1852
+ const essence = JSON.parse(readFileSync5(essencePath, "utf-8"));
1180
1853
  const validation = validateEssence(essence);
1181
1854
  if (!validation.valid) {
1182
1855
  console.log(`${RED}Essence validation failed:${RESET2}`);
@@ -1187,13 +1860,17 @@ async function cmdAudit() {
1187
1860
  return;
1188
1861
  }
1189
1862
  console.log(success("Essence is valid."));
1190
- const violations = evaluateGuard(essence, {});
1863
+ const { themeRegistry, patternRegistry } = buildRegistryContext();
1864
+ const violations = evaluateGuard(essence, { themeRegistry, patternRegistry });
1191
1865
  if (violations.length > 0) {
1192
1866
  console.log("");
1193
1867
  console.log(`${YELLOW2}Guard violations:${RESET2}`);
1194
1868
  for (const v of violations) {
1195
1869
  const vr = v;
1196
1870
  console.log(` ${YELLOW2}[${vr.rule}]${RESET2} ${vr.message}`);
1871
+ if (vr.suggestion) {
1872
+ console.log(` ${DIM2}Suggestion: ${vr.suggestion}${RESET2}`);
1873
+ }
1197
1874
  }
1198
1875
  } else {
1199
1876
  console.log(success("No guard violations."));
@@ -1208,6 +1885,135 @@ async function cmdAudit() {
1208
1885
  process.exitCode = 1;
1209
1886
  }
1210
1887
  }
1888
+ async function cmdTheme(args) {
1889
+ const subcommand = args[0];
1890
+ const projectRoot = process.cwd();
1891
+ if (!subcommand || subcommand === "help") {
1892
+ console.log(`
1893
+ ${BOLD2}decantr theme${RESET2} \u2014 Manage custom themes
1894
+
1895
+ ${BOLD2}Commands:${RESET2}
1896
+ ${cyan("create")} <name> Create a new custom theme
1897
+ ${cyan("create")} <name> --guided Interactive theme creation
1898
+ ${cyan("list")} List custom themes
1899
+ ${cyan("validate")} <name> Validate a custom theme
1900
+ ${cyan("delete")} <name> Delete a custom theme
1901
+ ${cyan("import")} <path> Import theme from JSON file
1902
+
1903
+ ${BOLD2}Examples:${RESET2}
1904
+ decantr theme create mytheme
1905
+ decantr theme list
1906
+ decantr theme validate mytheme
1907
+ decantr theme import ./external-theme.json
1908
+ `);
1909
+ return;
1910
+ }
1911
+ switch (subcommand) {
1912
+ case "create": {
1913
+ const name = args[1];
1914
+ if (!name) {
1915
+ console.error(error("Usage: decantr theme create <name>"));
1916
+ process.exitCode = 1;
1917
+ return;
1918
+ }
1919
+ const displayName = name.charAt(0).toUpperCase() + name.slice(1).replace(/-/g, " ");
1920
+ const result = createTheme(projectRoot, name, displayName);
1921
+ if (result.success) {
1922
+ console.log(success(`Created custom theme "${name}"`));
1923
+ console.log(dim(` Path: ${result.path}`));
1924
+ console.log("");
1925
+ console.log(`Use in essence: ${cyan(`"style": "custom:${name}"`)}`);
1926
+ } else {
1927
+ console.error(error(result.error || "Failed to create theme"));
1928
+ process.exitCode = 1;
1929
+ }
1930
+ break;
1931
+ }
1932
+ case "list": {
1933
+ const themes = listCustomThemes(projectRoot);
1934
+ if (themes.length === 0) {
1935
+ console.log(dim("No custom themes found."));
1936
+ console.log(dim('Run "decantr theme create <name>" to create one.'));
1937
+ } else {
1938
+ console.log(heading(`${themes.length} custom theme(s)`));
1939
+ for (const theme of themes) {
1940
+ console.log(` ${cyan(`custom:${theme.id}`)} ${dim(theme.description || theme.name)}`);
1941
+ }
1942
+ }
1943
+ break;
1944
+ }
1945
+ case "validate": {
1946
+ const name = args[1];
1947
+ if (!name) {
1948
+ console.error(error("Usage: decantr theme validate <name>"));
1949
+ process.exitCode = 1;
1950
+ return;
1951
+ }
1952
+ const themePath = join5(projectRoot, ".decantr", "custom", "themes", `${name}.json`);
1953
+ if (!existsSync5(themePath)) {
1954
+ console.error(error(`Theme "${name}" not found at ${themePath}`));
1955
+ process.exitCode = 1;
1956
+ return;
1957
+ }
1958
+ try {
1959
+ const theme = JSON.parse(readFileSync5(themePath, "utf-8"));
1960
+ const result = validateCustomTheme(theme);
1961
+ if (result.valid) {
1962
+ console.log(success(`Custom theme "${name}" is valid`));
1963
+ } else {
1964
+ console.error(error("Validation failed:"));
1965
+ for (const err of result.errors) {
1966
+ console.error(` ${RED}${err}${RESET2}`);
1967
+ }
1968
+ process.exitCode = 1;
1969
+ }
1970
+ } catch (e) {
1971
+ console.error(error(`Invalid JSON: ${e.message}`));
1972
+ process.exitCode = 1;
1973
+ }
1974
+ break;
1975
+ }
1976
+ case "delete": {
1977
+ const name = args[1];
1978
+ if (!name) {
1979
+ console.error(error("Usage: decantr theme delete <name>"));
1980
+ process.exitCode = 1;
1981
+ return;
1982
+ }
1983
+ const result = deleteTheme(projectRoot, name);
1984
+ if (result.success) {
1985
+ console.log(success(`Deleted custom theme "${name}"`));
1986
+ } else {
1987
+ console.error(error(result.error || "Failed to delete theme"));
1988
+ process.exitCode = 1;
1989
+ }
1990
+ break;
1991
+ }
1992
+ case "import": {
1993
+ const sourcePath = args[1];
1994
+ if (!sourcePath) {
1995
+ console.error(error("Usage: decantr theme import <path>"));
1996
+ process.exitCode = 1;
1997
+ return;
1998
+ }
1999
+ const result = importTheme(projectRoot, sourcePath);
2000
+ if (result.success) {
2001
+ console.log(success("Theme imported successfully"));
2002
+ console.log(dim(` Path: ${result.path}`));
2003
+ } else {
2004
+ console.error(error("Import failed:"));
2005
+ for (const err of result.errors || []) {
2006
+ console.error(` ${RED}${err}${RESET2}`);
2007
+ }
2008
+ process.exitCode = 1;
2009
+ }
2010
+ break;
2011
+ }
2012
+ default:
2013
+ console.error(error(`Unknown theme command: ${subcommand}`));
2014
+ process.exitCode = 1;
2015
+ }
2016
+ }
1211
2017
  function cmdHelp() {
1212
2018
  console.log(`
1213
2019
  ${BOLD2}decantr${RESET2} \u2014 Design intelligence for AI-generated UI
@@ -1218,9 +2024,11 @@ ${BOLD2}Usage:${RESET2}
1218
2024
  decantr sync
1219
2025
  decantr audit
1220
2026
  decantr search <query> [--type <type>]
2027
+ decantr suggest <query> [--type <type>]
1221
2028
  decantr get <type> <id>
1222
2029
  decantr list <type>
1223
2030
  decantr validate [path]
2031
+ decantr theme <subcommand>
1224
2032
  decantr help
1225
2033
 
1226
2034
  ${BOLD2}Init Options:${RESET2}
@@ -1243,9 +2051,11 @@ ${BOLD2}Commands:${RESET2}
1243
2051
  ${cyan("sync")} Sync registry content from API
1244
2052
  ${cyan("audit")} Validate essence and check for drift
1245
2053
  ${cyan("search")} Search the registry
2054
+ ${cyan("suggest")} Suggest patterns or alternatives for a query
1246
2055
  ${cyan("get")} Get full details of a registry item
1247
2056
  ${cyan("list")} List items by type
1248
2057
  ${cyan("validate")} Validate essence file
2058
+ ${cyan("theme")} Manage custom themes (create, list, validate, delete, import)
1249
2059
  ${cyan("help")} Show this help
1250
2060
 
1251
2061
  ${BOLD2}Examples:${RESET2}
@@ -1255,6 +2065,8 @@ ${BOLD2}Examples:${RESET2}
1255
2065
  decantr sync
1256
2066
  decantr audit
1257
2067
  decantr search dashboard
2068
+ decantr suggest leaderboard
2069
+ decantr suggest ranking --type pattern
1258
2070
  decantr list patterns
1259
2071
  `);
1260
2072
  }
@@ -1316,6 +2128,18 @@ async function main() {
1316
2128
  await cmdSearch(query, type);
1317
2129
  break;
1318
2130
  }
2131
+ case "suggest": {
2132
+ const query = args[1];
2133
+ if (!query) {
2134
+ console.error(error("Usage: decantr suggest <query> [--type <type>]"));
2135
+ process.exitCode = 1;
2136
+ return;
2137
+ }
2138
+ const typeIdx = args.indexOf("--type");
2139
+ const type = typeIdx !== -1 ? args[typeIdx + 1] : void 0;
2140
+ await cmdSuggest(query, type);
2141
+ break;
2142
+ }
1319
2143
  case "get": {
1320
2144
  const type = args[1];
1321
2145
  const id = args[2];
@@ -1341,6 +2165,10 @@ async function main() {
1341
2165
  await cmdValidate(args[1]);
1342
2166
  break;
1343
2167
  }
2168
+ case "theme": {
2169
+ await cmdTheme(args.slice(1));
2170
+ break;
2171
+ }
1344
2172
  default:
1345
2173
  console.error(error(`Unknown command: ${command}`));
1346
2174
  cmdHelp();