@decantr/cli 1.0.0-beta.10
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/LICENSE +21 -0
- package/dist/index.js +1353 -0
- package/package.json +33 -0
- package/src/templates/DECANTR.md.template +394 -0
- package/src/templates/essence-summary.md.template +50 -0
- package/src/templates/project.json.template +30 -0
- package/src/templates/task-add-page.md.template +111 -0
- package/src/templates/task-modify.md.template +171 -0
- package/src/templates/task-scaffold.md.template +65 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1353 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { readFileSync as readFileSync4, existsSync as existsSync4 } from "fs";
|
|
5
|
+
import { join as join4 } from "path";
|
|
6
|
+
import { validateEssence, evaluateGuard } from "@decantr/essence-spec";
|
|
7
|
+
import { createResolver, createRegistryClient } from "@decantr/registry";
|
|
8
|
+
|
|
9
|
+
// src/detect.ts
|
|
10
|
+
import { existsSync, readFileSync } from "fs";
|
|
11
|
+
import { join } from "path";
|
|
12
|
+
var RULE_FILES = [
|
|
13
|
+
"CLAUDE.md",
|
|
14
|
+
".cursorrules",
|
|
15
|
+
".cursor/rules",
|
|
16
|
+
"AGENTS.md",
|
|
17
|
+
"GEMINI.md",
|
|
18
|
+
"copilot-instructions.md"
|
|
19
|
+
];
|
|
20
|
+
function detectProject(projectRoot = process.cwd()) {
|
|
21
|
+
const result = {
|
|
22
|
+
framework: "unknown",
|
|
23
|
+
packageManager: "unknown",
|
|
24
|
+
hasTypeScript: false,
|
|
25
|
+
hasTailwind: false,
|
|
26
|
+
existingRuleFiles: [],
|
|
27
|
+
existingEssence: false,
|
|
28
|
+
projectRoot
|
|
29
|
+
};
|
|
30
|
+
result.existingEssence = existsSync(join(projectRoot, "decantr.essence.json"));
|
|
31
|
+
for (const ruleFile of RULE_FILES) {
|
|
32
|
+
if (existsSync(join(projectRoot, ruleFile))) {
|
|
33
|
+
result.existingRuleFiles.push(ruleFile);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (existsSync(join(projectRoot, "pnpm-lock.yaml"))) {
|
|
37
|
+
result.packageManager = "pnpm";
|
|
38
|
+
} else if (existsSync(join(projectRoot, "yarn.lock"))) {
|
|
39
|
+
result.packageManager = "yarn";
|
|
40
|
+
} else if (existsSync(join(projectRoot, "bun.lockb"))) {
|
|
41
|
+
result.packageManager = "bun";
|
|
42
|
+
} else if (existsSync(join(projectRoot, "package-lock.json"))) {
|
|
43
|
+
result.packageManager = "npm";
|
|
44
|
+
}
|
|
45
|
+
result.hasTypeScript = existsSync(join(projectRoot, "tsconfig.json"));
|
|
46
|
+
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"));
|
|
47
|
+
if (existsSync(join(projectRoot, "next.config.js")) || existsSync(join(projectRoot, "next.config.ts")) || existsSync(join(projectRoot, "next.config.mjs"))) {
|
|
48
|
+
result.framework = "nextjs";
|
|
49
|
+
result.version = getPackageVersion(projectRoot, "next");
|
|
50
|
+
return result;
|
|
51
|
+
}
|
|
52
|
+
if (existsSync(join(projectRoot, "nuxt.config.js")) || existsSync(join(projectRoot, "nuxt.config.ts"))) {
|
|
53
|
+
result.framework = "nuxt";
|
|
54
|
+
result.version = getPackageVersion(projectRoot, "nuxt");
|
|
55
|
+
return result;
|
|
56
|
+
}
|
|
57
|
+
if (existsSync(join(projectRoot, "astro.config.mjs")) || existsSync(join(projectRoot, "astro.config.ts"))) {
|
|
58
|
+
result.framework = "astro";
|
|
59
|
+
result.version = getPackageVersion(projectRoot, "astro");
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
62
|
+
if (existsSync(join(projectRoot, "svelte.config.js")) || existsSync(join(projectRoot, "svelte.config.ts"))) {
|
|
63
|
+
result.framework = "svelte";
|
|
64
|
+
result.version = getPackageVersion(projectRoot, "svelte");
|
|
65
|
+
return result;
|
|
66
|
+
}
|
|
67
|
+
if (existsSync(join(projectRoot, "angular.json"))) {
|
|
68
|
+
result.framework = "angular";
|
|
69
|
+
result.version = getPackageVersion(projectRoot, "@angular/core");
|
|
70
|
+
return result;
|
|
71
|
+
}
|
|
72
|
+
const packageJsonPath = join(projectRoot, "package.json");
|
|
73
|
+
if (existsSync(packageJsonPath)) {
|
|
74
|
+
try {
|
|
75
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
76
|
+
const deps = { ...packageJson.dependencies, ...packageJson.devDependencies };
|
|
77
|
+
if (deps.next) {
|
|
78
|
+
result.framework = "nextjs";
|
|
79
|
+
result.version = deps.next.replace(/^\^|~/, "");
|
|
80
|
+
} else if (deps.nuxt) {
|
|
81
|
+
result.framework = "nuxt";
|
|
82
|
+
result.version = deps.nuxt.replace(/^\^|~/, "");
|
|
83
|
+
} else if (deps.astro) {
|
|
84
|
+
result.framework = "astro";
|
|
85
|
+
result.version = deps.astro.replace(/^\^|~/, "");
|
|
86
|
+
} else if (deps.svelte) {
|
|
87
|
+
result.framework = "svelte";
|
|
88
|
+
result.version = deps.svelte.replace(/^\^|~/, "");
|
|
89
|
+
} else if (deps["@angular/core"]) {
|
|
90
|
+
result.framework = "angular";
|
|
91
|
+
result.version = deps["@angular/core"].replace(/^\^|~/, "");
|
|
92
|
+
} else if (deps.vue) {
|
|
93
|
+
result.framework = "vue";
|
|
94
|
+
result.version = deps.vue.replace(/^\^|~/, "");
|
|
95
|
+
} else if (deps.react) {
|
|
96
|
+
result.framework = "react";
|
|
97
|
+
result.version = deps.react.replace(/^\^|~/, "");
|
|
98
|
+
}
|
|
99
|
+
} catch {
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (result.framework === "unknown" && !existsSync(packageJsonPath)) {
|
|
103
|
+
if (existsSync(join(projectRoot, "index.html"))) {
|
|
104
|
+
result.framework = "html";
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return result;
|
|
108
|
+
}
|
|
109
|
+
function getPackageVersion(projectRoot, packageName) {
|
|
110
|
+
const packageJsonPath = join(projectRoot, "package.json");
|
|
111
|
+
if (!existsSync(packageJsonPath)) return void 0;
|
|
112
|
+
try {
|
|
113
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
114
|
+
const deps = { ...packageJson.dependencies, ...packageJson.devDependencies };
|
|
115
|
+
const version = deps[packageName];
|
|
116
|
+
return version?.replace(/^\^|~/, "");
|
|
117
|
+
} catch {
|
|
118
|
+
return void 0;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// src/prompts.ts
|
|
123
|
+
import { createInterface } from "readline";
|
|
124
|
+
var BOLD = "\x1B[1m";
|
|
125
|
+
var DIM = "\x1B[2m";
|
|
126
|
+
var RESET = "\x1B[0m";
|
|
127
|
+
var GREEN = "\x1B[32m";
|
|
128
|
+
var YELLOW = "\x1B[33m";
|
|
129
|
+
var CYAN = "\x1B[36m";
|
|
130
|
+
function ask(question, defaultValue) {
|
|
131
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
132
|
+
const prompt = defaultValue ? `${question} ${DIM}(${defaultValue})${RESET}: ` : `${question}: `;
|
|
133
|
+
return new Promise((resolve) => {
|
|
134
|
+
rl.question(prompt, (answer) => {
|
|
135
|
+
rl.close();
|
|
136
|
+
resolve(answer.trim() || defaultValue || "");
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
async function select(question, options, defaultIdx = 0, allowOther = false) {
|
|
141
|
+
console.log(`
|
|
142
|
+
${BOLD}${question}${RESET}`);
|
|
143
|
+
for (let i = 0; i < options.length; i++) {
|
|
144
|
+
const marker = i === defaultIdx ? `${GREEN}>${RESET}` : " ";
|
|
145
|
+
const desc = options[i].description ? ` ${DIM}\u2014 ${options[i].description}${RESET}` : "";
|
|
146
|
+
console.log(` ${marker} ${i + 1}. ${options[i].label}${desc}`);
|
|
147
|
+
}
|
|
148
|
+
if (allowOther) {
|
|
149
|
+
console.log(` ${options.length + 1}. ${DIM}other (enter custom value)${RESET}`);
|
|
150
|
+
}
|
|
151
|
+
const maxIdx = allowOther ? options.length + 1 : options.length;
|
|
152
|
+
const answer = await ask(`Choose (1-${maxIdx})`, String(defaultIdx + 1));
|
|
153
|
+
const idx = parseInt(answer, 10) - 1;
|
|
154
|
+
if (allowOther && idx === options.length) {
|
|
155
|
+
const custom = await ask("Enter custom value");
|
|
156
|
+
return custom;
|
|
157
|
+
}
|
|
158
|
+
const validIdx = Math.max(0, Math.min(idx, options.length - 1));
|
|
159
|
+
return options[validIdx].value;
|
|
160
|
+
}
|
|
161
|
+
async function confirm(question, defaultYes = true) {
|
|
162
|
+
const hint = defaultYes ? "Y/n" : "y/N";
|
|
163
|
+
const answer = await ask(`${question} [${hint}]`, defaultYes ? "y" : "n");
|
|
164
|
+
return answer.toLowerCase() === "y" || answer.toLowerCase() === "yes";
|
|
165
|
+
}
|
|
166
|
+
function warn(message) {
|
|
167
|
+
console.log(`
|
|
168
|
+
${YELLOW} Warning: ${message}${RESET}`);
|
|
169
|
+
}
|
|
170
|
+
function showDetection(detected) {
|
|
171
|
+
console.log(`
|
|
172
|
+
${CYAN}Detected project configuration:${RESET}`);
|
|
173
|
+
if (detected.framework !== "unknown") {
|
|
174
|
+
const version = detected.version ? ` ${detected.version}` : "";
|
|
175
|
+
console.log(` Framework: ${detected.framework}${version}`);
|
|
176
|
+
}
|
|
177
|
+
if (detected.packageManager !== "unknown") {
|
|
178
|
+
console.log(` Package manager: ${detected.packageManager}`);
|
|
179
|
+
}
|
|
180
|
+
if (detected.hasTypeScript) {
|
|
181
|
+
console.log(` TypeScript: ${GREEN}yes${RESET}`);
|
|
182
|
+
}
|
|
183
|
+
if (detected.hasTailwind) {
|
|
184
|
+
console.log(` Tailwind CSS: ${GREEN}yes${RESET}`);
|
|
185
|
+
}
|
|
186
|
+
if (detected.existingEssence) {
|
|
187
|
+
console.log(` Existing essence: ${YELLOW}yes${RESET}`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
async function runInteractivePrompts(detected, archetypes, blueprints, themes) {
|
|
191
|
+
showDetection(detected);
|
|
192
|
+
const blueprintOptions = [
|
|
193
|
+
{ value: "none", label: "none", description: "Start from scratch (blank canvas)" },
|
|
194
|
+
...blueprints.map((b) => ({
|
|
195
|
+
value: b.id,
|
|
196
|
+
label: b.id,
|
|
197
|
+
description: b.description
|
|
198
|
+
}))
|
|
199
|
+
];
|
|
200
|
+
const blueprint = await select("What are you building?", blueprintOptions, 0, true);
|
|
201
|
+
const isBlank = blueprint === "none";
|
|
202
|
+
const themeOptions = themes.map((t) => ({
|
|
203
|
+
value: t.id,
|
|
204
|
+
label: t.id,
|
|
205
|
+
description: t.description
|
|
206
|
+
}));
|
|
207
|
+
const defaultThemeIdx = themeOptions.findIndex((t) => t.value === "luminarum") || 0;
|
|
208
|
+
const theme = await select("Choose a theme", themeOptions, Math.max(0, defaultThemeIdx), true);
|
|
209
|
+
const mode = await select(
|
|
210
|
+
"Color mode",
|
|
211
|
+
[
|
|
212
|
+
{ value: "dark", label: "dark", description: "Dark background" },
|
|
213
|
+
{ value: "light", label: "light", description: "Light background" },
|
|
214
|
+
{ value: "auto", label: "auto", description: "Follow system preference" }
|
|
215
|
+
],
|
|
216
|
+
0
|
|
217
|
+
);
|
|
218
|
+
const shape = await select(
|
|
219
|
+
"Border shape",
|
|
220
|
+
[
|
|
221
|
+
{ value: "pill", label: "pill", description: "Fully rounded corners" },
|
|
222
|
+
{ value: "rounded", label: "rounded", description: "Moderately rounded" },
|
|
223
|
+
{ value: "sharp", label: "sharp", description: "No border radius" }
|
|
224
|
+
],
|
|
225
|
+
0,
|
|
226
|
+
true
|
|
227
|
+
);
|
|
228
|
+
const frameworkOptions = [
|
|
229
|
+
{ value: "react", label: "react", description: "React / Create React App" },
|
|
230
|
+
{ value: "nextjs", label: "nextjs", description: "Next.js" },
|
|
231
|
+
{ value: "vue", label: "vue", description: "Vue.js" },
|
|
232
|
+
{ value: "nuxt", label: "nuxt", description: "Nuxt" },
|
|
233
|
+
{ value: "svelte", label: "svelte", description: "Svelte / SvelteKit" },
|
|
234
|
+
{ value: "astro", label: "astro", description: "Astro" },
|
|
235
|
+
{ value: "angular", label: "angular", description: "Angular" },
|
|
236
|
+
{ value: "html", label: "html", description: "Plain HTML/CSS/JS" }
|
|
237
|
+
];
|
|
238
|
+
let defaultFrameworkIdx = frameworkOptions.findIndex((f) => f.value === detected.framework);
|
|
239
|
+
if (defaultFrameworkIdx < 0) defaultFrameworkIdx = 0;
|
|
240
|
+
const target = await select("Target framework", frameworkOptions, defaultFrameworkIdx, true);
|
|
241
|
+
if (detected.framework !== "unknown" && target !== detected.framework) {
|
|
242
|
+
warn(`This project appears to be ${detected.framework} but you selected ${target}.`);
|
|
243
|
+
const proceed = await confirm("Continue anyway?", false);
|
|
244
|
+
if (!proceed) {
|
|
245
|
+
console.log(`${DIM}Using detected framework: ${detected.framework}${RESET}`);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
const guardMode = await select(
|
|
249
|
+
"Guard enforcement level",
|
|
250
|
+
[
|
|
251
|
+
{ value: "creative", label: "creative", description: "Advisory only (new projects)" },
|
|
252
|
+
{ value: "guided", label: "guided", description: "Style, structure, recipe enforced" },
|
|
253
|
+
{ value: "strict", label: "strict", description: "All 5 rules enforced exactly" }
|
|
254
|
+
],
|
|
255
|
+
detected.existingEssence ? 1 : 2
|
|
256
|
+
// Default to guided for existing, strict for new
|
|
257
|
+
);
|
|
258
|
+
const density = await select(
|
|
259
|
+
"Spacing density",
|
|
260
|
+
[
|
|
261
|
+
{ value: "compact", label: "compact", description: "Dense UI, minimal spacing" },
|
|
262
|
+
{ value: "comfortable", label: "comfortable", description: "Balanced spacing" },
|
|
263
|
+
{ value: "spacious", label: "spacious", description: "Generous whitespace" }
|
|
264
|
+
],
|
|
265
|
+
1
|
|
266
|
+
);
|
|
267
|
+
const shellOptions = [
|
|
268
|
+
{ value: "sidebar-main", label: "sidebar-main", description: "Collapsible sidebar with main content" },
|
|
269
|
+
{ value: "top-nav-main", label: "top-nav-main", description: "Horizontal nav with full-width content" },
|
|
270
|
+
{ value: "centered", label: "centered", description: "Centered card (auth flows)" },
|
|
271
|
+
{ value: "full-bleed", label: "full-bleed", description: "No persistent nav (landing pages)" },
|
|
272
|
+
{ value: "minimal-header", label: "minimal-header", description: "Slim header with centered content" }
|
|
273
|
+
];
|
|
274
|
+
let defaultShellIdx = 0;
|
|
275
|
+
if (["nextjs", "nuxt", "astro"].includes(target)) {
|
|
276
|
+
defaultShellIdx = shellOptions.findIndex((s) => s.value === "top-nav-main");
|
|
277
|
+
}
|
|
278
|
+
const shell = await select("Default page shell (layout)", shellOptions, Math.max(0, defaultShellIdx), true);
|
|
279
|
+
return {
|
|
280
|
+
blueprint: isBlank ? void 0 : blueprint,
|
|
281
|
+
archetype: void 0,
|
|
282
|
+
// Will be derived from blueprint
|
|
283
|
+
theme,
|
|
284
|
+
mode,
|
|
285
|
+
shape,
|
|
286
|
+
target,
|
|
287
|
+
guard: guardMode,
|
|
288
|
+
density,
|
|
289
|
+
shell,
|
|
290
|
+
personality: ["professional"],
|
|
291
|
+
features: [],
|
|
292
|
+
existing: detected.existingEssence
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
function parseFlags(args, detected) {
|
|
296
|
+
const options = {};
|
|
297
|
+
if (typeof args.blueprint === "string") options.blueprint = args.blueprint;
|
|
298
|
+
if (typeof args.archetype === "string") options.archetype = args.archetype;
|
|
299
|
+
if (typeof args.theme === "string") options.theme = args.theme;
|
|
300
|
+
if (args.mode === "dark" || args.mode === "light" || args.mode === "auto") options.mode = args.mode;
|
|
301
|
+
if (typeof args.shape === "string") options.shape = args.shape;
|
|
302
|
+
if (typeof args.target === "string") options.target = args.target;
|
|
303
|
+
if (args.guard === "creative" || args.guard === "guided" || args.guard === "strict") options.guard = args.guard;
|
|
304
|
+
if (args.density === "compact" || args.density === "comfortable" || args.density === "spacious") options.density = args.density;
|
|
305
|
+
if (typeof args.shell === "string") options.shell = args.shell;
|
|
306
|
+
if (typeof args.personality === "string") options.personality = args.personality.split(",").map((s) => s.trim());
|
|
307
|
+
if (typeof args.features === "string") options.features = args.features.split(",").map((s) => s.trim());
|
|
308
|
+
if (args.existing === true) options.existing = true;
|
|
309
|
+
return options;
|
|
310
|
+
}
|
|
311
|
+
function mergeWithDefaults(flags, detected) {
|
|
312
|
+
return {
|
|
313
|
+
blueprint: flags.blueprint,
|
|
314
|
+
archetype: flags.archetype,
|
|
315
|
+
theme: flags.theme || "luminarum",
|
|
316
|
+
mode: flags.mode || "dark",
|
|
317
|
+
shape: flags.shape || "rounded",
|
|
318
|
+
target: flags.target || (detected.framework !== "unknown" ? detected.framework : "react"),
|
|
319
|
+
guard: flags.guard || (detected.existingEssence ? "guided" : "strict"),
|
|
320
|
+
density: flags.density || "comfortable",
|
|
321
|
+
shell: flags.shell || "sidebar-main",
|
|
322
|
+
personality: flags.personality || ["professional"],
|
|
323
|
+
features: flags.features || [],
|
|
324
|
+
existing: flags.existing || detected.existingEssence
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// src/scaffold.ts
|
|
329
|
+
import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync, appendFileSync } from "fs";
|
|
330
|
+
import { join as join2, dirname } from "path";
|
|
331
|
+
import { fileURLToPath } from "url";
|
|
332
|
+
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
333
|
+
var CLI_VERSION = "1.0.0";
|
|
334
|
+
function loadTemplate(name) {
|
|
335
|
+
const fromDist = join2(__dirname, "..", "src", "templates", name);
|
|
336
|
+
if (existsSync2(fromDist)) {
|
|
337
|
+
return readFileSync2(fromDist, "utf-8");
|
|
338
|
+
}
|
|
339
|
+
const fromSrc = join2(__dirname, "templates", name);
|
|
340
|
+
if (existsSync2(fromSrc)) {
|
|
341
|
+
return readFileSync2(fromSrc, "utf-8");
|
|
342
|
+
}
|
|
343
|
+
throw new Error(`Template not found: ${name}`);
|
|
344
|
+
}
|
|
345
|
+
function renderTemplate(template, vars) {
|
|
346
|
+
let result = template;
|
|
347
|
+
for (const [key, value] of Object.entries(vars)) {
|
|
348
|
+
result = result.replace(new RegExp(`\\{\\{${key}\\}\\}`, "g"), value);
|
|
349
|
+
}
|
|
350
|
+
return result;
|
|
351
|
+
}
|
|
352
|
+
function buildEssence(options, archetypeData) {
|
|
353
|
+
let structure = [
|
|
354
|
+
{ id: "home", shell: options.shell, layout: ["hero"] }
|
|
355
|
+
];
|
|
356
|
+
let features = options.features;
|
|
357
|
+
if (archetypeData?.pages) {
|
|
358
|
+
structure = archetypeData.pages.map((p) => ({
|
|
359
|
+
id: p.id,
|
|
360
|
+
shell: p.shell || options.shell,
|
|
361
|
+
// Ensure layout has at least one item (schema requires minItems: 1)
|
|
362
|
+
layout: p.default_layout?.length ? p.default_layout : ["hero"]
|
|
363
|
+
}));
|
|
364
|
+
}
|
|
365
|
+
if (archetypeData?.features) {
|
|
366
|
+
features = [.../* @__PURE__ */ new Set([...features, ...archetypeData.features])];
|
|
367
|
+
}
|
|
368
|
+
const contentGapMap = {
|
|
369
|
+
compact: "_gap2",
|
|
370
|
+
comfortable: "_gap4",
|
|
371
|
+
spacious: "_gap6"
|
|
372
|
+
};
|
|
373
|
+
const archetype = options.archetype || "custom";
|
|
374
|
+
return {
|
|
375
|
+
version: "2.0.0",
|
|
376
|
+
archetype,
|
|
377
|
+
theme: {
|
|
378
|
+
style: options.theme,
|
|
379
|
+
mode: options.mode,
|
|
380
|
+
recipe: options.theme,
|
|
381
|
+
// Recipe defaults to theme
|
|
382
|
+
shape: options.shape
|
|
383
|
+
},
|
|
384
|
+
personality: options.personality,
|
|
385
|
+
platform: {
|
|
386
|
+
type: "spa",
|
|
387
|
+
routing: "hash"
|
|
388
|
+
},
|
|
389
|
+
structure,
|
|
390
|
+
features,
|
|
391
|
+
guard: {
|
|
392
|
+
enforce_style: true,
|
|
393
|
+
enforce_recipe: true,
|
|
394
|
+
mode: options.guard
|
|
395
|
+
},
|
|
396
|
+
density: {
|
|
397
|
+
level: options.density,
|
|
398
|
+
content_gap: contentGapMap[options.density] || "_gap4"
|
|
399
|
+
},
|
|
400
|
+
target: options.target
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
function generateDecantrMd(essence, detected) {
|
|
404
|
+
const template = loadTemplate("DECANTR.md.template");
|
|
405
|
+
const pagesTable = essence.structure.map(
|
|
406
|
+
(p) => `| ${p.id} | ${p.shell} | ${p.layout.join(", ") || "none"} |`
|
|
407
|
+
).join("\n");
|
|
408
|
+
const allPatterns = [...new Set(essence.structure.flatMap((p) => p.layout))];
|
|
409
|
+
const patternsList = allPatterns.length > 0 ? allPatterns.map((p) => `- \`${p}\``).join("\n") : "- No patterns specified yet";
|
|
410
|
+
const projectSummary = [
|
|
411
|
+
`**Archetype:** ${essence.archetype || "custom"}`,
|
|
412
|
+
`**Target:** ${essence.target}`,
|
|
413
|
+
`**Theme:** ${essence.theme.style} (${essence.theme.mode} mode)`,
|
|
414
|
+
`**Guard Mode:** ${essence.guard.mode}`,
|
|
415
|
+
`**Pages:** ${essence.structure.map((s) => s.id).join(", ")}`
|
|
416
|
+
].join("\n");
|
|
417
|
+
const shellStructures = {
|
|
418
|
+
"sidebar-main": "nav (left) | header (top) | body (scrollable)",
|
|
419
|
+
"top-nav-main": "header (full width) | body (scrollable)",
|
|
420
|
+
"centered": "body (centered card)",
|
|
421
|
+
"full-bleed": "header (floating) | body (full page sections)",
|
|
422
|
+
"minimal-header": "header (slim) | body (centered)"
|
|
423
|
+
};
|
|
424
|
+
const defaultShell = essence.structure[0]?.shell || "sidebar-main";
|
|
425
|
+
const shellStructure = shellStructures[defaultShell] || "Custom shell layout";
|
|
426
|
+
const vars = {
|
|
427
|
+
GUARD_MODE: essence.guard.mode,
|
|
428
|
+
PROJECT_SUMMARY: projectSummary,
|
|
429
|
+
THEME_STYLE: essence.theme.style,
|
|
430
|
+
THEME_MODE: essence.theme.mode,
|
|
431
|
+
THEME_RECIPE: essence.theme.recipe,
|
|
432
|
+
TARGET: essence.target,
|
|
433
|
+
PAGES_TABLE: `| Page | Shell | Layout |
|
|
434
|
+
|------|-------|--------|
|
|
435
|
+
${pagesTable}`,
|
|
436
|
+
PATTERNS_LIST: patternsList,
|
|
437
|
+
DEFAULT_SHELL: defaultShell,
|
|
438
|
+
SHELL_STRUCTURE: shellStructure,
|
|
439
|
+
PERSONALITY: essence.personality.join(", "),
|
|
440
|
+
DENSITY: essence.density.level,
|
|
441
|
+
AVAILABLE_PATTERNS: "(See registry or .decantr/cache/patterns/)",
|
|
442
|
+
AVAILABLE_THEMES: "(See registry or .decantr/cache/themes/)",
|
|
443
|
+
AVAILABLE_SHELLS: "sidebar-main, top-nav-main, centered, full-bleed, minimal-header",
|
|
444
|
+
VERSION: CLI_VERSION
|
|
445
|
+
};
|
|
446
|
+
return renderTemplate(template, vars);
|
|
447
|
+
}
|
|
448
|
+
function generateProjectJson(detected, options, registrySource) {
|
|
449
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
450
|
+
const data = {
|
|
451
|
+
detected: {
|
|
452
|
+
framework: detected.framework,
|
|
453
|
+
version: detected.version || null,
|
|
454
|
+
packageManager: detected.packageManager,
|
|
455
|
+
hasTypeScript: detected.hasTypeScript,
|
|
456
|
+
hasTailwind: detected.hasTailwind,
|
|
457
|
+
existingRuleFiles: detected.existingRuleFiles
|
|
458
|
+
},
|
|
459
|
+
overrides: {
|
|
460
|
+
framework: options.target !== detected.framework ? options.target : null
|
|
461
|
+
},
|
|
462
|
+
sync: {
|
|
463
|
+
status: registrySource === "api" ? "synced" : "needs-sync",
|
|
464
|
+
lastSync: now,
|
|
465
|
+
registrySource,
|
|
466
|
+
cachedContent: {
|
|
467
|
+
archetypes: [],
|
|
468
|
+
patterns: [],
|
|
469
|
+
themes: [],
|
|
470
|
+
recipes: []
|
|
471
|
+
}
|
|
472
|
+
},
|
|
473
|
+
initialized: {
|
|
474
|
+
at: now,
|
|
475
|
+
via: "cli",
|
|
476
|
+
version: CLI_VERSION,
|
|
477
|
+
flags: buildFlagsString(options)
|
|
478
|
+
}
|
|
479
|
+
};
|
|
480
|
+
return JSON.stringify(data, null, 2);
|
|
481
|
+
}
|
|
482
|
+
function buildFlagsString(options) {
|
|
483
|
+
const flags = [];
|
|
484
|
+
if (options.blueprint) flags.push(`--blueprint=${options.blueprint}`);
|
|
485
|
+
if (options.theme) flags.push(`--theme=${options.theme}`);
|
|
486
|
+
if (options.mode) flags.push(`--mode=${options.mode}`);
|
|
487
|
+
if (options.guard) flags.push(`--guard=${options.guard}`);
|
|
488
|
+
return flags.join(" ");
|
|
489
|
+
}
|
|
490
|
+
function generateTaskContext(templateName, essence) {
|
|
491
|
+
const template = loadTemplate(templateName);
|
|
492
|
+
const defaultShell = essence.structure[0]?.shell || "sidebar-main";
|
|
493
|
+
const layout = essence.structure[0]?.layout.join(", ") || "none";
|
|
494
|
+
const scaffoldStructure = essence.structure.map((p) => {
|
|
495
|
+
const patterns = p.layout.length > 0 ? `
|
|
496
|
+
- Patterns: ${p.layout.join(", ")}` : "";
|
|
497
|
+
return `- **${p.id}** (${p.shell})${patterns}`;
|
|
498
|
+
}).join("\n");
|
|
499
|
+
const vars = {
|
|
500
|
+
TARGET: essence.target,
|
|
501
|
+
THEME_STYLE: essence.theme.style,
|
|
502
|
+
THEME_MODE: essence.theme.mode,
|
|
503
|
+
THEME_RECIPE: essence.theme.recipe,
|
|
504
|
+
DEFAULT_SHELL: defaultShell,
|
|
505
|
+
GUARD_MODE: essence.guard.mode,
|
|
506
|
+
LAYOUT: layout,
|
|
507
|
+
DENSITY: essence.density.level,
|
|
508
|
+
CONTENT_GAP: essence.density.content_gap,
|
|
509
|
+
SCAFFOLD_STRUCTURE: scaffoldStructure
|
|
510
|
+
};
|
|
511
|
+
return renderTemplate(template, vars);
|
|
512
|
+
}
|
|
513
|
+
function generateEssenceSummary(essence) {
|
|
514
|
+
const template = loadTemplate("essence-summary.md.template");
|
|
515
|
+
const pagesTable = `| Page | Shell | Layout |
|
|
516
|
+
|------|-------|--------|
|
|
517
|
+
${essence.structure.map((p) => `| ${p.id} | ${p.shell} | ${p.layout.join(", ") || "none"} |`).join("\n")}`;
|
|
518
|
+
const featuresList = essence.features.length > 0 ? essence.features.map((f) => `- ${f}`).join("\n") : "- No features specified";
|
|
519
|
+
const vars = {
|
|
520
|
+
ARCHETYPE: essence.archetype || "custom",
|
|
521
|
+
BLUEPRINT: essence.blueprint || "none",
|
|
522
|
+
PERSONALITY: essence.personality.join(", "),
|
|
523
|
+
TARGET: essence.target,
|
|
524
|
+
THEME_STYLE: essence.theme.style,
|
|
525
|
+
THEME_MODE: essence.theme.mode,
|
|
526
|
+
THEME_RECIPE: essence.theme.recipe,
|
|
527
|
+
SHAPE: essence.theme.shape,
|
|
528
|
+
PAGES_TABLE: pagesTable,
|
|
529
|
+
FEATURES_LIST: featuresList,
|
|
530
|
+
GUARD_MODE: essence.guard.mode,
|
|
531
|
+
ENFORCE_STYLE: String(essence.guard.enforce_style),
|
|
532
|
+
ENFORCE_RECIPE: String(essence.guard.enforce_recipe),
|
|
533
|
+
DENSITY: essence.density.level,
|
|
534
|
+
CONTENT_GAP: essence.density.content_gap,
|
|
535
|
+
LAST_UPDATED: (/* @__PURE__ */ new Date()).toISOString()
|
|
536
|
+
};
|
|
537
|
+
return renderTemplate(template, vars);
|
|
538
|
+
}
|
|
539
|
+
function updateGitignore(projectRoot) {
|
|
540
|
+
const gitignorePath = join2(projectRoot, ".gitignore");
|
|
541
|
+
const cacheEntry = ".decantr/cache/";
|
|
542
|
+
if (existsSync2(gitignorePath)) {
|
|
543
|
+
const content = readFileSync2(gitignorePath, "utf-8");
|
|
544
|
+
if (!content.includes(cacheEntry)) {
|
|
545
|
+
appendFileSync(gitignorePath, `
|
|
546
|
+
# Decantr cache
|
|
547
|
+
${cacheEntry}
|
|
548
|
+
`);
|
|
549
|
+
return true;
|
|
550
|
+
}
|
|
551
|
+
return false;
|
|
552
|
+
} else {
|
|
553
|
+
writeFileSync(gitignorePath, `# Decantr cache
|
|
554
|
+
${cacheEntry}
|
|
555
|
+
`);
|
|
556
|
+
return true;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
function scaffoldProject(projectRoot, options, detected, archetypeData, registrySource = "bundled") {
|
|
560
|
+
const essence = buildEssence(options, archetypeData);
|
|
561
|
+
const decantrDir = join2(projectRoot, ".decantr");
|
|
562
|
+
const contextDir = join2(decantrDir, "context");
|
|
563
|
+
const cacheDir = join2(decantrDir, "cache");
|
|
564
|
+
mkdirSync(contextDir, { recursive: true });
|
|
565
|
+
mkdirSync(cacheDir, { recursive: true });
|
|
566
|
+
const essencePath = join2(projectRoot, "decantr.essence.json");
|
|
567
|
+
writeFileSync(essencePath, JSON.stringify(essence, null, 2) + "\n");
|
|
568
|
+
const decantrMdPath = join2(projectRoot, "DECANTR.md");
|
|
569
|
+
writeFileSync(decantrMdPath, generateDecantrMd(essence, detected));
|
|
570
|
+
const projectJsonPath = join2(decantrDir, "project.json");
|
|
571
|
+
writeFileSync(projectJsonPath, generateProjectJson(detected, options, registrySource));
|
|
572
|
+
const contextFiles = [];
|
|
573
|
+
const scaffoldPath = join2(contextDir, "task-scaffold.md");
|
|
574
|
+
writeFileSync(scaffoldPath, generateTaskContext("task-scaffold.md.template", essence));
|
|
575
|
+
contextFiles.push(scaffoldPath);
|
|
576
|
+
const addPagePath = join2(contextDir, "task-add-page.md");
|
|
577
|
+
writeFileSync(addPagePath, generateTaskContext("task-add-page.md.template", essence));
|
|
578
|
+
contextFiles.push(addPagePath);
|
|
579
|
+
const modifyPath = join2(contextDir, "task-modify.md");
|
|
580
|
+
writeFileSync(modifyPath, generateTaskContext("task-modify.md.template", essence));
|
|
581
|
+
contextFiles.push(modifyPath);
|
|
582
|
+
const summaryPath = join2(contextDir, "essence-summary.md");
|
|
583
|
+
writeFileSync(summaryPath, generateEssenceSummary(essence));
|
|
584
|
+
contextFiles.push(summaryPath);
|
|
585
|
+
const gitignoreUpdated = updateGitignore(projectRoot);
|
|
586
|
+
return {
|
|
587
|
+
essencePath,
|
|
588
|
+
decantrMdPath,
|
|
589
|
+
projectJsonPath,
|
|
590
|
+
contextFiles,
|
|
591
|
+
gitignoreUpdated
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// src/registry.ts
|
|
596
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync2, readdirSync } from "fs";
|
|
597
|
+
import { join as join3, dirname as dirname2 } from "path";
|
|
598
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
599
|
+
var __dirname2 = dirname2(fileURLToPath2(import.meta.url));
|
|
600
|
+
var DEFAULT_API_URL = "https://decantr-registry.fly.dev/v1";
|
|
601
|
+
function getBundledContentRoot() {
|
|
602
|
+
const bundled = join3(__dirname2, "..", "..", "..", "content");
|
|
603
|
+
if (existsSync3(bundled)) return bundled;
|
|
604
|
+
const distBundled = join3(__dirname2, "..", "..", "..", "..", "content");
|
|
605
|
+
if (existsSync3(distBundled)) return distBundled;
|
|
606
|
+
return bundled;
|
|
607
|
+
}
|
|
608
|
+
async function fetchWithTimeout(url, timeoutMs = 5e3) {
|
|
609
|
+
const controller = new AbortController();
|
|
610
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
611
|
+
try {
|
|
612
|
+
const response = await fetch(url, { signal: controller.signal });
|
|
613
|
+
return response;
|
|
614
|
+
} finally {
|
|
615
|
+
clearTimeout(timeout);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
async function tryApi(endpoint, apiUrl = DEFAULT_API_URL) {
|
|
619
|
+
try {
|
|
620
|
+
const url = `${apiUrl}/${endpoint}`;
|
|
621
|
+
const response = await fetchWithTimeout(url);
|
|
622
|
+
if (!response.ok) return null;
|
|
623
|
+
const data = await response.json();
|
|
624
|
+
return {
|
|
625
|
+
data,
|
|
626
|
+
source: { type: "api", url: apiUrl }
|
|
627
|
+
};
|
|
628
|
+
} catch {
|
|
629
|
+
return null;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
function loadFromCache(cacheDir, contentType, id) {
|
|
633
|
+
const cachePath = id ? join3(cacheDir, contentType, `${id}.json`) : join3(cacheDir, contentType, "index.json");
|
|
634
|
+
if (!existsSync3(cachePath)) return null;
|
|
635
|
+
try {
|
|
636
|
+
const data = JSON.parse(readFileSync3(cachePath, "utf-8"));
|
|
637
|
+
return {
|
|
638
|
+
data,
|
|
639
|
+
source: { type: "cache" }
|
|
640
|
+
};
|
|
641
|
+
} catch {
|
|
642
|
+
return null;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
function loadFromBundled(contentType, id) {
|
|
646
|
+
const contentRoot = getBundledContentRoot();
|
|
647
|
+
if (id) {
|
|
648
|
+
const itemPath = join3(contentRoot, contentType, `${id}.json`);
|
|
649
|
+
if (!existsSync3(itemPath)) return null;
|
|
650
|
+
try {
|
|
651
|
+
const data = JSON.parse(readFileSync3(itemPath, "utf-8"));
|
|
652
|
+
return {
|
|
653
|
+
data,
|
|
654
|
+
source: { type: "bundled" }
|
|
655
|
+
};
|
|
656
|
+
} catch {
|
|
657
|
+
return null;
|
|
658
|
+
}
|
|
659
|
+
} else {
|
|
660
|
+
const dir = join3(contentRoot, contentType);
|
|
661
|
+
if (!existsSync3(dir)) return null;
|
|
662
|
+
try {
|
|
663
|
+
const files = readdirSync(dir).filter((f) => f.endsWith(".json"));
|
|
664
|
+
const items = files.map((f) => {
|
|
665
|
+
const content = JSON.parse(readFileSync3(join3(dir, f), "utf-8"));
|
|
666
|
+
return { id: content.id || f.replace(".json", ""), ...content };
|
|
667
|
+
});
|
|
668
|
+
return {
|
|
669
|
+
data: { items, total: items.length },
|
|
670
|
+
source: { type: "bundled" }
|
|
671
|
+
};
|
|
672
|
+
} catch {
|
|
673
|
+
return null;
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
function saveToCache(cacheDir, contentType, id, data) {
|
|
678
|
+
const dir = join3(cacheDir, contentType);
|
|
679
|
+
mkdirSync2(dir, { recursive: true });
|
|
680
|
+
const cachePath = id ? join3(dir, `${id}.json`) : join3(dir, "index.json");
|
|
681
|
+
writeFileSync2(cachePath, JSON.stringify(data, null, 2));
|
|
682
|
+
}
|
|
683
|
+
var RegistryClient = class {
|
|
684
|
+
cacheDir;
|
|
685
|
+
apiUrl;
|
|
686
|
+
offline;
|
|
687
|
+
constructor(options = {}) {
|
|
688
|
+
this.cacheDir = options.cacheDir || join3(process.cwd(), ".decantr", "cache");
|
|
689
|
+
this.apiUrl = options.apiUrl || DEFAULT_API_URL;
|
|
690
|
+
this.offline = options.offline || false;
|
|
691
|
+
}
|
|
692
|
+
/**
|
|
693
|
+
* Fetch archetypes list.
|
|
694
|
+
*/
|
|
695
|
+
async fetchArchetypes() {
|
|
696
|
+
if (!this.offline) {
|
|
697
|
+
const apiResult = await tryApi("archetypes", this.apiUrl);
|
|
698
|
+
if (apiResult) {
|
|
699
|
+
saveToCache(this.cacheDir, "archetypes", null, apiResult.data);
|
|
700
|
+
return apiResult;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
const cacheResult = loadFromCache(
|
|
704
|
+
this.cacheDir,
|
|
705
|
+
"archetypes"
|
|
706
|
+
);
|
|
707
|
+
if (cacheResult) return cacheResult;
|
|
708
|
+
const bundledResult = loadFromBundled("archetypes");
|
|
709
|
+
if (bundledResult) return bundledResult;
|
|
710
|
+
return {
|
|
711
|
+
data: { items: [], total: 0 },
|
|
712
|
+
source: { type: "bundled" }
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
/**
|
|
716
|
+
* Fetch a single archetype.
|
|
717
|
+
*/
|
|
718
|
+
async fetchArchetype(id) {
|
|
719
|
+
if (!this.offline) {
|
|
720
|
+
const apiResult = await tryApi(`archetypes/${id}`, this.apiUrl);
|
|
721
|
+
if (apiResult) {
|
|
722
|
+
saveToCache(this.cacheDir, "archetypes", id, apiResult.data);
|
|
723
|
+
return apiResult;
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
const cacheResult = loadFromCache(this.cacheDir, "archetypes", id);
|
|
727
|
+
if (cacheResult) return cacheResult;
|
|
728
|
+
return loadFromBundled("archetypes", id);
|
|
729
|
+
}
|
|
730
|
+
/**
|
|
731
|
+
* Fetch blueprints list.
|
|
732
|
+
*/
|
|
733
|
+
async fetchBlueprints() {
|
|
734
|
+
if (!this.offline) {
|
|
735
|
+
const apiResult = await tryApi("blueprints", this.apiUrl);
|
|
736
|
+
if (apiResult) {
|
|
737
|
+
saveToCache(this.cacheDir, "blueprints", null, apiResult.data);
|
|
738
|
+
return apiResult;
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
const cacheResult = loadFromCache(
|
|
742
|
+
this.cacheDir,
|
|
743
|
+
"blueprints"
|
|
744
|
+
);
|
|
745
|
+
if (cacheResult) return cacheResult;
|
|
746
|
+
const bundledResult = loadFromBundled("blueprints");
|
|
747
|
+
if (bundledResult) return bundledResult;
|
|
748
|
+
return {
|
|
749
|
+
data: { items: [], total: 0 },
|
|
750
|
+
source: { type: "bundled" }
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
/**
|
|
754
|
+
* Fetch a single blueprint.
|
|
755
|
+
*/
|
|
756
|
+
async fetchBlueprint(id) {
|
|
757
|
+
if (!this.offline) {
|
|
758
|
+
const apiResult = await tryApi(`blueprints/${id}`, this.apiUrl);
|
|
759
|
+
if (apiResult) {
|
|
760
|
+
saveToCache(this.cacheDir, "blueprints", id, apiResult.data);
|
|
761
|
+
return apiResult;
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
const cacheResult = loadFromCache(this.cacheDir, "blueprints", id);
|
|
765
|
+
if (cacheResult) return cacheResult;
|
|
766
|
+
return loadFromBundled("blueprints", id);
|
|
767
|
+
}
|
|
768
|
+
/**
|
|
769
|
+
* Fetch themes list.
|
|
770
|
+
*/
|
|
771
|
+
async fetchThemes() {
|
|
772
|
+
if (!this.offline) {
|
|
773
|
+
const apiResult = await tryApi("themes", this.apiUrl);
|
|
774
|
+
if (apiResult) {
|
|
775
|
+
saveToCache(this.cacheDir, "themes", null, apiResult.data);
|
|
776
|
+
return apiResult;
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
const cacheResult = loadFromCache(
|
|
780
|
+
this.cacheDir,
|
|
781
|
+
"themes"
|
|
782
|
+
);
|
|
783
|
+
if (cacheResult) return cacheResult;
|
|
784
|
+
const bundledResult = loadFromBundled("themes");
|
|
785
|
+
if (bundledResult) return bundledResult;
|
|
786
|
+
return {
|
|
787
|
+
data: { items: [], total: 0 },
|
|
788
|
+
source: { type: "bundled" }
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
/**
|
|
792
|
+
* Fetch patterns list.
|
|
793
|
+
*/
|
|
794
|
+
async fetchPatterns() {
|
|
795
|
+
if (!this.offline) {
|
|
796
|
+
const apiResult = await tryApi("patterns", this.apiUrl);
|
|
797
|
+
if (apiResult) {
|
|
798
|
+
saveToCache(this.cacheDir, "patterns", null, apiResult.data);
|
|
799
|
+
return apiResult;
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
const cacheResult = loadFromCache(
|
|
803
|
+
this.cacheDir,
|
|
804
|
+
"patterns"
|
|
805
|
+
);
|
|
806
|
+
if (cacheResult) return cacheResult;
|
|
807
|
+
const bundledResult = loadFromBundled("patterns");
|
|
808
|
+
if (bundledResult) return bundledResult;
|
|
809
|
+
return {
|
|
810
|
+
data: { items: [], total: 0 },
|
|
811
|
+
source: { type: "bundled" }
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
/**
|
|
815
|
+
* Check if API is available.
|
|
816
|
+
*/
|
|
817
|
+
async checkApiAvailability() {
|
|
818
|
+
if (this.offline) return false;
|
|
819
|
+
try {
|
|
820
|
+
const response = await fetchWithTimeout(`${this.apiUrl.replace("/v1", "")}/health`, 3e3);
|
|
821
|
+
return response.ok;
|
|
822
|
+
} catch {
|
|
823
|
+
return false;
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
/**
|
|
827
|
+
* Get the source used for the last fetch.
|
|
828
|
+
*/
|
|
829
|
+
getSourceType() {
|
|
830
|
+
return this.offline ? "bundled" : "api";
|
|
831
|
+
}
|
|
832
|
+
};
|
|
833
|
+
async function syncRegistry(cacheDir, apiUrl = DEFAULT_API_URL) {
|
|
834
|
+
const client = new RegistryClient({ cacheDir, apiUrl, offline: false });
|
|
835
|
+
const synced = [];
|
|
836
|
+
const failed = [];
|
|
837
|
+
const apiAvailable = await client.checkApiAvailability();
|
|
838
|
+
if (!apiAvailable) {
|
|
839
|
+
return { synced: [], failed: ["API unavailable"], source: "bundled" };
|
|
840
|
+
}
|
|
841
|
+
const types = ["archetypes", "blueprints", "themes", "patterns"];
|
|
842
|
+
for (const type of types) {
|
|
843
|
+
try {
|
|
844
|
+
const fetchMethod = `fetch${type.charAt(0).toUpperCase()}${type.slice(1)}`;
|
|
845
|
+
const result = await client[fetchMethod]();
|
|
846
|
+
if (result.source.type === "api") {
|
|
847
|
+
synced.push(type);
|
|
848
|
+
}
|
|
849
|
+
} catch {
|
|
850
|
+
failed.push(type);
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
return {
|
|
854
|
+
synced,
|
|
855
|
+
failed,
|
|
856
|
+
source: synced.length > 0 ? "api" : "bundled"
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
// src/index.ts
|
|
861
|
+
var BOLD2 = "\x1B[1m";
|
|
862
|
+
var DIM2 = "\x1B[2m";
|
|
863
|
+
var RESET2 = "\x1B[0m";
|
|
864
|
+
var RED = "\x1B[31m";
|
|
865
|
+
var GREEN2 = "\x1B[32m";
|
|
866
|
+
var CYAN2 = "\x1B[36m";
|
|
867
|
+
var YELLOW2 = "\x1B[33m";
|
|
868
|
+
function heading(text) {
|
|
869
|
+
return `
|
|
870
|
+
${BOLD2}${text}${RESET2}
|
|
871
|
+
`;
|
|
872
|
+
}
|
|
873
|
+
function success(text) {
|
|
874
|
+
return `${GREEN2}${text}${RESET2}`;
|
|
875
|
+
}
|
|
876
|
+
function error(text) {
|
|
877
|
+
return `${RED}${text}${RESET2}`;
|
|
878
|
+
}
|
|
879
|
+
function dim(text) {
|
|
880
|
+
return `${DIM2}${text}${RESET2}`;
|
|
881
|
+
}
|
|
882
|
+
function cyan(text) {
|
|
883
|
+
return `${CYAN2}${text}${RESET2}`;
|
|
884
|
+
}
|
|
885
|
+
function getContentRoot() {
|
|
886
|
+
const bundled = join4(import.meta.dirname, "..", "..", "..", "content");
|
|
887
|
+
return process.env.DECANTR_CONTENT_ROOT || bundled;
|
|
888
|
+
}
|
|
889
|
+
function getResolver() {
|
|
890
|
+
return createResolver({ contentRoot: getContentRoot() });
|
|
891
|
+
}
|
|
892
|
+
async function cmdSearch(query, type) {
|
|
893
|
+
const client = createRegistryClient();
|
|
894
|
+
const results = await client.search(query, type);
|
|
895
|
+
if (results.length === 0) {
|
|
896
|
+
console.log(dim(`No results for "${query}"`));
|
|
897
|
+
return;
|
|
898
|
+
}
|
|
899
|
+
console.log(heading(`${results.length} result(s) for "${query}"`));
|
|
900
|
+
for (const r of results) {
|
|
901
|
+
console.log(` ${cyan(r.type.padEnd(12))} ${BOLD2}${r.id}${RESET2}`);
|
|
902
|
+
console.log(` ${dim(r.description || "")}`);
|
|
903
|
+
console.log("");
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
async function cmdGet(type, id) {
|
|
907
|
+
const validTypes = ["pattern", "archetype", "recipe", "theme", "blueprint"];
|
|
908
|
+
if (!validTypes.includes(type)) {
|
|
909
|
+
console.error(error(`Invalid type "${type}". Must be one of: ${validTypes.join(", ")}`));
|
|
910
|
+
process.exitCode = 1;
|
|
911
|
+
return;
|
|
912
|
+
}
|
|
913
|
+
const resolver = getResolver();
|
|
914
|
+
let result = await resolver.resolve(type, id);
|
|
915
|
+
if (!result) {
|
|
916
|
+
const apiType = type === "blueprint" ? "blueprints" : `${type}s`;
|
|
917
|
+
try {
|
|
918
|
+
const res = await fetch(`https://decantr-registry.fly.dev/v1/${apiType}/${id}`);
|
|
919
|
+
if (res.ok) {
|
|
920
|
+
const item = await res.json();
|
|
921
|
+
if (!item.error) {
|
|
922
|
+
console.log(JSON.stringify(item, null, 2));
|
|
923
|
+
return;
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
} catch {
|
|
927
|
+
}
|
|
928
|
+
console.error(error(`${type} "${id}" not found.`));
|
|
929
|
+
process.exitCode = 1;
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
console.log(JSON.stringify(result.item, null, 2));
|
|
933
|
+
}
|
|
934
|
+
async function cmdValidate(path) {
|
|
935
|
+
const essencePath = path || join4(process.cwd(), "decantr.essence.json");
|
|
936
|
+
let raw;
|
|
937
|
+
try {
|
|
938
|
+
raw = readFileSync4(essencePath, "utf-8");
|
|
939
|
+
} catch {
|
|
940
|
+
console.error(error(`Could not read ${essencePath}`));
|
|
941
|
+
process.exitCode = 1;
|
|
942
|
+
return;
|
|
943
|
+
}
|
|
944
|
+
let essence;
|
|
945
|
+
try {
|
|
946
|
+
essence = JSON.parse(raw);
|
|
947
|
+
} catch (e) {
|
|
948
|
+
console.error(error(`Invalid JSON: ${e.message}`));
|
|
949
|
+
process.exitCode = 1;
|
|
950
|
+
return;
|
|
951
|
+
}
|
|
952
|
+
const result = validateEssence(essence);
|
|
953
|
+
if (result.valid) {
|
|
954
|
+
console.log(success("Essence is valid."));
|
|
955
|
+
} else {
|
|
956
|
+
console.error(error("Validation failed:"));
|
|
957
|
+
for (const err of result.errors) {
|
|
958
|
+
console.error(` ${RED}${err}${RESET2}`);
|
|
959
|
+
}
|
|
960
|
+
process.exitCode = 1;
|
|
961
|
+
}
|
|
962
|
+
try {
|
|
963
|
+
const violations = evaluateGuard(essence, {});
|
|
964
|
+
if (violations.length > 0) {
|
|
965
|
+
console.log(heading("Guard violations:"));
|
|
966
|
+
for (const v of violations) {
|
|
967
|
+
const vr = v;
|
|
968
|
+
console.log(` ${YELLOW2}[${vr.rule}]${RESET2} ${vr.message}`);
|
|
969
|
+
}
|
|
970
|
+
} else if (result.valid) {
|
|
971
|
+
console.log(success("No guard violations."));
|
|
972
|
+
}
|
|
973
|
+
} catch {
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
async function cmdList(type) {
|
|
977
|
+
const validTypes = ["patterns", "archetypes", "recipes", "themes", "blueprints"];
|
|
978
|
+
if (!validTypes.includes(type)) {
|
|
979
|
+
console.error(error(`Invalid type "${type}". Must be one of: ${validTypes.join(", ")}`));
|
|
980
|
+
process.exitCode = 1;
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
983
|
+
const { readdirSync: readdirSync2 } = await import("fs");
|
|
984
|
+
const dir = join4(getContentRoot(), type);
|
|
985
|
+
let found = false;
|
|
986
|
+
try {
|
|
987
|
+
const files = readdirSync2(dir).filter((f) => f.endsWith(".json"));
|
|
988
|
+
if (files.length > 0) {
|
|
989
|
+
found = true;
|
|
990
|
+
console.log(heading(`${files.length} ${type}`));
|
|
991
|
+
for (const f of files) {
|
|
992
|
+
const data = JSON.parse(readFileSync4(join4(dir, f), "utf-8"));
|
|
993
|
+
console.log(` ${cyan(data.id || f.replace(".json", ""))} ${dim(data.description || data.name || "")}`);
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
} catch {
|
|
997
|
+
}
|
|
998
|
+
if (!found) {
|
|
999
|
+
try {
|
|
1000
|
+
const res = await fetch(`https://decantr-registry.fly.dev/v1/${type}`);
|
|
1001
|
+
if (res.ok) {
|
|
1002
|
+
const data = await res.json();
|
|
1003
|
+
console.log(heading(`${data.total} ${type}`));
|
|
1004
|
+
for (const item of data.items) {
|
|
1005
|
+
console.log(` ${cyan(item.id)} ${dim(item.description || item.name || "")}`);
|
|
1006
|
+
}
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
1009
|
+
} catch {
|
|
1010
|
+
}
|
|
1011
|
+
console.log(dim(`No ${type} found.`));
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
async function cmdInit(args) {
|
|
1015
|
+
const projectRoot = process.cwd();
|
|
1016
|
+
console.log(heading("Decantr Project Setup"));
|
|
1017
|
+
const detected = detectProject(projectRoot);
|
|
1018
|
+
if (detected.existingEssence && !args.existing) {
|
|
1019
|
+
console.log(`${YELLOW2}Warning: decantr.essence.json already exists.${RESET2}`);
|
|
1020
|
+
const overwrite = await confirm("Overwrite existing configuration?", false);
|
|
1021
|
+
if (!overwrite) {
|
|
1022
|
+
console.log(dim("Cancelled."));
|
|
1023
|
+
return;
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
const registryClient = new RegistryClient({
|
|
1027
|
+
cacheDir: join4(projectRoot, ".decantr", "cache"),
|
|
1028
|
+
apiUrl: args.registry,
|
|
1029
|
+
offline: args.offline
|
|
1030
|
+
});
|
|
1031
|
+
console.log(dim("Fetching registry content..."));
|
|
1032
|
+
const [archetypesResult, blueprintsResult, themesResult] = await Promise.all([
|
|
1033
|
+
registryClient.fetchArchetypes(),
|
|
1034
|
+
registryClient.fetchBlueprints(),
|
|
1035
|
+
registryClient.fetchThemes()
|
|
1036
|
+
]);
|
|
1037
|
+
const registrySource = archetypesResult.source.type;
|
|
1038
|
+
if (registrySource === "bundled") {
|
|
1039
|
+
console.log(dim("Using bundled content (API unavailable)"));
|
|
1040
|
+
}
|
|
1041
|
+
const archetypes = archetypesResult.data.items;
|
|
1042
|
+
const blueprints = blueprintsResult.data.items;
|
|
1043
|
+
const themes = themesResult.data.items;
|
|
1044
|
+
let options;
|
|
1045
|
+
if (args.yes) {
|
|
1046
|
+
const flags = parseFlags(args, detected);
|
|
1047
|
+
options = mergeWithDefaults(flags, detected);
|
|
1048
|
+
} else {
|
|
1049
|
+
options = await runInteractivePrompts(detected, archetypes, blueprints, themes);
|
|
1050
|
+
}
|
|
1051
|
+
let archetypeData;
|
|
1052
|
+
if (options.blueprint) {
|
|
1053
|
+
const blueprintResult = await registryClient.fetchBlueprint(options.blueprint);
|
|
1054
|
+
if (blueprintResult) {
|
|
1055
|
+
const blueprint = blueprintResult.data;
|
|
1056
|
+
const primaryArchetype = blueprint.compose?.[0];
|
|
1057
|
+
if (primaryArchetype) {
|
|
1058
|
+
const archetypeResult = await registryClient.fetchArchetype(primaryArchetype);
|
|
1059
|
+
if (archetypeResult) {
|
|
1060
|
+
archetypeData = archetypeResult.data;
|
|
1061
|
+
options.archetype = primaryArchetype;
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
} else if (options.archetype) {
|
|
1066
|
+
const archetypeResult = await registryClient.fetchArchetype(options.archetype);
|
|
1067
|
+
if (archetypeResult) {
|
|
1068
|
+
archetypeData = archetypeResult.data;
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
console.log(heading("Scaffolding project..."));
|
|
1072
|
+
const result = scaffoldProject(
|
|
1073
|
+
projectRoot,
|
|
1074
|
+
options,
|
|
1075
|
+
detected,
|
|
1076
|
+
archetypeData,
|
|
1077
|
+
registrySource
|
|
1078
|
+
);
|
|
1079
|
+
console.log(success("\nProject scaffolded successfully!"));
|
|
1080
|
+
console.log("");
|
|
1081
|
+
console.log(` ${cyan("decantr.essence.json")} Design specification`);
|
|
1082
|
+
console.log(` ${cyan("DECANTR.md")} LLM instructions`);
|
|
1083
|
+
console.log(` ${cyan(".decantr/project.json")} Project state`);
|
|
1084
|
+
console.log(` ${cyan(".decantr/context/")} Task-specific guides`);
|
|
1085
|
+
if (result.gitignoreUpdated) {
|
|
1086
|
+
console.log(` ${dim(".gitignore updated to exclude .decantr/cache/")}`);
|
|
1087
|
+
}
|
|
1088
|
+
const essenceContent = readFileSync4(result.essencePath, "utf-8");
|
|
1089
|
+
const essence = JSON.parse(essenceContent);
|
|
1090
|
+
const validation = validateEssence(essence);
|
|
1091
|
+
if (validation.valid) {
|
|
1092
|
+
console.log(success("\nValidation passed."));
|
|
1093
|
+
} else {
|
|
1094
|
+
console.log(error(`
|
|
1095
|
+
Validation warnings: ${validation.errors.join(", ")}`));
|
|
1096
|
+
}
|
|
1097
|
+
console.log(heading("Next steps"));
|
|
1098
|
+
console.log("1. Review DECANTR.md to understand the methodology");
|
|
1099
|
+
console.log("2. Share DECANTR.md with your AI assistant");
|
|
1100
|
+
console.log("3. Start building! The AI will follow the essence spec.");
|
|
1101
|
+
console.log("");
|
|
1102
|
+
if (registrySource === "bundled") {
|
|
1103
|
+
console.log(dim('Run "decantr sync" when online to get the latest registry content.'));
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
async function cmdStatus() {
|
|
1107
|
+
const projectRoot = process.cwd();
|
|
1108
|
+
const essencePath = join4(projectRoot, "decantr.essence.json");
|
|
1109
|
+
const projectJsonPath = join4(projectRoot, ".decantr", "project.json");
|
|
1110
|
+
console.log(heading("Decantr Project Status"));
|
|
1111
|
+
if (!existsSync4(essencePath)) {
|
|
1112
|
+
console.log(`${RED}No decantr.essence.json found.${RESET2}`);
|
|
1113
|
+
console.log(dim('Run "decantr init" to create one.'));
|
|
1114
|
+
return;
|
|
1115
|
+
}
|
|
1116
|
+
try {
|
|
1117
|
+
const essence = JSON.parse(readFileSync4(essencePath, "utf-8"));
|
|
1118
|
+
const validation = validateEssence(essence);
|
|
1119
|
+
console.log(`${BOLD2}Essence:${RESET2}`);
|
|
1120
|
+
if (validation.valid) {
|
|
1121
|
+
console.log(` ${GREEN2}Valid${RESET2}`);
|
|
1122
|
+
} else {
|
|
1123
|
+
console.log(` ${RED}Invalid: ${validation.errors.join(", ")}${RESET2}`);
|
|
1124
|
+
}
|
|
1125
|
+
console.log(` Theme: ${essence.theme?.style || "unknown"} (${essence.theme?.mode || "unknown"})`);
|
|
1126
|
+
console.log(` Guard: ${essence.guard?.mode || "unknown"}`);
|
|
1127
|
+
console.log(` Pages: ${(essence.structure || []).length}`);
|
|
1128
|
+
} catch (e) {
|
|
1129
|
+
console.log(` ${RED}Error reading essence: ${e.message}${RESET2}`);
|
|
1130
|
+
}
|
|
1131
|
+
console.log("");
|
|
1132
|
+
console.log(`${BOLD2}Sync Status:${RESET2}`);
|
|
1133
|
+
if (existsSync4(projectJsonPath)) {
|
|
1134
|
+
try {
|
|
1135
|
+
const projectJson = JSON.parse(readFileSync4(projectJsonPath, "utf-8"));
|
|
1136
|
+
const syncStatus = projectJson.sync?.status || "unknown";
|
|
1137
|
+
const lastSync = projectJson.sync?.lastSync || "never";
|
|
1138
|
+
const source = projectJson.sync?.registrySource || "unknown";
|
|
1139
|
+
const statusColor = syncStatus === "synced" ? GREEN2 : YELLOW2;
|
|
1140
|
+
console.log(` Status: ${statusColor}${syncStatus}${RESET2}`);
|
|
1141
|
+
console.log(` Last sync: ${dim(lastSync)}`);
|
|
1142
|
+
console.log(` Source: ${dim(source)}`);
|
|
1143
|
+
} catch {
|
|
1144
|
+
console.log(` ${YELLOW2}Could not read project.json${RESET2}`);
|
|
1145
|
+
}
|
|
1146
|
+
} else {
|
|
1147
|
+
console.log(` ${YELLOW2}No .decantr/project.json found${RESET2}`);
|
|
1148
|
+
console.log(dim(' Run "decantr init" to create project files.'));
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
async function cmdSync() {
|
|
1152
|
+
const projectRoot = process.cwd();
|
|
1153
|
+
const cacheDir = join4(projectRoot, ".decantr", "cache");
|
|
1154
|
+
console.log(heading("Syncing registry content..."));
|
|
1155
|
+
const result = await syncRegistry(cacheDir);
|
|
1156
|
+
if (result.source === "api") {
|
|
1157
|
+
console.log(success("Sync completed successfully."));
|
|
1158
|
+
if (result.synced.length > 0) {
|
|
1159
|
+
console.log(` Synced: ${result.synced.join(", ")}`);
|
|
1160
|
+
}
|
|
1161
|
+
if (result.failed.length > 0) {
|
|
1162
|
+
console.log(` ${YELLOW2}Failed: ${result.failed.join(", ")}${RESET2}`);
|
|
1163
|
+
}
|
|
1164
|
+
} else {
|
|
1165
|
+
console.log(`${YELLOW2}Could not sync: API unavailable${RESET2}`);
|
|
1166
|
+
console.log(dim("Using bundled content."));
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
async function cmdAudit() {
|
|
1170
|
+
const projectRoot = process.cwd();
|
|
1171
|
+
const essencePath = join4(projectRoot, "decantr.essence.json");
|
|
1172
|
+
console.log(heading("Auditing project..."));
|
|
1173
|
+
if (!existsSync4(essencePath)) {
|
|
1174
|
+
console.log(`${RED}No decantr.essence.json found.${RESET2}`);
|
|
1175
|
+
process.exitCode = 1;
|
|
1176
|
+
return;
|
|
1177
|
+
}
|
|
1178
|
+
try {
|
|
1179
|
+
const essence = JSON.parse(readFileSync4(essencePath, "utf-8"));
|
|
1180
|
+
const validation = validateEssence(essence);
|
|
1181
|
+
if (!validation.valid) {
|
|
1182
|
+
console.log(`${RED}Essence validation failed:${RESET2}`);
|
|
1183
|
+
for (const err of validation.errors) {
|
|
1184
|
+
console.log(` ${RED}${err}${RESET2}`);
|
|
1185
|
+
}
|
|
1186
|
+
process.exitCode = 1;
|
|
1187
|
+
return;
|
|
1188
|
+
}
|
|
1189
|
+
console.log(success("Essence is valid."));
|
|
1190
|
+
const violations = evaluateGuard(essence, {});
|
|
1191
|
+
if (violations.length > 0) {
|
|
1192
|
+
console.log("");
|
|
1193
|
+
console.log(`${YELLOW2}Guard violations:${RESET2}`);
|
|
1194
|
+
for (const v of violations) {
|
|
1195
|
+
const vr = v;
|
|
1196
|
+
console.log(` ${YELLOW2}[${vr.rule}]${RESET2} ${vr.message}`);
|
|
1197
|
+
}
|
|
1198
|
+
} else {
|
|
1199
|
+
console.log(success("No guard violations."));
|
|
1200
|
+
}
|
|
1201
|
+
console.log("");
|
|
1202
|
+
console.log(`${BOLD2}Summary:${RESET2}`);
|
|
1203
|
+
console.log(` Pages defined: ${essence.structure.length}`);
|
|
1204
|
+
console.log(` Guard mode: ${essence.guard.mode}`);
|
|
1205
|
+
console.log(` Theme: ${essence.theme.style}`);
|
|
1206
|
+
} catch (e) {
|
|
1207
|
+
console.log(`${RED}Error: ${e.message}${RESET2}`);
|
|
1208
|
+
process.exitCode = 1;
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
function cmdHelp() {
|
|
1212
|
+
console.log(`
|
|
1213
|
+
${BOLD2}decantr${RESET2} \u2014 Design intelligence for AI-generated UI
|
|
1214
|
+
|
|
1215
|
+
${BOLD2}Usage:${RESET2}
|
|
1216
|
+
decantr init [options]
|
|
1217
|
+
decantr status
|
|
1218
|
+
decantr sync
|
|
1219
|
+
decantr audit
|
|
1220
|
+
decantr search <query> [--type <type>]
|
|
1221
|
+
decantr get <type> <id>
|
|
1222
|
+
decantr list <type>
|
|
1223
|
+
decantr validate [path]
|
|
1224
|
+
decantr help
|
|
1225
|
+
|
|
1226
|
+
${BOLD2}Init Options:${RESET2}
|
|
1227
|
+
--blueprint, -b Blueprint ID
|
|
1228
|
+
--theme Theme ID
|
|
1229
|
+
--mode Color mode: dark | light | auto
|
|
1230
|
+
--shape Border shape: pill | rounded | sharp
|
|
1231
|
+
--target Framework: react | vue | svelte | nextjs | html
|
|
1232
|
+
--guard Guard mode: creative | guided | strict
|
|
1233
|
+
--density Spacing: compact | comfortable | spacious
|
|
1234
|
+
--shell Default shell layout
|
|
1235
|
+
--existing Initialize in existing project
|
|
1236
|
+
--offline Force offline mode
|
|
1237
|
+
--yes, -y Accept defaults, skip confirmations
|
|
1238
|
+
--registry Custom registry URL
|
|
1239
|
+
|
|
1240
|
+
${BOLD2}Commands:${RESET2}
|
|
1241
|
+
${cyan("init")} Initialize a new Decantr project with full scaffolding
|
|
1242
|
+
${cyan("status")} Show project status and sync state
|
|
1243
|
+
${cyan("sync")} Sync registry content from API
|
|
1244
|
+
${cyan("audit")} Validate essence and check for drift
|
|
1245
|
+
${cyan("search")} Search the registry
|
|
1246
|
+
${cyan("get")} Get full details of a registry item
|
|
1247
|
+
${cyan("list")} List items by type
|
|
1248
|
+
${cyan("validate")} Validate essence file
|
|
1249
|
+
${cyan("help")} Show this help
|
|
1250
|
+
|
|
1251
|
+
${BOLD2}Examples:${RESET2}
|
|
1252
|
+
decantr init
|
|
1253
|
+
decantr init --blueprint=saas-dashboard --theme=luminarum --yes
|
|
1254
|
+
decantr status
|
|
1255
|
+
decantr sync
|
|
1256
|
+
decantr audit
|
|
1257
|
+
decantr search dashboard
|
|
1258
|
+
decantr list patterns
|
|
1259
|
+
`);
|
|
1260
|
+
}
|
|
1261
|
+
async function main() {
|
|
1262
|
+
const args = process.argv.slice(2);
|
|
1263
|
+
const command = args[0];
|
|
1264
|
+
if (!command || command === "help" || command === "--help" || command === "-h") {
|
|
1265
|
+
cmdHelp();
|
|
1266
|
+
return;
|
|
1267
|
+
}
|
|
1268
|
+
switch (command) {
|
|
1269
|
+
case "init": {
|
|
1270
|
+
const initArgs = {};
|
|
1271
|
+
for (let i = 1; i < args.length; i++) {
|
|
1272
|
+
const arg = args[i];
|
|
1273
|
+
if (arg === "--yes" || arg === "-y") {
|
|
1274
|
+
initArgs.yes = true;
|
|
1275
|
+
} else if (arg === "--offline") {
|
|
1276
|
+
initArgs.offline = true;
|
|
1277
|
+
} else if (arg === "--existing") {
|
|
1278
|
+
initArgs.existing = true;
|
|
1279
|
+
} else if (arg.startsWith("--")) {
|
|
1280
|
+
const [key, value] = arg.slice(2).split("=");
|
|
1281
|
+
if (value) {
|
|
1282
|
+
initArgs[key] = value;
|
|
1283
|
+
} else if (args[i + 1] && !args[i + 1].startsWith("-")) {
|
|
1284
|
+
initArgs[key] = args[++i];
|
|
1285
|
+
}
|
|
1286
|
+
} else if (arg.startsWith("-")) {
|
|
1287
|
+
const key = arg.slice(1);
|
|
1288
|
+
if (key === "b" && args[i + 1]) initArgs.blueprint = args[++i];
|
|
1289
|
+
if (key === "y") initArgs.yes = true;
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
await cmdInit(initArgs);
|
|
1293
|
+
break;
|
|
1294
|
+
}
|
|
1295
|
+
case "status": {
|
|
1296
|
+
await cmdStatus();
|
|
1297
|
+
break;
|
|
1298
|
+
}
|
|
1299
|
+
case "sync": {
|
|
1300
|
+
await cmdSync();
|
|
1301
|
+
break;
|
|
1302
|
+
}
|
|
1303
|
+
case "audit": {
|
|
1304
|
+
await cmdAudit();
|
|
1305
|
+
break;
|
|
1306
|
+
}
|
|
1307
|
+
case "search": {
|
|
1308
|
+
const query = args[1];
|
|
1309
|
+
if (!query) {
|
|
1310
|
+
console.error(error("Usage: decantr search <query> [--type <type>]"));
|
|
1311
|
+
process.exitCode = 1;
|
|
1312
|
+
return;
|
|
1313
|
+
}
|
|
1314
|
+
const typeIdx = args.indexOf("--type");
|
|
1315
|
+
const type = typeIdx !== -1 ? args[typeIdx + 1] : void 0;
|
|
1316
|
+
await cmdSearch(query, type);
|
|
1317
|
+
break;
|
|
1318
|
+
}
|
|
1319
|
+
case "get": {
|
|
1320
|
+
const type = args[1];
|
|
1321
|
+
const id = args[2];
|
|
1322
|
+
if (!type || !id) {
|
|
1323
|
+
console.error(error("Usage: decantr get <type> <id>"));
|
|
1324
|
+
process.exitCode = 1;
|
|
1325
|
+
return;
|
|
1326
|
+
}
|
|
1327
|
+
await cmdGet(type, id);
|
|
1328
|
+
break;
|
|
1329
|
+
}
|
|
1330
|
+
case "list": {
|
|
1331
|
+
const type = args[1];
|
|
1332
|
+
if (!type) {
|
|
1333
|
+
console.error(error("Usage: decantr list <type>"));
|
|
1334
|
+
process.exitCode = 1;
|
|
1335
|
+
return;
|
|
1336
|
+
}
|
|
1337
|
+
await cmdList(type);
|
|
1338
|
+
break;
|
|
1339
|
+
}
|
|
1340
|
+
case "validate": {
|
|
1341
|
+
await cmdValidate(args[1]);
|
|
1342
|
+
break;
|
|
1343
|
+
}
|
|
1344
|
+
default:
|
|
1345
|
+
console.error(error(`Unknown command: ${command}`));
|
|
1346
|
+
cmdHelp();
|
|
1347
|
+
process.exitCode = 1;
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
main().catch((e) => {
|
|
1351
|
+
console.error(error(e.message));
|
|
1352
|
+
process.exitCode = 1;
|
|
1353
|
+
});
|