@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 +903 -75
- package/package.json +1 -1
- package/src/templates/DECANTR.md.template +142 -3
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
|
|
5
|
-
import { join as
|
|
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
|
-
|
|
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
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
const
|
|
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
|
|
649
|
-
if (
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
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
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
const
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
}
|
|
672
|
-
}
|
|
673
|
-
|
|
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.
|
|
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 =
|
|
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 ||
|
|
1515
|
+
const essencePath = path || join5(process.cwd(), "decantr.essence.json");
|
|
936
1516
|
let raw;
|
|
937
1517
|
try {
|
|
938
|
-
raw =
|
|
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
|
|
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:
|
|
984
|
-
const
|
|
985
|
-
|
|
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
|
-
|
|
988
|
-
|
|
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(
|
|
993
|
-
|
|
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
|
-
|
|
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
|
|
1001
|
-
|
|
1002
|
-
|
|
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
|
-
|
|
1654
|
+
return;
|
|
1010
1655
|
}
|
|
1011
|
-
|
|
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:
|
|
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 =
|
|
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.
|
|
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 =
|
|
1109
|
-
const projectJsonPath =
|
|
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 (!
|
|
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(
|
|
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 (
|
|
1806
|
+
if (existsSync5(projectJsonPath)) {
|
|
1134
1807
|
try {
|
|
1135
|
-
const projectJson = JSON.parse(
|
|
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 =
|
|
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 =
|
|
1844
|
+
const essencePath = join5(projectRoot, "decantr.essence.json");
|
|
1172
1845
|
console.log(heading("Auditing project..."));
|
|
1173
|
-
if (!
|
|
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(
|
|
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
|
|
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();
|