@decantr/cli 1.0.0 → 1.1.1
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/bin.js +4 -0
- package/dist/{chunk-PDX44BCA.js → chunk-3RG5ZIWI.js} +0 -1
- package/dist/chunk-CT5XHG6I.js +203 -0
- package/dist/chunk-LXOY447U.js +2422 -0
- package/dist/{heal-2OPN63OT.js → heal-4DWU7BJS.js} +1 -2
- package/dist/index.js +3 -1965
- package/dist/{upgrade-FWICWIQW.js → upgrade-3YL3NFOG.js} +2 -3
- package/package.json +7 -7
- package/src/templates/DECANTR.md.template +53 -53
- package/LICENSE +0 -21
- package/dist/chunk-PWTUBGGJ.js +0 -359
|
@@ -0,0 +1,2422 @@
|
|
|
1
|
+
import {
|
|
2
|
+
RegistryClient,
|
|
3
|
+
syncRegistry
|
|
4
|
+
} from "./chunk-CT5XHG6I.js";
|
|
5
|
+
import {
|
|
6
|
+
__require
|
|
7
|
+
} from "./chunk-3RG5ZIWI.js";
|
|
8
|
+
|
|
9
|
+
// src/index.ts
|
|
10
|
+
import { readFileSync as readFileSync6, existsSync as existsSync7 } from "fs";
|
|
11
|
+
import { join as join7, dirname as dirname2 } from "path";
|
|
12
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
13
|
+
import { validateEssence, evaluateGuard } from "@decantr/essence-spec";
|
|
14
|
+
import { RegistryAPIClient as RegistryAPIClient2 } from "@decantr/registry";
|
|
15
|
+
|
|
16
|
+
// src/detect.ts
|
|
17
|
+
import { existsSync, readFileSync } from "fs";
|
|
18
|
+
import { join } from "path";
|
|
19
|
+
var RULE_FILES = [
|
|
20
|
+
"CLAUDE.md",
|
|
21
|
+
".cursorrules",
|
|
22
|
+
".cursor/rules",
|
|
23
|
+
"AGENTS.md",
|
|
24
|
+
"GEMINI.md",
|
|
25
|
+
"copilot-instructions.md"
|
|
26
|
+
];
|
|
27
|
+
function detectProject(projectRoot = process.cwd()) {
|
|
28
|
+
const result = {
|
|
29
|
+
framework: "unknown",
|
|
30
|
+
packageManager: "unknown",
|
|
31
|
+
hasTypeScript: false,
|
|
32
|
+
hasTailwind: false,
|
|
33
|
+
existingRuleFiles: [],
|
|
34
|
+
existingEssence: false,
|
|
35
|
+
projectRoot
|
|
36
|
+
};
|
|
37
|
+
result.existingEssence = existsSync(join(projectRoot, "decantr.essence.json"));
|
|
38
|
+
for (const ruleFile of RULE_FILES) {
|
|
39
|
+
if (existsSync(join(projectRoot, ruleFile))) {
|
|
40
|
+
result.existingRuleFiles.push(ruleFile);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (existsSync(join(projectRoot, "pnpm-lock.yaml"))) {
|
|
44
|
+
result.packageManager = "pnpm";
|
|
45
|
+
} else if (existsSync(join(projectRoot, "yarn.lock"))) {
|
|
46
|
+
result.packageManager = "yarn";
|
|
47
|
+
} else if (existsSync(join(projectRoot, "bun.lockb"))) {
|
|
48
|
+
result.packageManager = "bun";
|
|
49
|
+
} else if (existsSync(join(projectRoot, "package-lock.json"))) {
|
|
50
|
+
result.packageManager = "npm";
|
|
51
|
+
}
|
|
52
|
+
result.hasTypeScript = existsSync(join(projectRoot, "tsconfig.json"));
|
|
53
|
+
result.hasTailwind = existsSync(join(projectRoot, "tailwind.config.js")) || existsSync(join(projectRoot, "tailwind.config.ts")) || existsSync(join(projectRoot, "tailwind.config.mjs")) || existsSync(join(projectRoot, "tailwind.config.cjs"));
|
|
54
|
+
if (existsSync(join(projectRoot, "next.config.js")) || existsSync(join(projectRoot, "next.config.ts")) || existsSync(join(projectRoot, "next.config.mjs"))) {
|
|
55
|
+
result.framework = "nextjs";
|
|
56
|
+
result.version = getPackageVersion(projectRoot, "next");
|
|
57
|
+
return result;
|
|
58
|
+
}
|
|
59
|
+
if (existsSync(join(projectRoot, "nuxt.config.js")) || existsSync(join(projectRoot, "nuxt.config.ts"))) {
|
|
60
|
+
result.framework = "nuxt";
|
|
61
|
+
result.version = getPackageVersion(projectRoot, "nuxt");
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
if (existsSync(join(projectRoot, "astro.config.mjs")) || existsSync(join(projectRoot, "astro.config.ts"))) {
|
|
65
|
+
result.framework = "astro";
|
|
66
|
+
result.version = getPackageVersion(projectRoot, "astro");
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
69
|
+
if (existsSync(join(projectRoot, "svelte.config.js")) || existsSync(join(projectRoot, "svelte.config.ts"))) {
|
|
70
|
+
result.framework = "svelte";
|
|
71
|
+
result.version = getPackageVersion(projectRoot, "svelte");
|
|
72
|
+
return result;
|
|
73
|
+
}
|
|
74
|
+
if (existsSync(join(projectRoot, "angular.json"))) {
|
|
75
|
+
result.framework = "angular";
|
|
76
|
+
result.version = getPackageVersion(projectRoot, "@angular/core");
|
|
77
|
+
return result;
|
|
78
|
+
}
|
|
79
|
+
const packageJsonPath = join(projectRoot, "package.json");
|
|
80
|
+
if (existsSync(packageJsonPath)) {
|
|
81
|
+
try {
|
|
82
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
83
|
+
const deps = { ...packageJson.dependencies, ...packageJson.devDependencies };
|
|
84
|
+
if (deps.next) {
|
|
85
|
+
result.framework = "nextjs";
|
|
86
|
+
result.version = deps.next.replace(/^\^|~/, "");
|
|
87
|
+
} else if (deps.nuxt) {
|
|
88
|
+
result.framework = "nuxt";
|
|
89
|
+
result.version = deps.nuxt.replace(/^\^|~/, "");
|
|
90
|
+
} else if (deps.astro) {
|
|
91
|
+
result.framework = "astro";
|
|
92
|
+
result.version = deps.astro.replace(/^\^|~/, "");
|
|
93
|
+
} else if (deps.svelte) {
|
|
94
|
+
result.framework = "svelte";
|
|
95
|
+
result.version = deps.svelte.replace(/^\^|~/, "");
|
|
96
|
+
} else if (deps["@angular/core"]) {
|
|
97
|
+
result.framework = "angular";
|
|
98
|
+
result.version = deps["@angular/core"].replace(/^\^|~/, "");
|
|
99
|
+
} else if (deps.vue) {
|
|
100
|
+
result.framework = "vue";
|
|
101
|
+
result.version = deps.vue.replace(/^\^|~/, "");
|
|
102
|
+
} else if (deps.react) {
|
|
103
|
+
result.framework = "react";
|
|
104
|
+
result.version = deps.react.replace(/^\^|~/, "");
|
|
105
|
+
}
|
|
106
|
+
} catch {
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (result.framework === "unknown" && !existsSync(packageJsonPath)) {
|
|
110
|
+
if (existsSync(join(projectRoot, "index.html"))) {
|
|
111
|
+
result.framework = "html";
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return result;
|
|
115
|
+
}
|
|
116
|
+
function getPackageVersion(projectRoot, packageName) {
|
|
117
|
+
const packageJsonPath = join(projectRoot, "package.json");
|
|
118
|
+
if (!existsSync(packageJsonPath)) return void 0;
|
|
119
|
+
try {
|
|
120
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
121
|
+
const deps = { ...packageJson.dependencies, ...packageJson.devDependencies };
|
|
122
|
+
const version = deps[packageName];
|
|
123
|
+
return version?.replace(/^\^|~/, "");
|
|
124
|
+
} catch {
|
|
125
|
+
return void 0;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// src/prompts.ts
|
|
130
|
+
import { createInterface } from "readline";
|
|
131
|
+
var BOLD = "\x1B[1m";
|
|
132
|
+
var DIM = "\x1B[2m";
|
|
133
|
+
var RESET = "\x1B[0m";
|
|
134
|
+
var GREEN = "\x1B[32m";
|
|
135
|
+
var YELLOW = "\x1B[33m";
|
|
136
|
+
var CYAN = "\x1B[36m";
|
|
137
|
+
function ask(question, defaultValue) {
|
|
138
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
139
|
+
const prompt = defaultValue ? `${question} ${DIM}(${defaultValue})${RESET}: ` : `${question}: `;
|
|
140
|
+
return new Promise((resolve) => {
|
|
141
|
+
rl.question(prompt, (answer) => {
|
|
142
|
+
rl.close();
|
|
143
|
+
resolve(answer.trim() || defaultValue || "");
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
async function select(question, options, defaultIdx = 0, allowOther = false) {
|
|
148
|
+
console.log(`
|
|
149
|
+
${BOLD}${question}${RESET}`);
|
|
150
|
+
for (let i = 0; i < options.length; i++) {
|
|
151
|
+
const marker = i === defaultIdx ? `${GREEN}>${RESET}` : " ";
|
|
152
|
+
const desc = options[i].description ? ` ${DIM}\u2014 ${options[i].description}${RESET}` : "";
|
|
153
|
+
console.log(` ${marker} ${i + 1}. ${options[i].label}${desc}`);
|
|
154
|
+
}
|
|
155
|
+
if (allowOther) {
|
|
156
|
+
console.log(` ${options.length + 1}. ${DIM}other (enter custom value)${RESET}`);
|
|
157
|
+
}
|
|
158
|
+
const maxIdx = allowOther ? options.length + 1 : options.length;
|
|
159
|
+
const answer = await ask(`Choose (1-${maxIdx})`, String(defaultIdx + 1));
|
|
160
|
+
const idx = parseInt(answer, 10) - 1;
|
|
161
|
+
if (allowOther && idx === options.length) {
|
|
162
|
+
const custom = await ask("Enter custom value");
|
|
163
|
+
return custom;
|
|
164
|
+
}
|
|
165
|
+
const validIdx = Math.max(0, Math.min(idx, options.length - 1));
|
|
166
|
+
return options[validIdx].value;
|
|
167
|
+
}
|
|
168
|
+
async function confirm(question, defaultYes = true) {
|
|
169
|
+
const hint = defaultYes ? "Y/n" : "y/N";
|
|
170
|
+
const answer = await ask(`${question} [${hint}]`, defaultYes ? "y" : "n");
|
|
171
|
+
return answer.toLowerCase() === "y" || answer.toLowerCase() === "yes";
|
|
172
|
+
}
|
|
173
|
+
function warn(message) {
|
|
174
|
+
console.log(`
|
|
175
|
+
${YELLOW} Warning: ${message}${RESET}`);
|
|
176
|
+
}
|
|
177
|
+
function showDetection(detected) {
|
|
178
|
+
console.log(`
|
|
179
|
+
${CYAN}Detected project configuration:${RESET}`);
|
|
180
|
+
if (detected.framework !== "unknown") {
|
|
181
|
+
const version = detected.version ? ` ${detected.version}` : "";
|
|
182
|
+
console.log(` Framework: ${detected.framework}${version}`);
|
|
183
|
+
}
|
|
184
|
+
if (detected.packageManager !== "unknown") {
|
|
185
|
+
console.log(` Package manager: ${detected.packageManager}`);
|
|
186
|
+
}
|
|
187
|
+
if (detected.hasTypeScript) {
|
|
188
|
+
console.log(` TypeScript: ${GREEN}yes${RESET}`);
|
|
189
|
+
}
|
|
190
|
+
if (detected.hasTailwind) {
|
|
191
|
+
console.log(` Tailwind CSS: ${GREEN}yes${RESET}`);
|
|
192
|
+
}
|
|
193
|
+
if (detected.existingEssence) {
|
|
194
|
+
console.log(` Existing essence: ${YELLOW}yes${RESET}`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
async function runInteractivePrompts(detected, archetypes, blueprints, themes) {
|
|
198
|
+
showDetection(detected);
|
|
199
|
+
const blueprintOptions = [
|
|
200
|
+
{ value: "none", label: "none", description: "Start from scratch (blank canvas)" },
|
|
201
|
+
...blueprints.map((b) => ({
|
|
202
|
+
value: b.id,
|
|
203
|
+
label: b.id,
|
|
204
|
+
description: b.description
|
|
205
|
+
}))
|
|
206
|
+
];
|
|
207
|
+
const blueprint = await select("What are you building?", blueprintOptions, 0, true);
|
|
208
|
+
const isBlank = blueprint === "none";
|
|
209
|
+
const themeOptions = themes.map((t) => ({
|
|
210
|
+
value: t.id,
|
|
211
|
+
label: t.id,
|
|
212
|
+
description: t.description
|
|
213
|
+
}));
|
|
214
|
+
const defaultThemeIdx = themeOptions.findIndex((t) => t.value === "luminarum") || 0;
|
|
215
|
+
const theme = await select("Choose a theme", themeOptions, Math.max(0, defaultThemeIdx), true);
|
|
216
|
+
const mode = await select(
|
|
217
|
+
"Color mode",
|
|
218
|
+
[
|
|
219
|
+
{ value: "dark", label: "dark", description: "Dark background" },
|
|
220
|
+
{ value: "light", label: "light", description: "Light background" },
|
|
221
|
+
{ value: "auto", label: "auto", description: "Follow system preference" }
|
|
222
|
+
],
|
|
223
|
+
0
|
|
224
|
+
);
|
|
225
|
+
const shape = await select(
|
|
226
|
+
"Border shape",
|
|
227
|
+
[
|
|
228
|
+
{ value: "pill", label: "pill", description: "Fully rounded corners" },
|
|
229
|
+
{ value: "rounded", label: "rounded", description: "Moderately rounded" },
|
|
230
|
+
{ value: "sharp", label: "sharp", description: "No border radius" }
|
|
231
|
+
],
|
|
232
|
+
0,
|
|
233
|
+
true
|
|
234
|
+
);
|
|
235
|
+
const frameworkOptions = [
|
|
236
|
+
{ value: "react", label: "react", description: "React / Create React App" },
|
|
237
|
+
{ value: "nextjs", label: "nextjs", description: "Next.js" },
|
|
238
|
+
{ value: "vue", label: "vue", description: "Vue.js" },
|
|
239
|
+
{ value: "nuxt", label: "nuxt", description: "Nuxt" },
|
|
240
|
+
{ value: "svelte", label: "svelte", description: "Svelte / SvelteKit" },
|
|
241
|
+
{ value: "astro", label: "astro", description: "Astro" },
|
|
242
|
+
{ value: "angular", label: "angular", description: "Angular" },
|
|
243
|
+
{ value: "html", label: "html", description: "Plain HTML/CSS/JS" }
|
|
244
|
+
];
|
|
245
|
+
let defaultFrameworkIdx = frameworkOptions.findIndex((f) => f.value === detected.framework);
|
|
246
|
+
if (defaultFrameworkIdx < 0) defaultFrameworkIdx = 0;
|
|
247
|
+
const target = await select("Target framework", frameworkOptions, defaultFrameworkIdx, true);
|
|
248
|
+
if (detected.framework !== "unknown" && target !== detected.framework) {
|
|
249
|
+
warn(`This project appears to be ${detected.framework} but you selected ${target}.`);
|
|
250
|
+
const proceed = await confirm("Continue anyway?", false);
|
|
251
|
+
if (!proceed) {
|
|
252
|
+
console.log(`${DIM}Using detected framework: ${detected.framework}${RESET}`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
const guardMode = await select(
|
|
256
|
+
"Guard enforcement level",
|
|
257
|
+
[
|
|
258
|
+
{ value: "creative", label: "creative", description: "Advisory only (new projects)" },
|
|
259
|
+
{ value: "guided", label: "guided", description: "Style, structure, recipe enforced" },
|
|
260
|
+
{ value: "strict", label: "strict", description: "All 5 rules enforced exactly" }
|
|
261
|
+
],
|
|
262
|
+
detected.existingEssence ? 1 : 2
|
|
263
|
+
// Default to guided for existing, strict for new
|
|
264
|
+
);
|
|
265
|
+
const density = await select(
|
|
266
|
+
"Spacing density",
|
|
267
|
+
[
|
|
268
|
+
{ value: "compact", label: "compact", description: "Dense UI, minimal spacing" },
|
|
269
|
+
{ value: "comfortable", label: "comfortable", description: "Balanced spacing" },
|
|
270
|
+
{ value: "spacious", label: "spacious", description: "Generous whitespace" }
|
|
271
|
+
],
|
|
272
|
+
1
|
|
273
|
+
);
|
|
274
|
+
const shellOptions = [
|
|
275
|
+
{ value: "sidebar-main", label: "sidebar-main", description: "Collapsible sidebar with main content" },
|
|
276
|
+
{ value: "top-nav-main", label: "top-nav-main", description: "Horizontal nav with full-width content" },
|
|
277
|
+
{ value: "centered", label: "centered", description: "Centered card (auth flows)" },
|
|
278
|
+
{ value: "full-bleed", label: "full-bleed", description: "No persistent nav (landing pages)" },
|
|
279
|
+
{ value: "minimal-header", label: "minimal-header", description: "Slim header with centered content" }
|
|
280
|
+
];
|
|
281
|
+
let defaultShellIdx = 0;
|
|
282
|
+
if (["nextjs", "nuxt", "astro"].includes(target)) {
|
|
283
|
+
defaultShellIdx = shellOptions.findIndex((s) => s.value === "top-nav-main");
|
|
284
|
+
}
|
|
285
|
+
const shell = await select("Default page shell (layout)", shellOptions, Math.max(0, defaultShellIdx), true);
|
|
286
|
+
return {
|
|
287
|
+
blueprint: isBlank ? void 0 : blueprint,
|
|
288
|
+
archetype: void 0,
|
|
289
|
+
// Will be derived from blueprint
|
|
290
|
+
theme,
|
|
291
|
+
mode,
|
|
292
|
+
shape,
|
|
293
|
+
target,
|
|
294
|
+
guard: guardMode,
|
|
295
|
+
density,
|
|
296
|
+
shell,
|
|
297
|
+
personality: ["professional"],
|
|
298
|
+
features: [],
|
|
299
|
+
existing: detected.existingEssence
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
function parseFlags(args, detected) {
|
|
303
|
+
const options = {};
|
|
304
|
+
if (typeof args.blueprint === "string") options.blueprint = args.blueprint;
|
|
305
|
+
if (typeof args.archetype === "string") options.archetype = args.archetype;
|
|
306
|
+
if (typeof args.theme === "string") options.theme = args.theme;
|
|
307
|
+
if (args.mode === "dark" || args.mode === "light" || args.mode === "auto") options.mode = args.mode;
|
|
308
|
+
if (typeof args.shape === "string") options.shape = args.shape;
|
|
309
|
+
if (typeof args.target === "string") options.target = args.target;
|
|
310
|
+
if (args.guard === "creative" || args.guard === "guided" || args.guard === "strict") options.guard = args.guard;
|
|
311
|
+
if (args.density === "compact" || args.density === "comfortable" || args.density === "spacious") options.density = args.density;
|
|
312
|
+
if (typeof args.shell === "string") options.shell = args.shell;
|
|
313
|
+
if (typeof args.personality === "string") options.personality = args.personality.split(",").map((s) => s.trim());
|
|
314
|
+
if (typeof args.features === "string") options.features = args.features.split(",").map((s) => s.trim());
|
|
315
|
+
if (args.existing === true) options.existing = true;
|
|
316
|
+
return options;
|
|
317
|
+
}
|
|
318
|
+
function mergeWithDefaults(flags, detected) {
|
|
319
|
+
return {
|
|
320
|
+
blueprint: flags.blueprint,
|
|
321
|
+
archetype: flags.archetype,
|
|
322
|
+
theme: flags.theme || "luminarum",
|
|
323
|
+
mode: flags.mode || "dark",
|
|
324
|
+
shape: flags.shape || "rounded",
|
|
325
|
+
target: flags.target || (detected.framework !== "unknown" ? detected.framework : "react"),
|
|
326
|
+
guard: flags.guard || (detected.existingEssence ? "guided" : "strict"),
|
|
327
|
+
density: flags.density || "comfortable",
|
|
328
|
+
shell: flags.shell || "sidebar-main",
|
|
329
|
+
personality: flags.personality || ["professional"],
|
|
330
|
+
features: flags.features || [],
|
|
331
|
+
existing: flags.existing || detected.existingEssence
|
|
332
|
+
};
|
|
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
|
+
}
|
|
366
|
+
|
|
367
|
+
// src/scaffold.ts
|
|
368
|
+
import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync, appendFileSync } from "fs";
|
|
369
|
+
import { join as join2, dirname } from "path";
|
|
370
|
+
import { fileURLToPath } from "url";
|
|
371
|
+
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
372
|
+
var CLI_VERSION = "1.0.0";
|
|
373
|
+
function generateTokensCSS(themeData, mode) {
|
|
374
|
+
if (!themeData) {
|
|
375
|
+
return `/* No theme data available */
|
|
376
|
+
:root {
|
|
377
|
+
--d-primary: #6366f1;
|
|
378
|
+
--d-secondary: #a1a1aa;
|
|
379
|
+
--d-accent: #f59e0b;
|
|
380
|
+
--d-bg: #18181b;
|
|
381
|
+
--d-surface: #1f1f23;
|
|
382
|
+
--d-surface-raised: #27272a;
|
|
383
|
+
--d-border: #3f3f46;
|
|
384
|
+
--d-text: #fafafa;
|
|
385
|
+
--d-text-muted: #a1a1aa;
|
|
386
|
+
}
|
|
387
|
+
`;
|
|
388
|
+
}
|
|
389
|
+
const seed = themeData.seed || {};
|
|
390
|
+
const palette = themeData.palette || {};
|
|
391
|
+
const tokens = {
|
|
392
|
+
// Seed colors
|
|
393
|
+
"--d-primary": seed.primary || "#6366f1",
|
|
394
|
+
"--d-secondary": seed.secondary || "#a1a1aa",
|
|
395
|
+
"--d-accent": seed.accent || "#f59e0b",
|
|
396
|
+
// Palette colors (mode-aware)
|
|
397
|
+
"--d-bg": palette.background?.[mode] || "#18181b",
|
|
398
|
+
"--d-surface": palette.surface?.[mode] || "#1f1f23",
|
|
399
|
+
"--d-surface-raised": palette["surface-raised"]?.[mode] || "#27272a",
|
|
400
|
+
"--d-border": palette.border?.[mode] || "#3f3f46",
|
|
401
|
+
"--d-text": palette.text?.[mode] || "#fafafa",
|
|
402
|
+
"--d-text-muted": palette["text-muted"]?.[mode] || "#a1a1aa",
|
|
403
|
+
"--d-primary-hover": palette["primary-hover"]?.[mode] || seed.primary || "#6366f1",
|
|
404
|
+
// Spacing scale
|
|
405
|
+
"--d-gap-1": "0.25rem",
|
|
406
|
+
"--d-gap-2": "0.5rem",
|
|
407
|
+
"--d-gap-3": "0.75rem",
|
|
408
|
+
"--d-gap-4": "1rem",
|
|
409
|
+
"--d-gap-6": "1.5rem",
|
|
410
|
+
"--d-gap-8": "2rem",
|
|
411
|
+
"--d-gap-12": "3rem",
|
|
412
|
+
// Radii
|
|
413
|
+
"--d-radius": "0.5rem",
|
|
414
|
+
"--d-radius-sm": "0.25rem",
|
|
415
|
+
"--d-radius-lg": "0.75rem",
|
|
416
|
+
"--d-radius-xl": "1rem",
|
|
417
|
+
"--d-radius-full": "9999px",
|
|
418
|
+
// Shadows
|
|
419
|
+
"--d-shadow-sm": "0 1px 2px rgba(0,0,0,0.05)",
|
|
420
|
+
"--d-shadow": "0 1px 3px rgba(0,0,0,0.1)",
|
|
421
|
+
"--d-shadow-md": "0 4px 6px rgba(0,0,0,0.1)",
|
|
422
|
+
"--d-shadow-lg": "0 10px 15px rgba(0,0,0,0.1)",
|
|
423
|
+
// Status colors
|
|
424
|
+
"--d-success": themeData.tokens?.base?.success || "#22c55e",
|
|
425
|
+
"--d-error": themeData.tokens?.base?.danger || "#ef4444",
|
|
426
|
+
"--d-warning": themeData.tokens?.base?.warning || "#f59e0b",
|
|
427
|
+
"--d-info": "#3b82f6"
|
|
428
|
+
};
|
|
429
|
+
const lines = Object.entries(tokens).map(([key, value]) => ` ${key}: ${value};`).join("\n");
|
|
430
|
+
return `/* Generated by @decantr/cli */
|
|
431
|
+
:root {
|
|
432
|
+
${lines}
|
|
433
|
+
}
|
|
434
|
+
`;
|
|
435
|
+
}
|
|
436
|
+
function generateDecoratorsCSS(recipeData, themeName) {
|
|
437
|
+
if (!recipeData?.decorators) {
|
|
438
|
+
return `/* No recipe decorators available */`;
|
|
439
|
+
}
|
|
440
|
+
const decorators = recipeData.decorators;
|
|
441
|
+
const css = [
|
|
442
|
+
`/* Generated by @decantr/cli from recipe: ${themeName} */`,
|
|
443
|
+
""
|
|
444
|
+
];
|
|
445
|
+
for (const [name, description] of Object.entries(decorators)) {
|
|
446
|
+
css.push(generateDecoratorRule(name, description));
|
|
447
|
+
css.push("");
|
|
448
|
+
}
|
|
449
|
+
css.push(`/* Animation keyframes */
|
|
450
|
+
@keyframes decantr-fade-in {
|
|
451
|
+
from { opacity: 0; transform: translateY(8px); }
|
|
452
|
+
to { opacity: 1; transform: translateY(0); }
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
@keyframes decantr-pulse {
|
|
456
|
+
0%, 100% { opacity: 1; }
|
|
457
|
+
50% { opacity: 0.5; }
|
|
458
|
+
}
|
|
459
|
+
`);
|
|
460
|
+
return css.join("\n");
|
|
461
|
+
}
|
|
462
|
+
function generateDecoratorRule(name, description) {
|
|
463
|
+
const rules = [];
|
|
464
|
+
const descLower = description.toLowerCase();
|
|
465
|
+
if (descLower.includes("surface background") || descLower.includes("surface elevation")) {
|
|
466
|
+
rules.push("background: var(--d-surface)");
|
|
467
|
+
} else if (descLower.includes("background") && descLower.includes("theme")) {
|
|
468
|
+
rules.push("background: var(--d-bg)");
|
|
469
|
+
} else if (descLower.includes("primary-tinted") || descLower.includes("primary background")) {
|
|
470
|
+
rules.push("background: color-mix(in srgb, var(--d-primary) 15%, var(--d-surface))");
|
|
471
|
+
}
|
|
472
|
+
if (descLower.includes("1px border") || descLower.includes("subtle border")) {
|
|
473
|
+
rules.push("border: 1px solid var(--d-border)");
|
|
474
|
+
} else if (descLower.includes("border") && !descLower.includes("radius")) {
|
|
475
|
+
rules.push("border: 1px solid var(--d-border)");
|
|
476
|
+
}
|
|
477
|
+
const radiusMatch = descLower.match(/(\d+)px radius/);
|
|
478
|
+
if (radiusMatch) {
|
|
479
|
+
rules.push(`border-radius: ${radiusMatch[1]}px`);
|
|
480
|
+
} else if (descLower.includes("radius") || descLower.includes("rounded")) {
|
|
481
|
+
rules.push("border-radius: var(--d-radius)");
|
|
482
|
+
}
|
|
483
|
+
if (descLower.includes("hover shadow") || descLower.includes("shadow transition")) {
|
|
484
|
+
rules.push("transition: box-shadow 0.15s ease");
|
|
485
|
+
}
|
|
486
|
+
if (descLower.includes("elevation") || descLower.includes("shadow")) {
|
|
487
|
+
rules.push("box-shadow: var(--d-shadow)");
|
|
488
|
+
}
|
|
489
|
+
if (descLower.includes("entrance animation") || descLower.includes("fade")) {
|
|
490
|
+
rules.push("animation: decantr-fade-in 0.2s ease-out");
|
|
491
|
+
}
|
|
492
|
+
if (descLower.includes("pulse animation") || descLower.includes("skeleton")) {
|
|
493
|
+
rules.push("animation: decantr-pulse 1.5s ease-in-out infinite");
|
|
494
|
+
}
|
|
495
|
+
if (descLower.includes("blur") || descLower.includes("glass")) {
|
|
496
|
+
rules.push("backdrop-filter: blur(8px)");
|
|
497
|
+
}
|
|
498
|
+
if (descLower.includes("right-aligned")) {
|
|
499
|
+
rules.push("margin-left: auto");
|
|
500
|
+
} else if (descLower.includes("left-aligned")) {
|
|
501
|
+
rules.push("margin-right: auto");
|
|
502
|
+
}
|
|
503
|
+
if (descLower.includes("message bubble") || descLower.includes("bubble")) {
|
|
504
|
+
rules.push("padding: var(--d-gap-3) var(--d-gap-4)");
|
|
505
|
+
rules.push("border-radius: var(--d-radius-lg)");
|
|
506
|
+
rules.push("max-width: 80%");
|
|
507
|
+
}
|
|
508
|
+
if (rules.length === 0) {
|
|
509
|
+
return `/* .${name}: ${description} */`;
|
|
510
|
+
}
|
|
511
|
+
return `.${name} {
|
|
512
|
+
${rules.join(";\n ")};
|
|
513
|
+
}`;
|
|
514
|
+
}
|
|
515
|
+
function serializeLayoutItem(item) {
|
|
516
|
+
if (typeof item === "string") {
|
|
517
|
+
return item;
|
|
518
|
+
}
|
|
519
|
+
if (typeof item === "object" && item !== null) {
|
|
520
|
+
const obj = item;
|
|
521
|
+
if (typeof obj.pattern === "string") {
|
|
522
|
+
const preset = obj.preset ? ` (${obj.preset})` : "";
|
|
523
|
+
const alias = obj.as ? ` as ${obj.as}` : "";
|
|
524
|
+
return `${obj.pattern}${preset}${alias}`;
|
|
525
|
+
}
|
|
526
|
+
if (Array.isArray(obj.cols)) {
|
|
527
|
+
const cols = obj.cols.map(serializeLayoutItem).join(" | ");
|
|
528
|
+
const breakpoint = obj.at ? ` @${obj.at}` : "";
|
|
529
|
+
return `[${cols}]${breakpoint}`;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
return "custom";
|
|
533
|
+
}
|
|
534
|
+
function extractPatternNames(item) {
|
|
535
|
+
if (typeof item === "string") {
|
|
536
|
+
return [item];
|
|
537
|
+
}
|
|
538
|
+
if (typeof item === "object" && item !== null) {
|
|
539
|
+
const obj = item;
|
|
540
|
+
if (typeof obj.pattern === "string") {
|
|
541
|
+
return [obj.pattern];
|
|
542
|
+
}
|
|
543
|
+
if (Array.isArray(obj.cols)) {
|
|
544
|
+
return obj.cols.flatMap(extractPatternNames);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
return [];
|
|
548
|
+
}
|
|
549
|
+
function loadTemplate(name) {
|
|
550
|
+
const fromDist = join2(__dirname, "..", "src", "templates", name);
|
|
551
|
+
if (existsSync2(fromDist)) {
|
|
552
|
+
return readFileSync2(fromDist, "utf-8");
|
|
553
|
+
}
|
|
554
|
+
const fromSrc = join2(__dirname, "templates", name);
|
|
555
|
+
if (existsSync2(fromSrc)) {
|
|
556
|
+
return readFileSync2(fromSrc, "utf-8");
|
|
557
|
+
}
|
|
558
|
+
throw new Error(`Template not found: ${name}`);
|
|
559
|
+
}
|
|
560
|
+
function renderTemplate(template, vars) {
|
|
561
|
+
let result = template;
|
|
562
|
+
for (const [key, value] of Object.entries(vars)) {
|
|
563
|
+
result = result.replace(new RegExp(`\\{\\{${key}\\}\\}`, "g"), value);
|
|
564
|
+
}
|
|
565
|
+
return result;
|
|
566
|
+
}
|
|
567
|
+
function resolvePatternAlias(item, patterns) {
|
|
568
|
+
if (!patterns) return item;
|
|
569
|
+
if (typeof item === "string") {
|
|
570
|
+
const patternDef = patterns.find((p) => p.as === item);
|
|
571
|
+
if (patternDef) {
|
|
572
|
+
if (patternDef.preset) {
|
|
573
|
+
return { pattern: patternDef.pattern, preset: patternDef.preset };
|
|
574
|
+
}
|
|
575
|
+
return patternDef.pattern;
|
|
576
|
+
}
|
|
577
|
+
return item;
|
|
578
|
+
}
|
|
579
|
+
if (typeof item === "object" && item !== null) {
|
|
580
|
+
const obj = item;
|
|
581
|
+
if (Array.isArray(obj.cols)) {
|
|
582
|
+
return {
|
|
583
|
+
...obj,
|
|
584
|
+
cols: obj.cols.map((col) => resolvePatternAlias(col, patterns))
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
return item;
|
|
589
|
+
}
|
|
590
|
+
function buildEssence(options, archetypeData) {
|
|
591
|
+
let structure = [
|
|
592
|
+
{ id: "home", shell: options.shell, layout: ["hero"] }
|
|
593
|
+
];
|
|
594
|
+
let features = options.features;
|
|
595
|
+
if (archetypeData?.pages) {
|
|
596
|
+
structure = archetypeData.pages.map((p) => {
|
|
597
|
+
const resolvedLayout = (p.default_layout?.length ? p.default_layout : ["hero"]).map((item) => resolvePatternAlias(item, p.patterns));
|
|
598
|
+
return {
|
|
599
|
+
id: p.id,
|
|
600
|
+
shell: p.shell || options.shell,
|
|
601
|
+
layout: resolvedLayout
|
|
602
|
+
};
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
if (archetypeData?.features) {
|
|
606
|
+
features = [.../* @__PURE__ */ new Set([...features, ...archetypeData.features])];
|
|
607
|
+
}
|
|
608
|
+
const contentGapMap = {
|
|
609
|
+
compact: "_gap2",
|
|
610
|
+
comfortable: "_gap4",
|
|
611
|
+
spacious: "_gap6"
|
|
612
|
+
};
|
|
613
|
+
const archetype = options.archetype || "custom";
|
|
614
|
+
const essence = {
|
|
615
|
+
version: "2.0.0",
|
|
616
|
+
archetype,
|
|
617
|
+
theme: {
|
|
618
|
+
style: options.theme,
|
|
619
|
+
mode: options.mode,
|
|
620
|
+
recipe: options.theme,
|
|
621
|
+
// Recipe defaults to theme
|
|
622
|
+
shape: options.shape
|
|
623
|
+
},
|
|
624
|
+
personality: options.personality,
|
|
625
|
+
platform: {
|
|
626
|
+
type: "spa",
|
|
627
|
+
routing: "hash"
|
|
628
|
+
},
|
|
629
|
+
structure,
|
|
630
|
+
features,
|
|
631
|
+
guard: {
|
|
632
|
+
enforce_style: true,
|
|
633
|
+
enforce_recipe: true,
|
|
634
|
+
mode: options.guard
|
|
635
|
+
},
|
|
636
|
+
density: {
|
|
637
|
+
level: options.density,
|
|
638
|
+
content_gap: contentGapMap[options.density] || "_gap4"
|
|
639
|
+
},
|
|
640
|
+
target: options.target
|
|
641
|
+
};
|
|
642
|
+
if (options.accessibility) {
|
|
643
|
+
essence.accessibility = options.accessibility;
|
|
644
|
+
}
|
|
645
|
+
return essence;
|
|
646
|
+
}
|
|
647
|
+
function generateAccessibilitySection(essence, themeData) {
|
|
648
|
+
const accessibility = essence.accessibility;
|
|
649
|
+
if (!accessibility?.wcag_level || accessibility.wcag_level === "none") {
|
|
650
|
+
return "";
|
|
651
|
+
}
|
|
652
|
+
const wcagLevel = accessibility.wcag_level;
|
|
653
|
+
const cvdPreference = accessibility.cvd_preference || "none";
|
|
654
|
+
const cvdSupport = themeData?.cvd_support || [];
|
|
655
|
+
let section = `---
|
|
656
|
+
|
|
657
|
+
## Accessibility
|
|
658
|
+
|
|
659
|
+
**WCAG Level:** ${wcagLevel}
|
|
660
|
+
`;
|
|
661
|
+
if (cvdSupport.length > 0) {
|
|
662
|
+
section += `**CVD Support:** Theme supports ${cvdSupport.join(", ")}
|
|
663
|
+
**CVD Preference:** ${cvdPreference}
|
|
664
|
+
`;
|
|
665
|
+
}
|
|
666
|
+
section += `
|
|
667
|
+
### What This Means
|
|
668
|
+
|
|
669
|
+
This project requires WCAG 2.1 Level ${wcagLevel} compliance. You already know these rules \u2014 apply them:
|
|
670
|
+
|
|
671
|
+
- Semantic HTML structure
|
|
672
|
+
- Sufficient color contrast (4.5:1 for normal text, 3:1 for large text)
|
|
673
|
+
- Keyboard navigability for all interactive elements
|
|
674
|
+
- Visible focus indicators
|
|
675
|
+
- Meaningful alt text for images
|
|
676
|
+
- Proper heading hierarchy
|
|
677
|
+
`;
|
|
678
|
+
if (cvdSupport.length > 0) {
|
|
679
|
+
section += `
|
|
680
|
+
### CVD Implementation
|
|
681
|
+
|
|
682
|
+
The theme provides these data attributes:
|
|
683
|
+
|
|
684
|
+
\`\`\`html
|
|
685
|
+
<html data-theme="${essence.theme.style}" data-mode="${essence.theme.mode}" data-cvd="none">
|
|
686
|
+
\`\`\`
|
|
687
|
+
|
|
688
|
+
Valid \`data-cvd\` values for this theme: \`none\`, ${cvdSupport.map((m) => `\`${m}\``).join(", ")}
|
|
689
|
+
`;
|
|
690
|
+
if (cvdPreference === "auto") {
|
|
691
|
+
section += `
|
|
692
|
+
Detect user preference via \`prefers-contrast\` or user settings and apply accordingly.
|
|
693
|
+
`;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
section += `
|
|
697
|
+
---
|
|
698
|
+
`;
|
|
699
|
+
return section;
|
|
700
|
+
}
|
|
701
|
+
function generateSeoSection(essence, archetypeData) {
|
|
702
|
+
const seoHints = archetypeData?.seo_hints;
|
|
703
|
+
if (!seoHints) {
|
|
704
|
+
return "";
|
|
705
|
+
}
|
|
706
|
+
const schemaOrg = seoHints.schema_org || [];
|
|
707
|
+
const metaPriorities = seoHints.meta_priorities || [];
|
|
708
|
+
if (schemaOrg.length === 0 && metaPriorities.length === 0) {
|
|
709
|
+
return "";
|
|
710
|
+
}
|
|
711
|
+
let section = `---
|
|
712
|
+
|
|
713
|
+
## SEO Guidance
|
|
714
|
+
|
|
715
|
+
This archetype (\`${essence.archetype}\`) typically benefits from:
|
|
716
|
+
|
|
717
|
+
`;
|
|
718
|
+
if (schemaOrg.length > 0) {
|
|
719
|
+
section += `- **Schema.org:** ${schemaOrg.join(", ")}
|
|
720
|
+
`;
|
|
721
|
+
}
|
|
722
|
+
if (metaPriorities.length > 0) {
|
|
723
|
+
section += `- **Meta priorities:** ${metaPriorities.join(", ")}
|
|
724
|
+
`;
|
|
725
|
+
}
|
|
726
|
+
section += `
|
|
727
|
+
These are suggestions, not requirements. Apply where appropriate for the page content.
|
|
728
|
+
|
|
729
|
+
---
|
|
730
|
+
`;
|
|
731
|
+
return section;
|
|
732
|
+
}
|
|
733
|
+
function generateDecantrMd(essence, detected, themeData, recipeData, archetypeData) {
|
|
734
|
+
const template = loadTemplate("DECANTR.md.template");
|
|
735
|
+
const pagesTable = essence.structure.map((p) => {
|
|
736
|
+
const layoutStr = p.layout.map(serializeLayoutItem).join(", ") || "none";
|
|
737
|
+
return `| ${p.id} | ${p.shell} | ${layoutStr} |`;
|
|
738
|
+
}).join("\n");
|
|
739
|
+
const allPatternNames = [...new Set(essence.structure.flatMap((p) => p.layout.flatMap(extractPatternNames)))];
|
|
740
|
+
const patternsList = allPatternNames.length > 0 ? allPatternNames.map((p) => `- \`${p}\``).join("\n") : "- No patterns specified yet";
|
|
741
|
+
const projectSummary = [
|
|
742
|
+
`**Archetype:** ${essence.archetype || "custom"}`,
|
|
743
|
+
`**Target:** ${essence.target}`,
|
|
744
|
+
`**Theme:** ${essence.theme.style} (${essence.theme.mode} mode)`,
|
|
745
|
+
`**Guard Mode:** ${essence.guard.mode}`,
|
|
746
|
+
`**Pages:** ${essence.structure.map((s) => s.id).join(", ")}`
|
|
747
|
+
].join("\n");
|
|
748
|
+
const shellStructures = {
|
|
749
|
+
"sidebar-main": "nav (left) | header (top) | body (scrollable)",
|
|
750
|
+
"top-nav-main": "header (full width) | body (scrollable)",
|
|
751
|
+
"centered": "body (centered card)",
|
|
752
|
+
"full-bleed": "header (floating) | body (full page sections)",
|
|
753
|
+
"minimal-header": "header (slim) | body (centered)"
|
|
754
|
+
};
|
|
755
|
+
const defaultShell = essence.structure[0]?.shell || "sidebar-main";
|
|
756
|
+
const shellStructure = shellStructures[defaultShell] || "Custom shell layout";
|
|
757
|
+
let themeQuickRef = "";
|
|
758
|
+
if (themeData?.seed) {
|
|
759
|
+
const colors = Object.entries(themeData.seed).map(([name, hex]) => `- **${name}:** \`${hex}\``).join("\n");
|
|
760
|
+
themeQuickRef = `**Seed Colors:**
|
|
761
|
+
${colors}`;
|
|
762
|
+
}
|
|
763
|
+
if (recipeData?.decorators) {
|
|
764
|
+
const decorators = Object.entries(recipeData.decorators).slice(0, 5).map(([name, desc]) => `- \`${name}\` \u2014 ${desc}`).join("\n");
|
|
765
|
+
if (themeQuickRef) {
|
|
766
|
+
themeQuickRef += `
|
|
767
|
+
|
|
768
|
+
**Key Decorators:**
|
|
769
|
+
${decorators}`;
|
|
770
|
+
} else {
|
|
771
|
+
themeQuickRef = `**Key Decorators:**
|
|
772
|
+
${decorators}`;
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
if (!themeQuickRef) {
|
|
776
|
+
themeQuickRef = `See \`decantr get theme ${essence.theme.style}\` for details.`;
|
|
777
|
+
}
|
|
778
|
+
const accessibilitySection = generateAccessibilitySection(essence, themeData);
|
|
779
|
+
const seoSection = generateSeoSection(essence, archetypeData);
|
|
780
|
+
const vars = {
|
|
781
|
+
GUARD_MODE: essence.guard.mode,
|
|
782
|
+
PROJECT_SUMMARY: projectSummary,
|
|
783
|
+
THEME_STYLE: essence.theme.style,
|
|
784
|
+
THEME_MODE: essence.theme.mode,
|
|
785
|
+
THEME_RECIPE: essence.theme.recipe,
|
|
786
|
+
TARGET: essence.target,
|
|
787
|
+
PAGES_TABLE: `| Page | Shell | Layout |
|
|
788
|
+
|------|-------|--------|
|
|
789
|
+
${pagesTable}`,
|
|
790
|
+
PATTERNS_LIST: patternsList,
|
|
791
|
+
DEFAULT_SHELL: defaultShell,
|
|
792
|
+
SHELL_STRUCTURE: shellStructure,
|
|
793
|
+
PERSONALITY: essence.personality.join(", "),
|
|
794
|
+
DENSITY: essence.density.level,
|
|
795
|
+
AVAILABLE_PATTERNS: "(See registry or .decantr/cache/patterns/)",
|
|
796
|
+
AVAILABLE_THEMES: "(See registry or .decantr/cache/themes/)",
|
|
797
|
+
AVAILABLE_SHELLS: "sidebar-main, top-nav-main, centered, full-bleed, minimal-header",
|
|
798
|
+
VERSION: CLI_VERSION,
|
|
799
|
+
THEME_QUICK_REFERENCE: themeQuickRef,
|
|
800
|
+
ACCESSIBILITY_SECTION: accessibilitySection,
|
|
801
|
+
SEO_SECTION: seoSection
|
|
802
|
+
};
|
|
803
|
+
return renderTemplate(template, vars);
|
|
804
|
+
}
|
|
805
|
+
function generateProjectJson(detected, options, registrySource) {
|
|
806
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
807
|
+
const data = {
|
|
808
|
+
detected: {
|
|
809
|
+
framework: detected.framework,
|
|
810
|
+
version: detected.version || null,
|
|
811
|
+
packageManager: detected.packageManager,
|
|
812
|
+
hasTypeScript: detected.hasTypeScript,
|
|
813
|
+
hasTailwind: detected.hasTailwind,
|
|
814
|
+
existingRuleFiles: detected.existingRuleFiles
|
|
815
|
+
},
|
|
816
|
+
overrides: {
|
|
817
|
+
framework: options.target !== detected.framework ? options.target : null
|
|
818
|
+
},
|
|
819
|
+
sync: {
|
|
820
|
+
status: registrySource === "api" ? "synced" : "needs-sync",
|
|
821
|
+
lastSync: now,
|
|
822
|
+
registrySource,
|
|
823
|
+
cachedContent: {
|
|
824
|
+
archetypes: [],
|
|
825
|
+
patterns: [],
|
|
826
|
+
themes: [],
|
|
827
|
+
recipes: []
|
|
828
|
+
}
|
|
829
|
+
},
|
|
830
|
+
initialized: {
|
|
831
|
+
at: now,
|
|
832
|
+
via: "cli",
|
|
833
|
+
version: CLI_VERSION,
|
|
834
|
+
flags: buildFlagsString(options)
|
|
835
|
+
}
|
|
836
|
+
};
|
|
837
|
+
return JSON.stringify(data, null, 2);
|
|
838
|
+
}
|
|
839
|
+
function buildFlagsString(options) {
|
|
840
|
+
const flags = [];
|
|
841
|
+
if (options.blueprint) flags.push(`--blueprint=${options.blueprint}`);
|
|
842
|
+
if (options.theme) flags.push(`--theme=${options.theme}`);
|
|
843
|
+
if (options.mode) flags.push(`--mode=${options.mode}`);
|
|
844
|
+
if (options.guard) flags.push(`--guard=${options.guard}`);
|
|
845
|
+
return flags.join(" ");
|
|
846
|
+
}
|
|
847
|
+
function generateTaskContext(templateName, essence) {
|
|
848
|
+
const template = loadTemplate(templateName);
|
|
849
|
+
const defaultShell = essence.structure[0]?.shell || "sidebar-main";
|
|
850
|
+
const layout = essence.structure[0]?.layout.map(serializeLayoutItem).join(", ") || "none";
|
|
851
|
+
const scaffoldStructure = essence.structure.map((p) => {
|
|
852
|
+
const patterns = p.layout.length > 0 ? `
|
|
853
|
+
- Patterns: ${p.layout.map(serializeLayoutItem).join(", ")}` : "";
|
|
854
|
+
return `- **${p.id}** (${p.shell})${patterns}`;
|
|
855
|
+
}).join("\n");
|
|
856
|
+
const vars = {
|
|
857
|
+
TARGET: essence.target,
|
|
858
|
+
THEME_STYLE: essence.theme.style,
|
|
859
|
+
THEME_MODE: essence.theme.mode,
|
|
860
|
+
THEME_RECIPE: essence.theme.recipe,
|
|
861
|
+
DEFAULT_SHELL: defaultShell,
|
|
862
|
+
GUARD_MODE: essence.guard.mode,
|
|
863
|
+
LAYOUT: layout,
|
|
864
|
+
DENSITY: essence.density.level,
|
|
865
|
+
CONTENT_GAP: essence.density.content_gap,
|
|
866
|
+
SCAFFOLD_STRUCTURE: scaffoldStructure
|
|
867
|
+
};
|
|
868
|
+
return renderTemplate(template, vars);
|
|
869
|
+
}
|
|
870
|
+
function generateEssenceSummary(essence) {
|
|
871
|
+
const template = loadTemplate("essence-summary.md.template");
|
|
872
|
+
const pagesTable = `| Page | Shell | Layout |
|
|
873
|
+
|------|-------|--------|
|
|
874
|
+
${essence.structure.map((p) => `| ${p.id} | ${p.shell} | ${p.layout.map(serializeLayoutItem).join(", ") || "none"} |`).join("\n")}`;
|
|
875
|
+
const featuresList = essence.features.length > 0 ? essence.features.map((f) => `- ${f}`).join("\n") : "- No features specified";
|
|
876
|
+
const vars = {
|
|
877
|
+
ARCHETYPE: essence.archetype || "custom",
|
|
878
|
+
BLUEPRINT: essence.blueprint || "none",
|
|
879
|
+
PERSONALITY: essence.personality.join(", "),
|
|
880
|
+
TARGET: essence.target,
|
|
881
|
+
THEME_STYLE: essence.theme.style,
|
|
882
|
+
THEME_MODE: essence.theme.mode,
|
|
883
|
+
THEME_RECIPE: essence.theme.recipe,
|
|
884
|
+
SHAPE: essence.theme.shape,
|
|
885
|
+
PAGES_TABLE: pagesTable,
|
|
886
|
+
FEATURES_LIST: featuresList,
|
|
887
|
+
GUARD_MODE: essence.guard.mode,
|
|
888
|
+
ENFORCE_STYLE: String(essence.guard.enforce_style),
|
|
889
|
+
ENFORCE_RECIPE: String(essence.guard.enforce_recipe),
|
|
890
|
+
DENSITY: essence.density.level,
|
|
891
|
+
CONTENT_GAP: essence.density.content_gap,
|
|
892
|
+
LAST_UPDATED: (/* @__PURE__ */ new Date()).toISOString()
|
|
893
|
+
};
|
|
894
|
+
return renderTemplate(template, vars);
|
|
895
|
+
}
|
|
896
|
+
function updateGitignore(projectRoot) {
|
|
897
|
+
const gitignorePath = join2(projectRoot, ".gitignore");
|
|
898
|
+
const cacheEntry = ".decantr/cache/";
|
|
899
|
+
if (existsSync2(gitignorePath)) {
|
|
900
|
+
const content = readFileSync2(gitignorePath, "utf-8");
|
|
901
|
+
if (!content.includes(cacheEntry)) {
|
|
902
|
+
appendFileSync(gitignorePath, `
|
|
903
|
+
# Decantr cache
|
|
904
|
+
${cacheEntry}
|
|
905
|
+
`);
|
|
906
|
+
return true;
|
|
907
|
+
}
|
|
908
|
+
return false;
|
|
909
|
+
} else {
|
|
910
|
+
writeFileSync(gitignorePath, `# Decantr cache
|
|
911
|
+
${cacheEntry}
|
|
912
|
+
`);
|
|
913
|
+
return true;
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
function scaffoldProject(projectRoot, options, detected, archetypeData, registrySource = "cache", themeData, recipeData) {
|
|
917
|
+
const essence = buildEssence(options, archetypeData);
|
|
918
|
+
const decantrDir = join2(projectRoot, ".decantr");
|
|
919
|
+
const contextDir = join2(decantrDir, "context");
|
|
920
|
+
const cacheDir = join2(decantrDir, "cache");
|
|
921
|
+
mkdirSync(contextDir, { recursive: true });
|
|
922
|
+
mkdirSync(cacheDir, { recursive: true });
|
|
923
|
+
const essencePath = join2(projectRoot, "decantr.essence.json");
|
|
924
|
+
writeFileSync(essencePath, JSON.stringify(essence, null, 2) + "\n");
|
|
925
|
+
const decantrMdPath = join2(projectRoot, "DECANTR.md");
|
|
926
|
+
writeFileSync(decantrMdPath, generateDecantrMd(essence, detected, themeData, recipeData, archetypeData));
|
|
927
|
+
const projectJsonPath = join2(decantrDir, "project.json");
|
|
928
|
+
writeFileSync(projectJsonPath, generateProjectJson(detected, options, registrySource));
|
|
929
|
+
const contextFiles = [];
|
|
930
|
+
const scaffoldPath = join2(contextDir, "task-scaffold.md");
|
|
931
|
+
writeFileSync(scaffoldPath, generateTaskContext("task-scaffold.md.template", essence));
|
|
932
|
+
contextFiles.push(scaffoldPath);
|
|
933
|
+
const addPagePath = join2(contextDir, "task-add-page.md");
|
|
934
|
+
writeFileSync(addPagePath, generateTaskContext("task-add-page.md.template", essence));
|
|
935
|
+
contextFiles.push(addPagePath);
|
|
936
|
+
const modifyPath = join2(contextDir, "task-modify.md");
|
|
937
|
+
writeFileSync(modifyPath, generateTaskContext("task-modify.md.template", essence));
|
|
938
|
+
contextFiles.push(modifyPath);
|
|
939
|
+
const summaryPath = join2(contextDir, "essence-summary.md");
|
|
940
|
+
writeFileSync(summaryPath, generateEssenceSummary(essence));
|
|
941
|
+
contextFiles.push(summaryPath);
|
|
942
|
+
const stylesDir = join2(projectRoot, "src", "styles");
|
|
943
|
+
mkdirSync(stylesDir, { recursive: true });
|
|
944
|
+
const tokensPath = join2(stylesDir, "tokens.css");
|
|
945
|
+
writeFileSync(tokensPath, generateTokensCSS(themeData, essence.theme.mode));
|
|
946
|
+
const decoratorsPath = join2(stylesDir, "decorators.css");
|
|
947
|
+
writeFileSync(decoratorsPath, generateDecoratorsCSS(recipeData, essence.theme.style));
|
|
948
|
+
const gitignoreUpdated = updateGitignore(projectRoot);
|
|
949
|
+
return {
|
|
950
|
+
essencePath,
|
|
951
|
+
decantrMdPath,
|
|
952
|
+
projectJsonPath,
|
|
953
|
+
contextFiles,
|
|
954
|
+
cssFiles: [tokensPath, decoratorsPath],
|
|
955
|
+
gitignoreUpdated
|
|
956
|
+
};
|
|
957
|
+
}
|
|
958
|
+
function scaffoldMinimal(projectRoot) {
|
|
959
|
+
const decantrDir = join2(projectRoot, ".decantr");
|
|
960
|
+
const customDir = join2(decantrDir, "custom");
|
|
961
|
+
const contentTypes = ["patterns", "recipes", "themes", "blueprints", "archetypes", "shells"];
|
|
962
|
+
for (const type of contentTypes) {
|
|
963
|
+
mkdirSync(join2(customDir, type), { recursive: true });
|
|
964
|
+
}
|
|
965
|
+
const essence = {
|
|
966
|
+
version: "2.0.0",
|
|
967
|
+
archetype: "custom",
|
|
968
|
+
theme: {
|
|
969
|
+
style: "default",
|
|
970
|
+
mode: "dark",
|
|
971
|
+
recipe: "default",
|
|
972
|
+
shape: "rounded"
|
|
973
|
+
},
|
|
974
|
+
personality: ["clean", "modern"],
|
|
975
|
+
platform: {
|
|
976
|
+
type: "spa",
|
|
977
|
+
routing: "hash"
|
|
978
|
+
},
|
|
979
|
+
structure: [
|
|
980
|
+
{ id: "home", shell: "sidebar-main", layout: ["hero"] }
|
|
981
|
+
],
|
|
982
|
+
features: [],
|
|
983
|
+
guard: {
|
|
984
|
+
enforce_style: true,
|
|
985
|
+
enforce_recipe: true,
|
|
986
|
+
mode: "guided"
|
|
987
|
+
},
|
|
988
|
+
density: {
|
|
989
|
+
level: "comfortable",
|
|
990
|
+
content_gap: "_gap4"
|
|
991
|
+
},
|
|
992
|
+
target: "react"
|
|
993
|
+
};
|
|
994
|
+
const essencePath = join2(projectRoot, "decantr.essence.json");
|
|
995
|
+
writeFileSync(essencePath, JSON.stringify(essence, null, 2) + "\n");
|
|
996
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
997
|
+
const projectJson = {
|
|
998
|
+
detected: {
|
|
999
|
+
framework: "unknown",
|
|
1000
|
+
version: null,
|
|
1001
|
+
packageManager: "npm",
|
|
1002
|
+
hasTypeScript: false,
|
|
1003
|
+
hasTailwind: false,
|
|
1004
|
+
existingRuleFiles: []
|
|
1005
|
+
},
|
|
1006
|
+
overrides: {
|
|
1007
|
+
framework: null
|
|
1008
|
+
},
|
|
1009
|
+
sync: {
|
|
1010
|
+
status: "needs-sync",
|
|
1011
|
+
lastSync: now,
|
|
1012
|
+
registrySource: "cache",
|
|
1013
|
+
cachedContent: {
|
|
1014
|
+
archetypes: [],
|
|
1015
|
+
patterns: [],
|
|
1016
|
+
themes: [],
|
|
1017
|
+
recipes: []
|
|
1018
|
+
}
|
|
1019
|
+
},
|
|
1020
|
+
initialized: {
|
|
1021
|
+
at: now,
|
|
1022
|
+
via: "cli",
|
|
1023
|
+
version: CLI_VERSION,
|
|
1024
|
+
flags: "--offline --minimal"
|
|
1025
|
+
}
|
|
1026
|
+
};
|
|
1027
|
+
const projectJsonPath = join2(decantrDir, "project.json");
|
|
1028
|
+
writeFileSync(projectJsonPath, JSON.stringify(projectJson, null, 2));
|
|
1029
|
+
const decantrMdPath = join2(projectRoot, "DECANTR.md");
|
|
1030
|
+
const decantrMdContent = `# DECANTR.md
|
|
1031
|
+
|
|
1032
|
+
> This file was generated by \`decantr init\` in offline/minimal mode.
|
|
1033
|
+
> Run \`decantr upgrade\` when online to pull full registry content.
|
|
1034
|
+
|
|
1035
|
+
## Guard Mode: guided
|
|
1036
|
+
|
|
1037
|
+
## Project Summary
|
|
1038
|
+
|
|
1039
|
+
**Archetype:** custom
|
|
1040
|
+
**Target:** react
|
|
1041
|
+
**Theme:** default (dark mode)
|
|
1042
|
+
**Guard Mode:** guided
|
|
1043
|
+
**Pages:** home
|
|
1044
|
+
|
|
1045
|
+
## Pages
|
|
1046
|
+
|
|
1047
|
+
| Page | Shell | Layout |
|
|
1048
|
+
|------|-------|--------|
|
|
1049
|
+
| home | sidebar-main | hero |
|
|
1050
|
+
|
|
1051
|
+
## Quick Start
|
|
1052
|
+
|
|
1053
|
+
1. Edit \`decantr.essence.json\` to define your project structure.
|
|
1054
|
+
2. Run \`decantr sync\` when online to fetch registry content.
|
|
1055
|
+
3. Use \`decantr create <type> <name>\` to create custom content.
|
|
1056
|
+
4. Use \`decantr search <query>\` to find patterns and themes.
|
|
1057
|
+
5. Use \`decantr validate\` to check your essence file.
|
|
1058
|
+
|
|
1059
|
+
## Commands
|
|
1060
|
+
|
|
1061
|
+
- \`decantr status\` \u2014 Project health
|
|
1062
|
+
- \`decantr search\` \u2014 Search the registry
|
|
1063
|
+
- \`decantr get <type> <id>\` \u2014 Fetch content details
|
|
1064
|
+
- \`decantr validate\` \u2014 Validate essence file
|
|
1065
|
+
- \`decantr sync\` \u2014 Sync registry content
|
|
1066
|
+
- \`decantr create <type> <name>\` \u2014 Create custom content
|
|
1067
|
+
- \`decantr publish <type> <name>\` \u2014 Publish to community registry
|
|
1068
|
+
|
|
1069
|
+
---
|
|
1070
|
+
|
|
1071
|
+
*Generated by @decantr/cli v${CLI_VERSION}*
|
|
1072
|
+
`;
|
|
1073
|
+
writeFileSync(decantrMdPath, decantrMdContent);
|
|
1074
|
+
const gitignoreUpdated = updateGitignore(projectRoot);
|
|
1075
|
+
return {
|
|
1076
|
+
essencePath,
|
|
1077
|
+
decantrMdPath,
|
|
1078
|
+
projectJsonPath,
|
|
1079
|
+
contextFiles: [],
|
|
1080
|
+
cssFiles: [],
|
|
1081
|
+
gitignoreUpdated
|
|
1082
|
+
};
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
// src/theme-commands.ts
|
|
1086
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2, readdirSync, rmSync } from "fs";
|
|
1087
|
+
import { join as join3 } from "path";
|
|
1088
|
+
|
|
1089
|
+
// src/theme-templates.ts
|
|
1090
|
+
function getThemeSkeleton(id, name) {
|
|
1091
|
+
return {
|
|
1092
|
+
$schema: "https://decantr.ai/schemas/style-metadata.v1.json",
|
|
1093
|
+
id,
|
|
1094
|
+
name,
|
|
1095
|
+
description: "",
|
|
1096
|
+
tags: [],
|
|
1097
|
+
seed: {
|
|
1098
|
+
primary: "#6366F1",
|
|
1099
|
+
secondary: "#8B5CF6",
|
|
1100
|
+
accent: "#EC4899",
|
|
1101
|
+
background: "#0F172A"
|
|
1102
|
+
},
|
|
1103
|
+
palette: {},
|
|
1104
|
+
modes: ["dark"],
|
|
1105
|
+
shapes: ["rounded"],
|
|
1106
|
+
decantr_compat: ">=1.0.0",
|
|
1107
|
+
source: "custom"
|
|
1108
|
+
};
|
|
1109
|
+
}
|
|
1110
|
+
function getHowToThemeDoc() {
|
|
1111
|
+
return `# Custom Themes
|
|
1112
|
+
|
|
1113
|
+
Create custom themes for your Decantr project.
|
|
1114
|
+
|
|
1115
|
+
## Quick Start
|
|
1116
|
+
|
|
1117
|
+
\`\`\`bash
|
|
1118
|
+
decantr theme create mytheme
|
|
1119
|
+
\`\`\`
|
|
1120
|
+
|
|
1121
|
+
## Theme Structure
|
|
1122
|
+
|
|
1123
|
+
| Field | Required | Description |
|
|
1124
|
+
|-------|----------|-------------|
|
|
1125
|
+
| id | Yes | Unique identifier (matches filename) |
|
|
1126
|
+
| name | Yes | Display name |
|
|
1127
|
+
| description | No | Brief description |
|
|
1128
|
+
| tags | No | Searchable tags |
|
|
1129
|
+
| seed | Yes | Core colors: primary, secondary, accent, background |
|
|
1130
|
+
| palette | No | Extended color palette |
|
|
1131
|
+
| modes | Yes | Supported modes: ["light"], ["dark"], or both |
|
|
1132
|
+
| shapes | Yes | Supported shapes: sharp, rounded, pill |
|
|
1133
|
+
| decantr_compat | Yes | Version compatibility (e.g., ">=1.0.0") |
|
|
1134
|
+
| source | Yes | Must be "custom" |
|
|
1135
|
+
|
|
1136
|
+
## Using Your Theme
|
|
1137
|
+
|
|
1138
|
+
In \`decantr.essence.json\`:
|
|
1139
|
+
|
|
1140
|
+
\`\`\`json
|
|
1141
|
+
{
|
|
1142
|
+
"theme": {
|
|
1143
|
+
"style": "custom:mytheme",
|
|
1144
|
+
"mode": "dark"
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
\`\`\`
|
|
1148
|
+
|
|
1149
|
+
## Validation
|
|
1150
|
+
|
|
1151
|
+
\`\`\`bash
|
|
1152
|
+
decantr theme validate mytheme
|
|
1153
|
+
\`\`\`
|
|
1154
|
+
|
|
1155
|
+
## Reference
|
|
1156
|
+
|
|
1157
|
+
See registry themes for examples:
|
|
1158
|
+
|
|
1159
|
+
\`\`\`bash
|
|
1160
|
+
decantr get theme auradecantism
|
|
1161
|
+
\`\`\`
|
|
1162
|
+
`;
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
// src/theme-commands.ts
|
|
1166
|
+
var REQUIRED_FIELDS = ["id", "name", "seed", "modes", "shapes", "decantr_compat", "source"];
|
|
1167
|
+
var REQUIRED_SEED = ["primary", "secondary", "accent", "background"];
|
|
1168
|
+
var VALID_MODES = ["light", "dark"];
|
|
1169
|
+
var VALID_SHAPES = ["sharp", "rounded", "pill"];
|
|
1170
|
+
function validateCustomTheme(theme) {
|
|
1171
|
+
const errors = [];
|
|
1172
|
+
for (const field of REQUIRED_FIELDS) {
|
|
1173
|
+
if (!(field in theme)) {
|
|
1174
|
+
errors.push(`Missing required field: ${field}`);
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
if (theme.seed && typeof theme.seed === "object") {
|
|
1178
|
+
const seed = theme.seed;
|
|
1179
|
+
for (const color of REQUIRED_SEED) {
|
|
1180
|
+
if (!(color in seed)) {
|
|
1181
|
+
errors.push(`Missing seed color: ${color}`);
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
if (Array.isArray(theme.modes)) {
|
|
1186
|
+
for (const mode of theme.modes) {
|
|
1187
|
+
if (!VALID_MODES.includes(mode)) {
|
|
1188
|
+
errors.push(`Invalid mode "${mode}" - must be "light" or "dark"`);
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
if (Array.isArray(theme.shapes)) {
|
|
1193
|
+
for (const shape of theme.shapes) {
|
|
1194
|
+
if (!VALID_SHAPES.includes(shape)) {
|
|
1195
|
+
errors.push(`Invalid shape "${shape}" - use: sharp, rounded, pill`);
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
return {
|
|
1200
|
+
valid: errors.length === 0,
|
|
1201
|
+
errors
|
|
1202
|
+
};
|
|
1203
|
+
}
|
|
1204
|
+
function createTheme(projectRoot, id, name) {
|
|
1205
|
+
const customThemesDir = join3(projectRoot, ".decantr", "custom", "themes");
|
|
1206
|
+
const themePath = join3(customThemesDir, `${id}.json`);
|
|
1207
|
+
const howToPath = join3(customThemesDir, "how-to-theme.md");
|
|
1208
|
+
mkdirSync2(customThemesDir, { recursive: true });
|
|
1209
|
+
if (existsSync3(themePath)) {
|
|
1210
|
+
return {
|
|
1211
|
+
success: false,
|
|
1212
|
+
error: `Theme "${id}" already exists at ${themePath}`
|
|
1213
|
+
};
|
|
1214
|
+
}
|
|
1215
|
+
const skeleton = getThemeSkeleton(id, name);
|
|
1216
|
+
writeFileSync2(themePath, JSON.stringify(skeleton, null, 2));
|
|
1217
|
+
if (!existsSync3(howToPath)) {
|
|
1218
|
+
writeFileSync2(howToPath, getHowToThemeDoc());
|
|
1219
|
+
}
|
|
1220
|
+
return {
|
|
1221
|
+
success: true,
|
|
1222
|
+
path: themePath
|
|
1223
|
+
};
|
|
1224
|
+
}
|
|
1225
|
+
function listCustomThemes(projectRoot) {
|
|
1226
|
+
const customThemesDir = join3(projectRoot, ".decantr", "custom", "themes");
|
|
1227
|
+
if (!existsSync3(customThemesDir)) {
|
|
1228
|
+
return [];
|
|
1229
|
+
}
|
|
1230
|
+
const themes = [];
|
|
1231
|
+
try {
|
|
1232
|
+
const files = readdirSync(customThemesDir).filter((f) => f.endsWith(".json"));
|
|
1233
|
+
for (const file of files) {
|
|
1234
|
+
const filePath = join3(customThemesDir, file);
|
|
1235
|
+
try {
|
|
1236
|
+
const data = JSON.parse(readFileSync3(filePath, "utf-8"));
|
|
1237
|
+
themes.push({
|
|
1238
|
+
id: data.id || file.replace(".json", ""),
|
|
1239
|
+
name: data.name || data.id,
|
|
1240
|
+
description: data.description,
|
|
1241
|
+
path: filePath
|
|
1242
|
+
});
|
|
1243
|
+
} catch {
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
} catch {
|
|
1247
|
+
}
|
|
1248
|
+
return themes;
|
|
1249
|
+
}
|
|
1250
|
+
function deleteTheme(projectRoot, id) {
|
|
1251
|
+
const themePath = join3(projectRoot, ".decantr", "custom", "themes", `${id}.json`);
|
|
1252
|
+
if (!existsSync3(themePath)) {
|
|
1253
|
+
return {
|
|
1254
|
+
success: false,
|
|
1255
|
+
error: `Theme "${id}" not found at ${themePath}`
|
|
1256
|
+
};
|
|
1257
|
+
}
|
|
1258
|
+
try {
|
|
1259
|
+
rmSync(themePath);
|
|
1260
|
+
return { success: true };
|
|
1261
|
+
} catch (e) {
|
|
1262
|
+
return {
|
|
1263
|
+
success: false,
|
|
1264
|
+
error: `Failed to delete: ${e.message}`
|
|
1265
|
+
};
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
function importTheme(projectRoot, sourcePath) {
|
|
1269
|
+
if (!existsSync3(sourcePath)) {
|
|
1270
|
+
return {
|
|
1271
|
+
success: false,
|
|
1272
|
+
errors: [`Source file not found: ${sourcePath}`]
|
|
1273
|
+
};
|
|
1274
|
+
}
|
|
1275
|
+
let theme;
|
|
1276
|
+
try {
|
|
1277
|
+
theme = JSON.parse(readFileSync3(sourcePath, "utf-8"));
|
|
1278
|
+
} catch (e) {
|
|
1279
|
+
return {
|
|
1280
|
+
success: false,
|
|
1281
|
+
errors: [`Invalid JSON: ${e.message}`]
|
|
1282
|
+
};
|
|
1283
|
+
}
|
|
1284
|
+
const validation = validateCustomTheme(theme);
|
|
1285
|
+
if (!validation.valid) {
|
|
1286
|
+
return {
|
|
1287
|
+
success: false,
|
|
1288
|
+
errors: validation.errors
|
|
1289
|
+
};
|
|
1290
|
+
}
|
|
1291
|
+
theme.source = "custom";
|
|
1292
|
+
const id = theme.id;
|
|
1293
|
+
const customThemesDir = join3(projectRoot, ".decantr", "custom", "themes");
|
|
1294
|
+
const destPath = join3(customThemesDir, `${id}.json`);
|
|
1295
|
+
mkdirSync2(customThemesDir, { recursive: true });
|
|
1296
|
+
const howToPath = join3(customThemesDir, "how-to-theme.md");
|
|
1297
|
+
if (!existsSync3(howToPath)) {
|
|
1298
|
+
writeFileSync2(howToPath, getHowToThemeDoc());
|
|
1299
|
+
}
|
|
1300
|
+
writeFileSync2(destPath, JSON.stringify(theme, null, 2));
|
|
1301
|
+
return {
|
|
1302
|
+
success: true,
|
|
1303
|
+
path: destPath
|
|
1304
|
+
};
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
// src/auth.ts
|
|
1308
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync3, rmSync as rmSync2 } from "fs";
|
|
1309
|
+
import { join as join4 } from "path";
|
|
1310
|
+
import { homedir } from "os";
|
|
1311
|
+
var CONFIG_DIR = join4(homedir(), ".config", "decantr");
|
|
1312
|
+
var AUTH_FILE = join4(CONFIG_DIR, "auth.json");
|
|
1313
|
+
function getCredentials() {
|
|
1314
|
+
if (!existsSync4(AUTH_FILE)) return null;
|
|
1315
|
+
try {
|
|
1316
|
+
return JSON.parse(readFileSync4(AUTH_FILE, "utf-8"));
|
|
1317
|
+
} catch {
|
|
1318
|
+
return null;
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
function saveCredentials(creds) {
|
|
1322
|
+
mkdirSync3(CONFIG_DIR, { recursive: true });
|
|
1323
|
+
writeFileSync3(AUTH_FILE, JSON.stringify(creds, null, 2));
|
|
1324
|
+
}
|
|
1325
|
+
function clearCredentials() {
|
|
1326
|
+
if (existsSync4(AUTH_FILE)) {
|
|
1327
|
+
rmSync2(AUTH_FILE);
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
function getApiKeyOrToken() {
|
|
1331
|
+
const creds = getCredentials();
|
|
1332
|
+
if (!creds) return null;
|
|
1333
|
+
return creds.api_key || creds.access_token || null;
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
// src/commands/publish.ts
|
|
1337
|
+
import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
|
|
1338
|
+
import { join as join5 } from "path";
|
|
1339
|
+
import { RegistryAPIClient } from "@decantr/registry";
|
|
1340
|
+
var PLURAL_TO_SINGULAR = {
|
|
1341
|
+
patterns: "pattern",
|
|
1342
|
+
recipes: "recipe",
|
|
1343
|
+
themes: "theme",
|
|
1344
|
+
blueprints: "blueprint",
|
|
1345
|
+
archetypes: "archetype",
|
|
1346
|
+
shells: "shell"
|
|
1347
|
+
};
|
|
1348
|
+
var SINGULAR_TO_PLURAL = {
|
|
1349
|
+
pattern: "patterns",
|
|
1350
|
+
recipe: "recipes",
|
|
1351
|
+
theme: "themes",
|
|
1352
|
+
blueprint: "blueprints",
|
|
1353
|
+
archetype: "archetypes",
|
|
1354
|
+
shell: "shells"
|
|
1355
|
+
};
|
|
1356
|
+
async function cmdPublish(type, name, projectRoot = process.cwd()) {
|
|
1357
|
+
const token = getApiKeyOrToken();
|
|
1358
|
+
if (!token) {
|
|
1359
|
+
console.error("Not authenticated. Run `decantr login` first.");
|
|
1360
|
+
process.exitCode = 1;
|
|
1361
|
+
return;
|
|
1362
|
+
}
|
|
1363
|
+
const singularType = PLURAL_TO_SINGULAR[type] || type;
|
|
1364
|
+
const pluralType = SINGULAR_TO_PLURAL[type] || SINGULAR_TO_PLURAL[singularType] || `${type}s`;
|
|
1365
|
+
const customPath = join5(projectRoot, ".decantr", "custom", pluralType, `${name}.json`);
|
|
1366
|
+
if (!existsSync5(customPath)) {
|
|
1367
|
+
console.error(`Custom ${singularType} "${name}" not found at ${customPath}`);
|
|
1368
|
+
console.error(`Create one first: decantr create ${singularType} ${name}`);
|
|
1369
|
+
process.exitCode = 1;
|
|
1370
|
+
return;
|
|
1371
|
+
}
|
|
1372
|
+
let data;
|
|
1373
|
+
try {
|
|
1374
|
+
data = JSON.parse(readFileSync5(customPath, "utf-8"));
|
|
1375
|
+
} catch {
|
|
1376
|
+
console.error(`Failed to parse ${customPath}`);
|
|
1377
|
+
process.exitCode = 1;
|
|
1378
|
+
return;
|
|
1379
|
+
}
|
|
1380
|
+
const client = new RegistryAPIClient({
|
|
1381
|
+
apiKey: token
|
|
1382
|
+
});
|
|
1383
|
+
try {
|
|
1384
|
+
const result = await client.publishContent({
|
|
1385
|
+
type: pluralType,
|
|
1386
|
+
slug: name,
|
|
1387
|
+
version: data.version || "1.0.0",
|
|
1388
|
+
data,
|
|
1389
|
+
namespace: "@community",
|
|
1390
|
+
visibility: "public"
|
|
1391
|
+
});
|
|
1392
|
+
console.log(`Published ${singularType}/${name} to @community`);
|
|
1393
|
+
console.log(`Status: ${result.status}`);
|
|
1394
|
+
} catch (err) {
|
|
1395
|
+
console.error(`Failed to publish: ${err.message}`);
|
|
1396
|
+
process.exitCode = 1;
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
// src/commands/create.ts
|
|
1401
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "fs";
|
|
1402
|
+
import { join as join6 } from "path";
|
|
1403
|
+
var CONTENT_TYPES = ["pattern", "recipe", "theme", "blueprint", "archetype", "shell"];
|
|
1404
|
+
var PLURAL = {
|
|
1405
|
+
pattern: "patterns",
|
|
1406
|
+
recipe: "recipes",
|
|
1407
|
+
theme: "themes",
|
|
1408
|
+
blueprint: "blueprints",
|
|
1409
|
+
archetype: "archetypes",
|
|
1410
|
+
shell: "shells"
|
|
1411
|
+
};
|
|
1412
|
+
function getSkeleton(type, id, name) {
|
|
1413
|
+
const base = {
|
|
1414
|
+
id,
|
|
1415
|
+
name,
|
|
1416
|
+
description: "",
|
|
1417
|
+
version: "1.0.0",
|
|
1418
|
+
source: "custom"
|
|
1419
|
+
};
|
|
1420
|
+
switch (type) {
|
|
1421
|
+
case "pattern":
|
|
1422
|
+
return { ...base, components: [], presets: {}, layout: {} };
|
|
1423
|
+
case "recipe":
|
|
1424
|
+
return { ...base, shell: {}, spatial: {}, effects: {} };
|
|
1425
|
+
case "theme":
|
|
1426
|
+
return { ...base, seed: { primary: "#6500C6", secondary: "#0AF3EB", accent: "#F58882", background: "#0D0D1A" }, modes: ["dark"], shapes: ["rounded"] };
|
|
1427
|
+
case "blueprint":
|
|
1428
|
+
return { ...base, compose: [], theme: {}, personality: [] };
|
|
1429
|
+
case "archetype":
|
|
1430
|
+
return { ...base, pages: [], features: [], suggested_theme: "" };
|
|
1431
|
+
case "shell":
|
|
1432
|
+
return { ...base, regions: [], layout: "sidebar-main" };
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
function cmdCreate(type, name, projectRoot = process.cwd()) {
|
|
1436
|
+
if (!CONTENT_TYPES.includes(type)) {
|
|
1437
|
+
console.error(`Invalid type "${type}". Must be one of: ${CONTENT_TYPES.join(", ")}`);
|
|
1438
|
+
process.exitCode = 1;
|
|
1439
|
+
return;
|
|
1440
|
+
}
|
|
1441
|
+
const plural = PLURAL[type];
|
|
1442
|
+
const customDir = join6(projectRoot, ".decantr", "custom", plural);
|
|
1443
|
+
const filePath = join6(customDir, `${name}.json`);
|
|
1444
|
+
if (existsSync6(filePath)) {
|
|
1445
|
+
console.error(`${type} "${name}" already exists at ${filePath}`);
|
|
1446
|
+
process.exitCode = 1;
|
|
1447
|
+
return;
|
|
1448
|
+
}
|
|
1449
|
+
mkdirSync4(customDir, { recursive: true });
|
|
1450
|
+
const skeleton = getSkeleton(type, name, name.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()));
|
|
1451
|
+
writeFileSync4(filePath, JSON.stringify(skeleton, null, 2));
|
|
1452
|
+
console.log(`Created ${type} "${name}" at ${filePath}`);
|
|
1453
|
+
console.log(`Edit it, then publish with: decantr publish ${type} ${name}`);
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
// src/index.ts
|
|
1457
|
+
var BOLD2 = "\x1B[1m";
|
|
1458
|
+
var DIM2 = "\x1B[2m";
|
|
1459
|
+
var RESET2 = "\x1B[0m";
|
|
1460
|
+
var RED = "\x1B[31m";
|
|
1461
|
+
var GREEN2 = "\x1B[32m";
|
|
1462
|
+
var CYAN2 = "\x1B[36m";
|
|
1463
|
+
var YELLOW2 = "\x1B[33m";
|
|
1464
|
+
function heading(text) {
|
|
1465
|
+
return `
|
|
1466
|
+
${BOLD2}${text}${RESET2}
|
|
1467
|
+
`;
|
|
1468
|
+
}
|
|
1469
|
+
function success(text) {
|
|
1470
|
+
return `${GREEN2}${text}${RESET2}`;
|
|
1471
|
+
}
|
|
1472
|
+
function error(text) {
|
|
1473
|
+
return `${RED}${text}${RESET2}`;
|
|
1474
|
+
}
|
|
1475
|
+
function dim(text) {
|
|
1476
|
+
return `${DIM2}${text}${RESET2}`;
|
|
1477
|
+
}
|
|
1478
|
+
function cyan(text) {
|
|
1479
|
+
return `${CYAN2}${text}${RESET2}`;
|
|
1480
|
+
}
|
|
1481
|
+
function extractPatternName(item) {
|
|
1482
|
+
if (typeof item === "string") return item;
|
|
1483
|
+
if (typeof item === "object" && item !== null) {
|
|
1484
|
+
const obj = item;
|
|
1485
|
+
if (typeof obj.pattern === "string") return obj.pattern;
|
|
1486
|
+
if (Array.isArray(obj.cols)) {
|
|
1487
|
+
return obj.cols.map(extractPatternName).join(" | ");
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
return "custom";
|
|
1491
|
+
}
|
|
1492
|
+
function generateCuratedPrompt(ctx) {
|
|
1493
|
+
const lines = [];
|
|
1494
|
+
lines.push(`I'm building a ${ctx.archetype} application using ${ctx.target}.`);
|
|
1495
|
+
lines.push("");
|
|
1496
|
+
if (ctx.blueprint) {
|
|
1497
|
+
lines.push(`Blueprint: ${ctx.blueprint}`);
|
|
1498
|
+
}
|
|
1499
|
+
lines.push(`Theme: ${ctx.theme} (${ctx.mode} mode)`);
|
|
1500
|
+
lines.push(`Personality: ${ctx.personality.join(", ")}`);
|
|
1501
|
+
lines.push(`Guard mode: ${ctx.guard}`);
|
|
1502
|
+
lines.push("");
|
|
1503
|
+
lines.push("Pages to build:");
|
|
1504
|
+
for (const page of ctx.pages) {
|
|
1505
|
+
const patternNames = page.layout.map(extractPatternName);
|
|
1506
|
+
const patterns = patternNames.length > 0 ? patternNames.join(", ") : "custom";
|
|
1507
|
+
lines.push(` - ${page.id}: ${page.shell} shell with ${patterns}`);
|
|
1508
|
+
}
|
|
1509
|
+
if (ctx.features.length > 0) {
|
|
1510
|
+
lines.push("");
|
|
1511
|
+
lines.push(`Features: ${ctx.features.join(", ")}`);
|
|
1512
|
+
}
|
|
1513
|
+
lines.push("");
|
|
1514
|
+
lines.push("Please read DECANTR.md for the full design spec and methodology.");
|
|
1515
|
+
lines.push("Follow the guard rules and use the patterns from decantr.essence.json.");
|
|
1516
|
+
return lines.join("\n");
|
|
1517
|
+
}
|
|
1518
|
+
function boxedPrompt(content, title) {
|
|
1519
|
+
const lines = content.split("\n");
|
|
1520
|
+
const maxLen = Math.max(...lines.map((l) => l.length), title.length + 4);
|
|
1521
|
+
const width = maxLen + 4;
|
|
1522
|
+
const top = `\u250C${"\u2500".repeat(width - 2)}\u2510`;
|
|
1523
|
+
const titleLine = `\u2502 ${BOLD2}${title}${RESET2}${" ".repeat(width - title.length - 4)} \u2502`;
|
|
1524
|
+
const sep = `\u251C${"\u2500".repeat(width - 2)}\u2524`;
|
|
1525
|
+
const bottom = `\u2514${"\u2500".repeat(width - 2)}\u2518`;
|
|
1526
|
+
const body = lines.map((line) => {
|
|
1527
|
+
const padding = " ".repeat(width - line.length - 4);
|
|
1528
|
+
return `\u2502 ${line}${padding} \u2502`;
|
|
1529
|
+
}).join("\n");
|
|
1530
|
+
return `${top}
|
|
1531
|
+
${titleLine}
|
|
1532
|
+
${sep}
|
|
1533
|
+
${body}
|
|
1534
|
+
${bottom}`;
|
|
1535
|
+
}
|
|
1536
|
+
function getAPIClient() {
|
|
1537
|
+
return new RegistryAPIClient2({
|
|
1538
|
+
baseUrl: process.env.DECANTR_API_URL || void 0,
|
|
1539
|
+
apiKey: process.env.DECANTR_API_KEY || void 0
|
|
1540
|
+
});
|
|
1541
|
+
}
|
|
1542
|
+
async function cmdSearch(query, type) {
|
|
1543
|
+
const apiClient = getAPIClient();
|
|
1544
|
+
try {
|
|
1545
|
+
const response = await apiClient.search({ q: query, type });
|
|
1546
|
+
const results = response.results;
|
|
1547
|
+
if (results.length === 0) {
|
|
1548
|
+
console.log(dim(`No results for "${query}"`));
|
|
1549
|
+
return;
|
|
1550
|
+
}
|
|
1551
|
+
console.log(heading(`${results.length} result(s) for "${query}"`));
|
|
1552
|
+
for (const r of results) {
|
|
1553
|
+
console.log(` ${cyan(r.type.padEnd(12))} ${BOLD2}${r.slug}${RESET2}`);
|
|
1554
|
+
console.log(` ${dim(r.description || "")}`);
|
|
1555
|
+
console.log("");
|
|
1556
|
+
}
|
|
1557
|
+
} catch {
|
|
1558
|
+
console.log(dim(`Search failed. API may be unavailable.`));
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
async function cmdSuggest(query, type) {
|
|
1562
|
+
const apiClient = getAPIClient();
|
|
1563
|
+
const searchType = type || "pattern";
|
|
1564
|
+
try {
|
|
1565
|
+
const response = await apiClient.search({ q: query, type: searchType });
|
|
1566
|
+
const results = response.results;
|
|
1567
|
+
if (results.length === 0) {
|
|
1568
|
+
console.log(dim(`No suggestions for "${query}"`));
|
|
1569
|
+
console.log("");
|
|
1570
|
+
console.log("Try:");
|
|
1571
|
+
console.log(` ${cyan("decantr list patterns")} - see all patterns`);
|
|
1572
|
+
console.log(` ${cyan("decantr search <broader-term>")} - broaden your search`);
|
|
1573
|
+
return;
|
|
1574
|
+
}
|
|
1575
|
+
console.log(heading(`Suggestions for "${query}"`));
|
|
1576
|
+
const queryLower = query.toLowerCase();
|
|
1577
|
+
const exact = results.filter((r) => r.slug.toLowerCase().includes(queryLower));
|
|
1578
|
+
const related = results.filter((r) => !r.slug.toLowerCase().includes(queryLower));
|
|
1579
|
+
if (exact.length > 0) {
|
|
1580
|
+
console.log(`${BOLD2}Direct matches:${RESET2}`);
|
|
1581
|
+
for (const r of exact.slice(0, 3)) {
|
|
1582
|
+
console.log(` ${cyan(r.slug)} - ${r.description || ""}`);
|
|
1583
|
+
}
|
|
1584
|
+
console.log("");
|
|
1585
|
+
}
|
|
1586
|
+
if (related.length > 0) {
|
|
1587
|
+
console.log(`${BOLD2}Related:${RESET2}`);
|
|
1588
|
+
for (const r of related.slice(0, 5)) {
|
|
1589
|
+
console.log(` ${cyan(r.slug)} - ${r.description || ""}`);
|
|
1590
|
+
}
|
|
1591
|
+
console.log("");
|
|
1592
|
+
}
|
|
1593
|
+
console.log(dim(`Use "decantr get pattern <id>" for full details`));
|
|
1594
|
+
} catch {
|
|
1595
|
+
console.log(dim(`Suggestion search failed. API may be unavailable.`));
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
async function cmdGet(type, id) {
|
|
1599
|
+
const validTypes = ["pattern", "archetype", "recipe", "theme", "blueprint", "shell"];
|
|
1600
|
+
if (!validTypes.includes(type)) {
|
|
1601
|
+
console.error(error(`Invalid type "${type}". Must be one of: ${validTypes.join(", ")}`));
|
|
1602
|
+
process.exitCode = 1;
|
|
1603
|
+
return;
|
|
1604
|
+
}
|
|
1605
|
+
const typeMap = {
|
|
1606
|
+
pattern: "patterns",
|
|
1607
|
+
archetype: "archetypes",
|
|
1608
|
+
recipe: "recipes",
|
|
1609
|
+
theme: "themes",
|
|
1610
|
+
blueprint: "blueprints",
|
|
1611
|
+
shell: "shells"
|
|
1612
|
+
};
|
|
1613
|
+
const apiType = typeMap[type];
|
|
1614
|
+
const registryClient = new RegistryClient({
|
|
1615
|
+
cacheDir: join7(process.cwd(), ".decantr", "cache")
|
|
1616
|
+
});
|
|
1617
|
+
const result = await registryClient.fetchContentItem(apiType, id);
|
|
1618
|
+
if (result) {
|
|
1619
|
+
console.log(JSON.stringify(result.data, null, 2));
|
|
1620
|
+
return;
|
|
1621
|
+
}
|
|
1622
|
+
const currentDir = dirname2(fileURLToPath2(import.meta.url));
|
|
1623
|
+
const bundledFromDist = join7(currentDir, "..", "src", "bundled", apiType, `${id}.json`);
|
|
1624
|
+
const bundledFromSrc = join7(currentDir, "bundled", apiType, `${id}.json`);
|
|
1625
|
+
const bundledPath = existsSync7(bundledFromDist) ? bundledFromDist : existsSync7(bundledFromSrc) ? bundledFromSrc : null;
|
|
1626
|
+
if (bundledPath) {
|
|
1627
|
+
const data = JSON.parse(readFileSync6(bundledPath, "utf-8"));
|
|
1628
|
+
console.log(JSON.stringify(data, null, 2));
|
|
1629
|
+
return;
|
|
1630
|
+
}
|
|
1631
|
+
console.error(error(`${type} "${id}" not found.`));
|
|
1632
|
+
process.exitCode = 1;
|
|
1633
|
+
return;
|
|
1634
|
+
}
|
|
1635
|
+
function buildRegistryContext() {
|
|
1636
|
+
const { readdirSync: readdirSync2 } = __require("fs");
|
|
1637
|
+
const themeRegistry = /* @__PURE__ */ new Map();
|
|
1638
|
+
const patternRegistry = /* @__PURE__ */ new Map();
|
|
1639
|
+
const projectRoot = process.cwd();
|
|
1640
|
+
const cacheDir = join7(projectRoot, ".decantr", "cache");
|
|
1641
|
+
const customDir = join7(projectRoot, ".decantr", "custom");
|
|
1642
|
+
const cachedThemesDir = join7(cacheDir, "@official", "themes");
|
|
1643
|
+
try {
|
|
1644
|
+
if (existsSync7(cachedThemesDir)) {
|
|
1645
|
+
for (const f of readdirSync2(cachedThemesDir).filter((f2) => f2.endsWith(".json") && f2 !== "index.json")) {
|
|
1646
|
+
const data = JSON.parse(readFileSync6(join7(cachedThemesDir, f), "utf-8"));
|
|
1647
|
+
if (data.id && !themeRegistry.has(data.id)) {
|
|
1648
|
+
themeRegistry.set(data.id, { modes: data.modes || ["light", "dark"] });
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
} catch {
|
|
1653
|
+
}
|
|
1654
|
+
const customThemesDir = join7(customDir, "themes");
|
|
1655
|
+
try {
|
|
1656
|
+
if (existsSync7(customThemesDir)) {
|
|
1657
|
+
for (const f of readdirSync2(customThemesDir).filter((f2) => f2.endsWith(".json"))) {
|
|
1658
|
+
const data = JSON.parse(readFileSync6(join7(customThemesDir, f), "utf-8"));
|
|
1659
|
+
if (data.id) {
|
|
1660
|
+
themeRegistry.set(`custom:${data.id}`, { modes: data.modes || ["light", "dark"] });
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
} catch {
|
|
1665
|
+
}
|
|
1666
|
+
const cachedPatternsDir = join7(cacheDir, "@official", "patterns");
|
|
1667
|
+
try {
|
|
1668
|
+
if (existsSync7(cachedPatternsDir)) {
|
|
1669
|
+
for (const f of readdirSync2(cachedPatternsDir).filter((f2) => f2.endsWith(".json") && f2 !== "index.json")) {
|
|
1670
|
+
const data = JSON.parse(readFileSync6(join7(cachedPatternsDir, f), "utf-8"));
|
|
1671
|
+
if (data.id && !patternRegistry.has(data.id)) {
|
|
1672
|
+
patternRegistry.set(data.id, data);
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
} catch {
|
|
1677
|
+
}
|
|
1678
|
+
return { themeRegistry, patternRegistry };
|
|
1679
|
+
}
|
|
1680
|
+
async function cmdValidate(path) {
|
|
1681
|
+
const essencePath = path || join7(process.cwd(), "decantr.essence.json");
|
|
1682
|
+
let raw;
|
|
1683
|
+
try {
|
|
1684
|
+
raw = readFileSync6(essencePath, "utf-8");
|
|
1685
|
+
} catch {
|
|
1686
|
+
console.error(error(`Could not read ${essencePath}`));
|
|
1687
|
+
process.exitCode = 1;
|
|
1688
|
+
return;
|
|
1689
|
+
}
|
|
1690
|
+
let essence;
|
|
1691
|
+
try {
|
|
1692
|
+
essence = JSON.parse(raw);
|
|
1693
|
+
} catch (e) {
|
|
1694
|
+
console.error(error(`Invalid JSON: ${e.message}`));
|
|
1695
|
+
process.exitCode = 1;
|
|
1696
|
+
return;
|
|
1697
|
+
}
|
|
1698
|
+
const result = validateEssence(essence);
|
|
1699
|
+
if (result.valid) {
|
|
1700
|
+
console.log(success("Essence is valid."));
|
|
1701
|
+
} else {
|
|
1702
|
+
console.error(error("Validation failed:"));
|
|
1703
|
+
for (const err of result.errors) {
|
|
1704
|
+
console.error(` ${RED}${err}${RESET2}`);
|
|
1705
|
+
}
|
|
1706
|
+
process.exitCode = 1;
|
|
1707
|
+
}
|
|
1708
|
+
try {
|
|
1709
|
+
const { themeRegistry, patternRegistry } = buildRegistryContext();
|
|
1710
|
+
const violations = evaluateGuard(essence, { themeRegistry, patternRegistry });
|
|
1711
|
+
if (violations.length > 0) {
|
|
1712
|
+
console.log(heading("Guard violations:"));
|
|
1713
|
+
for (const v of violations) {
|
|
1714
|
+
const vr = v;
|
|
1715
|
+
console.log(` ${YELLOW2}[${vr.rule}]${RESET2} ${vr.message}`);
|
|
1716
|
+
if (vr.suggestion) {
|
|
1717
|
+
console.log(` ${DIM2}Suggestion: ${vr.suggestion}${RESET2}`);
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
} else if (result.valid) {
|
|
1721
|
+
console.log(success("No guard violations."));
|
|
1722
|
+
}
|
|
1723
|
+
} catch {
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
async function cmdList(type) {
|
|
1727
|
+
const validTypes = ["patterns", "archetypes", "recipes", "themes", "blueprints", "shells"];
|
|
1728
|
+
if (!validTypes.includes(type)) {
|
|
1729
|
+
console.error(error(`Invalid type "${type}". Must be one of: ${validTypes.join(", ")}`));
|
|
1730
|
+
process.exitCode = 1;
|
|
1731
|
+
return;
|
|
1732
|
+
}
|
|
1733
|
+
const registryClient = new RegistryClient({
|
|
1734
|
+
cacheDir: join7(process.cwd(), ".decantr", "cache")
|
|
1735
|
+
});
|
|
1736
|
+
const result = await registryClient.fetchContentList(type);
|
|
1737
|
+
const items = result.data.items;
|
|
1738
|
+
if (items.length === 0) {
|
|
1739
|
+
console.log(dim(`No ${type} found.`));
|
|
1740
|
+
return;
|
|
1741
|
+
}
|
|
1742
|
+
if (type === "themes") {
|
|
1743
|
+
const customItems = registryClient.listCustomContent("themes");
|
|
1744
|
+
const customIds = new Set(customItems.map((c) => c.id));
|
|
1745
|
+
const registryItems = items.filter((i) => !customIds.has(i.id));
|
|
1746
|
+
console.log(heading(`Registry themes (${registryItems.length}):`));
|
|
1747
|
+
for (const item of registryItems) {
|
|
1748
|
+
console.log(` ${cyan(item.id)} ${dim(item.description || item.name || "")}`);
|
|
1749
|
+
}
|
|
1750
|
+
if (customItems.length > 0) {
|
|
1751
|
+
console.log("");
|
|
1752
|
+
console.log(heading(`Custom themes (${customItems.length}):`));
|
|
1753
|
+
for (const item of customItems) {
|
|
1754
|
+
console.log(` ${cyan(`custom:${item.id}`)} ${dim(item.description || item.name || "")}`);
|
|
1755
|
+
}
|
|
1756
|
+
} else {
|
|
1757
|
+
console.log("");
|
|
1758
|
+
console.log(dim("Custom themes (0):"));
|
|
1759
|
+
console.log(dim(' Run "decantr theme create <name>" to create a custom theme.'));
|
|
1760
|
+
}
|
|
1761
|
+
} else {
|
|
1762
|
+
console.log(heading(`${items.length} ${type}`));
|
|
1763
|
+
for (const item of items) {
|
|
1764
|
+
console.log(` ${cyan(item.id)} ${dim(item.description || item.name || "")}`);
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
async function cmdInit(args) {
|
|
1769
|
+
const projectRoot = process.cwd();
|
|
1770
|
+
console.log(heading("Decantr Project Setup"));
|
|
1771
|
+
const detected = detectProject(projectRoot);
|
|
1772
|
+
if (detected.existingEssence && !args.existing) {
|
|
1773
|
+
console.log(`${YELLOW2}Warning: decantr.essence.json already exists.${RESET2}`);
|
|
1774
|
+
const overwrite = await confirm("Overwrite existing configuration?", false);
|
|
1775
|
+
if (!overwrite) {
|
|
1776
|
+
console.log(dim("Cancelled."));
|
|
1777
|
+
return;
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
const registryClient = new RegistryClient({
|
|
1781
|
+
cacheDir: join7(projectRoot, ".decantr", "cache"),
|
|
1782
|
+
apiUrl: args.registry,
|
|
1783
|
+
offline: args.offline
|
|
1784
|
+
});
|
|
1785
|
+
const apiAvailable = await registryClient.checkApiAvailability();
|
|
1786
|
+
let selectedBlueprint = "default";
|
|
1787
|
+
let registrySource = "cache";
|
|
1788
|
+
if (args.yes) {
|
|
1789
|
+
selectedBlueprint = args.blueprint || "default";
|
|
1790
|
+
} else if (!apiAvailable) {
|
|
1791
|
+
if (!args.blueprint) {
|
|
1792
|
+
console.log(`
|
|
1793
|
+
${YELLOW2}You're offline. Scaffolding minimal Decantr project.${RESET2}`);
|
|
1794
|
+
console.log(dim("Run `decantr sync` or `decantr upgrade` when online to pull full registry content.\n"));
|
|
1795
|
+
const result2 = scaffoldMinimal(projectRoot);
|
|
1796
|
+
console.log(success("\nProject scaffolded (minimal/offline)!\n"));
|
|
1797
|
+
console.log(" Files created:");
|
|
1798
|
+
console.log(` ${cyan("decantr.essence.json")} Design specification`);
|
|
1799
|
+
console.log(` ${cyan("DECANTR.md")} LLM instructions`);
|
|
1800
|
+
console.log(` ${cyan(".decantr/")} Project state & custom content dirs`);
|
|
1801
|
+
if (result2.gitignoreUpdated) {
|
|
1802
|
+
console.log(` ${dim(".gitignore updated")}`);
|
|
1803
|
+
}
|
|
1804
|
+
console.log("");
|
|
1805
|
+
console.log(" Next steps:");
|
|
1806
|
+
console.log(` 1. Run ${cyan("decantr sync")} when online`);
|
|
1807
|
+
console.log(` 2. Use ${cyan("decantr create <type> <name>")} to create custom content`);
|
|
1808
|
+
console.log(` 3. Review DECANTR.md for methodology`);
|
|
1809
|
+
return;
|
|
1810
|
+
}
|
|
1811
|
+
console.log(`
|
|
1812
|
+
${YELLOW2}You're offline. Scaffolding Decantr default.${RESET2}`);
|
|
1813
|
+
console.log(dim("Run `decantr upgrade` when online, or visit decantr.ai/registry\n"));
|
|
1814
|
+
selectedBlueprint = "default";
|
|
1815
|
+
} else {
|
|
1816
|
+
console.log(dim("Fetching registry content..."));
|
|
1817
|
+
const blueprintsResult2 = await registryClient.fetchBlueprints();
|
|
1818
|
+
registrySource = blueprintsResult2.source.type === "api" ? "api" : "cache";
|
|
1819
|
+
const { selectedBlueprint: selected } = await runSimplifiedInit(
|
|
1820
|
+
blueprintsResult2.data.items
|
|
1821
|
+
);
|
|
1822
|
+
selectedBlueprint = selected || "default";
|
|
1823
|
+
}
|
|
1824
|
+
const [archetypesResult, blueprintsResult, themesResult] = await Promise.all([
|
|
1825
|
+
registryClient.fetchArchetypes(),
|
|
1826
|
+
registryClient.fetchBlueprints(),
|
|
1827
|
+
registryClient.fetchThemes()
|
|
1828
|
+
]);
|
|
1829
|
+
if (archetypesResult.source.type === "api") {
|
|
1830
|
+
registrySource = "api";
|
|
1831
|
+
}
|
|
1832
|
+
const archetypes = archetypesResult.data.items;
|
|
1833
|
+
const blueprints = blueprintsResult.data.items;
|
|
1834
|
+
const themes = themesResult.data.items;
|
|
1835
|
+
let options;
|
|
1836
|
+
if (args.yes || selectedBlueprint !== "default") {
|
|
1837
|
+
const flags = parseFlags(args, detected);
|
|
1838
|
+
flags.blueprint = selectedBlueprint !== "default" ? selectedBlueprint : flags.blueprint;
|
|
1839
|
+
options = mergeWithDefaults(flags, detected);
|
|
1840
|
+
} else {
|
|
1841
|
+
options = await runInteractivePrompts(detected, archetypes, blueprints, themes);
|
|
1842
|
+
}
|
|
1843
|
+
let archetypeData;
|
|
1844
|
+
if (options.blueprint) {
|
|
1845
|
+
const blueprintResult = await registryClient.fetchBlueprint(options.blueprint);
|
|
1846
|
+
if (blueprintResult) {
|
|
1847
|
+
const blueprint = blueprintResult.data;
|
|
1848
|
+
if (blueprint.theme) {
|
|
1849
|
+
if (blueprint.theme.style && options.theme === "luminarum") {
|
|
1850
|
+
options.theme = blueprint.theme.style;
|
|
1851
|
+
}
|
|
1852
|
+
if (blueprint.theme.mode && options.mode === "dark") {
|
|
1853
|
+
options.mode = blueprint.theme.mode;
|
|
1854
|
+
}
|
|
1855
|
+
if (blueprint.theme.shape && options.shape === "rounded") {
|
|
1856
|
+
options.shape = blueprint.theme.shape;
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
const primaryArchetype = blueprint.compose?.[0];
|
|
1860
|
+
if (primaryArchetype) {
|
|
1861
|
+
const archetypeResult = await registryClient.fetchArchetype(primaryArchetype);
|
|
1862
|
+
if (archetypeResult) {
|
|
1863
|
+
archetypeData = archetypeResult.data;
|
|
1864
|
+
options.archetype = primaryArchetype;
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
} else if (options.archetype) {
|
|
1869
|
+
const archetypeResult = await registryClient.fetchArchetype(options.archetype);
|
|
1870
|
+
if (archetypeResult) {
|
|
1871
|
+
archetypeData = archetypeResult.data;
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
let themeData;
|
|
1875
|
+
let recipeData;
|
|
1876
|
+
if (options.theme) {
|
|
1877
|
+
const themeResult = await registryClient.fetchTheme(options.theme);
|
|
1878
|
+
if (themeResult) {
|
|
1879
|
+
const theme = themeResult.data;
|
|
1880
|
+
themeData = {
|
|
1881
|
+
seed: theme.seed,
|
|
1882
|
+
palette: theme.palette,
|
|
1883
|
+
tokens: theme.tokens
|
|
1884
|
+
};
|
|
1885
|
+
if (theme.decorators) {
|
|
1886
|
+
recipeData = { decorators: theme.decorators };
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1890
|
+
console.log(heading("Scaffolding project..."));
|
|
1891
|
+
const result = scaffoldProject(
|
|
1892
|
+
projectRoot,
|
|
1893
|
+
options,
|
|
1894
|
+
detected,
|
|
1895
|
+
archetypeData,
|
|
1896
|
+
registrySource,
|
|
1897
|
+
themeData,
|
|
1898
|
+
recipeData
|
|
1899
|
+
);
|
|
1900
|
+
console.log(success("\nProject scaffolded!\n"));
|
|
1901
|
+
console.log(" Files created:");
|
|
1902
|
+
console.log(` ${cyan("decantr.essence.json")} Design specification`);
|
|
1903
|
+
console.log(` ${cyan("DECANTR.md")} LLM instructions`);
|
|
1904
|
+
console.log(` ${cyan(".decantr/")} Project state & cache`);
|
|
1905
|
+
if (result.gitignoreUpdated) {
|
|
1906
|
+
console.log(` ${dim(".gitignore updated")}`);
|
|
1907
|
+
}
|
|
1908
|
+
console.log("");
|
|
1909
|
+
console.log(" Next steps:");
|
|
1910
|
+
console.log(" 1. Review DECANTR.md for methodology");
|
|
1911
|
+
console.log(" 2. Explore more at decantr.ai/registry");
|
|
1912
|
+
console.log("");
|
|
1913
|
+
console.log(" Commands:");
|
|
1914
|
+
console.log(` ${cyan("decantr status")} Project health`);
|
|
1915
|
+
console.log(` ${cyan("decantr search")} Search registry`);
|
|
1916
|
+
console.log(` ${cyan("decantr get")} Fetch content details`);
|
|
1917
|
+
console.log(` ${cyan("decantr validate")} Check essence file`);
|
|
1918
|
+
console.log(` ${cyan("decantr upgrade")} Update to latest patterns`);
|
|
1919
|
+
console.log(` ${cyan("decantr heal")} Fix drift issues`);
|
|
1920
|
+
const essenceContent = readFileSync6(result.essencePath, "utf-8");
|
|
1921
|
+
const essence = JSON.parse(essenceContent);
|
|
1922
|
+
const validation = validateEssence(essence);
|
|
1923
|
+
if (!validation.valid) {
|
|
1924
|
+
console.log(error(`
|
|
1925
|
+
Validation warnings: ${validation.errors.join(", ")}`));
|
|
1926
|
+
}
|
|
1927
|
+
console.log("");
|
|
1928
|
+
const promptCtx = {
|
|
1929
|
+
archetype: options.archetype || "custom",
|
|
1930
|
+
blueprint: options.blueprint,
|
|
1931
|
+
theme: options.theme,
|
|
1932
|
+
mode: options.mode,
|
|
1933
|
+
target: options.target,
|
|
1934
|
+
pages: essence.structure || [{ id: "home", shell: options.shell, layout: ["hero"] }],
|
|
1935
|
+
personality: options.personality,
|
|
1936
|
+
features: options.features,
|
|
1937
|
+
guard: options.guard
|
|
1938
|
+
};
|
|
1939
|
+
const curatedPrompt = generateCuratedPrompt(promptCtx);
|
|
1940
|
+
console.log(boxedPrompt(curatedPrompt, "Copy this prompt for your AI assistant"));
|
|
1941
|
+
console.log("");
|
|
1942
|
+
if (registrySource === "cache") {
|
|
1943
|
+
console.log(dim('Run "decantr sync" when online to get the latest registry content.'));
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1946
|
+
async function cmdStatus() {
|
|
1947
|
+
const projectRoot = process.cwd();
|
|
1948
|
+
const essencePath = join7(projectRoot, "decantr.essence.json");
|
|
1949
|
+
const projectJsonPath = join7(projectRoot, ".decantr", "project.json");
|
|
1950
|
+
console.log(heading("Decantr Project Status"));
|
|
1951
|
+
if (!existsSync7(essencePath)) {
|
|
1952
|
+
console.log(`${RED}No decantr.essence.json found.${RESET2}`);
|
|
1953
|
+
console.log(dim('Run "decantr init" to create one.'));
|
|
1954
|
+
return;
|
|
1955
|
+
}
|
|
1956
|
+
try {
|
|
1957
|
+
const essence = JSON.parse(readFileSync6(essencePath, "utf-8"));
|
|
1958
|
+
const validation = validateEssence(essence);
|
|
1959
|
+
console.log(`${BOLD2}Essence:${RESET2}`);
|
|
1960
|
+
if (validation.valid) {
|
|
1961
|
+
console.log(` ${GREEN2}Valid${RESET2}`);
|
|
1962
|
+
} else {
|
|
1963
|
+
console.log(` ${RED}Invalid: ${validation.errors.join(", ")}${RESET2}`);
|
|
1964
|
+
}
|
|
1965
|
+
console.log(` Theme: ${essence.theme?.style || "unknown"} (${essence.theme?.mode || "unknown"})`);
|
|
1966
|
+
console.log(` Guard: ${essence.guard?.mode || "unknown"}`);
|
|
1967
|
+
console.log(` Pages: ${(essence.structure || []).length}`);
|
|
1968
|
+
} catch (e) {
|
|
1969
|
+
console.log(` ${RED}Error reading essence: ${e.message}${RESET2}`);
|
|
1970
|
+
}
|
|
1971
|
+
console.log("");
|
|
1972
|
+
console.log(`${BOLD2}Sync Status:${RESET2}`);
|
|
1973
|
+
if (existsSync7(projectJsonPath)) {
|
|
1974
|
+
try {
|
|
1975
|
+
const projectJson = JSON.parse(readFileSync6(projectJsonPath, "utf-8"));
|
|
1976
|
+
const syncStatus = projectJson.sync?.status || "unknown";
|
|
1977
|
+
const lastSync = projectJson.sync?.lastSync || "never";
|
|
1978
|
+
const source = projectJson.sync?.registrySource || "unknown";
|
|
1979
|
+
const statusColor = syncStatus === "synced" ? GREEN2 : YELLOW2;
|
|
1980
|
+
console.log(` Status: ${statusColor}${syncStatus}${RESET2}`);
|
|
1981
|
+
console.log(` Last sync: ${dim(lastSync)}`);
|
|
1982
|
+
console.log(` Source: ${dim(source)}`);
|
|
1983
|
+
} catch {
|
|
1984
|
+
console.log(` ${YELLOW2}Could not read project.json${RESET2}`);
|
|
1985
|
+
}
|
|
1986
|
+
} else {
|
|
1987
|
+
console.log(` ${YELLOW2}No .decantr/project.json found${RESET2}`);
|
|
1988
|
+
console.log(dim(' Run "decantr init" to create project files.'));
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
async function cmdSync() {
|
|
1992
|
+
const projectRoot = process.cwd();
|
|
1993
|
+
const cacheDir = join7(projectRoot, ".decantr", "cache");
|
|
1994
|
+
console.log(heading("Syncing registry content..."));
|
|
1995
|
+
const result = await syncRegistry(cacheDir);
|
|
1996
|
+
if (result.synced.length > 0) {
|
|
1997
|
+
console.log(success("Sync completed successfully."));
|
|
1998
|
+
console.log(` Synced: ${result.synced.join(", ")}`);
|
|
1999
|
+
if (result.failed.length > 0) {
|
|
2000
|
+
console.log(` ${YELLOW2}Failed: ${result.failed.join(", ")}${RESET2}`);
|
|
2001
|
+
}
|
|
2002
|
+
} else {
|
|
2003
|
+
console.log(`${YELLOW2}Could not sync: API unavailable${RESET2}`);
|
|
2004
|
+
if (result.failed.length > 0) {
|
|
2005
|
+
console.log(` ${YELLOW2}Failed: ${result.failed.join(", ")}${RESET2}`);
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
}
|
|
2009
|
+
async function cmdAudit() {
|
|
2010
|
+
const projectRoot = process.cwd();
|
|
2011
|
+
const essencePath = join7(projectRoot, "decantr.essence.json");
|
|
2012
|
+
console.log(heading("Auditing project..."));
|
|
2013
|
+
if (!existsSync7(essencePath)) {
|
|
2014
|
+
console.log(`${RED}No decantr.essence.json found.${RESET2}`);
|
|
2015
|
+
process.exitCode = 1;
|
|
2016
|
+
return;
|
|
2017
|
+
}
|
|
2018
|
+
try {
|
|
2019
|
+
const essence = JSON.parse(readFileSync6(essencePath, "utf-8"));
|
|
2020
|
+
const validation = validateEssence(essence);
|
|
2021
|
+
if (!validation.valid) {
|
|
2022
|
+
console.log(`${RED}Essence validation failed:${RESET2}`);
|
|
2023
|
+
for (const err of validation.errors) {
|
|
2024
|
+
console.log(` ${RED}${err}${RESET2}`);
|
|
2025
|
+
}
|
|
2026
|
+
process.exitCode = 1;
|
|
2027
|
+
return;
|
|
2028
|
+
}
|
|
2029
|
+
console.log(success("Essence is valid."));
|
|
2030
|
+
const { themeRegistry, patternRegistry } = buildRegistryContext();
|
|
2031
|
+
const violations = evaluateGuard(essence, { themeRegistry, patternRegistry });
|
|
2032
|
+
if (violations.length > 0) {
|
|
2033
|
+
console.log("");
|
|
2034
|
+
console.log(`${YELLOW2}Guard violations:${RESET2}`);
|
|
2035
|
+
for (const v of violations) {
|
|
2036
|
+
const vr = v;
|
|
2037
|
+
console.log(` ${YELLOW2}[${vr.rule}]${RESET2} ${vr.message}`);
|
|
2038
|
+
if (vr.suggestion) {
|
|
2039
|
+
console.log(` ${DIM2}Suggestion: ${vr.suggestion}${RESET2}`);
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
2042
|
+
} else {
|
|
2043
|
+
console.log(success("No guard violations."));
|
|
2044
|
+
}
|
|
2045
|
+
console.log("");
|
|
2046
|
+
console.log(`${BOLD2}Summary:${RESET2}`);
|
|
2047
|
+
console.log(` Pages defined: ${essence.structure.length}`);
|
|
2048
|
+
console.log(` Guard mode: ${essence.guard.mode}`);
|
|
2049
|
+
console.log(` Theme: ${essence.theme.style}`);
|
|
2050
|
+
} catch (e) {
|
|
2051
|
+
console.log(`${RED}Error: ${e.message}${RESET2}`);
|
|
2052
|
+
process.exitCode = 1;
|
|
2053
|
+
}
|
|
2054
|
+
}
|
|
2055
|
+
async function cmdTheme(args) {
|
|
2056
|
+
const subcommand = args[0];
|
|
2057
|
+
const projectRoot = process.cwd();
|
|
2058
|
+
if (!subcommand || subcommand === "help") {
|
|
2059
|
+
console.log(`
|
|
2060
|
+
${BOLD2}decantr theme${RESET2} \u2014 Manage custom themes
|
|
2061
|
+
|
|
2062
|
+
${BOLD2}Commands:${RESET2}
|
|
2063
|
+
${cyan("create")} <name> Create a new custom theme
|
|
2064
|
+
${cyan("create")} <name> --guided Interactive theme creation
|
|
2065
|
+
${cyan("list")} List custom themes
|
|
2066
|
+
${cyan("validate")} <name> Validate a custom theme
|
|
2067
|
+
${cyan("delete")} <name> Delete a custom theme
|
|
2068
|
+
${cyan("import")} <path> Import theme from JSON file
|
|
2069
|
+
|
|
2070
|
+
${BOLD2}Examples:${RESET2}
|
|
2071
|
+
decantr theme create mytheme
|
|
2072
|
+
decantr theme list
|
|
2073
|
+
decantr theme validate mytheme
|
|
2074
|
+
decantr theme import ./external-theme.json
|
|
2075
|
+
`);
|
|
2076
|
+
return;
|
|
2077
|
+
}
|
|
2078
|
+
switch (subcommand) {
|
|
2079
|
+
case "create": {
|
|
2080
|
+
const name = args[1];
|
|
2081
|
+
if (!name) {
|
|
2082
|
+
console.error(error("Usage: decantr theme create <name>"));
|
|
2083
|
+
process.exitCode = 1;
|
|
2084
|
+
return;
|
|
2085
|
+
}
|
|
2086
|
+
const displayName = name.charAt(0).toUpperCase() + name.slice(1).replace(/-/g, " ");
|
|
2087
|
+
const result = createTheme(projectRoot, name, displayName);
|
|
2088
|
+
if (result.success) {
|
|
2089
|
+
console.log(success(`Created custom theme "${name}"`));
|
|
2090
|
+
console.log(dim(` Path: ${result.path}`));
|
|
2091
|
+
console.log("");
|
|
2092
|
+
console.log(`Use in essence: ${cyan(`"style": "custom:${name}"`)}`);
|
|
2093
|
+
} else {
|
|
2094
|
+
console.error(error(result.error || "Failed to create theme"));
|
|
2095
|
+
process.exitCode = 1;
|
|
2096
|
+
}
|
|
2097
|
+
break;
|
|
2098
|
+
}
|
|
2099
|
+
case "list": {
|
|
2100
|
+
const themes = listCustomThemes(projectRoot);
|
|
2101
|
+
if (themes.length === 0) {
|
|
2102
|
+
console.log(dim("No custom themes found."));
|
|
2103
|
+
console.log(dim('Run "decantr theme create <name>" to create one.'));
|
|
2104
|
+
} else {
|
|
2105
|
+
console.log(heading(`${themes.length} custom theme(s)`));
|
|
2106
|
+
for (const theme of themes) {
|
|
2107
|
+
console.log(` ${cyan(`custom:${theme.id}`)} ${dim(theme.description || theme.name)}`);
|
|
2108
|
+
}
|
|
2109
|
+
}
|
|
2110
|
+
break;
|
|
2111
|
+
}
|
|
2112
|
+
case "validate": {
|
|
2113
|
+
const name = args[1];
|
|
2114
|
+
if (!name) {
|
|
2115
|
+
console.error(error("Usage: decantr theme validate <name>"));
|
|
2116
|
+
process.exitCode = 1;
|
|
2117
|
+
return;
|
|
2118
|
+
}
|
|
2119
|
+
const themePath = join7(projectRoot, ".decantr", "custom", "themes", `${name}.json`);
|
|
2120
|
+
if (!existsSync7(themePath)) {
|
|
2121
|
+
console.error(error(`Theme "${name}" not found at ${themePath}`));
|
|
2122
|
+
process.exitCode = 1;
|
|
2123
|
+
return;
|
|
2124
|
+
}
|
|
2125
|
+
try {
|
|
2126
|
+
const theme = JSON.parse(readFileSync6(themePath, "utf-8"));
|
|
2127
|
+
const result = validateCustomTheme(theme);
|
|
2128
|
+
if (result.valid) {
|
|
2129
|
+
console.log(success(`Custom theme "${name}" is valid`));
|
|
2130
|
+
} else {
|
|
2131
|
+
console.error(error("Validation failed:"));
|
|
2132
|
+
for (const err of result.errors) {
|
|
2133
|
+
console.error(` ${RED}${err}${RESET2}`);
|
|
2134
|
+
}
|
|
2135
|
+
process.exitCode = 1;
|
|
2136
|
+
}
|
|
2137
|
+
} catch (e) {
|
|
2138
|
+
console.error(error(`Invalid JSON: ${e.message}`));
|
|
2139
|
+
process.exitCode = 1;
|
|
2140
|
+
}
|
|
2141
|
+
break;
|
|
2142
|
+
}
|
|
2143
|
+
case "delete": {
|
|
2144
|
+
const name = args[1];
|
|
2145
|
+
if (!name) {
|
|
2146
|
+
console.error(error("Usage: decantr theme delete <name>"));
|
|
2147
|
+
process.exitCode = 1;
|
|
2148
|
+
return;
|
|
2149
|
+
}
|
|
2150
|
+
const result = deleteTheme(projectRoot, name);
|
|
2151
|
+
if (result.success) {
|
|
2152
|
+
console.log(success(`Deleted custom theme "${name}"`));
|
|
2153
|
+
} else {
|
|
2154
|
+
console.error(error(result.error || "Failed to delete theme"));
|
|
2155
|
+
process.exitCode = 1;
|
|
2156
|
+
}
|
|
2157
|
+
break;
|
|
2158
|
+
}
|
|
2159
|
+
case "import": {
|
|
2160
|
+
const sourcePath = args[1];
|
|
2161
|
+
if (!sourcePath) {
|
|
2162
|
+
console.error(error("Usage: decantr theme import <path>"));
|
|
2163
|
+
process.exitCode = 1;
|
|
2164
|
+
return;
|
|
2165
|
+
}
|
|
2166
|
+
const result = importTheme(projectRoot, sourcePath);
|
|
2167
|
+
if (result.success) {
|
|
2168
|
+
console.log(success("Theme imported successfully"));
|
|
2169
|
+
console.log(dim(` Path: ${result.path}`));
|
|
2170
|
+
} else {
|
|
2171
|
+
console.error(error("Import failed:"));
|
|
2172
|
+
for (const err of result.errors || []) {
|
|
2173
|
+
console.error(` ${RED}${err}${RESET2}`);
|
|
2174
|
+
}
|
|
2175
|
+
process.exitCode = 1;
|
|
2176
|
+
}
|
|
2177
|
+
break;
|
|
2178
|
+
}
|
|
2179
|
+
default:
|
|
2180
|
+
console.error(error(`Unknown theme command: ${subcommand}`));
|
|
2181
|
+
process.exitCode = 1;
|
|
2182
|
+
}
|
|
2183
|
+
}
|
|
2184
|
+
function cmdHelp() {
|
|
2185
|
+
console.log(`
|
|
2186
|
+
${BOLD2}decantr${RESET2} \u2014 Design intelligence for AI-generated UI
|
|
2187
|
+
|
|
2188
|
+
${BOLD2}Usage:${RESET2}
|
|
2189
|
+
decantr init [options]
|
|
2190
|
+
decantr status
|
|
2191
|
+
decantr sync
|
|
2192
|
+
decantr audit
|
|
2193
|
+
decantr search <query> [--type <type>]
|
|
2194
|
+
decantr suggest <query> [--type <type>]
|
|
2195
|
+
decantr get <type> <id>
|
|
2196
|
+
decantr list <type>
|
|
2197
|
+
decantr validate [path]
|
|
2198
|
+
decantr theme <subcommand>
|
|
2199
|
+
decantr create <type> <name>
|
|
2200
|
+
decantr publish <type> <name>
|
|
2201
|
+
decantr login
|
|
2202
|
+
decantr logout
|
|
2203
|
+
decantr help
|
|
2204
|
+
|
|
2205
|
+
${BOLD2}Init Options:${RESET2}
|
|
2206
|
+
--blueprint, -b Blueprint ID
|
|
2207
|
+
--theme Theme ID
|
|
2208
|
+
--mode Color mode: dark | light | auto
|
|
2209
|
+
--shape Border shape: pill | rounded | sharp
|
|
2210
|
+
--target Framework: react | vue | svelte | nextjs | html
|
|
2211
|
+
--guard Guard mode: creative | guided | strict
|
|
2212
|
+
--density Spacing: compact | comfortable | spacious
|
|
2213
|
+
--shell Default shell layout
|
|
2214
|
+
--existing Initialize in existing project
|
|
2215
|
+
--offline Force offline mode
|
|
2216
|
+
--yes, -y Accept defaults, skip confirmations
|
|
2217
|
+
--registry Custom registry URL
|
|
2218
|
+
|
|
2219
|
+
${BOLD2}Commands:${RESET2}
|
|
2220
|
+
${cyan("init")} Initialize a new Decantr project with full scaffolding
|
|
2221
|
+
${cyan("status")} Show project status and sync state
|
|
2222
|
+
${cyan("sync")} Sync registry content from API
|
|
2223
|
+
${cyan("audit")} Validate essence and check for drift
|
|
2224
|
+
${cyan("search")} Search the registry
|
|
2225
|
+
${cyan("suggest")} Suggest patterns or alternatives for a query
|
|
2226
|
+
${cyan("get")} Get full details of a registry item
|
|
2227
|
+
${cyan("list")} List items by type
|
|
2228
|
+
${cyan("validate")} Validate essence file
|
|
2229
|
+
${cyan("theme")} Manage custom themes (create, list, validate, delete, import)
|
|
2230
|
+
${cyan("create")} Create a custom content item (pattern, recipe, theme, etc.)
|
|
2231
|
+
${cyan("publish")} Publish a custom content item to the community registry
|
|
2232
|
+
${cyan("login")} Authenticate with the Decantr registry
|
|
2233
|
+
${cyan("logout")} Remove stored credentials
|
|
2234
|
+
${cyan("help")} Show this help
|
|
2235
|
+
|
|
2236
|
+
${BOLD2}Examples:${RESET2}
|
|
2237
|
+
decantr init
|
|
2238
|
+
decantr init --blueprint=saas-dashboard --theme=luminarum --yes
|
|
2239
|
+
decantr status
|
|
2240
|
+
decantr sync
|
|
2241
|
+
decantr audit
|
|
2242
|
+
decantr search dashboard
|
|
2243
|
+
decantr suggest leaderboard
|
|
2244
|
+
decantr suggest ranking --type pattern
|
|
2245
|
+
decantr list patterns
|
|
2246
|
+
decantr create pattern my-card
|
|
2247
|
+
decantr publish pattern my-card
|
|
2248
|
+
`);
|
|
2249
|
+
}
|
|
2250
|
+
async function main() {
|
|
2251
|
+
const args = process.argv.slice(2);
|
|
2252
|
+
const command = args[0];
|
|
2253
|
+
if (!command || command === "help" || command === "--help" || command === "-h") {
|
|
2254
|
+
cmdHelp();
|
|
2255
|
+
return;
|
|
2256
|
+
}
|
|
2257
|
+
switch (command) {
|
|
2258
|
+
case "init": {
|
|
2259
|
+
const initArgs = {};
|
|
2260
|
+
for (let i = 1; i < args.length; i++) {
|
|
2261
|
+
const arg = args[i];
|
|
2262
|
+
if (arg === "--yes" || arg === "-y") {
|
|
2263
|
+
initArgs.yes = true;
|
|
2264
|
+
} else if (arg === "--offline") {
|
|
2265
|
+
initArgs.offline = true;
|
|
2266
|
+
} else if (arg === "--existing") {
|
|
2267
|
+
initArgs.existing = true;
|
|
2268
|
+
} else if (arg.startsWith("--")) {
|
|
2269
|
+
const [key, value] = arg.slice(2).split("=");
|
|
2270
|
+
if (value) {
|
|
2271
|
+
initArgs[key] = value;
|
|
2272
|
+
} else if (args[i + 1] && !args[i + 1].startsWith("-")) {
|
|
2273
|
+
initArgs[key] = args[++i];
|
|
2274
|
+
}
|
|
2275
|
+
} else if (arg.startsWith("-")) {
|
|
2276
|
+
const key = arg.slice(1);
|
|
2277
|
+
if (key === "b" && args[i + 1]) initArgs.blueprint = args[++i];
|
|
2278
|
+
if (key === "y") initArgs.yes = true;
|
|
2279
|
+
}
|
|
2280
|
+
}
|
|
2281
|
+
await cmdInit(initArgs);
|
|
2282
|
+
break;
|
|
2283
|
+
}
|
|
2284
|
+
case "status": {
|
|
2285
|
+
await cmdStatus();
|
|
2286
|
+
break;
|
|
2287
|
+
}
|
|
2288
|
+
case "sync": {
|
|
2289
|
+
await cmdSync();
|
|
2290
|
+
break;
|
|
2291
|
+
}
|
|
2292
|
+
case "upgrade": {
|
|
2293
|
+
const { cmdUpgrade } = await import("./upgrade-3YL3NFOG.js");
|
|
2294
|
+
await cmdUpgrade(process.cwd());
|
|
2295
|
+
break;
|
|
2296
|
+
}
|
|
2297
|
+
case "heal": {
|
|
2298
|
+
const { cmdHeal } = await import("./heal-4DWU7BJS.js");
|
|
2299
|
+
await cmdHeal(process.cwd());
|
|
2300
|
+
break;
|
|
2301
|
+
}
|
|
2302
|
+
case "audit": {
|
|
2303
|
+
await cmdAudit();
|
|
2304
|
+
break;
|
|
2305
|
+
}
|
|
2306
|
+
case "search": {
|
|
2307
|
+
const query = args[1];
|
|
2308
|
+
if (!query) {
|
|
2309
|
+
console.error(error("Usage: decantr search <query> [--type <type>]"));
|
|
2310
|
+
process.exitCode = 1;
|
|
2311
|
+
return;
|
|
2312
|
+
}
|
|
2313
|
+
const typeIdx = args.indexOf("--type");
|
|
2314
|
+
const type = typeIdx !== -1 ? args[typeIdx + 1] : void 0;
|
|
2315
|
+
await cmdSearch(query, type);
|
|
2316
|
+
break;
|
|
2317
|
+
}
|
|
2318
|
+
case "suggest": {
|
|
2319
|
+
const query = args[1];
|
|
2320
|
+
if (!query) {
|
|
2321
|
+
console.error(error("Usage: decantr suggest <query> [--type <type>]"));
|
|
2322
|
+
process.exitCode = 1;
|
|
2323
|
+
return;
|
|
2324
|
+
}
|
|
2325
|
+
const typeIdx = args.indexOf("--type");
|
|
2326
|
+
const type = typeIdx !== -1 ? args[typeIdx + 1] : void 0;
|
|
2327
|
+
await cmdSuggest(query, type);
|
|
2328
|
+
break;
|
|
2329
|
+
}
|
|
2330
|
+
case "get": {
|
|
2331
|
+
const type = args[1];
|
|
2332
|
+
const id = args[2];
|
|
2333
|
+
if (!type || !id) {
|
|
2334
|
+
console.error(error("Usage: decantr get <type> <id>"));
|
|
2335
|
+
process.exitCode = 1;
|
|
2336
|
+
return;
|
|
2337
|
+
}
|
|
2338
|
+
await cmdGet(type, id);
|
|
2339
|
+
break;
|
|
2340
|
+
}
|
|
2341
|
+
case "list": {
|
|
2342
|
+
const type = args[1];
|
|
2343
|
+
if (!type) {
|
|
2344
|
+
console.error(error("Usage: decantr list <type>"));
|
|
2345
|
+
process.exitCode = 1;
|
|
2346
|
+
return;
|
|
2347
|
+
}
|
|
2348
|
+
await cmdList(type);
|
|
2349
|
+
break;
|
|
2350
|
+
}
|
|
2351
|
+
case "validate": {
|
|
2352
|
+
await cmdValidate(args[1]);
|
|
2353
|
+
break;
|
|
2354
|
+
}
|
|
2355
|
+
case "theme": {
|
|
2356
|
+
await cmdTheme(args.slice(1));
|
|
2357
|
+
break;
|
|
2358
|
+
}
|
|
2359
|
+
case "login": {
|
|
2360
|
+
const apiKeyArg = args[1];
|
|
2361
|
+
if (apiKeyArg && apiKeyArg.startsWith("--api-key=")) {
|
|
2362
|
+
const key = apiKeyArg.split("=")[1];
|
|
2363
|
+
saveCredentials({ access_token: key, api_key: key });
|
|
2364
|
+
console.log(success("API key saved."));
|
|
2365
|
+
} else {
|
|
2366
|
+
console.log(heading("Decantr Login"));
|
|
2367
|
+
console.log(" To authenticate, get your API key from the Decantr dashboard:");
|
|
2368
|
+
console.log("");
|
|
2369
|
+
console.log(` ${cyan("https://decantr.ai/dashboard/api-keys")}`);
|
|
2370
|
+
console.log("");
|
|
2371
|
+
console.log(" Then run:");
|
|
2372
|
+
console.log(` ${cyan("decantr login --api-key=<your-key>")}`);
|
|
2373
|
+
console.log("");
|
|
2374
|
+
console.log(" Or set the environment variable:");
|
|
2375
|
+
console.log(` ${cyan("export DECANTR_API_KEY=<your-key>")}`);
|
|
2376
|
+
const existingCreds = getCredentials();
|
|
2377
|
+
if (existingCreds) {
|
|
2378
|
+
console.log("");
|
|
2379
|
+
console.log(dim("You are currently authenticated."));
|
|
2380
|
+
}
|
|
2381
|
+
}
|
|
2382
|
+
break;
|
|
2383
|
+
}
|
|
2384
|
+
case "logout": {
|
|
2385
|
+
clearCredentials();
|
|
2386
|
+
console.log(success("Logged out. Credentials removed."));
|
|
2387
|
+
break;
|
|
2388
|
+
}
|
|
2389
|
+
case "create": {
|
|
2390
|
+
const type = args[1];
|
|
2391
|
+
const name = args[2];
|
|
2392
|
+
if (!type || !name) {
|
|
2393
|
+
console.error(error("Usage: decantr create <type> <name>"));
|
|
2394
|
+
console.error(dim("Types: pattern, recipe, theme, blueprint, archetype, shell"));
|
|
2395
|
+
process.exitCode = 1;
|
|
2396
|
+
break;
|
|
2397
|
+
}
|
|
2398
|
+
cmdCreate(type, name);
|
|
2399
|
+
break;
|
|
2400
|
+
}
|
|
2401
|
+
case "publish": {
|
|
2402
|
+
const type = args[1];
|
|
2403
|
+
const name = args[2];
|
|
2404
|
+
if (!type || !name) {
|
|
2405
|
+
console.error(error("Usage: decantr publish <type> <name>"));
|
|
2406
|
+
console.error(dim("Types: pattern, recipe, theme, blueprint, archetype, shell"));
|
|
2407
|
+
process.exitCode = 1;
|
|
2408
|
+
break;
|
|
2409
|
+
}
|
|
2410
|
+
await cmdPublish(type, name);
|
|
2411
|
+
break;
|
|
2412
|
+
}
|
|
2413
|
+
default:
|
|
2414
|
+
console.error(error(`Unknown command: ${command}`));
|
|
2415
|
+
cmdHelp();
|
|
2416
|
+
process.exitCode = 1;
|
|
2417
|
+
}
|
|
2418
|
+
}
|
|
2419
|
+
main().catch((e) => {
|
|
2420
|
+
console.error(error(e.message));
|
|
2421
|
+
process.exitCode = 1;
|
|
2422
|
+
});
|