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