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