create-fornix 0.0.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/index.js +4612 -0
- package/dist/index.js.map +1 -0
- package/package.json +54 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,4612 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { runMain } from "citty";
|
|
5
|
+
|
|
6
|
+
// src/cli/index.ts
|
|
7
|
+
import { defineCommand as defineCommand8 } from "citty";
|
|
8
|
+
|
|
9
|
+
// src/cli/commands/create.ts
|
|
10
|
+
import { defineCommand } from "citty";
|
|
11
|
+
import { resolve, basename as basename2 } from "path";
|
|
12
|
+
import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
13
|
+
import { join as join4 } from "path";
|
|
14
|
+
import * as p2 from "@clack/prompts";
|
|
15
|
+
import pc3 from "picocolors";
|
|
16
|
+
|
|
17
|
+
// src/utils/result.ts
|
|
18
|
+
function ok(value) {
|
|
19
|
+
return { ok: true, value };
|
|
20
|
+
}
|
|
21
|
+
function err(error) {
|
|
22
|
+
return { ok: false, error };
|
|
23
|
+
}
|
|
24
|
+
function isOk(result) {
|
|
25
|
+
return result.ok === true;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// src/scaffold/config-validator.ts
|
|
29
|
+
function validateConfig(config, manifests) {
|
|
30
|
+
const errors = [];
|
|
31
|
+
checkBlocksExist(config, manifests, errors);
|
|
32
|
+
checkRequiredModes(config, manifests, errors);
|
|
33
|
+
checkDatabaseRequiresServer(config, errors);
|
|
34
|
+
checkDefaultLocaleInLocales(config, errors);
|
|
35
|
+
if (errors.length > 0) {
|
|
36
|
+
return err(errors);
|
|
37
|
+
}
|
|
38
|
+
return ok(config);
|
|
39
|
+
}
|
|
40
|
+
function checkBlocksExist(config, manifests, errors) {
|
|
41
|
+
for (const block of config.blocks) {
|
|
42
|
+
if (!(block.name in manifests)) {
|
|
43
|
+
errors.push({
|
|
44
|
+
field: "blocks",
|
|
45
|
+
message: `Block '${block.name}' not found in registry`
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function checkRequiredModes(config, manifests, errors) {
|
|
51
|
+
if (config.renderMode === "static") {
|
|
52
|
+
for (const block of config.blocks) {
|
|
53
|
+
const manifest2 = manifests[block.name];
|
|
54
|
+
if (manifest2?.requiredMode === "server" || manifest2?.requiredMode === "hybrid") {
|
|
55
|
+
errors.push({
|
|
56
|
+
field: "blocks",
|
|
57
|
+
message: `Block '${block.name}' requires '${manifest2.requiredMode}' rendering, but config uses 'static' mode`
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function checkDatabaseRequiresServer(config, errors) {
|
|
64
|
+
if (config.database !== "none" && config.renderMode === "static") {
|
|
65
|
+
errors.push({
|
|
66
|
+
field: "database",
|
|
67
|
+
message: `Database '${config.database}' requires server or hybrid rendering, but config uses 'static' mode`
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function checkDefaultLocaleInLocales(config, errors) {
|
|
72
|
+
if (!config.locales.includes(config.defaultLocale)) {
|
|
73
|
+
errors.push({
|
|
74
|
+
field: "defaultLocale",
|
|
75
|
+
message: `defaultLocale '${config.defaultLocale}' is not in the locales array [${config.locales.join(", ")}]`
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// src/scaffold/dependency-resolver.ts
|
|
81
|
+
function resolveDependencies(selected, manifests) {
|
|
82
|
+
const unique = [...new Set(selected)];
|
|
83
|
+
for (const name of unique) {
|
|
84
|
+
if (!(name in manifests)) {
|
|
85
|
+
return err({
|
|
86
|
+
kind: "BlockNotFoundError",
|
|
87
|
+
message: `Block '${name}' not found in registry`,
|
|
88
|
+
blockName: name
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
const allBlocks = /* @__PURE__ */ new Set();
|
|
93
|
+
const collectError = collectTransitiveDependencies(
|
|
94
|
+
unique,
|
|
95
|
+
manifests,
|
|
96
|
+
allBlocks,
|
|
97
|
+
/* @__PURE__ */ new Set()
|
|
98
|
+
);
|
|
99
|
+
if (collectError !== void 0) {
|
|
100
|
+
return err(collectError);
|
|
101
|
+
}
|
|
102
|
+
const conflictError = detectConflicts(allBlocks, manifests);
|
|
103
|
+
if (conflictError !== void 0) {
|
|
104
|
+
return err(conflictError);
|
|
105
|
+
}
|
|
106
|
+
const sortResult = topologicalSort(allBlocks, manifests);
|
|
107
|
+
if (sortResult === void 0) {
|
|
108
|
+
return err({
|
|
109
|
+
kind: "CircularDependencyError",
|
|
110
|
+
message: "Unexpected circular dependency detected during sort",
|
|
111
|
+
chain: []
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
return ok(sortResult);
|
|
115
|
+
}
|
|
116
|
+
function collectTransitiveDependencies(blocks, manifests, collected, visiting) {
|
|
117
|
+
for (const name of blocks) {
|
|
118
|
+
if (collected.has(name)) {
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (visiting.has(name)) {
|
|
122
|
+
return {
|
|
123
|
+
kind: "CircularDependencyError",
|
|
124
|
+
message: `Circular dependency detected involving '${name}'`,
|
|
125
|
+
chain: [...visiting, name]
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
if (!(name in manifests)) {
|
|
129
|
+
return {
|
|
130
|
+
kind: "BlockNotFoundError",
|
|
131
|
+
message: `Block '${name}' not found in registry (required as dependency)`,
|
|
132
|
+
blockName: name
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
visiting.add(name);
|
|
136
|
+
const manifest2 = manifests[name];
|
|
137
|
+
if (manifest2.requires.length > 0) {
|
|
138
|
+
const depError = collectTransitiveDependencies(
|
|
139
|
+
manifest2.requires,
|
|
140
|
+
manifests,
|
|
141
|
+
collected,
|
|
142
|
+
visiting
|
|
143
|
+
);
|
|
144
|
+
if (depError !== void 0) {
|
|
145
|
+
return depError;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
visiting.delete(name);
|
|
149
|
+
collected.add(name);
|
|
150
|
+
}
|
|
151
|
+
return void 0;
|
|
152
|
+
}
|
|
153
|
+
function detectConflicts(blocks, manifests) {
|
|
154
|
+
for (const name of blocks) {
|
|
155
|
+
const manifest2 = manifests[name];
|
|
156
|
+
for (const conflicting of manifest2.conflicts) {
|
|
157
|
+
if (blocks.has(conflicting)) {
|
|
158
|
+
return {
|
|
159
|
+
kind: "DependencyConflictError",
|
|
160
|
+
message: `Block '${name}' conflicts with '${conflicting}'`,
|
|
161
|
+
blockA: name,
|
|
162
|
+
blockB: conflicting
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return void 0;
|
|
168
|
+
}
|
|
169
|
+
function topologicalSort(blocks, manifests) {
|
|
170
|
+
const inDegree = /* @__PURE__ */ new Map();
|
|
171
|
+
const dependents = /* @__PURE__ */ new Map();
|
|
172
|
+
for (const name of blocks) {
|
|
173
|
+
if (!inDegree.has(name)) {
|
|
174
|
+
inDegree.set(name, 0);
|
|
175
|
+
}
|
|
176
|
+
if (!dependents.has(name)) {
|
|
177
|
+
dependents.set(name, []);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
for (const name of blocks) {
|
|
181
|
+
const manifest2 = manifests[name];
|
|
182
|
+
for (const dep of manifest2.requires) {
|
|
183
|
+
if (blocks.has(dep)) {
|
|
184
|
+
inDegree.set(name, (inDegree.get(name) ?? 0) + 1);
|
|
185
|
+
const list = dependents.get(dep) ?? [];
|
|
186
|
+
list.push(name);
|
|
187
|
+
dependents.set(dep, list);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
const queue = [];
|
|
192
|
+
for (const [name, degree] of inDegree) {
|
|
193
|
+
if (degree === 0) {
|
|
194
|
+
queue.push(name);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
const sorted = [];
|
|
198
|
+
while (queue.length > 0) {
|
|
199
|
+
const current = queue.shift();
|
|
200
|
+
sorted.push(current);
|
|
201
|
+
for (const dependent of dependents.get(current) ?? []) {
|
|
202
|
+
const newDegree = (inDegree.get(dependent) ?? 1) - 1;
|
|
203
|
+
inDegree.set(dependent, newDegree);
|
|
204
|
+
if (newDegree === 0) {
|
|
205
|
+
queue.push(dependent);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
if (sorted.length !== blocks.size) {
|
|
210
|
+
return void 0;
|
|
211
|
+
}
|
|
212
|
+
return sorted;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// src/scaffold/structure-generator.ts
|
|
216
|
+
function generateStructure(config) {
|
|
217
|
+
const files = {};
|
|
218
|
+
const adapterDeps = getAdapterDependencies(config);
|
|
219
|
+
const pkg = {
|
|
220
|
+
name: config.projectName,
|
|
221
|
+
type: "module",
|
|
222
|
+
version: "0.0.1",
|
|
223
|
+
scripts: {
|
|
224
|
+
dev: "astro dev",
|
|
225
|
+
start: "astro dev",
|
|
226
|
+
build: "astro check && astro build",
|
|
227
|
+
preview: "astro preview",
|
|
228
|
+
astro: "astro"
|
|
229
|
+
},
|
|
230
|
+
dependencies: {
|
|
231
|
+
astro: "^5.2.0",
|
|
232
|
+
"@astrojs/check": "^0.9.0",
|
|
233
|
+
...config.cssEngine === "tailwind" && {
|
|
234
|
+
tailwindcss: "^4.0.0",
|
|
235
|
+
"@tailwindcss/vite": "^4.0.0"
|
|
236
|
+
},
|
|
237
|
+
...adapterDeps
|
|
238
|
+
},
|
|
239
|
+
devDependencies: {
|
|
240
|
+
typescript: "^5.7.0"
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
files["package.json"] = JSON.stringify(pkg, null, 2) + "\n";
|
|
244
|
+
const tsconfig = {
|
|
245
|
+
extends: "astro/tsconfigs/strict",
|
|
246
|
+
compilerOptions: {
|
|
247
|
+
jsx: "preserve",
|
|
248
|
+
jsxImportSource: "react",
|
|
249
|
+
// Even if not using React yet, good default for UI frameworks
|
|
250
|
+
strictNullChecks: true,
|
|
251
|
+
baseUrl: ".",
|
|
252
|
+
paths: {
|
|
253
|
+
"@/*": ["src/*"]
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
files["tsconfig.json"] = JSON.stringify(tsconfig, null, 2) + "\n";
|
|
258
|
+
files[".gitignore"] = `
|
|
259
|
+
# build output
|
|
260
|
+
dist/
|
|
261
|
+
.astro/
|
|
262
|
+
|
|
263
|
+
# dependencies
|
|
264
|
+
node_modules/
|
|
265
|
+
|
|
266
|
+
# logs
|
|
267
|
+
npm-debug.log*
|
|
268
|
+
yarn-debug.log*
|
|
269
|
+
yarn-error.log*
|
|
270
|
+
pnpm-debug.log*
|
|
271
|
+
|
|
272
|
+
# environment variables
|
|
273
|
+
.env
|
|
274
|
+
.env.production
|
|
275
|
+
.env.development
|
|
276
|
+
|
|
277
|
+
# OS files
|
|
278
|
+
.DS_Store
|
|
279
|
+
Thumbs.db
|
|
280
|
+
`.trim() + "\n";
|
|
281
|
+
files["src/pages/index.astro"] = `
|
|
282
|
+
---
|
|
283
|
+
import Layout from '../layouts/Layout.astro';
|
|
284
|
+
---
|
|
285
|
+
<Layout title="Welcome to Astro.">
|
|
286
|
+
<main>
|
|
287
|
+
<h1>Welcome to <span class="text-gradient">${config.projectName}</span></h1>
|
|
288
|
+
</main>
|
|
289
|
+
</Layout>
|
|
290
|
+
`.trim() + "\n";
|
|
291
|
+
files["src/layouts/Layout.astro"] = `
|
|
292
|
+
---
|
|
293
|
+
interface Props {
|
|
294
|
+
title: string;
|
|
295
|
+
}
|
|
296
|
+
const { title } = Astro.props;
|
|
297
|
+
---
|
|
298
|
+
<!doctype html>
|
|
299
|
+
<html lang="en">
|
|
300
|
+
<head>
|
|
301
|
+
<meta charset="UTF-8" />
|
|
302
|
+
<meta name="description" content="Astro description" />
|
|
303
|
+
<meta name="viewport" content="width=device-width" />
|
|
304
|
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
305
|
+
<meta name="generator" content={Astro.generator} />
|
|
306
|
+
<title>{title}</title>
|
|
307
|
+
</head>
|
|
308
|
+
<body>
|
|
309
|
+
<slot />
|
|
310
|
+
</body>
|
|
311
|
+
</html>
|
|
312
|
+
`.trim() + "\n";
|
|
313
|
+
if (config.deployTarget === "cloudflare") {
|
|
314
|
+
const wrangler = {
|
|
315
|
+
name: config.projectName,
|
|
316
|
+
compatibility_date: "2025-02-14",
|
|
317
|
+
// ... Add observability/pages_build_output_dir as needed.
|
|
318
|
+
// Basic structure:
|
|
319
|
+
pages_build_output_dir: "dist",
|
|
320
|
+
observability: {
|
|
321
|
+
enabled: true
|
|
322
|
+
}
|
|
323
|
+
};
|
|
324
|
+
files["wrangler.json"] = JSON.stringify(wrangler, null, 2) + "\n";
|
|
325
|
+
}
|
|
326
|
+
return files;
|
|
327
|
+
}
|
|
328
|
+
function getAdapterDependencies(config) {
|
|
329
|
+
if (config.renderMode === "static" && config.deployTarget === "static") {
|
|
330
|
+
return {};
|
|
331
|
+
}
|
|
332
|
+
const adapterMap = {
|
|
333
|
+
cloudflare: { "@astrojs/cloudflare": "^12.0.0" },
|
|
334
|
+
vercel: { "@astrojs/vercel": "^8.0.0" },
|
|
335
|
+
netlify: { "@astrojs/netlify": "^6.0.0" }
|
|
336
|
+
};
|
|
337
|
+
return adapterMap[config.deployTarget] ?? {};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// src/scaffold/config-generators/astro-config.ts
|
|
341
|
+
import { parseModule, generateCode, builders } from "magicast";
|
|
342
|
+
var ADAPTER_MAP = {
|
|
343
|
+
cloudflare: { package: "@astrojs/cloudflare", identifier: "cloudflare" },
|
|
344
|
+
vercel: { package: "@astrojs/vercel", identifier: "vercel" },
|
|
345
|
+
netlify: { package: "@astrojs/netlify", identifier: "netlify" }
|
|
346
|
+
};
|
|
347
|
+
function generateAstroConfig(config, blocks = []) {
|
|
348
|
+
try {
|
|
349
|
+
const module = parseModule("export default defineConfig({});");
|
|
350
|
+
module.imports.$add({
|
|
351
|
+
from: "astro/config",
|
|
352
|
+
imported: "defineConfig",
|
|
353
|
+
local: "defineConfig"
|
|
354
|
+
});
|
|
355
|
+
const configObject = module.exports.default.$args[0];
|
|
356
|
+
if (config.renderMode === "server") {
|
|
357
|
+
configObject.output = "server";
|
|
358
|
+
}
|
|
359
|
+
const adapter = ADAPTER_MAP[config.deployTarget];
|
|
360
|
+
if (adapter) {
|
|
361
|
+
module.imports.$add({
|
|
362
|
+
from: adapter.package,
|
|
363
|
+
imported: "default",
|
|
364
|
+
local: adapter.identifier
|
|
365
|
+
});
|
|
366
|
+
configObject.adapter = builders.functionCall(adapter.identifier);
|
|
367
|
+
}
|
|
368
|
+
if (config.locales.length >= 2) {
|
|
369
|
+
configObject.i18n = {
|
|
370
|
+
defaultLocale: config.defaultLocale,
|
|
371
|
+
locales: config.locales,
|
|
372
|
+
routing: {
|
|
373
|
+
prefixDefaultLocale: false
|
|
374
|
+
}
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
const hasMdx = blocks.some((b) => b.dependencies && b.dependencies["@astrojs/mdx"]);
|
|
378
|
+
if (hasMdx) {
|
|
379
|
+
module.imports.$add({
|
|
380
|
+
from: "@astrojs/mdx",
|
|
381
|
+
imported: "default",
|
|
382
|
+
local: "mdx"
|
|
383
|
+
});
|
|
384
|
+
configObject.integrations = configObject.integrations || [];
|
|
385
|
+
configObject.integrations.push(builders.functionCall("mdx"));
|
|
386
|
+
}
|
|
387
|
+
const { code } = generateCode(module);
|
|
388
|
+
return ok(code);
|
|
389
|
+
} catch (error) {
|
|
390
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
391
|
+
return err(new Error(`Failed to generate astro.config.mjs: ${message}`));
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// src/scaffold/config-generators/tailwind-config.ts
|
|
396
|
+
function generateTailwindConfig(config) {
|
|
397
|
+
if (config.cssEngine !== "tailwind") {
|
|
398
|
+
return ok(null);
|
|
399
|
+
}
|
|
400
|
+
const themeBlock = [
|
|
401
|
+
"@theme {",
|
|
402
|
+
" --color-primary: var(--color-primary);",
|
|
403
|
+
" --color-secondary: var(--color-secondary);",
|
|
404
|
+
" --color-accent: var(--color-accent);",
|
|
405
|
+
" --color-background: var(--color-background);",
|
|
406
|
+
" --color-foreground: var(--color-foreground);",
|
|
407
|
+
"}"
|
|
408
|
+
].join("\n");
|
|
409
|
+
const lines = [
|
|
410
|
+
'@import "tailwindcss";',
|
|
411
|
+
"",
|
|
412
|
+
`@source "../src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}";`,
|
|
413
|
+
"",
|
|
414
|
+
themeBlock,
|
|
415
|
+
""
|
|
416
|
+
];
|
|
417
|
+
return ok(lines.join("\n"));
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// src/scaffold/config-generators/palette-css.ts
|
|
421
|
+
var COLOR_TOKENS = [
|
|
422
|
+
"primary",
|
|
423
|
+
"secondary",
|
|
424
|
+
"accent",
|
|
425
|
+
"background",
|
|
426
|
+
"foreground"
|
|
427
|
+
];
|
|
428
|
+
var CURRENT_PATH = "src/styles/palettes/_current.css";
|
|
429
|
+
function generatePaletteCSS(palette, themeSwitcher, allPalettes) {
|
|
430
|
+
try {
|
|
431
|
+
const files = {};
|
|
432
|
+
files[CURRENT_PATH] = buildPaletteFile(palette.colors);
|
|
433
|
+
if (themeSwitcher && allPalettes && allPalettes.length > 0) {
|
|
434
|
+
for (const registryPalette of allPalettes) {
|
|
435
|
+
const path = `src/styles/palettes/${registryPalette.name}.css`;
|
|
436
|
+
files[path] = buildPaletteFile(registryPalette.colors);
|
|
437
|
+
}
|
|
438
|
+
const paletteNames = allPalettes.map((palette2) => palette2.name);
|
|
439
|
+
const script = buildSwitcherScript(paletteNames);
|
|
440
|
+
return ok({ files, switcherScript: script });
|
|
441
|
+
}
|
|
442
|
+
return ok({ files });
|
|
443
|
+
} catch (error) {
|
|
444
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
445
|
+
return err(new Error(`Failed to generate palette CSS: ${message}`));
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
function buildPaletteFile(colors) {
|
|
449
|
+
const properties = COLOR_TOKENS.map(
|
|
450
|
+
(token) => ` --color-${token}: ${colors[token]};`
|
|
451
|
+
).join("\n");
|
|
452
|
+
return `:root {
|
|
453
|
+
${properties}
|
|
454
|
+
}
|
|
455
|
+
`;
|
|
456
|
+
}
|
|
457
|
+
function buildSwitcherScript(paletteNames) {
|
|
458
|
+
return `(function () {
|
|
459
|
+
const PALETTES = ${JSON.stringify(paletteNames)};
|
|
460
|
+
const STORAGE_KEY = "fornix-palette";
|
|
461
|
+
|
|
462
|
+
function loadPalette(name) {
|
|
463
|
+
const link = document.getElementById("fornix-palette-link");
|
|
464
|
+
if (link) {
|
|
465
|
+
link.setAttribute("href", "/styles/palettes/" + name + ".css");
|
|
466
|
+
}
|
|
467
|
+
localStorage.setItem(STORAGE_KEY, name);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function getCurrentPalette() {
|
|
471
|
+
return localStorage.getItem(STORAGE_KEY) || PALETTES[0];
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
document.addEventListener("DOMContentLoaded", function () {
|
|
475
|
+
const saved = getCurrentPalette();
|
|
476
|
+
loadPalette(saved);
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
window.__fornixSwitchPalette = loadPalette;
|
|
480
|
+
window.__fornixPalettes = PALETTES;
|
|
481
|
+
})();
|
|
482
|
+
`;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// src/scaffold/block-placer.ts
|
|
486
|
+
function placeBlocks(blocks, blockSources, config) {
|
|
487
|
+
const files = {};
|
|
488
|
+
const context = {
|
|
489
|
+
renderMode: config.renderMode,
|
|
490
|
+
deployTarget: config.deployTarget,
|
|
491
|
+
cssEngine: config.cssEngine,
|
|
492
|
+
database: config.database
|
|
493
|
+
};
|
|
494
|
+
for (const block of blocks) {
|
|
495
|
+
const sources = blockSources[block.name];
|
|
496
|
+
if (!sources) {
|
|
497
|
+
return err(
|
|
498
|
+
new Error(
|
|
499
|
+
`Block source content not found for '${block.name}'`
|
|
500
|
+
)
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
for (const file of block.files) {
|
|
504
|
+
if (file.condition && !evaluateCondition(file.condition, context)) {
|
|
505
|
+
continue;
|
|
506
|
+
}
|
|
507
|
+
const content = sources[file.source];
|
|
508
|
+
if (content === void 0) {
|
|
509
|
+
return err(
|
|
510
|
+
new Error(
|
|
511
|
+
`Source file '${file.source}' not found in block '${block.name}'`
|
|
512
|
+
)
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
files[file.destination] = content;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
return ok(files);
|
|
519
|
+
}
|
|
520
|
+
function evaluateCondition(condition, context) {
|
|
521
|
+
const notEqualsMatch = condition.match(
|
|
522
|
+
/^(\w+)\s*!==\s*'([^']*)'$/
|
|
523
|
+
);
|
|
524
|
+
if (notEqualsMatch) {
|
|
525
|
+
const [, field, value] = notEqualsMatch;
|
|
526
|
+
const contextValue = getContextValue(field, context);
|
|
527
|
+
return contextValue !== value;
|
|
528
|
+
}
|
|
529
|
+
const equalsMatch = condition.match(
|
|
530
|
+
/^(\w+)\s*===\s*'([^']*)'$/
|
|
531
|
+
);
|
|
532
|
+
if (equalsMatch) {
|
|
533
|
+
const [, field, value] = equalsMatch;
|
|
534
|
+
const contextValue = getContextValue(field, context);
|
|
535
|
+
return contextValue === value;
|
|
536
|
+
}
|
|
537
|
+
return true;
|
|
538
|
+
}
|
|
539
|
+
function getContextValue(field, context) {
|
|
540
|
+
if (field in context) {
|
|
541
|
+
return context[field];
|
|
542
|
+
}
|
|
543
|
+
return void 0;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// src/scaffold/content-wiring.ts
|
|
547
|
+
var TYPE_DIRECTORY = {
|
|
548
|
+
section: "sections",
|
|
549
|
+
integration: "integrations",
|
|
550
|
+
feature: "features",
|
|
551
|
+
layout: "layouts"
|
|
552
|
+
};
|
|
553
|
+
function wireContent(blocks, defaultContent, config) {
|
|
554
|
+
const files = {};
|
|
555
|
+
const isMultiLocale = config.locales.length >= 2;
|
|
556
|
+
const blocksWithContent = blocks.filter(
|
|
557
|
+
(block) => block.ai?.contentSlots !== void 0 && Object.keys(block.ai.contentSlots).length > 0 || block.collections !== void 0 && block.collections.length > 0
|
|
558
|
+
);
|
|
559
|
+
if (blocksWithContent.length === 0) {
|
|
560
|
+
return ok(files);
|
|
561
|
+
}
|
|
562
|
+
files["src/content/config.ts"] = generateContentConfig(blocksWithContent);
|
|
563
|
+
for (const block of blocksWithContent) {
|
|
564
|
+
if (!block.ai?.contentSlots || Object.keys(block.ai.contentSlots).length === 0) {
|
|
565
|
+
continue;
|
|
566
|
+
}
|
|
567
|
+
const subdirectory = TYPE_DIRECTORY[block.type] ?? block.type;
|
|
568
|
+
const content = defaultContent[block.name] ?? buildDefaultFromSlots(block.ai.contentSlots);
|
|
569
|
+
const jsonContent = JSON.stringify(content, null, 2) + "\n";
|
|
570
|
+
if (isMultiLocale) {
|
|
571
|
+
for (const locale of config.locales) {
|
|
572
|
+
const path = `src/content/${locale}/${subdirectory}/${block.name}.json`;
|
|
573
|
+
files[path] = jsonContent;
|
|
574
|
+
}
|
|
575
|
+
} else {
|
|
576
|
+
const path = `src/content/${subdirectory}/${block.name}.json`;
|
|
577
|
+
files[path] = jsonContent;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
return ok(files);
|
|
581
|
+
}
|
|
582
|
+
function generateContentConfig(blocks) {
|
|
583
|
+
const imports = [
|
|
584
|
+
'import { defineCollection, z } from "astro:content";'
|
|
585
|
+
];
|
|
586
|
+
const collections = [];
|
|
587
|
+
for (const block of blocks) {
|
|
588
|
+
if (block.collections && block.collections.length > 0) {
|
|
589
|
+
for (const col of block.collections) {
|
|
590
|
+
const importName = `${block.name.replace(/-/g, "")}${col.name}Schema`;
|
|
591
|
+
const importPath = col.schemaSource.replace(/\.ts$/, "");
|
|
592
|
+
imports.push(`import { schema as ${importName} } from "${importPath}";`);
|
|
593
|
+
collections.push(
|
|
594
|
+
` "${col.name}": defineCollection({
|
|
595
|
+
type: "${col.type}",
|
|
596
|
+
schema: ${importName},
|
|
597
|
+
})`
|
|
598
|
+
);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
const slots = block.ai?.contentSlots;
|
|
602
|
+
if (slots && Object.keys(slots).length > 0) {
|
|
603
|
+
const schemaFields = Object.entries(slots).map(([name, slot]) => ` ${name}: ${zodTypeForSlot(slot)},`).join("\n");
|
|
604
|
+
collections.push(
|
|
605
|
+
` "${block.name}": defineCollection({
|
|
606
|
+
type: "data",
|
|
607
|
+
schema: z.object({
|
|
608
|
+
${schemaFields}
|
|
609
|
+
}),
|
|
610
|
+
})`
|
|
611
|
+
);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
const lines = [
|
|
615
|
+
...imports,
|
|
616
|
+
"",
|
|
617
|
+
"export const collections = {",
|
|
618
|
+
collections.join(",\n"),
|
|
619
|
+
"};",
|
|
620
|
+
""
|
|
621
|
+
];
|
|
622
|
+
return lines.join("\n");
|
|
623
|
+
}
|
|
624
|
+
function zodTypeForSlot(slot) {
|
|
625
|
+
switch (slot.type) {
|
|
626
|
+
case "string":
|
|
627
|
+
return "z.string()";
|
|
628
|
+
case "number":
|
|
629
|
+
return "z.number()";
|
|
630
|
+
case "boolean":
|
|
631
|
+
return "z.boolean()";
|
|
632
|
+
case "array":
|
|
633
|
+
return "z.array(z.unknown())";
|
|
634
|
+
case "object":
|
|
635
|
+
return "z.record(z.unknown())";
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
function buildDefaultFromSlots(slots) {
|
|
639
|
+
const content = {};
|
|
640
|
+
for (const [name, slot] of Object.entries(slots)) {
|
|
641
|
+
content[name] = defaultValueForType(slot.type);
|
|
642
|
+
}
|
|
643
|
+
return content;
|
|
644
|
+
}
|
|
645
|
+
function defaultValueForType(type) {
|
|
646
|
+
switch (type) {
|
|
647
|
+
case "string":
|
|
648
|
+
return "";
|
|
649
|
+
case "number":
|
|
650
|
+
return 0;
|
|
651
|
+
case "boolean":
|
|
652
|
+
return false;
|
|
653
|
+
case "array":
|
|
654
|
+
return [];
|
|
655
|
+
case "object":
|
|
656
|
+
return {};
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// src/scaffold/i18n-wiring.ts
|
|
661
|
+
function wireI18n(config) {
|
|
662
|
+
const files = {};
|
|
663
|
+
if (config.locales.length < 2) {
|
|
664
|
+
return ok(files);
|
|
665
|
+
}
|
|
666
|
+
files["src/i18n/utils.ts"] = generateI18nUtils(config);
|
|
667
|
+
files["src/pages/[locale]/index.astro"] = generateLocaleIndexPage(config);
|
|
668
|
+
return ok(files);
|
|
669
|
+
}
|
|
670
|
+
function generateI18nUtils(config) {
|
|
671
|
+
const localesArray = config.locales.map((locale) => `"${locale}"`).join(", ");
|
|
672
|
+
return `export const locales = [${localesArray}] as const;
|
|
673
|
+
export type Locale = (typeof locales)[number];
|
|
674
|
+
export const defaultLocale: Locale = "${config.defaultLocale}";
|
|
675
|
+
|
|
676
|
+
/**
|
|
677
|
+
* Gets the current locale from the URL pathname.
|
|
678
|
+
* Falls back to the default locale if not found.
|
|
679
|
+
*/
|
|
680
|
+
export function getLocale(pathname: string): Locale {
|
|
681
|
+
const segments = pathname.split("/").filter(Boolean);
|
|
682
|
+
const candidate = segments[0];
|
|
683
|
+
if (candidate && (locales as readonly string[]).includes(candidate)) {
|
|
684
|
+
return candidate as Locale;
|
|
685
|
+
}
|
|
686
|
+
return defaultLocale;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/**
|
|
690
|
+
* Simple translation lookup.
|
|
691
|
+
* Given a translations record keyed by locale, returns the value
|
|
692
|
+
* for the specified locale (or falls back to default).
|
|
693
|
+
*/
|
|
694
|
+
export function t<T>(
|
|
695
|
+
translations: Record<Locale, T>,
|
|
696
|
+
locale: Locale,
|
|
697
|
+
): T {
|
|
698
|
+
return translations[locale] ?? translations[defaultLocale];
|
|
699
|
+
}
|
|
700
|
+
`;
|
|
701
|
+
}
|
|
702
|
+
function generateLocaleIndexPage(config) {
|
|
703
|
+
return `---
|
|
704
|
+
import { locales } from "../../i18n/utils";
|
|
705
|
+
import Layout from "../../layouts/Layout.astro";
|
|
706
|
+
|
|
707
|
+
export function getStaticPaths() {
|
|
708
|
+
return locales.map((locale) => ({ params: { locale } }));
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
const { locale } = Astro.params;
|
|
712
|
+
---
|
|
713
|
+
<Layout title="${config.projectName}">
|
|
714
|
+
<main>
|
|
715
|
+
<h1>${config.projectName}</h1>
|
|
716
|
+
<p>Locale: {locale}</p>
|
|
717
|
+
</main>
|
|
718
|
+
</Layout>
|
|
719
|
+
`;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// src/scaffold/agent-context-generator.ts
|
|
723
|
+
function getI18nInfo(config) {
|
|
724
|
+
if (config.locales.length >= 2) {
|
|
725
|
+
return `
|
|
726
|
+
### i18n
|
|
727
|
+
i18n is enabled. The project supports the following locales: \`${config.locales.join(", ")}\` with \`${config.defaultLocale}\` as default.
|
|
728
|
+
Content is organized in locale-specific folders under \`src/content/\`:
|
|
729
|
+
${config.locales.map((l) => `- \`src/content/${l}/\``).join("\n")}
|
|
730
|
+
`;
|
|
731
|
+
}
|
|
732
|
+
return `
|
|
733
|
+
### Content
|
|
734
|
+
Content is located under \`src/content/\`.
|
|
735
|
+
`;
|
|
736
|
+
}
|
|
737
|
+
function getBlocksTable(blocks) {
|
|
738
|
+
if (blocks.length === 0) return "No blocks installed.\n";
|
|
739
|
+
const rows = blocks.map((b) => `| \`${b.name}\` | ${b.type} | ${b.description} |`).join("\n");
|
|
740
|
+
return `| Block | Type | Description |
|
|
741
|
+
|-------|------|-------------|
|
|
742
|
+
${rows}
|
|
743
|
+
`;
|
|
744
|
+
}
|
|
745
|
+
function generateAgentContext(config, blocks) {
|
|
746
|
+
const i18nSection = getI18nInfo(config);
|
|
747
|
+
const blocksTable = getBlocksTable(blocks);
|
|
748
|
+
const content = `# Fornix Project: ${config.projectName}
|
|
749
|
+
|
|
750
|
+
## Architecture
|
|
751
|
+
- **Render Mode:** ${config.renderMode}
|
|
752
|
+
- **Deploy Target:** ${config.deployTarget}
|
|
753
|
+
- **Database:** ${config.database}
|
|
754
|
+
- **CSS Engine:** ${config.cssEngine}
|
|
755
|
+
- **Package Manager:** ${config.packageManager}
|
|
756
|
+
|
|
757
|
+
## Installed Blocks
|
|
758
|
+
${blocksTable}
|
|
759
|
+
## Content & Locales
|
|
760
|
+
${i18nSection}
|
|
761
|
+
## CLI Commands
|
|
762
|
+
- \`npx create-fornix [dir]\` \u2014 scaffold a new project (AI mode default, \`--manual\` for interactive)
|
|
763
|
+
- \`fornix add <block>\` / \`fornix remove <block>\` \u2014 manage blocks
|
|
764
|
+
- \`fornix list\` / \`fornix status\` \u2014 inspect registry and project
|
|
765
|
+
- \`fornix mcp serve\` \u2014 MCP server for AI agent integration
|
|
766
|
+
|
|
767
|
+
> **Note to AI Agents:** You should respect the Fornix architectural guidelines by updating JSON content for text changes, and interacting via MCP rather than modifying block structures ad hoc.
|
|
768
|
+
`;
|
|
769
|
+
const cursorContent = `---
|
|
770
|
+
description: Fornix Project Architecture and Context
|
|
771
|
+
globs: *
|
|
772
|
+
---
|
|
773
|
+
${content}
|
|
774
|
+
`;
|
|
775
|
+
return {
|
|
776
|
+
"CLAUDE.md": content,
|
|
777
|
+
".cursor/rules/fornix.mdc": cursorContent
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// src/scaffold/pipeline.ts
|
|
782
|
+
function scaffold(input) {
|
|
783
|
+
const { config, manifests, blockSources, blockDefaultContent, allPalettes } = input;
|
|
784
|
+
const validationResult = validateConfig(config, manifests);
|
|
785
|
+
if (!isOk(validationResult)) {
|
|
786
|
+
const messages = validationResult.error.map((validationError) => validationError.message).join("; ");
|
|
787
|
+
return err(new Error(`Config validation failed: ${messages}`));
|
|
788
|
+
}
|
|
789
|
+
const selectedBlockNames = config.blocks.map((block) => block.name);
|
|
790
|
+
const dependencyResult = resolveDependencies(selectedBlockNames, manifests);
|
|
791
|
+
if (!isOk(dependencyResult)) {
|
|
792
|
+
return err(new Error(`Dependency resolution failed: ${dependencyResult.error.message}`));
|
|
793
|
+
}
|
|
794
|
+
const resolvedBlockNames = dependencyResult.value;
|
|
795
|
+
const files = {};
|
|
796
|
+
const structureFiles = generateStructure(config);
|
|
797
|
+
Object.assign(files, structureFiles);
|
|
798
|
+
const resolvedManifests = resolvedBlockNames.filter((name) => manifests[name] !== void 0).map((name) => manifests[name]);
|
|
799
|
+
const astroConfigResult = generateAstroConfig(config, resolvedManifests);
|
|
800
|
+
if (!isOk(astroConfigResult)) {
|
|
801
|
+
return err(astroConfigResult.error);
|
|
802
|
+
}
|
|
803
|
+
files["astro.config.mjs"] = astroConfigResult.value;
|
|
804
|
+
const tailwindResult = generateTailwindConfig(config);
|
|
805
|
+
if (!isOk(tailwindResult)) {
|
|
806
|
+
return err(tailwindResult.error);
|
|
807
|
+
}
|
|
808
|
+
if (tailwindResult.value !== null) {
|
|
809
|
+
files["tailwind.css"] = tailwindResult.value;
|
|
810
|
+
}
|
|
811
|
+
const paletteResult = generatePaletteCSS(
|
|
812
|
+
config.palette,
|
|
813
|
+
config.themeSwitcher,
|
|
814
|
+
allPalettes.length > 0 ? allPalettes : void 0
|
|
815
|
+
);
|
|
816
|
+
if (!isOk(paletteResult)) {
|
|
817
|
+
return err(paletteResult.error);
|
|
818
|
+
}
|
|
819
|
+
Object.assign(files, paletteResult.value.files);
|
|
820
|
+
if (paletteResult.value.switcherScript) {
|
|
821
|
+
files["src/scripts/theme-switcher.js"] = paletteResult.value.switcherScript;
|
|
822
|
+
}
|
|
823
|
+
const blockPlaceResult = placeBlocks(resolvedManifests, blockSources, config);
|
|
824
|
+
if (!isOk(blockPlaceResult)) {
|
|
825
|
+
return err(blockPlaceResult.error);
|
|
826
|
+
}
|
|
827
|
+
Object.assign(files, blockPlaceResult.value);
|
|
828
|
+
const contentResult = wireContent(resolvedManifests, blockDefaultContent, config);
|
|
829
|
+
if (!isOk(contentResult)) {
|
|
830
|
+
return err(contentResult.error);
|
|
831
|
+
}
|
|
832
|
+
Object.assign(files, contentResult.value);
|
|
833
|
+
const i18nResult = wireI18n(config);
|
|
834
|
+
if (!isOk(i18nResult)) {
|
|
835
|
+
return err(i18nResult.error);
|
|
836
|
+
}
|
|
837
|
+
Object.assign(files, i18nResult.value);
|
|
838
|
+
const envVars = collectEnvVars(resolvedManifests);
|
|
839
|
+
if (envVars.length > 0) {
|
|
840
|
+
files[".env.example"] = generateEnvExample(envVars);
|
|
841
|
+
}
|
|
842
|
+
files["fornix.json"] = generateProjectManifest(config, resolvedManifests);
|
|
843
|
+
const agentContextFiles = generateAgentContext(config, resolvedManifests);
|
|
844
|
+
Object.assign(files, agentContextFiles);
|
|
845
|
+
return ok({ files, resolvedBlockNames });
|
|
846
|
+
}
|
|
847
|
+
function collectEnvVars(blocks) {
|
|
848
|
+
const entries = [];
|
|
849
|
+
const seen = /* @__PURE__ */ new Set();
|
|
850
|
+
for (const block of blocks) {
|
|
851
|
+
for (const envVar of block.envVars) {
|
|
852
|
+
if (!seen.has(envVar.name)) {
|
|
853
|
+
seen.add(envVar.name);
|
|
854
|
+
entries.push({
|
|
855
|
+
name: envVar.name,
|
|
856
|
+
description: envVar.description,
|
|
857
|
+
required: envVar.required,
|
|
858
|
+
blockName: block.name
|
|
859
|
+
});
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
return entries;
|
|
864
|
+
}
|
|
865
|
+
function generateEnvExample(envVars) {
|
|
866
|
+
const lines = [
|
|
867
|
+
"# Environment Variables",
|
|
868
|
+
"# Generated by Fornix \u2014 fill in your values",
|
|
869
|
+
""
|
|
870
|
+
];
|
|
871
|
+
for (const envVar of envVars) {
|
|
872
|
+
lines.push(`# ${envVar.description} (from ${envVar.blockName})`);
|
|
873
|
+
lines.push(`${envVar.name}=`);
|
|
874
|
+
lines.push("");
|
|
875
|
+
}
|
|
876
|
+
return lines.join("\n");
|
|
877
|
+
}
|
|
878
|
+
function generateProjectManifest(config, blocks) {
|
|
879
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
880
|
+
const manifest2 = {
|
|
881
|
+
version: "1.0.0",
|
|
882
|
+
createdAt: now,
|
|
883
|
+
createdWith: config.createdWith,
|
|
884
|
+
renderMode: config.renderMode,
|
|
885
|
+
deployTarget: config.deployTarget,
|
|
886
|
+
database: config.database,
|
|
887
|
+
locales: config.locales,
|
|
888
|
+
defaultLocale: config.defaultLocale,
|
|
889
|
+
palette: config.palette,
|
|
890
|
+
themeSwitcher: config.themeSwitcher,
|
|
891
|
+
blocks: blocks.map((block) => ({
|
|
892
|
+
name: block.name,
|
|
893
|
+
version: block.version,
|
|
894
|
+
variant: config.blocks.find((selection) => selection.name === block.name)?.variant ?? "default",
|
|
895
|
+
installedAt: now
|
|
896
|
+
}))
|
|
897
|
+
};
|
|
898
|
+
return JSON.stringify(manifest2, null, 2) + "\n";
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
// src/cli/fixture-registry.ts
|
|
902
|
+
import { readFileSync, readdirSync } from "fs";
|
|
903
|
+
import { join, dirname } from "path";
|
|
904
|
+
import { fileURLToPath } from "url";
|
|
905
|
+
function manifest(name, overrides = {}) {
|
|
906
|
+
return {
|
|
907
|
+
schemaVersion: 1,
|
|
908
|
+
name,
|
|
909
|
+
version: "1.0.0",
|
|
910
|
+
type: "section",
|
|
911
|
+
description: `Block ${name}`,
|
|
912
|
+
category: "general",
|
|
913
|
+
tags: [],
|
|
914
|
+
dependencies: {},
|
|
915
|
+
requires: [],
|
|
916
|
+
conflicts: [],
|
|
917
|
+
envVars: [],
|
|
918
|
+
variants: ["default"],
|
|
919
|
+
slots: [],
|
|
920
|
+
files: [
|
|
921
|
+
{
|
|
922
|
+
source: `${name}.astro`,
|
|
923
|
+
destination: `src/components/sections/${name}.astro`
|
|
924
|
+
}
|
|
925
|
+
],
|
|
926
|
+
...overrides
|
|
927
|
+
};
|
|
928
|
+
}
|
|
929
|
+
var FIXTURE_MANIFESTS = {
|
|
930
|
+
"hero-gradient": manifest("hero-gradient", {
|
|
931
|
+
category: "hero",
|
|
932
|
+
ai: {
|
|
933
|
+
whenToUse: "Landing page hero with gradient background",
|
|
934
|
+
whenNotToUse: "Internal pages",
|
|
935
|
+
pairsWith: ["cta-simple", "features-grid"],
|
|
936
|
+
contentSlots: {
|
|
937
|
+
headline: { type: "string", description: "Main headline" },
|
|
938
|
+
subheadline: { type: "string", description: "Sub headline" }
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
}),
|
|
942
|
+
"footer-minimal": manifest("footer-minimal", {
|
|
943
|
+
category: "footer"
|
|
944
|
+
}),
|
|
945
|
+
"cta-simple": manifest("cta-simple", {
|
|
946
|
+
category: "cta"
|
|
947
|
+
}),
|
|
948
|
+
"features-grid": manifest("features-grid", {
|
|
949
|
+
category: "features"
|
|
950
|
+
}),
|
|
951
|
+
"auth-better-auth": manifest("auth-better-auth", {
|
|
952
|
+
type: "integration",
|
|
953
|
+
category: "auth",
|
|
954
|
+
requiredMode: "server",
|
|
955
|
+
requires: ["db-d1"],
|
|
956
|
+
envVars: [
|
|
957
|
+
{ name: "AUTH_SECRET", description: "Auth secret key", required: true },
|
|
958
|
+
{ name: "GITHUB_CLIENT_ID", description: "GitHub OAuth client ID", required: true },
|
|
959
|
+
{ name: "GITHUB_CLIENT_SECRET", description: "GitHub OAuth client secret", required: true }
|
|
960
|
+
],
|
|
961
|
+
files: [
|
|
962
|
+
{ source: "auth.ts", destination: "src/lib/auth.ts" },
|
|
963
|
+
{ source: "middleware.ts", destination: "src/middleware/auth.ts" },
|
|
964
|
+
{ source: "auth-api.ts", destination: "src/pages/api/auth/[...all].ts" },
|
|
965
|
+
{ source: "login.astro", destination: "src/pages/login.astro" },
|
|
966
|
+
{ source: "signup.astro", destination: "src/pages/signup.astro" }
|
|
967
|
+
]
|
|
968
|
+
}),
|
|
969
|
+
"db-d1": manifest("db-d1", {
|
|
970
|
+
type: "integration",
|
|
971
|
+
category: "database",
|
|
972
|
+
requiredMode: "server",
|
|
973
|
+
envVars: [
|
|
974
|
+
{ name: "D1_DATABASE_ID", description: "Cloudflare D1 database ID", required: true }
|
|
975
|
+
],
|
|
976
|
+
files: [
|
|
977
|
+
{ source: "db.ts", destination: "src/lib/db.ts" },
|
|
978
|
+
{ source: "schema.ts", destination: "src/lib/db-schema.ts" },
|
|
979
|
+
{ source: "drizzle.config.ts", destination: "drizzle.config.ts" },
|
|
980
|
+
{ source: "migrations/.gitkeep", destination: "drizzle/migrations/.gitkeep" }
|
|
981
|
+
]
|
|
982
|
+
}),
|
|
983
|
+
"blog-mdx": manifest("blog-mdx", {
|
|
984
|
+
type: "feature",
|
|
985
|
+
category: "blog",
|
|
986
|
+
files: [
|
|
987
|
+
{ source: "schema.ts", destination: "src/content/blog-schema.ts" },
|
|
988
|
+
{ source: "pages/index.astro", destination: "src/pages/blog/index.astro" },
|
|
989
|
+
{ source: "pages/[slug].astro", destination: "src/pages/blog/[slug].astro" },
|
|
990
|
+
{ source: "pages/rss.xml.ts", destination: "src/pages/rss.xml.ts" }
|
|
991
|
+
]
|
|
992
|
+
}),
|
|
993
|
+
"docs-collection": manifest("docs-collection", {
|
|
994
|
+
type: "feature",
|
|
995
|
+
category: "docs",
|
|
996
|
+
files: [
|
|
997
|
+
{ source: "schema.ts", destination: "src/content/docs-schema.ts" },
|
|
998
|
+
{ source: "pages/[...slug].astro", destination: "src/pages/docs/[...slug].astro" }
|
|
999
|
+
]
|
|
1000
|
+
}),
|
|
1001
|
+
"layout-marketing": manifest("layout-marketing", {
|
|
1002
|
+
type: "layout",
|
|
1003
|
+
category: "marketing",
|
|
1004
|
+
files: [
|
|
1005
|
+
{ source: "layout-marketing.astro", destination: "src/layouts/layout-marketing.astro" },
|
|
1006
|
+
{ source: "default-content.json", destination: "src/content/layouts/layout-marketing.json" }
|
|
1007
|
+
],
|
|
1008
|
+
ai: {
|
|
1009
|
+
whenToUse: "marketing pages",
|
|
1010
|
+
whenNotToUse: "dashboards",
|
|
1011
|
+
pairsWith: [],
|
|
1012
|
+
contentSlots: {
|
|
1013
|
+
headerLinks: { type: "array" },
|
|
1014
|
+
footerText: { type: "string" }
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
}),
|
|
1018
|
+
"layout-docs": manifest("layout-docs", {
|
|
1019
|
+
type: "layout",
|
|
1020
|
+
category: "docs",
|
|
1021
|
+
files: [
|
|
1022
|
+
{ source: "layout-docs.astro", destination: "src/layouts/layout-docs.astro" },
|
|
1023
|
+
{ source: "default-content.json", destination: "src/content/layouts/layout-docs.json" }
|
|
1024
|
+
],
|
|
1025
|
+
ai: {
|
|
1026
|
+
whenToUse: "docs",
|
|
1027
|
+
whenNotToUse: "marketing",
|
|
1028
|
+
pairsWith: [],
|
|
1029
|
+
contentSlots: {
|
|
1030
|
+
headerTitle: { type: "string" },
|
|
1031
|
+
footerText: { type: "string" }
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
}),
|
|
1035
|
+
"layout-dashboard": manifest("layout-dashboard", {
|
|
1036
|
+
type: "layout",
|
|
1037
|
+
category: "dashboard",
|
|
1038
|
+
requiredMode: "server",
|
|
1039
|
+
requires: ["auth-better-auth"],
|
|
1040
|
+
files: [
|
|
1041
|
+
{ source: "layout-dashboard.astro", destination: "src/layouts/layout-dashboard.astro" },
|
|
1042
|
+
{ source: "default-content.json", destination: "src/content/layouts/layout-dashboard.json" }
|
|
1043
|
+
],
|
|
1044
|
+
ai: {
|
|
1045
|
+
whenToUse: "dashboards",
|
|
1046
|
+
whenNotToUse: "marketing",
|
|
1047
|
+
pairsWith: [],
|
|
1048
|
+
contentSlots: {
|
|
1049
|
+
sidebarLinks: { type: "array" },
|
|
1050
|
+
logoutText: { type: "string" }
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
}),
|
|
1054
|
+
"hero-video": manifest("hero-video"),
|
|
1055
|
+
"features-bento": manifest("features-bento"),
|
|
1056
|
+
"pricing-table": manifest("pricing-table"),
|
|
1057
|
+
"faq-accordion": manifest("faq-accordion"),
|
|
1058
|
+
"footer-rich": manifest("footer-rich"),
|
|
1059
|
+
"testimonials-carousel": manifest("testimonials-carousel"),
|
|
1060
|
+
"contact-form": manifest("contact-form"),
|
|
1061
|
+
"hero-split": manifest("hero-split"),
|
|
1062
|
+
"header-transparent": manifest("header-transparent"),
|
|
1063
|
+
"cta-newsletter": manifest("cta-newsletter"),
|
|
1064
|
+
"header-sticky": manifest("header-sticky")
|
|
1065
|
+
};
|
|
1066
|
+
var FIXTURE_BLOCK_SOURCES = {
|
|
1067
|
+
"hero-gradient": {
|
|
1068
|
+
"hero-gradient.astro": `---
|
|
1069
|
+
interface Props {
|
|
1070
|
+
headline?: string;
|
|
1071
|
+
subheadline?: string;
|
|
1072
|
+
}
|
|
1073
|
+
const { headline = "Welcome", subheadline = "" } = Astro.props;
|
|
1074
|
+
---
|
|
1075
|
+
<section class="hero-gradient">
|
|
1076
|
+
<h1>{headline}</h1>
|
|
1077
|
+
{subheadline && <p>{subheadline}</p>}
|
|
1078
|
+
</section>
|
|
1079
|
+
`
|
|
1080
|
+
},
|
|
1081
|
+
"footer-minimal": {
|
|
1082
|
+
"footer-minimal.astro": `<footer class="footer-minimal">
|
|
1083
|
+
<p>© {new Date().getFullYear()}</p>
|
|
1084
|
+
</footer>
|
|
1085
|
+
`
|
|
1086
|
+
},
|
|
1087
|
+
"cta-simple": {
|
|
1088
|
+
"cta-simple.astro": `<section class="cta-simple">
|
|
1089
|
+
<h2>Get Started</h2>
|
|
1090
|
+
<a href="#" class="btn">Start Now</a>
|
|
1091
|
+
</section>
|
|
1092
|
+
`
|
|
1093
|
+
},
|
|
1094
|
+
"features-grid": {
|
|
1095
|
+
"features-grid.astro": `<section class="features-grid">
|
|
1096
|
+
<h2>Features</h2>
|
|
1097
|
+
<div class="grid"></div>
|
|
1098
|
+
</section>
|
|
1099
|
+
`
|
|
1100
|
+
},
|
|
1101
|
+
"auth-better-auth": {
|
|
1102
|
+
"auth.ts": `import { getDb } from "./db";
|
|
1103
|
+
|
|
1104
|
+
type D1Database = any;
|
|
1105
|
+
|
|
1106
|
+
export function createAuth(d1: D1Database) {
|
|
1107
|
+
const db = getDb(d1);
|
|
1108
|
+
return { handler: (req: Request) => new Response("auth") };
|
|
1109
|
+
}
|
|
1110
|
+
`,
|
|
1111
|
+
"middleware.ts": `import type { MiddlewareHandler } from "astro";
|
|
1112
|
+
import { createAuth } from "../lib/auth";
|
|
1113
|
+
|
|
1114
|
+
export const authMiddleware: MiddlewareHandler = async (context, next) => {
|
|
1115
|
+
return next();
|
|
1116
|
+
};
|
|
1117
|
+
`,
|
|
1118
|
+
"auth-api.ts": `import type { APIRoute } from "astro";
|
|
1119
|
+
import { createAuth } from "../../../lib/auth";
|
|
1120
|
+
|
|
1121
|
+
export const ALL: APIRoute = async (context) => {
|
|
1122
|
+
const d1 = (context.locals as Record<string, { env: { DB: any } }>).runtime.env.DB;
|
|
1123
|
+
const auth = createAuth(d1);
|
|
1124
|
+
return auth.handler(context.request);
|
|
1125
|
+
};
|
|
1126
|
+
`,
|
|
1127
|
+
"login.astro": `---
|
|
1128
|
+
---
|
|
1129
|
+
<main class="auth-page"><h1>Login</h1></main>
|
|
1130
|
+
`,
|
|
1131
|
+
"signup.astro": `---
|
|
1132
|
+
---
|
|
1133
|
+
<main class="auth-page"><h1>Sign Up</h1></main>
|
|
1134
|
+
`
|
|
1135
|
+
},
|
|
1136
|
+
"db-d1": {
|
|
1137
|
+
"db.ts": `import * as schema from "./db-schema";
|
|
1138
|
+
|
|
1139
|
+
export type Database = any;
|
|
1140
|
+
|
|
1141
|
+
export function getDb(d1: any): Database {
|
|
1142
|
+
return schema;
|
|
1143
|
+
}
|
|
1144
|
+
`,
|
|
1145
|
+
"schema.ts": `// Database schema \u2014 define your tables here.
|
|
1146
|
+
// See: https://orm.drizzle.team/docs/sql-schema-declaration
|
|
1147
|
+
export const users = {};
|
|
1148
|
+
`,
|
|
1149
|
+
"drizzle.config.ts": `export default {
|
|
1150
|
+
schema: "./src/lib/db-schema.ts",
|
|
1151
|
+
out: "./drizzle/migrations",
|
|
1152
|
+
dialect: "sqlite",
|
|
1153
|
+
} as any;
|
|
1154
|
+
`,
|
|
1155
|
+
"migrations/.gitkeep": ""
|
|
1156
|
+
},
|
|
1157
|
+
"blog-mdx": {
|
|
1158
|
+
"schema.ts": `import { z, defineCollection } from "astro:content";
|
|
1159
|
+
export const blog = defineCollection({ schema: z.any() });
|
|
1160
|
+
`,
|
|
1161
|
+
"pages/index.astro": `---
|
|
1162
|
+
---
|
|
1163
|
+
<h1>Blog</h1>`,
|
|
1164
|
+
"pages/[slug].astro": `---
|
|
1165
|
+
export function getStaticPaths() { return [{ params: { slug: '1' } }]; }
|
|
1166
|
+
---
|
|
1167
|
+
<h1>Post</h1>`,
|
|
1168
|
+
"pages/rss.xml.ts": `export const GET = () => new Response("RSS");`
|
|
1169
|
+
},
|
|
1170
|
+
"docs-collection": {
|
|
1171
|
+
"schema.ts": `import { z, defineCollection } from "astro:content";
|
|
1172
|
+
export const docs = defineCollection({ schema: z.any() });
|
|
1173
|
+
`,
|
|
1174
|
+
"pages/[...slug].astro": `---
|
|
1175
|
+
export function getStaticPaths() { return [{ params: { slug: '1' } }]; }
|
|
1176
|
+
---
|
|
1177
|
+
<h1>Docs</h1>`
|
|
1178
|
+
},
|
|
1179
|
+
"layout-marketing": {
|
|
1180
|
+
"layout-marketing.astro": `---
|
|
1181
|
+
---
|
|
1182
|
+
<slot />`,
|
|
1183
|
+
"default-content.json": `{ "headerLinks": [], "footerText": "" }`
|
|
1184
|
+
},
|
|
1185
|
+
"layout-docs": {
|
|
1186
|
+
"layout-docs.astro": `---
|
|
1187
|
+
---
|
|
1188
|
+
<slot />`,
|
|
1189
|
+
"default-content.json": `{ "headerTitle": "", "footerText": "" }`
|
|
1190
|
+
},
|
|
1191
|
+
"layout-dashboard": {
|
|
1192
|
+
"layout-dashboard.astro": `---
|
|
1193
|
+
---
|
|
1194
|
+
<slot />`,
|
|
1195
|
+
"default-content.json": `{ "sidebarLinks": [], "logoutText": "" }`
|
|
1196
|
+
},
|
|
1197
|
+
"hero-video": { "hero-video.astro": "<section>Hero Video</section>\n" },
|
|
1198
|
+
"features-bento": { "features-bento.astro": "<section>Features Bento</section>\n" },
|
|
1199
|
+
"pricing-table": { "pricing-table.astro": "<section>Pricing</section>\n" },
|
|
1200
|
+
"faq-accordion": { "faq-accordion.astro": "<section>FAQ</section>\n" },
|
|
1201
|
+
"footer-rich": { "footer-rich.astro": "<section>Footer</section>\n" },
|
|
1202
|
+
"testimonials-carousel": { "testimonials-carousel.astro": "<section>Testimonials</section>\n" },
|
|
1203
|
+
"contact-form": { "contact-form.astro": "<section>Contact</section>\n" },
|
|
1204
|
+
"hero-split": { "hero-split.astro": "<section>Hero Split</section>\n" },
|
|
1205
|
+
"header-transparent": { "header-transparent.astro": "<header>Transparent</header>\n" },
|
|
1206
|
+
"cta-newsletter": { "cta-newsletter.astro": "<section>CTA</section>\n" },
|
|
1207
|
+
"header-sticky": { "header-sticky.astro": "<header>Sticky</header>\n" }
|
|
1208
|
+
};
|
|
1209
|
+
var FIXTURE_DEFAULT_CONTENT = {};
|
|
1210
|
+
function loadAllPalettes() {
|
|
1211
|
+
try {
|
|
1212
|
+
const registryPath = findRegistryPalettesDir();
|
|
1213
|
+
if (!registryPath) return [];
|
|
1214
|
+
const files = readdirSync(registryPath).filter((f) => f.endsWith(".json"));
|
|
1215
|
+
const palettes = [];
|
|
1216
|
+
for (const file of files) {
|
|
1217
|
+
try {
|
|
1218
|
+
const content = readFileSync(join(registryPath, file), "utf-8");
|
|
1219
|
+
palettes.push(JSON.parse(content));
|
|
1220
|
+
} catch {
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
return palettes;
|
|
1224
|
+
} catch {
|
|
1225
|
+
return [];
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
function findRegistryPalettesDir() {
|
|
1229
|
+
try {
|
|
1230
|
+
const thisDir = dirname(fileURLToPath(import.meta.url));
|
|
1231
|
+
const monorepoRoot = join(thisDir, "..", "..", "..");
|
|
1232
|
+
const palettesDir = join(monorepoRoot, "packages", "fornix-registry", "palettes");
|
|
1233
|
+
readdirSync(palettesDir);
|
|
1234
|
+
return palettesDir;
|
|
1235
|
+
} catch {
|
|
1236
|
+
return null;
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
// src/prompts/manual-flow.ts
|
|
1241
|
+
import * as p from "@clack/prompts";
|
|
1242
|
+
import pc from "picocolors";
|
|
1243
|
+
async function runManualFlow(input) {
|
|
1244
|
+
p.intro(pc.bgCyan(pc.black(" Fornix \u2014 Create your project ")));
|
|
1245
|
+
const projectName = await p.text({
|
|
1246
|
+
message: "What is your project name?",
|
|
1247
|
+
placeholder: input.defaultProjectName,
|
|
1248
|
+
defaultValue: input.defaultProjectName,
|
|
1249
|
+
validate(value) {
|
|
1250
|
+
if (!value.trim()) return "Project name is required";
|
|
1251
|
+
if (!/^[a-z0-9-]+$/i.test(value.trim())) {
|
|
1252
|
+
return "Project name must only contain letters, numbers, and dashes";
|
|
1253
|
+
}
|
|
1254
|
+
return void 0;
|
|
1255
|
+
}
|
|
1256
|
+
});
|
|
1257
|
+
if (p.isCancel(projectName)) return handleCancel();
|
|
1258
|
+
const renderMode = await p.select({
|
|
1259
|
+
message: "Choose a render mode",
|
|
1260
|
+
options: [
|
|
1261
|
+
{ value: "static", label: "Static (SSG)", hint: "Pre-built HTML, fastest" },
|
|
1262
|
+
{ value: "hybrid", label: "Hybrid", hint: "Static by default, opt into SSR per page" },
|
|
1263
|
+
{ value: "server", label: "Server (SSR)", hint: "Server-rendered on every request" }
|
|
1264
|
+
]
|
|
1265
|
+
});
|
|
1266
|
+
if (p.isCancel(renderMode)) return handleCancel();
|
|
1267
|
+
const deployTarget = await p.select({
|
|
1268
|
+
message: "Where will you deploy?",
|
|
1269
|
+
options: [
|
|
1270
|
+
{ value: "cloudflare", label: "Cloudflare Pages", hint: "Recommended" },
|
|
1271
|
+
{ value: "vercel", label: "Vercel" },
|
|
1272
|
+
{ value: "netlify", label: "Netlify" },
|
|
1273
|
+
{ value: "static", label: "Static hosting", hint: "No adapter needed" }
|
|
1274
|
+
]
|
|
1275
|
+
});
|
|
1276
|
+
if (p.isCancel(deployTarget)) return handleCancel();
|
|
1277
|
+
const cssEngine = await p.select({
|
|
1278
|
+
message: "Choose a CSS engine",
|
|
1279
|
+
options: [
|
|
1280
|
+
{ value: "tailwind", label: "Tailwind CSS v4", hint: "Recommended" },
|
|
1281
|
+
{ value: "vanilla", label: "Vanilla CSS", hint: "No framework" }
|
|
1282
|
+
]
|
|
1283
|
+
});
|
|
1284
|
+
if (p.isCancel(cssEngine)) return handleCancel();
|
|
1285
|
+
const blockOptions = buildBlockOptions(input.manifests);
|
|
1286
|
+
let selectedBlocks = [];
|
|
1287
|
+
if (blockOptions.length > 0) {
|
|
1288
|
+
const blocks = await p.multiselect({
|
|
1289
|
+
message: "Select blocks to include (space to toggle, enter to confirm)",
|
|
1290
|
+
options: blockOptions,
|
|
1291
|
+
required: false
|
|
1292
|
+
});
|
|
1293
|
+
if (p.isCancel(blocks)) return handleCancel();
|
|
1294
|
+
selectedBlocks = blocks;
|
|
1295
|
+
}
|
|
1296
|
+
const localesInput = await p.text({
|
|
1297
|
+
message: "Locales (comma-separated, e.g. en,es,ar)",
|
|
1298
|
+
placeholder: "en",
|
|
1299
|
+
defaultValue: "en"
|
|
1300
|
+
});
|
|
1301
|
+
if (p.isCancel(localesInput)) return handleCancel();
|
|
1302
|
+
const locales = localesInput.split(",").map((l) => l.trim()).filter(Boolean);
|
|
1303
|
+
const defaultLocale = locales[0] ?? "en";
|
|
1304
|
+
const paletteOptions = buildPaletteOptions(input.allPalettes);
|
|
1305
|
+
let selectedPalette;
|
|
1306
|
+
if (paletteOptions.length > 0) {
|
|
1307
|
+
const paletteChoice = await p.select({
|
|
1308
|
+
message: "Choose a default color palette",
|
|
1309
|
+
options: paletteOptions
|
|
1310
|
+
});
|
|
1311
|
+
if (p.isCancel(paletteChoice)) return handleCancel();
|
|
1312
|
+
selectedPalette = input.allPalettes.find((pal) => pal.name === paletteChoice);
|
|
1313
|
+
}
|
|
1314
|
+
let themeSwitcher = false;
|
|
1315
|
+
if (input.allPalettes.length >= 2) {
|
|
1316
|
+
const paletteCount = input.allPalettes.length;
|
|
1317
|
+
const switcherChoice = await p.confirm({
|
|
1318
|
+
message: `Enable theme switcher? (includes all ${paletteCount} registry palettes for runtime switching)`,
|
|
1319
|
+
initialValue: false
|
|
1320
|
+
});
|
|
1321
|
+
if (p.isCancel(switcherChoice)) return handleCancel();
|
|
1322
|
+
themeSwitcher = switcherChoice;
|
|
1323
|
+
}
|
|
1324
|
+
const config = {
|
|
1325
|
+
projectName: projectName.trim(),
|
|
1326
|
+
projectDir: `./${projectName.trim()}`,
|
|
1327
|
+
renderMode,
|
|
1328
|
+
deployTarget,
|
|
1329
|
+
database: "none",
|
|
1330
|
+
cssEngine,
|
|
1331
|
+
packageManager: "pnpm",
|
|
1332
|
+
blocks: selectedBlocks.map((name) => ({ name, variant: "default" })),
|
|
1333
|
+
locales,
|
|
1334
|
+
defaultLocale,
|
|
1335
|
+
palette: {
|
|
1336
|
+
...selectedPalette ? { preset: selectedPalette.name } : {},
|
|
1337
|
+
colors: selectedPalette?.colors ?? {
|
|
1338
|
+
primary: "#6366f1",
|
|
1339
|
+
secondary: "#818cf8",
|
|
1340
|
+
accent: "#c084fc",
|
|
1341
|
+
background: "#0f172a",
|
|
1342
|
+
foreground: "#f8fafc"
|
|
1343
|
+
}
|
|
1344
|
+
},
|
|
1345
|
+
themeSwitcher,
|
|
1346
|
+
createdWith: "manual"
|
|
1347
|
+
};
|
|
1348
|
+
p.note(
|
|
1349
|
+
buildSummary(config, selectedBlocks, selectedPalette),
|
|
1350
|
+
"Project Summary"
|
|
1351
|
+
);
|
|
1352
|
+
const confirmed = await p.confirm({
|
|
1353
|
+
message: "Create this project?",
|
|
1354
|
+
initialValue: true
|
|
1355
|
+
});
|
|
1356
|
+
if (p.isCancel(confirmed) || !confirmed) return handleCancel();
|
|
1357
|
+
return config;
|
|
1358
|
+
}
|
|
1359
|
+
function handleCancel() {
|
|
1360
|
+
p.cancel("Operation cancelled.");
|
|
1361
|
+
return null;
|
|
1362
|
+
}
|
|
1363
|
+
function buildBlockOptions(manifests) {
|
|
1364
|
+
const blocks = Object.values(manifests);
|
|
1365
|
+
const categories = /* @__PURE__ */ new Map();
|
|
1366
|
+
for (const block of blocks) {
|
|
1367
|
+
const category = block.category ?? "other";
|
|
1368
|
+
if (!categories.has(category)) {
|
|
1369
|
+
categories.set(category, []);
|
|
1370
|
+
}
|
|
1371
|
+
categories.get(category).push(block);
|
|
1372
|
+
}
|
|
1373
|
+
const options = [];
|
|
1374
|
+
for (const [category, categoryBlocks] of categories) {
|
|
1375
|
+
for (const block of categoryBlocks) {
|
|
1376
|
+
options.push({
|
|
1377
|
+
value: block.name,
|
|
1378
|
+
label: `${block.name}`,
|
|
1379
|
+
hint: `${category} \u2014 ${block.description}`
|
|
1380
|
+
});
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
return options;
|
|
1384
|
+
}
|
|
1385
|
+
function buildPaletteOptions(palettes) {
|
|
1386
|
+
if (palettes.length === 0) return [];
|
|
1387
|
+
const sorted = [...palettes].sort((a, b) => {
|
|
1388
|
+
const catA = a.category ?? "other";
|
|
1389
|
+
const catB = b.category ?? "other";
|
|
1390
|
+
if (catA !== catB) return catA.localeCompare(catB);
|
|
1391
|
+
return a.displayName.localeCompare(b.displayName);
|
|
1392
|
+
});
|
|
1393
|
+
return sorted.map((palette) => {
|
|
1394
|
+
const modeLabel = palette.mode === "dark" ? "\u{1F319}" : "\u2600\uFE0F";
|
|
1395
|
+
const category = palette.category ?? "other";
|
|
1396
|
+
return {
|
|
1397
|
+
value: palette.name,
|
|
1398
|
+
label: `${modeLabel} ${palette.displayName}`,
|
|
1399
|
+
hint: `${category} \xB7 ${palette.colors.primary}`
|
|
1400
|
+
};
|
|
1401
|
+
});
|
|
1402
|
+
}
|
|
1403
|
+
function buildSummary(config, blockNames, palette) {
|
|
1404
|
+
const lines = [];
|
|
1405
|
+
lines.push(`${pc.bold("Project:")} ${config.projectName}`);
|
|
1406
|
+
lines.push(`${pc.bold("Render mode:")} ${config.renderMode}`);
|
|
1407
|
+
lines.push(`${pc.bold("Deploy to:")} ${config.deployTarget}`);
|
|
1408
|
+
lines.push(`${pc.bold("CSS engine:")} ${config.cssEngine}`);
|
|
1409
|
+
if (blockNames.length > 0) {
|
|
1410
|
+
lines.push(`${pc.bold("Blocks:")} ${blockNames.join(", ")}`);
|
|
1411
|
+
} else {
|
|
1412
|
+
lines.push(`${pc.bold("Blocks:")} ${pc.dim("(none)")}`);
|
|
1413
|
+
}
|
|
1414
|
+
lines.push(`${pc.bold("Locales:")} ${config.locales.join(", ")} (default: ${config.defaultLocale})`);
|
|
1415
|
+
if (palette) {
|
|
1416
|
+
lines.push(`${pc.bold("Palette:")} ${palette.displayName} (${palette.mode})`);
|
|
1417
|
+
} else {
|
|
1418
|
+
lines.push(`${pc.bold("Palette:")} ${pc.dim("default")}`);
|
|
1419
|
+
}
|
|
1420
|
+
if (config.themeSwitcher) {
|
|
1421
|
+
lines.push(`${pc.bold("Theme switcher:")} ${pc.green("yes")} (all registry palettes included)`);
|
|
1422
|
+
} else {
|
|
1423
|
+
lines.push(`${pc.bold("Theme switcher:")} ${pc.dim("no")}`);
|
|
1424
|
+
}
|
|
1425
|
+
return lines.join("\n");
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
// src/scaffold/post-scaffold.ts
|
|
1429
|
+
import { execSync } from "child_process";
|
|
1430
|
+
import { writeFileSync } from "fs";
|
|
1431
|
+
import { join as join2, basename } from "path";
|
|
1432
|
+
import pc2 from "picocolors";
|
|
1433
|
+
function runPostScaffold(input, callbacks) {
|
|
1434
|
+
const log = callbacks?.onLog ?? ((msg) => console.log(msg));
|
|
1435
|
+
const warn = callbacks?.onWarn ?? ((msg) => console.warn(msg));
|
|
1436
|
+
const { config, resolvedBlockNames, filesWritten, verbose } = input;
|
|
1437
|
+
const projectDir = config.projectDir;
|
|
1438
|
+
const projectName = basename(projectDir);
|
|
1439
|
+
generateClaudeMd(projectDir, config, resolvedBlockNames);
|
|
1440
|
+
if (verbose) log(pc2.dim(" created CLAUDE.md"));
|
|
1441
|
+
let installSuccess = false;
|
|
1442
|
+
if (!input.skipInstall) {
|
|
1443
|
+
installSuccess = installDependencies(projectDir, config.packageManager, verbose, log, warn);
|
|
1444
|
+
}
|
|
1445
|
+
let gitSuccess = false;
|
|
1446
|
+
if (!input.skipGit) {
|
|
1447
|
+
gitSuccess = initGit(projectDir, verbose, log, warn);
|
|
1448
|
+
}
|
|
1449
|
+
log("");
|
|
1450
|
+
log(pc2.green(pc2.bold("\u2714 Project created successfully!")));
|
|
1451
|
+
log("");
|
|
1452
|
+
log(` ${pc2.bold("Project:")} ${config.projectName}`);
|
|
1453
|
+
log(` ${pc2.bold("Dir:")} ${projectDir}`);
|
|
1454
|
+
log(` ${pc2.bold("Render:")} ${config.renderMode}`);
|
|
1455
|
+
log(` ${pc2.bold("Deploy:")} ${config.deployTarget}`);
|
|
1456
|
+
log(` ${pc2.bold("CSS:")} ${config.cssEngine}`);
|
|
1457
|
+
if (resolvedBlockNames.length > 0) {
|
|
1458
|
+
log(` ${pc2.bold("Blocks:")} ${resolvedBlockNames.join(", ")}`);
|
|
1459
|
+
}
|
|
1460
|
+
if (config.locales.length > 1) {
|
|
1461
|
+
log(` ${pc2.bold("Locales:")} ${config.locales.join(", ")} (default: ${config.defaultLocale})`);
|
|
1462
|
+
}
|
|
1463
|
+
if (config.palette.preset) {
|
|
1464
|
+
log(` ${pc2.bold("Palette:")} ${config.palette.preset}`);
|
|
1465
|
+
}
|
|
1466
|
+
log(` ${pc2.bold("Files:")} ${filesWritten} files written`);
|
|
1467
|
+
if (installSuccess) {
|
|
1468
|
+
log(` ${pc2.bold("Deps:")} ${pc2.green("installed")}`);
|
|
1469
|
+
}
|
|
1470
|
+
if (gitSuccess) {
|
|
1471
|
+
log(` ${pc2.bold("Git:")} ${pc2.green("initialized")}`);
|
|
1472
|
+
}
|
|
1473
|
+
log("");
|
|
1474
|
+
log(pc2.dim(" Next steps:"));
|
|
1475
|
+
log(pc2.dim(` cd ${projectName}`));
|
|
1476
|
+
if (!installSuccess) {
|
|
1477
|
+
log(pc2.dim(` ${config.packageManager} install`));
|
|
1478
|
+
}
|
|
1479
|
+
log(pc2.dim(` ${config.packageManager} dev`));
|
|
1480
|
+
log(pc2.dim(` fornix add <block>`));
|
|
1481
|
+
log("");
|
|
1482
|
+
}
|
|
1483
|
+
function installDependencies(projectDir, packageManager, verbose, log, warn) {
|
|
1484
|
+
try {
|
|
1485
|
+
log(pc2.dim(" Installing dependencies..."));
|
|
1486
|
+
const cmd = `${packageManager} install`;
|
|
1487
|
+
execSync(cmd, {
|
|
1488
|
+
cwd: projectDir,
|
|
1489
|
+
stdio: verbose ? "inherit" : "pipe",
|
|
1490
|
+
timeout: 12e4
|
|
1491
|
+
});
|
|
1492
|
+
return true;
|
|
1493
|
+
} catch {
|
|
1494
|
+
warn(pc2.yellow(" \u26A0 Could not install dependencies automatically."));
|
|
1495
|
+
warn(pc2.dim(` Run '${packageManager} install' manually.`));
|
|
1496
|
+
return false;
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
function initGit(projectDir, verbose, log, warn) {
|
|
1500
|
+
try {
|
|
1501
|
+
if (verbose) log(pc2.dim(" Initializing git repository..."));
|
|
1502
|
+
execSync("git init", {
|
|
1503
|
+
cwd: projectDir,
|
|
1504
|
+
stdio: "pipe",
|
|
1505
|
+
timeout: 1e4
|
|
1506
|
+
});
|
|
1507
|
+
execSync("git add -A", {
|
|
1508
|
+
cwd: projectDir,
|
|
1509
|
+
stdio: "pipe",
|
|
1510
|
+
timeout: 1e4
|
|
1511
|
+
});
|
|
1512
|
+
execSync('git commit -m "Initial commit \u2014 scaffolded by Fornix"', {
|
|
1513
|
+
cwd: projectDir,
|
|
1514
|
+
stdio: "pipe",
|
|
1515
|
+
timeout: 1e4
|
|
1516
|
+
});
|
|
1517
|
+
return true;
|
|
1518
|
+
} catch {
|
|
1519
|
+
warn(pc2.yellow(" \u26A0 Could not initialize git repository."));
|
|
1520
|
+
warn(pc2.dim(" Run 'git init' manually."));
|
|
1521
|
+
return false;
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
function generateClaudeMd(projectDir, config, blockNames) {
|
|
1525
|
+
const lines = [];
|
|
1526
|
+
lines.push("# CLAUDE.md");
|
|
1527
|
+
lines.push("");
|
|
1528
|
+
lines.push("Project context for AI assistants. Auto-generated by Fornix.");
|
|
1529
|
+
lines.push("");
|
|
1530
|
+
lines.push("## Project Overview");
|
|
1531
|
+
lines.push("");
|
|
1532
|
+
lines.push(`- **Name:** ${config.projectName}`);
|
|
1533
|
+
lines.push(`- **Framework:** Astro`);
|
|
1534
|
+
lines.push(`- **Render mode:** ${config.renderMode}`);
|
|
1535
|
+
lines.push(`- **Deploy target:** ${config.deployTarget}`);
|
|
1536
|
+
lines.push(`- **CSS engine:** ${config.cssEngine}`);
|
|
1537
|
+
lines.push(`- **Package manager:** ${config.packageManager}`);
|
|
1538
|
+
lines.push("");
|
|
1539
|
+
if (blockNames.length > 0) {
|
|
1540
|
+
lines.push("## Installed Blocks");
|
|
1541
|
+
lines.push("");
|
|
1542
|
+
for (const name of blockNames) {
|
|
1543
|
+
const variant = config.blocks.find((b) => b.name === name)?.variant ?? "default";
|
|
1544
|
+
lines.push(`- \`${name}\` (variant: ${variant})`);
|
|
1545
|
+
}
|
|
1546
|
+
lines.push("");
|
|
1547
|
+
}
|
|
1548
|
+
if (config.locales.length > 1) {
|
|
1549
|
+
lines.push("## Internationalization");
|
|
1550
|
+
lines.push("");
|
|
1551
|
+
lines.push(`- **Locales:** ${config.locales.join(", ")}`);
|
|
1552
|
+
lines.push(`- **Default locale:** ${config.defaultLocale}`);
|
|
1553
|
+
lines.push("- Content is organized per locale in `src/content/{locale}/`");
|
|
1554
|
+
lines.push("- Routes are prefixed with `[locale]` parameter");
|
|
1555
|
+
lines.push("");
|
|
1556
|
+
}
|
|
1557
|
+
lines.push("## Styling");
|
|
1558
|
+
lines.push("");
|
|
1559
|
+
if (config.palette.preset) {
|
|
1560
|
+
lines.push(`- **Palette:** ${config.palette.preset}`);
|
|
1561
|
+
}
|
|
1562
|
+
lines.push("- Colors are defined as CSS custom properties in `src/styles/palettes/_current.css`");
|
|
1563
|
+
lines.push("- Use `var(--color-primary)`, `var(--color-secondary)`, etc. for theming");
|
|
1564
|
+
if (config.themeSwitcher) {
|
|
1565
|
+
lines.push("- **Theme switcher** is enabled \u2014 all registry palettes are available at runtime");
|
|
1566
|
+
lines.push("- Palette CSS files are in `src/styles/palettes/`");
|
|
1567
|
+
}
|
|
1568
|
+
lines.push("");
|
|
1569
|
+
lines.push("## Key Commands");
|
|
1570
|
+
lines.push("");
|
|
1571
|
+
lines.push(`- \`${config.packageManager} dev\` \u2014 start development server`);
|
|
1572
|
+
lines.push(`- \`${config.packageManager} build\` \u2014 build for production`);
|
|
1573
|
+
lines.push("- `fornix add <block>` \u2014 add a new block");
|
|
1574
|
+
lines.push("- `fornix remove <block>` \u2014 remove a block");
|
|
1575
|
+
lines.push("- `fornix status` \u2014 show project configuration");
|
|
1576
|
+
lines.push("");
|
|
1577
|
+
lines.push("## File Structure");
|
|
1578
|
+
lines.push("");
|
|
1579
|
+
lines.push("```");
|
|
1580
|
+
lines.push("src/");
|
|
1581
|
+
lines.push("\u251C\u2500\u2500 components/sections/ \u2190 block components");
|
|
1582
|
+
lines.push("\u251C\u2500\u2500 content/ \u2190 JSON/MD content (from blocks)");
|
|
1583
|
+
lines.push("\u251C\u2500\u2500 layouts/ \u2190 page layouts");
|
|
1584
|
+
lines.push("\u251C\u2500\u2500 pages/ \u2190 Astro pages (routes)");
|
|
1585
|
+
lines.push("\u251C\u2500\u2500 styles/ \u2190 CSS and palette files");
|
|
1586
|
+
if (config.renderMode === "server") {
|
|
1587
|
+
lines.push("\u251C\u2500\u2500 middleware/ \u2190 request middleware");
|
|
1588
|
+
lines.push("\u251C\u2500\u2500 lib/ \u2190 server utilities");
|
|
1589
|
+
lines.push("\u251C\u2500\u2500 pages/api/ \u2190 API endpoints");
|
|
1590
|
+
}
|
|
1591
|
+
if (config.locales.length > 1) {
|
|
1592
|
+
lines.push("\u251C\u2500\u2500 i18n/ \u2190 translation utilities");
|
|
1593
|
+
}
|
|
1594
|
+
lines.push("```");
|
|
1595
|
+
lines.push("");
|
|
1596
|
+
const content = lines.join("\n");
|
|
1597
|
+
writeFileSync(join2(projectDir, "CLAUDE.md"), content, "utf-8");
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
// src/ai/prompt-builder.ts
|
|
1601
|
+
function buildSystemPrompt(registry) {
|
|
1602
|
+
const sections = [
|
|
1603
|
+
buildIdentity(),
|
|
1604
|
+
buildBlocksCatalog(registry.blocks),
|
|
1605
|
+
buildPalettesCatalog(registry.palettes),
|
|
1606
|
+
buildConstraintRules(registry.blocks),
|
|
1607
|
+
buildOutputInstructions()
|
|
1608
|
+
];
|
|
1609
|
+
return sections.join("\n\n");
|
|
1610
|
+
}
|
|
1611
|
+
function buildIdentity() {
|
|
1612
|
+
return `# Fornix AI Assistant
|
|
1613
|
+
|
|
1614
|
+
You are the Fornix AI assistant. You analyze user descriptions of websites and produce structured configuration for the Fornix scaffold engine.
|
|
1615
|
+
|
|
1616
|
+
Your job:
|
|
1617
|
+
1. Understand what the user wants to build
|
|
1618
|
+
2. Select appropriate blocks from the available catalog
|
|
1619
|
+
3. Choose a palette that fits the brand and industry
|
|
1620
|
+
4. Determine the correct render mode, deploy target, and other settings
|
|
1621
|
+
5. Generate content for each block's content slots
|
|
1622
|
+
|
|
1623
|
+
You MUST only select blocks that exist in the catalog below. Never invent block names.`;
|
|
1624
|
+
}
|
|
1625
|
+
function buildBlocksCatalog(blocks) {
|
|
1626
|
+
const lines = ["# Available Blocks\n"];
|
|
1627
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
1628
|
+
for (const block of blocks) {
|
|
1629
|
+
if (!grouped.has(block.type)) {
|
|
1630
|
+
grouped.set(block.type, []);
|
|
1631
|
+
}
|
|
1632
|
+
grouped.get(block.type).push(block);
|
|
1633
|
+
}
|
|
1634
|
+
for (const [type, typeBlocks] of grouped) {
|
|
1635
|
+
lines.push(`## ${type.toUpperCase()} Blocks
|
|
1636
|
+
`);
|
|
1637
|
+
for (const block of typeBlocks) {
|
|
1638
|
+
lines.push(`### ${block.name}`);
|
|
1639
|
+
lines.push(`- **Description:** ${block.description}`);
|
|
1640
|
+
lines.push(`- **Category:** ${block.category}`);
|
|
1641
|
+
lines.push(`- **Tags:** ${block.tags.join(", ") || "none"}`);
|
|
1642
|
+
if (block.ai) {
|
|
1643
|
+
lines.push(`- **When to use:** ${block.ai.whenToUse}`);
|
|
1644
|
+
lines.push(`- **When NOT to use:** ${block.ai.whenNotToUse}`);
|
|
1645
|
+
if (block.ai.pairsWith.length > 0) {
|
|
1646
|
+
lines.push(
|
|
1647
|
+
`- **Pairs with:** ${block.ai.pairsWith.join(", ")}`
|
|
1648
|
+
);
|
|
1649
|
+
}
|
|
1650
|
+
if (block.ai.contentSlots && Object.keys(block.ai.contentSlots).length > 0) {
|
|
1651
|
+
lines.push(`- **Content slots:**`);
|
|
1652
|
+
for (const [slotName, slot] of Object.entries(
|
|
1653
|
+
block.ai.contentSlots
|
|
1654
|
+
)) {
|
|
1655
|
+
const desc = slot.description ? ` \u2014 ${slot.description}` : "";
|
|
1656
|
+
const maxLen = slot.maxLength ? ` (max ${slot.maxLength} chars)` : "";
|
|
1657
|
+
lines.push(` - \`${slotName}\` (${slot.type})${desc}${maxLen}`);
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
if (block.requires.length > 0) {
|
|
1662
|
+
lines.push(`- **Requires:** ${block.requires.join(", ")}`);
|
|
1663
|
+
}
|
|
1664
|
+
if (block.conflicts.length > 0) {
|
|
1665
|
+
lines.push(`- **Conflicts with:** ${block.conflicts.join(", ")}`);
|
|
1666
|
+
}
|
|
1667
|
+
if (block.requiredMode) {
|
|
1668
|
+
lines.push(`- **Required mode:** ${block.requiredMode}`);
|
|
1669
|
+
}
|
|
1670
|
+
if (Object.keys(block.dependencies).length > 0) {
|
|
1671
|
+
lines.push(
|
|
1672
|
+
`- **npm deps:** ${Object.keys(block.dependencies).join(", ")}`
|
|
1673
|
+
);
|
|
1674
|
+
}
|
|
1675
|
+
lines.push("");
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
return lines.join("\n");
|
|
1679
|
+
}
|
|
1680
|
+
function buildPalettesCatalog(palettes) {
|
|
1681
|
+
const lines = ["# Available Palettes\n"];
|
|
1682
|
+
lines.push(
|
|
1683
|
+
"Select a palette that fits the brand, industry, and visual style.\n"
|
|
1684
|
+
);
|
|
1685
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
1686
|
+
for (const palette of palettes) {
|
|
1687
|
+
if (!grouped.has(palette.category)) {
|
|
1688
|
+
grouped.set(palette.category, []);
|
|
1689
|
+
}
|
|
1690
|
+
grouped.get(palette.category).push(palette);
|
|
1691
|
+
}
|
|
1692
|
+
for (const [category, categoryPalettes] of grouped) {
|
|
1693
|
+
const names = categoryPalettes.map(
|
|
1694
|
+
(p3) => `${p3.displayName} (${p3.mode})`
|
|
1695
|
+
).join(", ");
|
|
1696
|
+
lines.push(`- **${category}:** ${names}`);
|
|
1697
|
+
}
|
|
1698
|
+
lines.push("");
|
|
1699
|
+
lines.push("Palette names (use these exact values):");
|
|
1700
|
+
for (const palette of palettes) {
|
|
1701
|
+
lines.push(`- \`${palette.name}\` \u2014 ${palette.displayName} (${palette.category}, ${palette.mode})`);
|
|
1702
|
+
}
|
|
1703
|
+
return lines.join("\n");
|
|
1704
|
+
}
|
|
1705
|
+
function buildConstraintRules(blocks) {
|
|
1706
|
+
const lines = ["# Constraint Rules\n"];
|
|
1707
|
+
lines.push("## Render Modes");
|
|
1708
|
+
lines.push("- `static` \u2014 Pre-rendered HTML, no server. Best for blogs, docs, landing pages.");
|
|
1709
|
+
lines.push("- `hybrid` \u2014 Mix of static + server routes. Best for mostly-static sites with some dynamic features.");
|
|
1710
|
+
lines.push("- `server` \u2014 Full server rendering. Required for auth, databases, real-time features.\n");
|
|
1711
|
+
const serverBlocks = blocks.filter((b) => b.requiredMode === "server");
|
|
1712
|
+
if (serverBlocks.length > 0) {
|
|
1713
|
+
lines.push("## Server-Required Blocks");
|
|
1714
|
+
lines.push(
|
|
1715
|
+
"These blocks REQUIRE `renderMode: 'server'`. Selecting them with static mode is invalid:"
|
|
1716
|
+
);
|
|
1717
|
+
for (const block of serverBlocks) {
|
|
1718
|
+
lines.push(`- \`${block.name}\` (${block.requiredMode})`);
|
|
1719
|
+
}
|
|
1720
|
+
lines.push("");
|
|
1721
|
+
}
|
|
1722
|
+
const blocksWithDeps = blocks.filter((b) => b.requires.length > 0);
|
|
1723
|
+
if (blocksWithDeps.length > 0) {
|
|
1724
|
+
lines.push("## Block Dependencies");
|
|
1725
|
+
lines.push(
|
|
1726
|
+
"These blocks require other blocks to be included:"
|
|
1727
|
+
);
|
|
1728
|
+
for (const block of blocksWithDeps) {
|
|
1729
|
+
lines.push(
|
|
1730
|
+
`- \`${block.name}\` requires: ${block.requires.map((r) => `\`${r}\``).join(", ")}`
|
|
1731
|
+
);
|
|
1732
|
+
}
|
|
1733
|
+
lines.push("");
|
|
1734
|
+
}
|
|
1735
|
+
const blocksWithConflicts = blocks.filter(
|
|
1736
|
+
(b) => b.conflicts.length > 0
|
|
1737
|
+
);
|
|
1738
|
+
if (blocksWithConflicts.length > 0) {
|
|
1739
|
+
lines.push("## Block Conflicts");
|
|
1740
|
+
lines.push(
|
|
1741
|
+
"These blocks cannot be used together:"
|
|
1742
|
+
);
|
|
1743
|
+
for (const block of blocksWithConflicts) {
|
|
1744
|
+
lines.push(
|
|
1745
|
+
`- \`${block.name}\` conflicts with: ${block.conflicts.map((c) => `\`${c}\``).join(", ")}`
|
|
1746
|
+
);
|
|
1747
|
+
}
|
|
1748
|
+
lines.push("");
|
|
1749
|
+
}
|
|
1750
|
+
lines.push("## Deterministic Rules (applied automatically)");
|
|
1751
|
+
lines.push("The following rules are applied by the rules engine BEFORE your output:");
|
|
1752
|
+
lines.push("- Auth or user accounts \u2192 `renderMode: server`, `auth-better-auth` + `db-d1` auto-added");
|
|
1753
|
+
lines.push("- Payments or e-commerce \u2192 upgrade to `hybrid` if static, `payments-stripe` auto-added");
|
|
1754
|
+
lines.push("- Blog with no dynamic content \u2192 `renderMode: static`, `blog-mdx` auto-added");
|
|
1755
|
+
lines.push("- Cloudflare deploy target \u2192 `analytics-cf` auto-added");
|
|
1756
|
+
lines.push("- Multiple languages \u2192 i18n mode enabled, locales set");
|
|
1757
|
+
lines.push("- Theme switcher requested \u2192 `theme-switcher` block auto-added");
|
|
1758
|
+
lines.push("\nDo NOT include auto-added blocks in your recommendations. Focus on section and feature blocks.");
|
|
1759
|
+
return lines.join("\n");
|
|
1760
|
+
}
|
|
1761
|
+
function buildOutputInstructions() {
|
|
1762
|
+
return `# Output Instructions
|
|
1763
|
+
|
|
1764
|
+
Return a structured IntentSchema object with these fields:
|
|
1765
|
+
- \`siteType\`: One of: landing-page, saas, agency, portfolio, blog, docs, ecommerce, dashboard, community, other
|
|
1766
|
+
- \`industry\`: The industry or niche
|
|
1767
|
+
- \`brand\`: { name, tagline?, description, targetAudience?, tone }
|
|
1768
|
+
- Boolean signals: needsAuth, needsPayments, needsBlog, needsDocs, needsDashboard, needsContactForm, needsNewsletter, hasDynamicContent, hasEcommerce, hasUserAccounts
|
|
1769
|
+
- \`prefersDarkMode\`: Whether the user prefers dark mode
|
|
1770
|
+
- \`visualStyle\`: One of: minimal, bold, glassmorphism, gradient, flat, neo-brutalist
|
|
1771
|
+
- \`languages\`: Array of language codes (e.g. ['en', 'ar'])
|
|
1772
|
+
- \`palettePreference\`: One of: custom, prebuilt, ai-generated, unspecified
|
|
1773
|
+
- \`wantsThemeSwitcher\`: Whether the user wants theme switching
|
|
1774
|
+
- \`recommendedBlocks\`: Array of { blockName, reason, confidence (0-1) }
|
|
1775
|
+
- \`uncertainties\`: Array of { topic, question, defaultAssumption }
|
|
1776
|
+
- \`overallConfidence\`: 0-1 score
|
|
1777
|
+
|
|
1778
|
+
ONLY recommend blocks that exist in the catalog above. Be precise with block names.`;
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
// src/ai/schemas.ts
|
|
1782
|
+
import { z } from "zod";
|
|
1783
|
+
var BrandSchema = z.object({
|
|
1784
|
+
name: z.string().min(1),
|
|
1785
|
+
tagline: z.string().optional(),
|
|
1786
|
+
description: z.string().min(1),
|
|
1787
|
+
targetAudience: z.string().optional(),
|
|
1788
|
+
tone: z.string().min(1)
|
|
1789
|
+
});
|
|
1790
|
+
var RecommendedBlockSchema = z.object({
|
|
1791
|
+
blockName: z.string().min(1),
|
|
1792
|
+
reason: z.string().min(1),
|
|
1793
|
+
confidence: z.number().min(0).max(1)
|
|
1794
|
+
});
|
|
1795
|
+
var UncertaintySchema = z.object({
|
|
1796
|
+
topic: z.string().min(1),
|
|
1797
|
+
question: z.string().min(1),
|
|
1798
|
+
defaultAssumption: z.string().min(1)
|
|
1799
|
+
});
|
|
1800
|
+
var IntentSchema = z.object({
|
|
1801
|
+
siteType: z.enum([
|
|
1802
|
+
"landing-page",
|
|
1803
|
+
"saas",
|
|
1804
|
+
"agency",
|
|
1805
|
+
"portfolio",
|
|
1806
|
+
"blog",
|
|
1807
|
+
"docs",
|
|
1808
|
+
"ecommerce",
|
|
1809
|
+
"dashboard",
|
|
1810
|
+
"community",
|
|
1811
|
+
"other"
|
|
1812
|
+
]),
|
|
1813
|
+
industry: z.string().min(1),
|
|
1814
|
+
brand: BrandSchema,
|
|
1815
|
+
needsAuth: z.boolean(),
|
|
1816
|
+
needsPayments: z.boolean(),
|
|
1817
|
+
needsBlog: z.boolean(),
|
|
1818
|
+
needsDocs: z.boolean(),
|
|
1819
|
+
needsDashboard: z.boolean(),
|
|
1820
|
+
needsContactForm: z.boolean(),
|
|
1821
|
+
needsNewsletter: z.boolean(),
|
|
1822
|
+
hasDynamicContent: z.boolean(),
|
|
1823
|
+
hasEcommerce: z.boolean(),
|
|
1824
|
+
hasUserAccounts: z.boolean(),
|
|
1825
|
+
prefersDarkMode: z.boolean(),
|
|
1826
|
+
visualStyle: z.enum([
|
|
1827
|
+
"minimal",
|
|
1828
|
+
"bold",
|
|
1829
|
+
"glassmorphism",
|
|
1830
|
+
"gradient",
|
|
1831
|
+
"flat",
|
|
1832
|
+
"neo-brutalist"
|
|
1833
|
+
]),
|
|
1834
|
+
languages: z.array(z.string().min(1)).default([]),
|
|
1835
|
+
palettePreference: z.enum(["custom", "prebuilt", "ai-generated", "unspecified"]),
|
|
1836
|
+
wantsThemeSwitcher: z.boolean(),
|
|
1837
|
+
recommendedBlocks: z.array(RecommendedBlockSchema),
|
|
1838
|
+
uncertainties: z.array(UncertaintySchema),
|
|
1839
|
+
overallConfidence: z.number().min(0).max(1)
|
|
1840
|
+
});
|
|
1841
|
+
|
|
1842
|
+
// src/ai/rules.ts
|
|
1843
|
+
import { z as z2 } from "zod";
|
|
1844
|
+
var IntentSchema2 = z2.object({
|
|
1845
|
+
siteType: z2.enum([
|
|
1846
|
+
"landing-page",
|
|
1847
|
+
"saas",
|
|
1848
|
+
"agency",
|
|
1849
|
+
"portfolio",
|
|
1850
|
+
"blog",
|
|
1851
|
+
"docs",
|
|
1852
|
+
"ecommerce",
|
|
1853
|
+
"dashboard",
|
|
1854
|
+
"community",
|
|
1855
|
+
"other"
|
|
1856
|
+
]),
|
|
1857
|
+
industry: z2.string(),
|
|
1858
|
+
brand: z2.object({
|
|
1859
|
+
name: z2.string(),
|
|
1860
|
+
tagline: z2.string().optional(),
|
|
1861
|
+
description: z2.string(),
|
|
1862
|
+
targetAudience: z2.string().optional(),
|
|
1863
|
+
tone: z2.string()
|
|
1864
|
+
}),
|
|
1865
|
+
needsAuth: z2.boolean(),
|
|
1866
|
+
needsPayments: z2.boolean(),
|
|
1867
|
+
needsBlog: z2.boolean(),
|
|
1868
|
+
needsDocs: z2.boolean(),
|
|
1869
|
+
needsDashboard: z2.boolean(),
|
|
1870
|
+
needsContactForm: z2.boolean(),
|
|
1871
|
+
needsNewsletter: z2.boolean(),
|
|
1872
|
+
hasDynamicContent: z2.boolean(),
|
|
1873
|
+
hasEcommerce: z2.boolean(),
|
|
1874
|
+
hasUserAccounts: z2.boolean(),
|
|
1875
|
+
prefersDarkMode: z2.boolean(),
|
|
1876
|
+
visualStyle: z2.enum([
|
|
1877
|
+
"minimal",
|
|
1878
|
+
"bold",
|
|
1879
|
+
"glassmorphism",
|
|
1880
|
+
"gradient",
|
|
1881
|
+
"flat",
|
|
1882
|
+
"neo-brutalist"
|
|
1883
|
+
]),
|
|
1884
|
+
languages: z2.array(z2.string()),
|
|
1885
|
+
palettePreference: z2.enum([
|
|
1886
|
+
"custom",
|
|
1887
|
+
"prebuilt",
|
|
1888
|
+
"ai-generated",
|
|
1889
|
+
"unspecified"
|
|
1890
|
+
]),
|
|
1891
|
+
wantsThemeSwitcher: z2.boolean(),
|
|
1892
|
+
recommendedBlocks: z2.array(
|
|
1893
|
+
z2.object({
|
|
1894
|
+
blockName: z2.string(),
|
|
1895
|
+
reason: z2.string(),
|
|
1896
|
+
confidence: z2.number()
|
|
1897
|
+
})
|
|
1898
|
+
),
|
|
1899
|
+
uncertainties: z2.array(
|
|
1900
|
+
z2.object({
|
|
1901
|
+
topic: z2.string(),
|
|
1902
|
+
question: z2.string(),
|
|
1903
|
+
defaultAssumption: z2.string()
|
|
1904
|
+
})
|
|
1905
|
+
),
|
|
1906
|
+
overallConfidence: z2.number().min(0).max(1)
|
|
1907
|
+
});
|
|
1908
|
+
function hasBlock(config, name) {
|
|
1909
|
+
return config.blocks.some((b) => b.name === name);
|
|
1910
|
+
}
|
|
1911
|
+
function addBlock(config, name) {
|
|
1912
|
+
if (!hasBlock(config, name)) {
|
|
1913
|
+
config.blocks.push({ name, variant: "default" });
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1916
|
+
var authRule = {
|
|
1917
|
+
name: "auth",
|
|
1918
|
+
apply(intent, config) {
|
|
1919
|
+
if (intent.needsAuth || intent.hasUserAccounts) {
|
|
1920
|
+
config.renderMode = "server";
|
|
1921
|
+
addBlock(config, "db-d1");
|
|
1922
|
+
addBlock(config, "auth-better-auth");
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
};
|
|
1926
|
+
var paymentsRule = {
|
|
1927
|
+
name: "payments",
|
|
1928
|
+
apply(intent, config) {
|
|
1929
|
+
if (intent.needsPayments || intent.hasEcommerce) {
|
|
1930
|
+
if (config.renderMode === "static") {
|
|
1931
|
+
config.renderMode = "hybrid";
|
|
1932
|
+
}
|
|
1933
|
+
addBlock(config, "payments-stripe");
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
};
|
|
1937
|
+
var blogRule = {
|
|
1938
|
+
name: "blog",
|
|
1939
|
+
apply(intent, config) {
|
|
1940
|
+
if (intent.needsBlog && !intent.hasDynamicContent) {
|
|
1941
|
+
config.renderMode = "static";
|
|
1942
|
+
addBlock(config, "blog-mdx");
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1945
|
+
};
|
|
1946
|
+
var cloudflareAnalyticsRule = {
|
|
1947
|
+
name: "cloudflare-analytics",
|
|
1948
|
+
apply(_intent, config) {
|
|
1949
|
+
if (config.deployTarget === "cloudflare") {
|
|
1950
|
+
addBlock(config, "analytics-cf");
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1953
|
+
};
|
|
1954
|
+
var i18nRule = {
|
|
1955
|
+
name: "i18n",
|
|
1956
|
+
apply(intent, config) {
|
|
1957
|
+
if (intent.languages.length >= 2) {
|
|
1958
|
+
config.locales = [...intent.languages];
|
|
1959
|
+
config.defaultLocale = intent.languages[0];
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
};
|
|
1963
|
+
var themeSwitcherRule = {
|
|
1964
|
+
name: "theme-switcher",
|
|
1965
|
+
apply(intent, config) {
|
|
1966
|
+
if (intent.wantsThemeSwitcher) {
|
|
1967
|
+
config.themeSwitcher = true;
|
|
1968
|
+
addBlock(config, "theme-switcher");
|
|
1969
|
+
}
|
|
1970
|
+
}
|
|
1971
|
+
};
|
|
1972
|
+
var RULES = [
|
|
1973
|
+
authRule,
|
|
1974
|
+
paymentsRule,
|
|
1975
|
+
blogRule,
|
|
1976
|
+
cloudflareAnalyticsRule,
|
|
1977
|
+
i18nRule,
|
|
1978
|
+
themeSwitcherRule
|
|
1979
|
+
];
|
|
1980
|
+
function applyRules(intent, config) {
|
|
1981
|
+
for (const rule of RULES) {
|
|
1982
|
+
rule.apply(intent, config);
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1985
|
+
function createDefaultConfig(overrides = {}) {
|
|
1986
|
+
return {
|
|
1987
|
+
renderMode: "static",
|
|
1988
|
+
deployTarget: "cloudflare",
|
|
1989
|
+
database: "none",
|
|
1990
|
+
blocks: [],
|
|
1991
|
+
locales: ["en"],
|
|
1992
|
+
defaultLocale: "en",
|
|
1993
|
+
themeSwitcher: false,
|
|
1994
|
+
...overrides
|
|
1995
|
+
};
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1998
|
+
// src/schemas/config.ts
|
|
1999
|
+
import { z as z3 } from "zod";
|
|
2000
|
+
var BlockSelectionSchema = z3.object({
|
|
2001
|
+
name: z3.string().min(1),
|
|
2002
|
+
variant: z3.string().min(1)
|
|
2003
|
+
});
|
|
2004
|
+
var PaletteColorsSchema = z3.object({
|
|
2005
|
+
primary: z3.string().min(1),
|
|
2006
|
+
secondary: z3.string().min(1),
|
|
2007
|
+
accent: z3.string().min(1),
|
|
2008
|
+
background: z3.string().min(1),
|
|
2009
|
+
foreground: z3.string().min(1)
|
|
2010
|
+
});
|
|
2011
|
+
var PaletteConfigSchema = z3.object({
|
|
2012
|
+
preset: z3.string().min(1).optional(),
|
|
2013
|
+
colors: PaletteColorsSchema
|
|
2014
|
+
});
|
|
2015
|
+
var ResolvedConfigSchema = z3.object({
|
|
2016
|
+
projectName: z3.string().min(1),
|
|
2017
|
+
projectDir: z3.string().min(1),
|
|
2018
|
+
renderMode: z3.enum(["static", "hybrid", "server"]),
|
|
2019
|
+
deployTarget: z3.enum(["cloudflare", "vercel", "netlify", "static"]),
|
|
2020
|
+
database: z3.enum(["none", "d1", "turso", "astro-db", "postgres"]),
|
|
2021
|
+
cssEngine: z3.enum(["tailwind", "vanilla"]),
|
|
2022
|
+
packageManager: z3.enum(["npm", "pnpm", "bun"]),
|
|
2023
|
+
blocks: z3.array(BlockSelectionSchema),
|
|
2024
|
+
locales: z3.array(z3.string().min(1)).transform((locales) => locales.length === 0 ? ["en"] : locales),
|
|
2025
|
+
defaultLocale: z3.string().min(1),
|
|
2026
|
+
palette: PaletteConfigSchema,
|
|
2027
|
+
themeSwitcher: z3.boolean().default(false),
|
|
2028
|
+
content: z3.record(z3.record(z3.unknown())).optional(),
|
|
2029
|
+
createdWith: z3.enum(["ai", "manual", "recipe", "mcp"])
|
|
2030
|
+
}).refine(
|
|
2031
|
+
(config) => config.locales.includes(config.defaultLocale),
|
|
2032
|
+
{
|
|
2033
|
+
message: "defaultLocale must be included in the locales array",
|
|
2034
|
+
path: ["defaultLocale"]
|
|
2035
|
+
}
|
|
2036
|
+
);
|
|
2037
|
+
|
|
2038
|
+
// src/ai/conversation.ts
|
|
2039
|
+
var CONFIDENCE_THRESHOLD = 0.8;
|
|
2040
|
+
var MAX_CLARIFICATION_ROUNDS = 3;
|
|
2041
|
+
var BLOCK_CONFIDENCE_THRESHOLD = 0.7;
|
|
2042
|
+
async function runAIConversation(options) {
|
|
2043
|
+
const { provider, registry, description, projectName, projectDir } = options;
|
|
2044
|
+
const intentResult = await analyzeDescription(provider, registry, description);
|
|
2045
|
+
if (!intentResult.ok) {
|
|
2046
|
+
return intentResult;
|
|
2047
|
+
}
|
|
2048
|
+
let intent = intentResult.value;
|
|
2049
|
+
if (options.askQuestion && needsClarification(intent)) {
|
|
2050
|
+
const clarifiedIntent = await runClarificationLoop(
|
|
2051
|
+
provider,
|
|
2052
|
+
registry,
|
|
2053
|
+
intent,
|
|
2054
|
+
description,
|
|
2055
|
+
options.askQuestion
|
|
2056
|
+
);
|
|
2057
|
+
if (!clarifiedIntent.ok) {
|
|
2058
|
+
return clarifiedIntent;
|
|
2059
|
+
}
|
|
2060
|
+
intent = clarifiedIntent.value;
|
|
2061
|
+
}
|
|
2062
|
+
const mutableConfig = intentToMutableConfig(intent);
|
|
2063
|
+
applyRules(intent, mutableConfig);
|
|
2064
|
+
filterBlocksToRegistry(mutableConfig, registry);
|
|
2065
|
+
addRecommendedBlocks(intent, mutableConfig, registry);
|
|
2066
|
+
const palette = selectPalette(intent, registry);
|
|
2067
|
+
const content = generateContent(intent, mutableConfig, registry);
|
|
2068
|
+
return assembleConfig({
|
|
2069
|
+
projectName,
|
|
2070
|
+
projectDir,
|
|
2071
|
+
mutableConfig,
|
|
2072
|
+
palette,
|
|
2073
|
+
content,
|
|
2074
|
+
intent
|
|
2075
|
+
});
|
|
2076
|
+
}
|
|
2077
|
+
async function analyzeDescription(provider, registry, description) {
|
|
2078
|
+
const systemPrompt = buildSystemPrompt(registry);
|
|
2079
|
+
try {
|
|
2080
|
+
const intent = await provider.generate({
|
|
2081
|
+
system: systemPrompt,
|
|
2082
|
+
prompt: buildAnalysisPrompt(description),
|
|
2083
|
+
schema: IntentSchema
|
|
2084
|
+
});
|
|
2085
|
+
return ok(intent);
|
|
2086
|
+
} catch (error) {
|
|
2087
|
+
const message = error instanceof Error ? error.message : "Unknown provider error";
|
|
2088
|
+
return err({
|
|
2089
|
+
kind: "ProviderError",
|
|
2090
|
+
message: `Analysis failed: ${message}`,
|
|
2091
|
+
provider: provider.name
|
|
2092
|
+
});
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
2095
|
+
function buildAnalysisPrompt(description) {
|
|
2096
|
+
return [
|
|
2097
|
+
"Analyze the following website description and produce a structured IntentSchema.",
|
|
2098
|
+
"",
|
|
2099
|
+
"## User Description",
|
|
2100
|
+
description,
|
|
2101
|
+
"",
|
|
2102
|
+
"## Instructions",
|
|
2103
|
+
"- Extract the site type, industry, and brand information",
|
|
2104
|
+
"- Identify which features are needed (auth, payments, blog, etc.)",
|
|
2105
|
+
"- Detect any language requirements for i18n",
|
|
2106
|
+
"- Recommend appropriate blocks from the catalog",
|
|
2107
|
+
"- Note any uncertainties that need clarification",
|
|
2108
|
+
"- Set overallConfidence based on how well you understand the requirements"
|
|
2109
|
+
].join("\n");
|
|
2110
|
+
}
|
|
2111
|
+
function needsClarification(intent) {
|
|
2112
|
+
return intent.overallConfidence < CONFIDENCE_THRESHOLD || intent.uncertainties.length > 0;
|
|
2113
|
+
}
|
|
2114
|
+
function extractQuestions(intent) {
|
|
2115
|
+
return intent.uncertainties.map((uncertainty, index) => ({
|
|
2116
|
+
id: `uncertainty-${index}`,
|
|
2117
|
+
topic: uncertainty.topic,
|
|
2118
|
+
question: uncertainty.question,
|
|
2119
|
+
defaultAnswer: uncertainty.defaultAssumption
|
|
2120
|
+
}));
|
|
2121
|
+
}
|
|
2122
|
+
async function runClarificationLoop(provider, registry, intent, originalDescription, askQuestion) {
|
|
2123
|
+
let currentIntent = intent;
|
|
2124
|
+
let round = 0;
|
|
2125
|
+
while (round < MAX_CLARIFICATION_ROUNDS && needsClarification(currentIntent)) {
|
|
2126
|
+
const questions = extractQuestions(currentIntent);
|
|
2127
|
+
if (questions.length === 0) break;
|
|
2128
|
+
const answers = [];
|
|
2129
|
+
for (const question of questions) {
|
|
2130
|
+
const answer = await askQuestion(question);
|
|
2131
|
+
answers.push(answer);
|
|
2132
|
+
}
|
|
2133
|
+
const clarificationContext = buildClarificationPrompt(
|
|
2134
|
+
originalDescription,
|
|
2135
|
+
currentIntent,
|
|
2136
|
+
answers
|
|
2137
|
+
);
|
|
2138
|
+
const reanalysisResult = await analyzeDescription(
|
|
2139
|
+
provider,
|
|
2140
|
+
registry,
|
|
2141
|
+
clarificationContext
|
|
2142
|
+
);
|
|
2143
|
+
if (!reanalysisResult.ok) {
|
|
2144
|
+
return reanalysisResult;
|
|
2145
|
+
}
|
|
2146
|
+
currentIntent = reanalysisResult.value;
|
|
2147
|
+
round++;
|
|
2148
|
+
}
|
|
2149
|
+
return ok(currentIntent);
|
|
2150
|
+
}
|
|
2151
|
+
function buildClarificationPrompt(originalDescription, intent, answers) {
|
|
2152
|
+
const answerLines = answers.map((answer) => `- ${answer.questionId}: ${answer.value}`).join("\n");
|
|
2153
|
+
return [
|
|
2154
|
+
originalDescription,
|
|
2155
|
+
"",
|
|
2156
|
+
"## Additional Context from Follow-up Questions",
|
|
2157
|
+
answerLines,
|
|
2158
|
+
"",
|
|
2159
|
+
`## Previous Analysis`,
|
|
2160
|
+
`Site type: ${intent.siteType}, Industry: ${intent.industry}`,
|
|
2161
|
+
`Brand: ${intent.brand.name} \u2014 ${intent.brand.description}`,
|
|
2162
|
+
"",
|
|
2163
|
+
"Please refine the analysis with this additional information.",
|
|
2164
|
+
"Increase overallConfidence if the answers resolve previous uncertainties."
|
|
2165
|
+
].join("\n");
|
|
2166
|
+
}
|
|
2167
|
+
function intentToMutableConfig(intent) {
|
|
2168
|
+
return createDefaultConfig({
|
|
2169
|
+
locales: intent.languages.length > 0 ? [...intent.languages] : ["en"],
|
|
2170
|
+
defaultLocale: intent.languages.length > 0 ? intent.languages[0] : "en",
|
|
2171
|
+
themeSwitcher: intent.wantsThemeSwitcher
|
|
2172
|
+
});
|
|
2173
|
+
}
|
|
2174
|
+
function filterBlocksToRegistry(config, registry) {
|
|
2175
|
+
const registryBlockNames = new Set(registry.blocks.map((b) => b.name));
|
|
2176
|
+
config.blocks = config.blocks.filter((b) => registryBlockNames.has(b.name));
|
|
2177
|
+
}
|
|
2178
|
+
function addRecommendedBlocks(intent, config, registry) {
|
|
2179
|
+
const registryBlockNames = new Set(registry.blocks.map((b) => b.name));
|
|
2180
|
+
for (const recommendation of intent.recommendedBlocks) {
|
|
2181
|
+
if (recommendation.confidence >= BLOCK_CONFIDENCE_THRESHOLD && registryBlockNames.has(recommendation.blockName) && !config.blocks.some((b) => b.name === recommendation.blockName)) {
|
|
2182
|
+
config.blocks.push({
|
|
2183
|
+
name: recommendation.blockName,
|
|
2184
|
+
variant: "default"
|
|
2185
|
+
});
|
|
2186
|
+
}
|
|
2187
|
+
}
|
|
2188
|
+
}
|
|
2189
|
+
function selectPalette(intent, registry) {
|
|
2190
|
+
const palettes = registry.palettes;
|
|
2191
|
+
if (intent.palettePreference === "prebuilt" && palettes.length > 0) {
|
|
2192
|
+
const matched = findBestPalette(intent, palettes);
|
|
2193
|
+
return {
|
|
2194
|
+
preset: matched.name,
|
|
2195
|
+
colors: { ...matched.colors }
|
|
2196
|
+
};
|
|
2197
|
+
}
|
|
2198
|
+
if (intent.palettePreference === "ai-generated") {
|
|
2199
|
+
const matched = findBestPalette(intent, palettes);
|
|
2200
|
+
return {
|
|
2201
|
+
colors: { ...matched.colors }
|
|
2202
|
+
};
|
|
2203
|
+
}
|
|
2204
|
+
if (palettes.length > 0) {
|
|
2205
|
+
const matched = findBestPalette(intent, palettes);
|
|
2206
|
+
return {
|
|
2207
|
+
preset: matched.name,
|
|
2208
|
+
colors: { ...matched.colors }
|
|
2209
|
+
};
|
|
2210
|
+
}
|
|
2211
|
+
return {
|
|
2212
|
+
colors: {
|
|
2213
|
+
primary: "#3b82f6",
|
|
2214
|
+
secondary: "#6366f1",
|
|
2215
|
+
accent: "#8b5cf6",
|
|
2216
|
+
background: "#ffffff",
|
|
2217
|
+
foreground: "#1a1a2e"
|
|
2218
|
+
}
|
|
2219
|
+
};
|
|
2220
|
+
}
|
|
2221
|
+
function findBestPalette(intent, palettes) {
|
|
2222
|
+
const modePreference = intent.prefersDarkMode ? "dark" : "light";
|
|
2223
|
+
const modeMatched = palettes.filter((p3) => p3.mode === modePreference);
|
|
2224
|
+
const candidates = modeMatched.length > 0 ? modeMatched : palettes;
|
|
2225
|
+
const industryLower = intent.industry.toLowerCase();
|
|
2226
|
+
const industryMatched = candidates.find(
|
|
2227
|
+
(p3) => p3.name.includes(industryLower) || p3.category.toLowerCase().includes(industryLower)
|
|
2228
|
+
);
|
|
2229
|
+
if (industryMatched) {
|
|
2230
|
+
return industryMatched;
|
|
2231
|
+
}
|
|
2232
|
+
return candidates[0];
|
|
2233
|
+
}
|
|
2234
|
+
function generateContent(intent, config, registry) {
|
|
2235
|
+
const selectedBlockNames = new Set(config.blocks.map((b) => b.name));
|
|
2236
|
+
const blocksWithContent = registry.blocks.filter(
|
|
2237
|
+
(b) => selectedBlockNames.has(b.name) && b.ai?.contentSlots && Object.keys(b.ai.contentSlots).length > 0
|
|
2238
|
+
);
|
|
2239
|
+
if (blocksWithContent.length === 0) {
|
|
2240
|
+
return void 0;
|
|
2241
|
+
}
|
|
2242
|
+
const content = {};
|
|
2243
|
+
const locales = config.locales;
|
|
2244
|
+
for (const block of blocksWithContent) {
|
|
2245
|
+
const slots = block.ai?.contentSlots;
|
|
2246
|
+
if (!slots) continue;
|
|
2247
|
+
const blockContent = {};
|
|
2248
|
+
for (const locale of locales) {
|
|
2249
|
+
const localeContent = {};
|
|
2250
|
+
for (const [slotName, slot] of Object.entries(slots)) {
|
|
2251
|
+
localeContent[slotName] = generateSlotContent(
|
|
2252
|
+
slotName,
|
|
2253
|
+
slot,
|
|
2254
|
+
intent,
|
|
2255
|
+
locale
|
|
2256
|
+
);
|
|
2257
|
+
}
|
|
2258
|
+
blockContent[locale] = localeContent;
|
|
2259
|
+
}
|
|
2260
|
+
content[block.name] = blockContent;
|
|
2261
|
+
}
|
|
2262
|
+
return Object.keys(content).length > 0 ? content : void 0;
|
|
2263
|
+
}
|
|
2264
|
+
function generateSlotContent(slotName, slot, intent, locale) {
|
|
2265
|
+
const brandName = intent.brand.name;
|
|
2266
|
+
const localeSuffix = locale !== "en" ? ` (${locale})` : "";
|
|
2267
|
+
switch (slot.type) {
|
|
2268
|
+
case "string": {
|
|
2269
|
+
if (slotName.includes("headline") || slotName.includes("heading") || slotName.includes("title")) {
|
|
2270
|
+
return `${brandName} \u2014 ${intent.brand.tagline ?? intent.brand.description}${localeSuffix}`;
|
|
2271
|
+
}
|
|
2272
|
+
if (slotName.includes("button") || slotName.includes("cta")) {
|
|
2273
|
+
return `Get Started${localeSuffix}`;
|
|
2274
|
+
}
|
|
2275
|
+
if (slotName.includes("copyright")) {
|
|
2276
|
+
return `\xA9 ${(/* @__PURE__ */ new Date()).getFullYear()} ${brandName}. All rights reserved.${localeSuffix}`;
|
|
2277
|
+
}
|
|
2278
|
+
return `${slot.description ?? slotName}${localeSuffix}`;
|
|
2279
|
+
}
|
|
2280
|
+
case "array":
|
|
2281
|
+
return [];
|
|
2282
|
+
case "number":
|
|
2283
|
+
return 0;
|
|
2284
|
+
case "boolean":
|
|
2285
|
+
return false;
|
|
2286
|
+
case "object":
|
|
2287
|
+
return {};
|
|
2288
|
+
default:
|
|
2289
|
+
return slot.example ?? null;
|
|
2290
|
+
}
|
|
2291
|
+
}
|
|
2292
|
+
function assembleConfig(input) {
|
|
2293
|
+
const raw = {
|
|
2294
|
+
projectName: input.projectName,
|
|
2295
|
+
projectDir: input.projectDir,
|
|
2296
|
+
renderMode: input.mutableConfig.renderMode,
|
|
2297
|
+
deployTarget: input.mutableConfig.deployTarget,
|
|
2298
|
+
database: input.mutableConfig.database,
|
|
2299
|
+
cssEngine: "tailwind",
|
|
2300
|
+
packageManager: "pnpm",
|
|
2301
|
+
blocks: input.mutableConfig.blocks.map((b) => ({
|
|
2302
|
+
name: b.name,
|
|
2303
|
+
variant: b.variant
|
|
2304
|
+
})),
|
|
2305
|
+
locales: input.mutableConfig.locales,
|
|
2306
|
+
defaultLocale: input.mutableConfig.defaultLocale,
|
|
2307
|
+
palette: {
|
|
2308
|
+
preset: input.palette.preset,
|
|
2309
|
+
colors: { ...input.palette.colors }
|
|
2310
|
+
},
|
|
2311
|
+
themeSwitcher: input.mutableConfig.themeSwitcher,
|
|
2312
|
+
content: input.content,
|
|
2313
|
+
createdWith: "ai"
|
|
2314
|
+
};
|
|
2315
|
+
const parsed = ResolvedConfigSchema.safeParse(raw);
|
|
2316
|
+
if (!parsed.success) {
|
|
2317
|
+
return err({
|
|
2318
|
+
kind: "ValidationError",
|
|
2319
|
+
message: `Config validation failed: ${parsed.error.message}`,
|
|
2320
|
+
provider: "conversation"
|
|
2321
|
+
});
|
|
2322
|
+
}
|
|
2323
|
+
return ok(parsed.data);
|
|
2324
|
+
}
|
|
2325
|
+
|
|
2326
|
+
// src/ai/providers/openai-provider.ts
|
|
2327
|
+
function createOpenAIProvider(apiKey) {
|
|
2328
|
+
const key = apiKey ?? process.env["OPENAI_API_KEY"];
|
|
2329
|
+
return {
|
|
2330
|
+
name: "openai",
|
|
2331
|
+
async generate(prompt, systemPrompt) {
|
|
2332
|
+
if (!key) {
|
|
2333
|
+
return err({
|
|
2334
|
+
kind: "ProviderError",
|
|
2335
|
+
message: "OPENAI_API_KEY not set. Set it in your environment or pass it explicitly.",
|
|
2336
|
+
prompt
|
|
2337
|
+
});
|
|
2338
|
+
}
|
|
2339
|
+
try {
|
|
2340
|
+
const { generateObject } = await import("ai");
|
|
2341
|
+
const { openai } = await import("@ai-sdk/openai");
|
|
2342
|
+
const result = await generateObject({
|
|
2343
|
+
model: openai("gpt-4o-mini", { structuredOutputs: true }),
|
|
2344
|
+
schema: IntentSchema2,
|
|
2345
|
+
system: systemPrompt ?? "You are the Fornix AI assistant. Analyze the user's website description and produce a structured Intent object.",
|
|
2346
|
+
prompt,
|
|
2347
|
+
maxTokens: 2e3
|
|
2348
|
+
});
|
|
2349
|
+
return ok(result.object);
|
|
2350
|
+
} catch (error) {
|
|
2351
|
+
const message = error instanceof Error ? error.message : "Unknown OpenAI error";
|
|
2352
|
+
return err({
|
|
2353
|
+
kind: "ProviderError",
|
|
2354
|
+
message: `OpenAI generation failed: ${message}`,
|
|
2355
|
+
prompt
|
|
2356
|
+
});
|
|
2357
|
+
}
|
|
2358
|
+
}
|
|
2359
|
+
};
|
|
2360
|
+
}
|
|
2361
|
+
|
|
2362
|
+
// src/ai/providers/ollama-provider.ts
|
|
2363
|
+
var DEFAULT_OLLAMA_URL = "http://localhost:11434";
|
|
2364
|
+
var DEFAULT_MODEL = "llama3.1";
|
|
2365
|
+
function createOllamaProvider(opts = {}) {
|
|
2366
|
+
const baseUrl = opts.baseUrl ?? DEFAULT_OLLAMA_URL;
|
|
2367
|
+
const model = opts.model ?? DEFAULT_MODEL;
|
|
2368
|
+
return {
|
|
2369
|
+
name: "ollama",
|
|
2370
|
+
async generate(prompt, systemPrompt) {
|
|
2371
|
+
try {
|
|
2372
|
+
const response = await fetch(`${baseUrl}/api/chat`, {
|
|
2373
|
+
method: "POST",
|
|
2374
|
+
headers: { "Content-Type": "application/json" },
|
|
2375
|
+
body: JSON.stringify({
|
|
2376
|
+
model,
|
|
2377
|
+
stream: false,
|
|
2378
|
+
format: "json",
|
|
2379
|
+
messages: [
|
|
2380
|
+
{
|
|
2381
|
+
role: "system",
|
|
2382
|
+
content: (systemPrompt ?? "You are the Fornix AI assistant.") + "\n\nYou MUST respond with a valid JSON object matching the IntentSchema. Do NOT include any text outside the JSON."
|
|
2383
|
+
},
|
|
2384
|
+
{ role: "user", content: prompt }
|
|
2385
|
+
]
|
|
2386
|
+
})
|
|
2387
|
+
});
|
|
2388
|
+
if (!response.ok) {
|
|
2389
|
+
return err({
|
|
2390
|
+
kind: "ProviderError",
|
|
2391
|
+
message: `Ollama returned status ${response.status}: ${await response.text()}`,
|
|
2392
|
+
prompt
|
|
2393
|
+
});
|
|
2394
|
+
}
|
|
2395
|
+
const data = await response.json();
|
|
2396
|
+
const content = data.message?.content;
|
|
2397
|
+
if (!content) {
|
|
2398
|
+
return err({
|
|
2399
|
+
kind: "ProviderError",
|
|
2400
|
+
message: "Ollama returned empty response",
|
|
2401
|
+
prompt
|
|
2402
|
+
});
|
|
2403
|
+
}
|
|
2404
|
+
const parsed = JSON.parse(content);
|
|
2405
|
+
const validated = IntentSchema2.parse(parsed);
|
|
2406
|
+
return ok(validated);
|
|
2407
|
+
} catch (error) {
|
|
2408
|
+
const message = error instanceof Error ? error.message : "Unknown Ollama error";
|
|
2409
|
+
return err({
|
|
2410
|
+
kind: "ProviderError",
|
|
2411
|
+
message: `Ollama generation failed: ${message}`,
|
|
2412
|
+
prompt
|
|
2413
|
+
});
|
|
2414
|
+
}
|
|
2415
|
+
}
|
|
2416
|
+
};
|
|
2417
|
+
}
|
|
2418
|
+
async function isOllamaRunning(baseUrl = DEFAULT_OLLAMA_URL) {
|
|
2419
|
+
try {
|
|
2420
|
+
const controller = new AbortController();
|
|
2421
|
+
const timeout = setTimeout(() => controller.abort(), 1e3);
|
|
2422
|
+
const response = await fetch(baseUrl, { signal: controller.signal });
|
|
2423
|
+
clearTimeout(timeout);
|
|
2424
|
+
return response.ok;
|
|
2425
|
+
} catch {
|
|
2426
|
+
return false;
|
|
2427
|
+
}
|
|
2428
|
+
}
|
|
2429
|
+
|
|
2430
|
+
// src/ai/providers/cloudflare-provider.ts
|
|
2431
|
+
var DEFAULT_MODEL2 = "@cf/meta/llama-3.1-8b-instruct";
|
|
2432
|
+
function createCloudflareProvider(opts = {}) {
|
|
2433
|
+
const accountId = opts.accountId ?? process.env["CLOUDFLARE_ACCOUNT_ID"];
|
|
2434
|
+
const apiToken = opts.apiToken ?? process.env["CLOUDFLARE_API_TOKEN"];
|
|
2435
|
+
const model = opts.model ?? DEFAULT_MODEL2;
|
|
2436
|
+
return {
|
|
2437
|
+
name: "cloudflare",
|
|
2438
|
+
async generate(prompt, systemPrompt) {
|
|
2439
|
+
if (!accountId || !apiToken) {
|
|
2440
|
+
return err({
|
|
2441
|
+
kind: "ProviderError",
|
|
2442
|
+
message: "CLOUDFLARE_ACCOUNT_ID and CLOUDFLARE_API_TOKEN must be set.",
|
|
2443
|
+
prompt
|
|
2444
|
+
});
|
|
2445
|
+
}
|
|
2446
|
+
try {
|
|
2447
|
+
const url = `https://api.cloudflare.com/client/v4/accounts/${accountId}/ai/run/${model}`;
|
|
2448
|
+
const response = await fetch(url, {
|
|
2449
|
+
method: "POST",
|
|
2450
|
+
headers: {
|
|
2451
|
+
Authorization: `Bearer ${apiToken}`,
|
|
2452
|
+
"Content-Type": "application/json"
|
|
2453
|
+
},
|
|
2454
|
+
body: JSON.stringify({
|
|
2455
|
+
messages: [
|
|
2456
|
+
{
|
|
2457
|
+
role: "system",
|
|
2458
|
+
content: (systemPrompt ?? "You are the Fornix AI assistant.") + "\n\nRespond with a valid JSON object matching the IntentSchema. No text outside JSON."
|
|
2459
|
+
},
|
|
2460
|
+
{ role: "user", content: prompt }
|
|
2461
|
+
]
|
|
2462
|
+
})
|
|
2463
|
+
});
|
|
2464
|
+
if (!response.ok) {
|
|
2465
|
+
return err({
|
|
2466
|
+
kind: "ProviderError",
|
|
2467
|
+
message: `Cloudflare AI returned status ${response.status}: ${await response.text()}`,
|
|
2468
|
+
prompt
|
|
2469
|
+
});
|
|
2470
|
+
}
|
|
2471
|
+
const data = await response.json();
|
|
2472
|
+
const content = data.result?.response;
|
|
2473
|
+
if (!content) {
|
|
2474
|
+
return err({
|
|
2475
|
+
kind: "ProviderError",
|
|
2476
|
+
message: "Cloudflare AI returned empty response",
|
|
2477
|
+
prompt
|
|
2478
|
+
});
|
|
2479
|
+
}
|
|
2480
|
+
const parsed = JSON.parse(content);
|
|
2481
|
+
const validated = IntentSchema2.parse(parsed);
|
|
2482
|
+
return ok(validated);
|
|
2483
|
+
} catch (error) {
|
|
2484
|
+
const message = error instanceof Error ? error.message : "Unknown Cloudflare AI error";
|
|
2485
|
+
return err({
|
|
2486
|
+
kind: "ProviderError",
|
|
2487
|
+
message: `Cloudflare AI generation failed: ${message}`,
|
|
2488
|
+
prompt
|
|
2489
|
+
});
|
|
2490
|
+
}
|
|
2491
|
+
}
|
|
2492
|
+
};
|
|
2493
|
+
}
|
|
2494
|
+
|
|
2495
|
+
// src/ai/providers/mock-provider.ts
|
|
2496
|
+
import { readFileSync as readFileSync2, existsSync } from "fs";
|
|
2497
|
+
import { join as join3, dirname as dirname2 } from "path";
|
|
2498
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2499
|
+
var FIXTURE_ENTRIES = [
|
|
2500
|
+
{
|
|
2501
|
+
keywords: ["fintech", "finance", "banking", "financial"],
|
|
2502
|
+
filename: "fintech-landing.json"
|
|
2503
|
+
},
|
|
2504
|
+
{
|
|
2505
|
+
keywords: ["blog", "personal", "travel", "lifestyle", "writing"],
|
|
2506
|
+
filename: "personal-blog.json"
|
|
2507
|
+
},
|
|
2508
|
+
{
|
|
2509
|
+
keywords: ["saas", "dashboard", "project management", "productivity", "taskflow"],
|
|
2510
|
+
filename: "saas-dashboard.json"
|
|
2511
|
+
},
|
|
2512
|
+
{
|
|
2513
|
+
keywords: ["agency", "multilingual", "design agency", "boutique", "international"],
|
|
2514
|
+
filename: "multilingual-agency.json"
|
|
2515
|
+
},
|
|
2516
|
+
{
|
|
2517
|
+
keywords: ["portfolio", "freelance", "developer", "personal site"],
|
|
2518
|
+
filename: "portfolio.json"
|
|
2519
|
+
}
|
|
2520
|
+
];
|
|
2521
|
+
function createMockProvider() {
|
|
2522
|
+
return {
|
|
2523
|
+
name: "mock",
|
|
2524
|
+
async generate(prompt) {
|
|
2525
|
+
const lowerPrompt = prompt.toLowerCase();
|
|
2526
|
+
const match = FIXTURE_ENTRIES.find(
|
|
2527
|
+
(entry) => entry.keywords.some((kw) => lowerPrompt.includes(kw))
|
|
2528
|
+
);
|
|
2529
|
+
if (!match) {
|
|
2530
|
+
return err({
|
|
2531
|
+
kind: "ProviderError",
|
|
2532
|
+
message: `No fixture matched for prompt. Keywords tried: ${FIXTURE_ENTRIES.flatMap((e) => e.keywords).join(", ")}`,
|
|
2533
|
+
prompt
|
|
2534
|
+
});
|
|
2535
|
+
}
|
|
2536
|
+
const fixtureDir = resolveFixtureDir();
|
|
2537
|
+
const filePath = join3(fixtureDir, match.filename);
|
|
2538
|
+
try {
|
|
2539
|
+
const raw = readFileSync2(filePath, "utf-8");
|
|
2540
|
+
const parsed = JSON.parse(raw);
|
|
2541
|
+
const validated = IntentSchema2.parse(parsed);
|
|
2542
|
+
return ok(validated);
|
|
2543
|
+
} catch (error) {
|
|
2544
|
+
const message = error instanceof Error ? error.message : "Unknown parse error";
|
|
2545
|
+
return err({
|
|
2546
|
+
kind: "ProviderError",
|
|
2547
|
+
message: `Failed to load fixture '${match.filename}': ${message}`,
|
|
2548
|
+
prompt
|
|
2549
|
+
});
|
|
2550
|
+
}
|
|
2551
|
+
}
|
|
2552
|
+
};
|
|
2553
|
+
}
|
|
2554
|
+
function resolveFixtureDir() {
|
|
2555
|
+
const thisDir = dirname2(fileURLToPath2(import.meta.url));
|
|
2556
|
+
const fromSource = join3(thisDir, "..", "..", "..", "tests", "fixtures", "ai-responses");
|
|
2557
|
+
if (existsSync(fromSource)) return fromSource;
|
|
2558
|
+
const fromDist = join3(thisDir, "..", "tests", "fixtures", "ai-responses");
|
|
2559
|
+
if (existsSync(fromDist)) return fromDist;
|
|
2560
|
+
return fromSource;
|
|
2561
|
+
}
|
|
2562
|
+
|
|
2563
|
+
// src/ai/resolve-provider.ts
|
|
2564
|
+
async function resolveProvider(opts = {}) {
|
|
2565
|
+
const env = opts.env ?? process.env;
|
|
2566
|
+
if (opts.provider) {
|
|
2567
|
+
return createProviderByName(opts.provider, env);
|
|
2568
|
+
}
|
|
2569
|
+
if (!opts.skipOllamaDetect) {
|
|
2570
|
+
const ollamaRunning = await isOllamaRunning();
|
|
2571
|
+
if (ollamaRunning) {
|
|
2572
|
+
return createOllamaProvider();
|
|
2573
|
+
}
|
|
2574
|
+
}
|
|
2575
|
+
if (env["OPENAI_API_KEY"]) {
|
|
2576
|
+
return createOpenAIProvider(env["OPENAI_API_KEY"]);
|
|
2577
|
+
}
|
|
2578
|
+
if (env["CLOUDFLARE_ACCOUNT_ID"] && env["CLOUDFLARE_API_TOKEN"]) {
|
|
2579
|
+
return createCloudflareProvider({
|
|
2580
|
+
accountId: env["CLOUDFLARE_ACCOUNT_ID"],
|
|
2581
|
+
apiToken: env["CLOUDFLARE_API_TOKEN"]
|
|
2582
|
+
});
|
|
2583
|
+
}
|
|
2584
|
+
return null;
|
|
2585
|
+
}
|
|
2586
|
+
function createProviderByName(name, env) {
|
|
2587
|
+
switch (name) {
|
|
2588
|
+
case "openai":
|
|
2589
|
+
return createOpenAIProvider(env["OPENAI_API_KEY"]);
|
|
2590
|
+
case "ollama":
|
|
2591
|
+
return createOllamaProvider();
|
|
2592
|
+
case "cloudflare":
|
|
2593
|
+
return createCloudflareProvider({
|
|
2594
|
+
accountId: env["CLOUDFLARE_ACCOUNT_ID"],
|
|
2595
|
+
apiToken: env["CLOUDFLARE_API_TOKEN"]
|
|
2596
|
+
});
|
|
2597
|
+
case "mock":
|
|
2598
|
+
return createMockProvider();
|
|
2599
|
+
}
|
|
2600
|
+
}
|
|
2601
|
+
|
|
2602
|
+
// src/cli/recipes.ts
|
|
2603
|
+
var RECIPES = {
|
|
2604
|
+
saas: {
|
|
2605
|
+
renderMode: "server",
|
|
2606
|
+
deployTarget: "cloudflare",
|
|
2607
|
+
database: "d1",
|
|
2608
|
+
cssEngine: "tailwind",
|
|
2609
|
+
packageManager: "pnpm",
|
|
2610
|
+
blocks: [
|
|
2611
|
+
{ name: "hero-video", variant: "default" },
|
|
2612
|
+
{ name: "features-bento", variant: "default" },
|
|
2613
|
+
{ name: "pricing-table", variant: "default" },
|
|
2614
|
+
{ name: "faq-accordion", variant: "default" },
|
|
2615
|
+
{ name: "footer-rich", variant: "default" },
|
|
2616
|
+
{ name: "auth-better-auth", variant: "default" },
|
|
2617
|
+
{ name: "db-d1", variant: "default" }
|
|
2618
|
+
],
|
|
2619
|
+
locales: ["en"],
|
|
2620
|
+
defaultLocale: "en",
|
|
2621
|
+
palette: {
|
|
2622
|
+
preset: "obsidian",
|
|
2623
|
+
colors: {
|
|
2624
|
+
primary: "#818cf8",
|
|
2625
|
+
secondary: "#c084fc",
|
|
2626
|
+
accent: "#38bdf8",
|
|
2627
|
+
background: "#0f172a",
|
|
2628
|
+
foreground: "#f8fafc"
|
|
2629
|
+
}
|
|
2630
|
+
},
|
|
2631
|
+
themeSwitcher: false
|
|
2632
|
+
},
|
|
2633
|
+
agency: {
|
|
2634
|
+
renderMode: "static",
|
|
2635
|
+
deployTarget: "cloudflare",
|
|
2636
|
+
database: "none",
|
|
2637
|
+
cssEngine: "tailwind",
|
|
2638
|
+
packageManager: "pnpm",
|
|
2639
|
+
blocks: [
|
|
2640
|
+
{ name: "hero-gradient", variant: "default" },
|
|
2641
|
+
{ name: "features-grid", variant: "default" },
|
|
2642
|
+
{ name: "testimonials-carousel", variant: "default" },
|
|
2643
|
+
{ name: "contact-form", variant: "default" },
|
|
2644
|
+
{ name: "footer-rich", variant: "default" }
|
|
2645
|
+
],
|
|
2646
|
+
locales: ["en"],
|
|
2647
|
+
defaultLocale: "en",
|
|
2648
|
+
palette: {
|
|
2649
|
+
preset: "ocean-breeze",
|
|
2650
|
+
colors: {
|
|
2651
|
+
primary: "#0ea5e9",
|
|
2652
|
+
secondary: "#38bdf8",
|
|
2653
|
+
accent: "#7dd3fc",
|
|
2654
|
+
background: "#0c4a6e",
|
|
2655
|
+
foreground: "#f0f9ff"
|
|
2656
|
+
}
|
|
2657
|
+
},
|
|
2658
|
+
themeSwitcher: true
|
|
2659
|
+
},
|
|
2660
|
+
docs: {
|
|
2661
|
+
renderMode: "static",
|
|
2662
|
+
deployTarget: "cloudflare",
|
|
2663
|
+
database: "none",
|
|
2664
|
+
cssEngine: "tailwind",
|
|
2665
|
+
packageManager: "pnpm",
|
|
2666
|
+
blocks: [
|
|
2667
|
+
{ name: "header-sticky", variant: "default" },
|
|
2668
|
+
{ name: "docs-collection", variant: "default" },
|
|
2669
|
+
{ name: "footer-minimal", variant: "default" }
|
|
2670
|
+
],
|
|
2671
|
+
locales: ["en"],
|
|
2672
|
+
defaultLocale: "en",
|
|
2673
|
+
palette: {
|
|
2674
|
+
preset: "snow",
|
|
2675
|
+
colors: {
|
|
2676
|
+
primary: "#3b82f6",
|
|
2677
|
+
secondary: "#60a5fa",
|
|
2678
|
+
accent: "#93c5fd",
|
|
2679
|
+
background: "#ffffff",
|
|
2680
|
+
foreground: "#0f172a"
|
|
2681
|
+
}
|
|
2682
|
+
},
|
|
2683
|
+
themeSwitcher: false
|
|
2684
|
+
},
|
|
2685
|
+
blog: {
|
|
2686
|
+
renderMode: "static",
|
|
2687
|
+
deployTarget: "cloudflare",
|
|
2688
|
+
database: "none",
|
|
2689
|
+
cssEngine: "tailwind",
|
|
2690
|
+
packageManager: "pnpm",
|
|
2691
|
+
blocks: [
|
|
2692
|
+
{ name: "header-transparent", variant: "default" },
|
|
2693
|
+
{ name: "hero-split", variant: "default" },
|
|
2694
|
+
{ name: "blog-mdx", variant: "default" },
|
|
2695
|
+
{ name: "cta-newsletter", variant: "default" },
|
|
2696
|
+
{ name: "footer-rich", variant: "default" }
|
|
2697
|
+
],
|
|
2698
|
+
locales: ["en"],
|
|
2699
|
+
defaultLocale: "en",
|
|
2700
|
+
palette: {
|
|
2701
|
+
preset: "corporate-blue",
|
|
2702
|
+
colors: {
|
|
2703
|
+
primary: "#1d4ed8",
|
|
2704
|
+
secondary: "#2563eb",
|
|
2705
|
+
accent: "#3b82f6",
|
|
2706
|
+
background: "#f8fafc",
|
|
2707
|
+
foreground: "#0f172a"
|
|
2708
|
+
}
|
|
2709
|
+
},
|
|
2710
|
+
themeSwitcher: false
|
|
2711
|
+
},
|
|
2712
|
+
portfolio: {
|
|
2713
|
+
renderMode: "static",
|
|
2714
|
+
deployTarget: "cloudflare",
|
|
2715
|
+
database: "none",
|
|
2716
|
+
cssEngine: "tailwind",
|
|
2717
|
+
packageManager: "pnpm",
|
|
2718
|
+
blocks: [
|
|
2719
|
+
{ name: "hero-gradient", variant: "default" },
|
|
2720
|
+
{ name: "features-grid", variant: "default" },
|
|
2721
|
+
{ name: "contact-form", variant: "default" },
|
|
2722
|
+
{ name: "footer-minimal", variant: "default" }
|
|
2723
|
+
],
|
|
2724
|
+
locales: ["en"],
|
|
2725
|
+
defaultLocale: "en",
|
|
2726
|
+
palette: {
|
|
2727
|
+
preset: "midnight",
|
|
2728
|
+
colors: {
|
|
2729
|
+
primary: "#6366f1",
|
|
2730
|
+
secondary: "#818cf8",
|
|
2731
|
+
accent: "#c084fc",
|
|
2732
|
+
background: "#0f172a",
|
|
2733
|
+
foreground: "#f8fafc"
|
|
2734
|
+
}
|
|
2735
|
+
},
|
|
2736
|
+
themeSwitcher: false
|
|
2737
|
+
}
|
|
2738
|
+
};
|
|
2739
|
+
|
|
2740
|
+
// src/utils/project-name.ts
|
|
2741
|
+
var VALID_NAME_RE = /^[a-z][a-z0-9-]*$/;
|
|
2742
|
+
function validateProjectName(name) {
|
|
2743
|
+
if (!name || name.length === 0) {
|
|
2744
|
+
return err("Project name cannot be empty.");
|
|
2745
|
+
}
|
|
2746
|
+
if (!VALID_NAME_RE.test(name)) {
|
|
2747
|
+
const suggestion = suggestProjectName(name);
|
|
2748
|
+
return err(
|
|
2749
|
+
`"${name}" is not a valid project name. Try: "${suggestion}" instead.`
|
|
2750
|
+
);
|
|
2751
|
+
}
|
|
2752
|
+
return ok(name);
|
|
2753
|
+
}
|
|
2754
|
+
function suggestProjectName(invalid) {
|
|
2755
|
+
let name = invalid.toLowerCase().replace(/[_\s]+/g, "-").replace(/[^a-z0-9-]/g, "").replace(/-{2,}/g, "-").replace(/^[-.]/, "").replace(/-$/, "");
|
|
2756
|
+
if (!name || name.length === 0 || !/^[a-z]/.test(name)) {
|
|
2757
|
+
return "my-project";
|
|
2758
|
+
}
|
|
2759
|
+
return name;
|
|
2760
|
+
}
|
|
2761
|
+
|
|
2762
|
+
// src/cli/commands/create.ts
|
|
2763
|
+
var DEFAULT_COLORS = {
|
|
2764
|
+
primary: "#6366f1",
|
|
2765
|
+
secondary: "#818cf8",
|
|
2766
|
+
accent: "#c084fc",
|
|
2767
|
+
background: "#0f172a",
|
|
2768
|
+
foreground: "#f8fafc"
|
|
2769
|
+
};
|
|
2770
|
+
var DEFAULT_AI_DESCRIPTION = "Build a fintech landing page with user authentication and a modern dark theme";
|
|
2771
|
+
var VALID_PROVIDER_NAMES = ["openai", "ollama", "cloudflare", "mock"];
|
|
2772
|
+
var createCommand = defineCommand({
|
|
2773
|
+
meta: {
|
|
2774
|
+
name: "create",
|
|
2775
|
+
description: "Scaffold a new Fornix project"
|
|
2776
|
+
},
|
|
2777
|
+
args: {
|
|
2778
|
+
dir: {
|
|
2779
|
+
type: "positional",
|
|
2780
|
+
description: "Project directory (defaults to current directory)",
|
|
2781
|
+
required: false
|
|
2782
|
+
},
|
|
2783
|
+
ai: {
|
|
2784
|
+
type: "boolean",
|
|
2785
|
+
description: "AI-assisted mode (default)",
|
|
2786
|
+
default: true
|
|
2787
|
+
},
|
|
2788
|
+
manual: {
|
|
2789
|
+
type: "boolean",
|
|
2790
|
+
description: "Traditional interactive prompts",
|
|
2791
|
+
default: false
|
|
2792
|
+
},
|
|
2793
|
+
yes: {
|
|
2794
|
+
type: "boolean",
|
|
2795
|
+
alias: "y",
|
|
2796
|
+
description: "Accept defaults, non-interactive",
|
|
2797
|
+
default: false
|
|
2798
|
+
},
|
|
2799
|
+
description: {
|
|
2800
|
+
type: "string",
|
|
2801
|
+
description: "Project description for AI mode (used when --yes skips the prompt)"
|
|
2802
|
+
},
|
|
2803
|
+
render: {
|
|
2804
|
+
type: "string",
|
|
2805
|
+
description: "Set render mode: static, hybrid, server"
|
|
2806
|
+
},
|
|
2807
|
+
deploy: {
|
|
2808
|
+
type: "string",
|
|
2809
|
+
description: "Set deploy target: cloudflare, vercel, netlify, static"
|
|
2810
|
+
},
|
|
2811
|
+
blocks: {
|
|
2812
|
+
type: "string",
|
|
2813
|
+
description: "Comma-separated block names"
|
|
2814
|
+
},
|
|
2815
|
+
database: {
|
|
2816
|
+
type: "string",
|
|
2817
|
+
description: "Set database: none, d1, turso, astro-db, postgres"
|
|
2818
|
+
},
|
|
2819
|
+
css: {
|
|
2820
|
+
type: "string",
|
|
2821
|
+
description: "Set CSS engine: tailwind (default) or vanilla"
|
|
2822
|
+
},
|
|
2823
|
+
locales: {
|
|
2824
|
+
type: "string",
|
|
2825
|
+
description: "Comma-separated locale codes (e.g. en,es,ar)"
|
|
2826
|
+
},
|
|
2827
|
+
palette: {
|
|
2828
|
+
type: "string",
|
|
2829
|
+
description: "Use a pre-built palette by name"
|
|
2830
|
+
},
|
|
2831
|
+
"theme-switcher": {
|
|
2832
|
+
type: "boolean",
|
|
2833
|
+
description: "Include the theme switcher for runtime palette swapping",
|
|
2834
|
+
default: false
|
|
2835
|
+
},
|
|
2836
|
+
"dry-run": {
|
|
2837
|
+
type: "boolean",
|
|
2838
|
+
description: "Show what would be generated without writing",
|
|
2839
|
+
default: false
|
|
2840
|
+
},
|
|
2841
|
+
install: {
|
|
2842
|
+
type: "boolean",
|
|
2843
|
+
description: "Install dependencies after scaffold (use --no-install to skip)",
|
|
2844
|
+
default: true
|
|
2845
|
+
},
|
|
2846
|
+
git: {
|
|
2847
|
+
type: "boolean",
|
|
2848
|
+
description: "Initialize git repo (use --no-git to skip)",
|
|
2849
|
+
default: true
|
|
2850
|
+
},
|
|
2851
|
+
provider: {
|
|
2852
|
+
type: "string",
|
|
2853
|
+
description: "Force a specific AI provider"
|
|
2854
|
+
},
|
|
2855
|
+
recipe: {
|
|
2856
|
+
type: "string",
|
|
2857
|
+
description: "Use a preset recipe (saas, agency, docs)"
|
|
2858
|
+
},
|
|
2859
|
+
verbose: {
|
|
2860
|
+
type: "boolean",
|
|
2861
|
+
description: "Detailed output",
|
|
2862
|
+
default: false
|
|
2863
|
+
}
|
|
2864
|
+
},
|
|
2865
|
+
async run({ args }) {
|
|
2866
|
+
const allPalettes = loadAllPalettes();
|
|
2867
|
+
const hasExplicitFlags = !!(args.render || args.deploy || args.blocks || args.database || args.css || args.locales || args.palette || args.recipe);
|
|
2868
|
+
if (args.manual && !args.yes) {
|
|
2869
|
+
const defaultProjectName = args.dir ? basename2(resolve(args.dir)) : "my-project";
|
|
2870
|
+
const config = await runManualFlow({
|
|
2871
|
+
defaultProjectName,
|
|
2872
|
+
manifests: FIXTURE_MANIFESTS,
|
|
2873
|
+
allPalettes
|
|
2874
|
+
});
|
|
2875
|
+
if (!config) {
|
|
2876
|
+
process.exitCode = 0;
|
|
2877
|
+
return;
|
|
2878
|
+
}
|
|
2879
|
+
const projectDir = args.dir ? resolve(args.dir) : resolve(config.projectDir);
|
|
2880
|
+
const finalConfig = { ...config, projectDir };
|
|
2881
|
+
return runScaffold(finalConfig, allPalettes, args["dry-run"] ?? false, args.verbose ?? false, !(args.install ?? true), !(args.git ?? true));
|
|
2882
|
+
}
|
|
2883
|
+
if (args.manual || hasExplicitFlags) {
|
|
2884
|
+
return runFlagDrivenMode(args, allPalettes);
|
|
2885
|
+
}
|
|
2886
|
+
return runAIMode(args, allPalettes);
|
|
2887
|
+
}
|
|
2888
|
+
});
|
|
2889
|
+
async function runAIMode(args, allPalettes) {
|
|
2890
|
+
const providerName = parseProviderName(args.provider);
|
|
2891
|
+
if (args.provider && !providerName) {
|
|
2892
|
+
console.error(pc3.red(`\u2716 Unknown provider: ${String(args.provider)}`));
|
|
2893
|
+
console.error(pc3.dim(` Available: ${VALID_PROVIDER_NAMES.join(", ")}`));
|
|
2894
|
+
process.exitCode = 1;
|
|
2895
|
+
return;
|
|
2896
|
+
}
|
|
2897
|
+
const legacyProvider = await resolveProvider({
|
|
2898
|
+
provider: providerName,
|
|
2899
|
+
skipOllamaDetect: false
|
|
2900
|
+
});
|
|
2901
|
+
if (!legacyProvider) {
|
|
2902
|
+
showNoProviderGuide();
|
|
2903
|
+
process.exitCode = 1;
|
|
2904
|
+
return;
|
|
2905
|
+
}
|
|
2906
|
+
const provider = adaptProvider(legacyProvider);
|
|
2907
|
+
const description = await getDescription(args);
|
|
2908
|
+
if (!description) {
|
|
2909
|
+
process.exitCode = 0;
|
|
2910
|
+
return;
|
|
2911
|
+
}
|
|
2912
|
+
const registry = {
|
|
2913
|
+
blocks: Object.values(FIXTURE_MANIFESTS),
|
|
2914
|
+
palettes: [...allPalettes]
|
|
2915
|
+
};
|
|
2916
|
+
const projectDir = resolve(String(args.dir ?? "."));
|
|
2917
|
+
const projectName = basename2(projectDir);
|
|
2918
|
+
const nameResult = validateProjectName(projectName);
|
|
2919
|
+
if (!nameResult.ok) {
|
|
2920
|
+
console.error(pc3.red(`\u2717 ${nameResult.error}`));
|
|
2921
|
+
process.exitCode = 1;
|
|
2922
|
+
return;
|
|
2923
|
+
}
|
|
2924
|
+
const result = await runAIConversation({
|
|
2925
|
+
provider,
|
|
2926
|
+
registry,
|
|
2927
|
+
description,
|
|
2928
|
+
projectName,
|
|
2929
|
+
projectDir
|
|
2930
|
+
});
|
|
2931
|
+
if (!result.ok) {
|
|
2932
|
+
console.error(pc3.red(`\u2716 AI conversation failed: ${result.error.message}`));
|
|
2933
|
+
process.exitCode = 1;
|
|
2934
|
+
return;
|
|
2935
|
+
}
|
|
2936
|
+
const config = result.value;
|
|
2937
|
+
if (!args.yes) {
|
|
2938
|
+
showAISummary(config);
|
|
2939
|
+
const confirmed = await p2.confirm({
|
|
2940
|
+
message: "Create this project?",
|
|
2941
|
+
initialValue: true
|
|
2942
|
+
});
|
|
2943
|
+
if (p2.isCancel(confirmed) || !confirmed) {
|
|
2944
|
+
p2.cancel("Operation cancelled.");
|
|
2945
|
+
process.exitCode = 0;
|
|
2946
|
+
return;
|
|
2947
|
+
}
|
|
2948
|
+
}
|
|
2949
|
+
return runScaffold(
|
|
2950
|
+
config,
|
|
2951
|
+
allPalettes,
|
|
2952
|
+
(args["dry-run"] ?? false) === true,
|
|
2953
|
+
(args.verbose ?? false) === true,
|
|
2954
|
+
!((args.install ?? true) === true),
|
|
2955
|
+
!((args.git ?? true) === true)
|
|
2956
|
+
);
|
|
2957
|
+
}
|
|
2958
|
+
function runFlagDrivenMode(args, allPalettes) {
|
|
2959
|
+
const projectDir = resolve(String(args.dir ?? "."));
|
|
2960
|
+
const projectName = basename2(projectDir);
|
|
2961
|
+
const nameResult = validateProjectName(projectName);
|
|
2962
|
+
if (!nameResult.ok) {
|
|
2963
|
+
console.error(pc3.red(`\u2717 ${nameResult.error}`));
|
|
2964
|
+
process.exitCode = 1;
|
|
2965
|
+
return;
|
|
2966
|
+
}
|
|
2967
|
+
const recipeName = args.recipe ? String(args.recipe) : void 0;
|
|
2968
|
+
let recipeOverlay = {};
|
|
2969
|
+
if (recipeName) {
|
|
2970
|
+
if (RECIPES[recipeName]) {
|
|
2971
|
+
recipeOverlay = RECIPES[recipeName];
|
|
2972
|
+
console.log(pc3.green(`\u2714 Using recipe: ${pc3.bold(recipeName)}`));
|
|
2973
|
+
} else {
|
|
2974
|
+
console.error(pc3.red(`\u2716 Recipe "${recipeName}" not found.`));
|
|
2975
|
+
console.error(pc3.dim(` Available: ${Object.keys(RECIPES).join(", ")}`));
|
|
2976
|
+
process.exitCode = 1;
|
|
2977
|
+
return;
|
|
2978
|
+
}
|
|
2979
|
+
}
|
|
2980
|
+
const renderMode = String(args.render ?? recipeOverlay.renderMode ?? "static");
|
|
2981
|
+
const deployTarget = String(args.deploy ?? recipeOverlay.deployTarget ?? "cloudflare");
|
|
2982
|
+
const database = String(args.database ?? recipeOverlay.database ?? "none");
|
|
2983
|
+
const cssEngine = String(args.css ?? recipeOverlay.cssEngine ?? "tailwind");
|
|
2984
|
+
const localesRaw = String(args.locales ?? "");
|
|
2985
|
+
let locales = ["en"];
|
|
2986
|
+
let defaultLocale = "en";
|
|
2987
|
+
if (localesRaw) {
|
|
2988
|
+
locales = localesRaw.split(",").map((l) => l.trim()).filter(Boolean);
|
|
2989
|
+
defaultLocale = locales[0] ?? "en";
|
|
2990
|
+
} else if (recipeOverlay.locales) {
|
|
2991
|
+
locales = [...recipeOverlay.locales];
|
|
2992
|
+
defaultLocale = recipeOverlay.defaultLocale ?? locales[0] ?? "en";
|
|
2993
|
+
}
|
|
2994
|
+
const themeSwitcher = args["theme-switcher"] !== void 0 ? args["theme-switcher"] === true : recipeOverlay.themeSwitcher ?? false;
|
|
2995
|
+
const blocksString = args.blocks ? String(args.blocks) : "";
|
|
2996
|
+
let blocks = recipeOverlay.blocks ? [...recipeOverlay.blocks] : [];
|
|
2997
|
+
if (blocksString) {
|
|
2998
|
+
const blockNames = blocksString.split(",").map((b) => b.trim()).filter(Boolean);
|
|
2999
|
+
blocks = blockNames.map((name) => ({ name, variant: "default" }));
|
|
3000
|
+
}
|
|
3001
|
+
if (blocks.length === 0) {
|
|
3002
|
+
console.error(pc3.red("\u2717 No blocks selected."));
|
|
3003
|
+
console.error(pc3.dim(" Use --recipe saas for a pre-built set, or drop --manual to let AI choose blocks for you."));
|
|
3004
|
+
process.exitCode = 1;
|
|
3005
|
+
return;
|
|
3006
|
+
}
|
|
3007
|
+
let paletteColors = recipeOverlay.palette ? { ...recipeOverlay.palette.colors } : { ...DEFAULT_COLORS };
|
|
3008
|
+
let palettePreset = recipeOverlay.palette?.preset;
|
|
3009
|
+
if (args.palette) {
|
|
3010
|
+
const paletteName = String(args.palette);
|
|
3011
|
+
const found = allPalettes.find((palette) => palette.name === paletteName);
|
|
3012
|
+
if (found) {
|
|
3013
|
+
paletteColors = { ...found.colors };
|
|
3014
|
+
palettePreset = found.name;
|
|
3015
|
+
} else {
|
|
3016
|
+
console.error(pc3.red(`\u2716 Palette "${paletteName}" not found in registry.`));
|
|
3017
|
+
console.error(pc3.dim(` Available: ${allPalettes.map((palette) => palette.name).join(", ")}`));
|
|
3018
|
+
process.exitCode = 1;
|
|
3019
|
+
return;
|
|
3020
|
+
}
|
|
3021
|
+
}
|
|
3022
|
+
const config = {
|
|
3023
|
+
projectName,
|
|
3024
|
+
projectDir,
|
|
3025
|
+
renderMode,
|
|
3026
|
+
deployTarget,
|
|
3027
|
+
database,
|
|
3028
|
+
cssEngine,
|
|
3029
|
+
packageManager: "pnpm",
|
|
3030
|
+
blocks,
|
|
3031
|
+
locales,
|
|
3032
|
+
defaultLocale,
|
|
3033
|
+
palette: {
|
|
3034
|
+
...palettePreset ? { preset: palettePreset } : {},
|
|
3035
|
+
colors: paletteColors
|
|
3036
|
+
},
|
|
3037
|
+
themeSwitcher,
|
|
3038
|
+
createdWith: recipeName ? "recipe" : "manual"
|
|
3039
|
+
};
|
|
3040
|
+
return runScaffold(
|
|
3041
|
+
config,
|
|
3042
|
+
allPalettes,
|
|
3043
|
+
(args["dry-run"] ?? false) === true,
|
|
3044
|
+
(args.verbose ?? false) === true,
|
|
3045
|
+
!((args.install ?? true) === true),
|
|
3046
|
+
!((args.git ?? true) === true)
|
|
3047
|
+
);
|
|
3048
|
+
}
|
|
3049
|
+
function runScaffold(config, allPalettes, dryRun, verbose, skipInstall, skipGit) {
|
|
3050
|
+
const input = {
|
|
3051
|
+
config,
|
|
3052
|
+
manifests: FIXTURE_MANIFESTS,
|
|
3053
|
+
blockSources: FIXTURE_BLOCK_SOURCES,
|
|
3054
|
+
blockDefaultContent: FIXTURE_DEFAULT_CONTENT,
|
|
3055
|
+
allPalettes
|
|
3056
|
+
};
|
|
3057
|
+
const result = scaffold(input);
|
|
3058
|
+
if (!isOk(result)) {
|
|
3059
|
+
const errMsg = result.error.message;
|
|
3060
|
+
console.error(pc3.red(`\u2717 Scaffold failed: ${errMsg}`));
|
|
3061
|
+
if ("blockA" in result.error && "blockB" in result.error) {
|
|
3062
|
+
const conflict = result.error;
|
|
3063
|
+
console.error(pc3.yellow(` Block '${conflict.blockA}' conflicts with '${conflict.blockB}'.`));
|
|
3064
|
+
console.error(pc3.dim(" Remove one of these blocks and try again."));
|
|
3065
|
+
}
|
|
3066
|
+
if ("blockName" in result.error) {
|
|
3067
|
+
const notFound = result.error;
|
|
3068
|
+
console.error(pc3.yellow(` Block '${notFound.blockName}' was not found in the registry.`));
|
|
3069
|
+
console.error(pc3.dim(" If you are offline, cached blocks will be used when available."));
|
|
3070
|
+
console.error(pc3.dim(" Run 'fornix list' to see available blocks."));
|
|
3071
|
+
}
|
|
3072
|
+
process.exitCode = 1;
|
|
3073
|
+
return;
|
|
3074
|
+
}
|
|
3075
|
+
if (dryRun) {
|
|
3076
|
+
console.log(pc3.bold("\n\u{1F4CB} Dry run \u2014 files that would be created:\n"));
|
|
3077
|
+
const sortedFiles = Object.keys(result.value.files).sort();
|
|
3078
|
+
for (const file of sortedFiles) {
|
|
3079
|
+
console.log(pc3.dim(" ") + file);
|
|
3080
|
+
}
|
|
3081
|
+
console.log(pc3.dim(`
|
|
3082
|
+
Total: ${sortedFiles.length} files`));
|
|
3083
|
+
return;
|
|
3084
|
+
}
|
|
3085
|
+
const files = result.value.files;
|
|
3086
|
+
let filesWritten = 0;
|
|
3087
|
+
for (const [relativePath, content] of Object.entries(files)) {
|
|
3088
|
+
const fullPath = join4(config.projectDir, relativePath);
|
|
3089
|
+
const parentDir = join4(fullPath, "..");
|
|
3090
|
+
mkdirSync2(parentDir, { recursive: true });
|
|
3091
|
+
writeFileSync2(fullPath, content, "utf-8");
|
|
3092
|
+
filesWritten++;
|
|
3093
|
+
if (verbose) {
|
|
3094
|
+
console.log(pc3.dim(` created ${relativePath}`));
|
|
3095
|
+
}
|
|
3096
|
+
}
|
|
3097
|
+
runPostScaffold({
|
|
3098
|
+
config,
|
|
3099
|
+
resolvedBlockNames: result.value.resolvedBlockNames,
|
|
3100
|
+
filesWritten,
|
|
3101
|
+
verbose,
|
|
3102
|
+
skipInstall,
|
|
3103
|
+
skipGit
|
|
3104
|
+
});
|
|
3105
|
+
}
|
|
3106
|
+
function adaptProvider(legacy) {
|
|
3107
|
+
return {
|
|
3108
|
+
name: legacy.name,
|
|
3109
|
+
async generate(options) {
|
|
3110
|
+
const result = await legacy.generate(options.prompt);
|
|
3111
|
+
if (!result.ok) {
|
|
3112
|
+
throw new Error(result.error.message);
|
|
3113
|
+
}
|
|
3114
|
+
return options.schema.parse(result.value);
|
|
3115
|
+
},
|
|
3116
|
+
async *stream() {
|
|
3117
|
+
yield "";
|
|
3118
|
+
}
|
|
3119
|
+
};
|
|
3120
|
+
}
|
|
3121
|
+
function parseProviderName(value) {
|
|
3122
|
+
if (!value || typeof value !== "string") return void 0;
|
|
3123
|
+
if (VALID_PROVIDER_NAMES.includes(value)) {
|
|
3124
|
+
return value;
|
|
3125
|
+
}
|
|
3126
|
+
return void 0;
|
|
3127
|
+
}
|
|
3128
|
+
async function getDescription(args) {
|
|
3129
|
+
if (args.yes) {
|
|
3130
|
+
return typeof args.description === "string" && args.description.length > 0 ? args.description : DEFAULT_AI_DESCRIPTION;
|
|
3131
|
+
}
|
|
3132
|
+
p2.intro(pc3.bgCyan(pc3.black(" Fornix \u2014 AI Mode ")));
|
|
3133
|
+
const input = await p2.text({
|
|
3134
|
+
message: "Describe the website you want to build:",
|
|
3135
|
+
placeholder: "e.g., A fintech landing page with user authentication and a modern dark theme",
|
|
3136
|
+
validate(value) {
|
|
3137
|
+
if (!value.trim()) return "Please describe what you want to build";
|
|
3138
|
+
return void 0;
|
|
3139
|
+
}
|
|
3140
|
+
});
|
|
3141
|
+
if (p2.isCancel(input)) {
|
|
3142
|
+
p2.cancel("Operation cancelled.");
|
|
3143
|
+
return null;
|
|
3144
|
+
}
|
|
3145
|
+
return String(input);
|
|
3146
|
+
}
|
|
3147
|
+
function showAISummary(config) {
|
|
3148
|
+
const lines = [];
|
|
3149
|
+
lines.push(`${pc3.bold("Project:")} ${config.projectName}`);
|
|
3150
|
+
lines.push(`${pc3.bold("Render mode:")} ${config.renderMode}`);
|
|
3151
|
+
lines.push(`${pc3.bold("Deploy to:")} ${config.deployTarget}`);
|
|
3152
|
+
lines.push(`${pc3.bold("CSS engine:")} ${config.cssEngine}`);
|
|
3153
|
+
const blockNames = config.blocks.map((b) => b.name);
|
|
3154
|
+
lines.push(`${pc3.bold("Blocks:")} ${blockNames.length > 0 ? blockNames.join(", ") : pc3.dim("(none)")}`);
|
|
3155
|
+
lines.push(`${pc3.bold("Locales:")} ${config.locales.join(", ")} (default: ${config.defaultLocale})`);
|
|
3156
|
+
if (config.palette.preset) {
|
|
3157
|
+
lines.push(`${pc3.bold("Palette:")} ${config.palette.preset}`);
|
|
3158
|
+
}
|
|
3159
|
+
if (config.themeSwitcher) {
|
|
3160
|
+
lines.push(`${pc3.bold("Theme switcher:")} ${pc3.green("yes")}`);
|
|
3161
|
+
}
|
|
3162
|
+
lines.push(`${pc3.bold("Created with:")} ${pc3.cyan("AI")}`);
|
|
3163
|
+
p2.note(lines.join("\n"), "AI-Generated Configuration");
|
|
3164
|
+
}
|
|
3165
|
+
function showNoProviderGuide() {
|
|
3166
|
+
console.error(pc3.red("\n\u2716 No AI provider found.\n"));
|
|
3167
|
+
console.error("To use AI mode, set up one of the following:\n");
|
|
3168
|
+
console.error(pc3.bold(" Option 1: OpenAI"));
|
|
3169
|
+
console.error(" export OPENAI_API_KEY=sk-...\n");
|
|
3170
|
+
console.error(pc3.bold(" Option 2: Ollama (free, local)"));
|
|
3171
|
+
console.error(" Install from https://ollama.com and run: ollama pull llama3.1\n");
|
|
3172
|
+
console.error(pc3.bold(" Option 3: Cloudflare Workers AI"));
|
|
3173
|
+
console.error(" export CLOUDFLARE_ACCOUNT_ID=... CLOUDFLARE_API_TOKEN=...\n");
|
|
3174
|
+
console.error(pc3.dim(" Or use manual mode: npx create-fornix --manual\n"));
|
|
3175
|
+
}
|
|
3176
|
+
|
|
3177
|
+
// src/cli/commands/add.ts
|
|
3178
|
+
import { defineCommand as defineCommand2 } from "citty";
|
|
3179
|
+
import pc4 from "picocolors";
|
|
3180
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, existsSync as existsSync2, mkdirSync as mkdirSync3 } from "fs";
|
|
3181
|
+
import { join as join5, dirname as dirname3 } from "path";
|
|
3182
|
+
var addCommand = defineCommand2({
|
|
3183
|
+
meta: {
|
|
3184
|
+
name: "add",
|
|
3185
|
+
description: "Add a block to an existing Fornix project"
|
|
3186
|
+
},
|
|
3187
|
+
args: {
|
|
3188
|
+
block: {
|
|
3189
|
+
type: "positional",
|
|
3190
|
+
description: "Block name to add (e.g. hero-gradient, auth-better-auth)",
|
|
3191
|
+
required: true
|
|
3192
|
+
},
|
|
3193
|
+
variant: {
|
|
3194
|
+
type: "string",
|
|
3195
|
+
description: "Block variant to use (defaults to 'default')",
|
|
3196
|
+
default: "default"
|
|
3197
|
+
},
|
|
3198
|
+
"dry-run": {
|
|
3199
|
+
type: "boolean",
|
|
3200
|
+
description: "Show what would change without writing",
|
|
3201
|
+
default: false
|
|
3202
|
+
},
|
|
3203
|
+
verbose: {
|
|
3204
|
+
type: "boolean",
|
|
3205
|
+
description: "Detailed output",
|
|
3206
|
+
default: false
|
|
3207
|
+
}
|
|
3208
|
+
},
|
|
3209
|
+
run({ args }) {
|
|
3210
|
+
const typedArgs = args;
|
|
3211
|
+
const cwd = process.cwd();
|
|
3212
|
+
const manifestPath = join5(cwd, "fornix.json");
|
|
3213
|
+
if (!existsSync2(manifestPath)) {
|
|
3214
|
+
console.error(
|
|
3215
|
+
pc4.red("\u2717 No fornix.json found. Are you in a Fornix project?")
|
|
3216
|
+
);
|
|
3217
|
+
process.exit(1);
|
|
3218
|
+
}
|
|
3219
|
+
const manifestRaw = readFileSync3(manifestPath, "utf-8");
|
|
3220
|
+
const manifest2 = JSON.parse(manifestRaw);
|
|
3221
|
+
const blockName = typedArgs.block;
|
|
3222
|
+
const blockManifest = FIXTURE_MANIFESTS[blockName];
|
|
3223
|
+
if (!blockManifest) {
|
|
3224
|
+
console.error(pc4.red(`\u2717 Block '${blockName}' not found in registry.`));
|
|
3225
|
+
console.error(
|
|
3226
|
+
pc4.dim(
|
|
3227
|
+
` Available: ${Object.keys(FIXTURE_MANIFESTS).join(", ")}`
|
|
3228
|
+
)
|
|
3229
|
+
);
|
|
3230
|
+
process.exit(1);
|
|
3231
|
+
}
|
|
3232
|
+
const installedNames = new Set(manifest2.blocks.map((b) => b.name));
|
|
3233
|
+
if (installedNames.has(blockName)) {
|
|
3234
|
+
console.log(
|
|
3235
|
+
pc4.yellow(`\u26A0 Block '${blockName}' is already installed.`)
|
|
3236
|
+
);
|
|
3237
|
+
return;
|
|
3238
|
+
}
|
|
3239
|
+
const blocksToAdd = resolveDependencies2(blockName, installedNames);
|
|
3240
|
+
for (const name of blocksToAdd) {
|
|
3241
|
+
const m = FIXTURE_MANIFESTS[name];
|
|
3242
|
+
if (m?.requiredMode && manifest2.renderMode !== m.requiredMode) {
|
|
3243
|
+
console.error(
|
|
3244
|
+
pc4.red(
|
|
3245
|
+
`\u2717 Block '${name}' requires '${m.requiredMode}' mode, but project uses '${manifest2.renderMode}'.`
|
|
3246
|
+
)
|
|
3247
|
+
);
|
|
3248
|
+
process.exit(1);
|
|
3249
|
+
}
|
|
3250
|
+
}
|
|
3251
|
+
const filesToWrite = [];
|
|
3252
|
+
for (const name of blocksToAdd) {
|
|
3253
|
+
const bManifest = FIXTURE_MANIFESTS[name];
|
|
3254
|
+
const sources = FIXTURE_BLOCK_SOURCES[name];
|
|
3255
|
+
if (!bManifest || !sources) {
|
|
3256
|
+
console.error(pc4.red(`\u2717 Source files not found for block '${name}'.`));
|
|
3257
|
+
process.exit(1);
|
|
3258
|
+
}
|
|
3259
|
+
for (const file of bManifest.files) {
|
|
3260
|
+
const content = sources[file.source];
|
|
3261
|
+
if (content === void 0) {
|
|
3262
|
+
console.error(
|
|
3263
|
+
pc4.red(
|
|
3264
|
+
`\u2717 Source file '${file.source}' not found for block '${name}'.`
|
|
3265
|
+
)
|
|
3266
|
+
);
|
|
3267
|
+
process.exit(1);
|
|
3268
|
+
}
|
|
3269
|
+
filesToWrite.push({
|
|
3270
|
+
path: join5(cwd, file.destination),
|
|
3271
|
+
content
|
|
3272
|
+
});
|
|
3273
|
+
}
|
|
3274
|
+
}
|
|
3275
|
+
if (typedArgs["dry-run"]) {
|
|
3276
|
+
console.log(pc4.bold("\n Dry run \u2014 no files written\n"));
|
|
3277
|
+
for (const name of blocksToAdd) {
|
|
3278
|
+
const isDep = name !== blockName;
|
|
3279
|
+
console.log(
|
|
3280
|
+
` ${isDep ? pc4.dim("(dep)") : pc4.green("+")} ${pc4.bold(name)}`
|
|
3281
|
+
);
|
|
3282
|
+
}
|
|
3283
|
+
console.log();
|
|
3284
|
+
for (const file of filesToWrite) {
|
|
3285
|
+
console.log(` ${pc4.dim("\u2192")} ${file.path}`);
|
|
3286
|
+
}
|
|
3287
|
+
console.log();
|
|
3288
|
+
return;
|
|
3289
|
+
}
|
|
3290
|
+
for (const file of filesToWrite) {
|
|
3291
|
+
mkdirSync3(dirname3(file.path), { recursive: true });
|
|
3292
|
+
writeFileSync3(file.path, file.content);
|
|
3293
|
+
if (typedArgs.verbose) {
|
|
3294
|
+
console.log(` ${pc4.dim("\u2192")} ${file.path}`);
|
|
3295
|
+
}
|
|
3296
|
+
}
|
|
3297
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3298
|
+
for (const name of blocksToAdd) {
|
|
3299
|
+
const bManifest = FIXTURE_MANIFESTS[name];
|
|
3300
|
+
if (!bManifest) continue;
|
|
3301
|
+
manifest2.blocks.push({
|
|
3302
|
+
name,
|
|
3303
|
+
version: bManifest.version,
|
|
3304
|
+
variant: name === blockName ? typedArgs.variant : "default",
|
|
3305
|
+
installedAt: now
|
|
3306
|
+
});
|
|
3307
|
+
}
|
|
3308
|
+
writeFileSync3(manifestPath, JSON.stringify(manifest2, null, 2) + "\n");
|
|
3309
|
+
console.log();
|
|
3310
|
+
for (const name of blocksToAdd) {
|
|
3311
|
+
const isDep = name !== blockName;
|
|
3312
|
+
if (isDep) {
|
|
3313
|
+
console.log(
|
|
3314
|
+
` ${pc4.green("+")} ${pc4.bold(name)} ${pc4.dim("(auto-added dependency)")}`
|
|
3315
|
+
);
|
|
3316
|
+
} else {
|
|
3317
|
+
console.log(` ${pc4.green("+")} ${pc4.bold(name)}`);
|
|
3318
|
+
}
|
|
3319
|
+
}
|
|
3320
|
+
console.log(
|
|
3321
|
+
pc4.dim(
|
|
3322
|
+
`
|
|
3323
|
+
${filesToWrite.length} files placed, fornix.json updated.`
|
|
3324
|
+
)
|
|
3325
|
+
);
|
|
3326
|
+
console.log();
|
|
3327
|
+
}
|
|
3328
|
+
});
|
|
3329
|
+
function resolveDependencies2(blockName, installedNames) {
|
|
3330
|
+
const result = [];
|
|
3331
|
+
const visited = /* @__PURE__ */ new Set();
|
|
3332
|
+
function walk(name) {
|
|
3333
|
+
if (visited.has(name) || installedNames.has(name)) return;
|
|
3334
|
+
visited.add(name);
|
|
3335
|
+
const manifest2 = FIXTURE_MANIFESTS[name];
|
|
3336
|
+
if (!manifest2) return;
|
|
3337
|
+
for (const dep of manifest2.requires) {
|
|
3338
|
+
walk(dep);
|
|
3339
|
+
}
|
|
3340
|
+
result.push(name);
|
|
3341
|
+
}
|
|
3342
|
+
walk(blockName);
|
|
3343
|
+
return result;
|
|
3344
|
+
}
|
|
3345
|
+
|
|
3346
|
+
// src/cli/commands/remove.ts
|
|
3347
|
+
import { defineCommand as defineCommand3 } from "citty";
|
|
3348
|
+
import pc5 from "picocolors";
|
|
3349
|
+
import { readFileSync as readFileSync4, writeFileSync as writeFileSync4, existsSync as existsSync3, unlinkSync, readdirSync as readdirSync2, rmdirSync } from "fs";
|
|
3350
|
+
import { join as join6, dirname as dirname4 } from "path";
|
|
3351
|
+
var removeCommand = defineCommand3({
|
|
3352
|
+
meta: {
|
|
3353
|
+
name: "remove",
|
|
3354
|
+
description: "Remove a block from an existing Fornix project"
|
|
3355
|
+
},
|
|
3356
|
+
args: {
|
|
3357
|
+
block: {
|
|
3358
|
+
type: "positional",
|
|
3359
|
+
description: "Block name to remove",
|
|
3360
|
+
required: true
|
|
3361
|
+
},
|
|
3362
|
+
"dry-run": {
|
|
3363
|
+
type: "boolean",
|
|
3364
|
+
description: "Show what would change without writing",
|
|
3365
|
+
default: false
|
|
3366
|
+
},
|
|
3367
|
+
force: {
|
|
3368
|
+
type: "boolean",
|
|
3369
|
+
description: "Force removal even if other blocks depend on this one",
|
|
3370
|
+
default: false
|
|
3371
|
+
},
|
|
3372
|
+
verbose: {
|
|
3373
|
+
type: "boolean",
|
|
3374
|
+
description: "Detailed output",
|
|
3375
|
+
default: false
|
|
3376
|
+
}
|
|
3377
|
+
},
|
|
3378
|
+
run({ args }) {
|
|
3379
|
+
const typedArgs = args;
|
|
3380
|
+
const cwd = process.cwd();
|
|
3381
|
+
const manifestPath = join6(cwd, "fornix.json");
|
|
3382
|
+
if (!existsSync3(manifestPath)) {
|
|
3383
|
+
console.error(
|
|
3384
|
+
pc5.red("\u2717 No fornix.json found. Are you in a Fornix project?")
|
|
3385
|
+
);
|
|
3386
|
+
process.exit(1);
|
|
3387
|
+
}
|
|
3388
|
+
const manifestRaw = readFileSync4(manifestPath, "utf-8");
|
|
3389
|
+
const manifest2 = JSON.parse(manifestRaw);
|
|
3390
|
+
const blockName = typedArgs.block;
|
|
3391
|
+
const installedNames = new Set(manifest2.blocks.map((b) => b.name));
|
|
3392
|
+
if (!installedNames.has(blockName)) {
|
|
3393
|
+
console.log(
|
|
3394
|
+
pc5.yellow(`\u26A0 Block '${blockName}' is not installed.`)
|
|
3395
|
+
);
|
|
3396
|
+
return;
|
|
3397
|
+
}
|
|
3398
|
+
const dependents = findDependents(blockName, installedNames);
|
|
3399
|
+
if (dependents.length > 0 && !typedArgs.force) {
|
|
3400
|
+
console.log(
|
|
3401
|
+
pc5.yellow(
|
|
3402
|
+
`\u26A0 Block '${blockName}' is required by: ${dependents.join(", ")}`
|
|
3403
|
+
)
|
|
3404
|
+
);
|
|
3405
|
+
console.log(
|
|
3406
|
+
pc5.dim(" Use --force to remove anyway, or remove dependent blocks first.")
|
|
3407
|
+
);
|
|
3408
|
+
return;
|
|
3409
|
+
}
|
|
3410
|
+
const blockManifest = FIXTURE_MANIFESTS[blockName];
|
|
3411
|
+
const filesToRemove = [];
|
|
3412
|
+
if (blockManifest) {
|
|
3413
|
+
for (const file of blockManifest.files) {
|
|
3414
|
+
const filePath = join6(cwd, file.destination);
|
|
3415
|
+
if (existsSync3(filePath)) {
|
|
3416
|
+
filesToRemove.push(filePath);
|
|
3417
|
+
}
|
|
3418
|
+
}
|
|
3419
|
+
}
|
|
3420
|
+
if (typedArgs["dry-run"]) {
|
|
3421
|
+
console.log(pc5.bold("\n Dry run \u2014 no files removed\n"));
|
|
3422
|
+
console.log(` ${pc5.red("-")} ${pc5.bold(blockName)}`);
|
|
3423
|
+
for (const file of filesToRemove) {
|
|
3424
|
+
console.log(` ${pc5.dim("\xD7")} ${file}`);
|
|
3425
|
+
}
|
|
3426
|
+
console.log();
|
|
3427
|
+
return;
|
|
3428
|
+
}
|
|
3429
|
+
for (const filePath of filesToRemove) {
|
|
3430
|
+
unlinkSync(filePath);
|
|
3431
|
+
if (typedArgs.verbose) {
|
|
3432
|
+
console.log(` ${pc5.dim("\xD7")} ${filePath}`);
|
|
3433
|
+
}
|
|
3434
|
+
tryRemoveEmptyDir(dirname4(filePath), cwd);
|
|
3435
|
+
}
|
|
3436
|
+
manifest2.blocks = manifest2.blocks.filter((b) => b.name !== blockName);
|
|
3437
|
+
writeFileSync4(manifestPath, JSON.stringify(manifest2, null, 2) + "\n");
|
|
3438
|
+
console.log();
|
|
3439
|
+
console.log(` ${pc5.red("-")} ${pc5.bold(blockName)} removed`);
|
|
3440
|
+
if (dependents.length > 0) {
|
|
3441
|
+
console.log(
|
|
3442
|
+
pc5.yellow(
|
|
3443
|
+
` \u26A0 Warning: ${dependents.join(", ")} may no longer work correctly.`
|
|
3444
|
+
)
|
|
3445
|
+
);
|
|
3446
|
+
}
|
|
3447
|
+
console.log(
|
|
3448
|
+
pc5.dim(
|
|
3449
|
+
`
|
|
3450
|
+
${filesToRemove.length} files removed, fornix.json updated.`
|
|
3451
|
+
)
|
|
3452
|
+
);
|
|
3453
|
+
console.log();
|
|
3454
|
+
}
|
|
3455
|
+
});
|
|
3456
|
+
function findDependents(blockName, installedNames) {
|
|
3457
|
+
const dependents = [];
|
|
3458
|
+
for (const name of installedNames) {
|
|
3459
|
+
const manifest2 = FIXTURE_MANIFESTS[name];
|
|
3460
|
+
if (manifest2 && manifest2.requires.includes(blockName)) {
|
|
3461
|
+
dependents.push(name);
|
|
3462
|
+
}
|
|
3463
|
+
}
|
|
3464
|
+
return dependents;
|
|
3465
|
+
}
|
|
3466
|
+
function tryRemoveEmptyDir(dirPath, rootPath) {
|
|
3467
|
+
if (dirPath === rootPath || !dirPath.startsWith(rootPath)) return;
|
|
3468
|
+
try {
|
|
3469
|
+
const entries = readdirSync2(dirPath);
|
|
3470
|
+
if (entries.length === 0) {
|
|
3471
|
+
rmdirSync(dirPath);
|
|
3472
|
+
tryRemoveEmptyDir(dirname4(dirPath), rootPath);
|
|
3473
|
+
}
|
|
3474
|
+
} catch {
|
|
3475
|
+
}
|
|
3476
|
+
}
|
|
3477
|
+
|
|
3478
|
+
// src/cli/commands/list.ts
|
|
3479
|
+
import { defineCommand as defineCommand4 } from "citty";
|
|
3480
|
+
import pc6 from "picocolors";
|
|
3481
|
+
var listCommand = defineCommand4({
|
|
3482
|
+
meta: {
|
|
3483
|
+
name: "list",
|
|
3484
|
+
description: "List available blocks from the Fornix registry"
|
|
3485
|
+
},
|
|
3486
|
+
args: {
|
|
3487
|
+
category: {
|
|
3488
|
+
type: "string",
|
|
3489
|
+
description: "Filter blocks by category"
|
|
3490
|
+
},
|
|
3491
|
+
type: {
|
|
3492
|
+
type: "string",
|
|
3493
|
+
description: "Filter blocks by type (section, integration)"
|
|
3494
|
+
},
|
|
3495
|
+
json: {
|
|
3496
|
+
type: "boolean",
|
|
3497
|
+
description: "Output as JSON",
|
|
3498
|
+
default: false
|
|
3499
|
+
},
|
|
3500
|
+
verbose: {
|
|
3501
|
+
type: "boolean",
|
|
3502
|
+
description: "Show full block details",
|
|
3503
|
+
default: false
|
|
3504
|
+
}
|
|
3505
|
+
},
|
|
3506
|
+
run({ args }) {
|
|
3507
|
+
const typedArgs = args;
|
|
3508
|
+
const blocks = getFilteredBlocks(typedArgs);
|
|
3509
|
+
if (blocks.length === 0) {
|
|
3510
|
+
console.log(pc6.yellow("No blocks found matching your filters."));
|
|
3511
|
+
return;
|
|
3512
|
+
}
|
|
3513
|
+
if (typedArgs.json) {
|
|
3514
|
+
printJson(blocks);
|
|
3515
|
+
} else {
|
|
3516
|
+
printFormatted(blocks, typedArgs.verbose);
|
|
3517
|
+
}
|
|
3518
|
+
}
|
|
3519
|
+
});
|
|
3520
|
+
function getFilteredBlocks(args) {
|
|
3521
|
+
let blocks = Object.values(FIXTURE_MANIFESTS);
|
|
3522
|
+
if (args.type) {
|
|
3523
|
+
const filterType = args.type.toLowerCase();
|
|
3524
|
+
blocks = blocks.filter((b) => b.type === filterType);
|
|
3525
|
+
}
|
|
3526
|
+
if (args.category) {
|
|
3527
|
+
const filterCategory = args.category.toLowerCase();
|
|
3528
|
+
blocks = blocks.filter((b) => b.category === filterCategory);
|
|
3529
|
+
}
|
|
3530
|
+
return blocks.sort((a, b) => a.name.localeCompare(b.name));
|
|
3531
|
+
}
|
|
3532
|
+
function printJson(blocks) {
|
|
3533
|
+
const output = blocks.map((b) => ({
|
|
3534
|
+
name: b.name,
|
|
3535
|
+
type: b.type,
|
|
3536
|
+
category: b.category,
|
|
3537
|
+
description: b.description,
|
|
3538
|
+
tags: b.tags,
|
|
3539
|
+
requiredMode: b.requiredMode,
|
|
3540
|
+
requires: b.requires
|
|
3541
|
+
}));
|
|
3542
|
+
console.log(JSON.stringify(output, null, 2));
|
|
3543
|
+
}
|
|
3544
|
+
function printFormatted(blocks, verbose) {
|
|
3545
|
+
console.log();
|
|
3546
|
+
console.log(
|
|
3547
|
+
pc6.bold(` \u{1F4E6} Fornix Blocks`) + pc6.dim(` (${blocks.length} available)`)
|
|
3548
|
+
);
|
|
3549
|
+
console.log();
|
|
3550
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
3551
|
+
for (const block of blocks) {
|
|
3552
|
+
const type = block.type;
|
|
3553
|
+
if (!grouped.has(type)) {
|
|
3554
|
+
grouped.set(type, []);
|
|
3555
|
+
}
|
|
3556
|
+
grouped.get(type).push(block);
|
|
3557
|
+
}
|
|
3558
|
+
const typeIcons = {
|
|
3559
|
+
section: "\u{1F9E9}",
|
|
3560
|
+
integration: "\u2699\uFE0F",
|
|
3561
|
+
feature: "\u2728",
|
|
3562
|
+
layout: "\u{1F4D0}"
|
|
3563
|
+
};
|
|
3564
|
+
for (const [type, typeBlocks] of grouped) {
|
|
3565
|
+
const icon = typeIcons[type] ?? "\u{1F4E6}";
|
|
3566
|
+
console.log(
|
|
3567
|
+
` ${icon} ${pc6.bold(pc6.cyan(type.toUpperCase()))} ${pc6.dim(`(${typeBlocks.length})`)}`
|
|
3568
|
+
);
|
|
3569
|
+
console.log();
|
|
3570
|
+
for (const block of typeBlocks) {
|
|
3571
|
+
const name = pc6.bold(pc6.white(block.name));
|
|
3572
|
+
const desc = pc6.dim(block.description);
|
|
3573
|
+
const tags = block.tags.map((t) => pc6.dim(`#${t}`)).join(" ");
|
|
3574
|
+
console.log(` ${name}`);
|
|
3575
|
+
console.log(` ${desc}`);
|
|
3576
|
+
if (verbose) {
|
|
3577
|
+
console.log(` ${pc6.dim("Category:")} ${block.category}`);
|
|
3578
|
+
if (block.requiredMode) {
|
|
3579
|
+
console.log(
|
|
3580
|
+
` ${pc6.dim("Requires:")} ${pc6.yellow(block.requiredMode)} mode`
|
|
3581
|
+
);
|
|
3582
|
+
}
|
|
3583
|
+
if (block.requires.length > 0) {
|
|
3584
|
+
console.log(
|
|
3585
|
+
` ${pc6.dim("Depends on:")} ${block.requires.join(", ")}`
|
|
3586
|
+
);
|
|
3587
|
+
}
|
|
3588
|
+
if (Object.keys(block.dependencies).length > 0) {
|
|
3589
|
+
const deps = Object.entries(block.dependencies).map(([k, v]) => `${k}@${v}`).join(", ");
|
|
3590
|
+
console.log(` ${pc6.dim("npm deps:")} ${deps}`);
|
|
3591
|
+
}
|
|
3592
|
+
}
|
|
3593
|
+
console.log(` ${tags}`);
|
|
3594
|
+
console.log();
|
|
3595
|
+
}
|
|
3596
|
+
}
|
|
3597
|
+
}
|
|
3598
|
+
|
|
3599
|
+
// src/cli/commands/status.ts
|
|
3600
|
+
import { defineCommand as defineCommand5 } from "citty";
|
|
3601
|
+
import pc7 from "picocolors";
|
|
3602
|
+
import { readFileSync as readFileSync5, existsSync as existsSync4 } from "fs";
|
|
3603
|
+
import { join as join7 } from "path";
|
|
3604
|
+
var statusCommand = defineCommand5({
|
|
3605
|
+
meta: {
|
|
3606
|
+
name: "status",
|
|
3607
|
+
description: "Show current Fornix project configuration and installed blocks"
|
|
3608
|
+
},
|
|
3609
|
+
args: {
|
|
3610
|
+
json: {
|
|
3611
|
+
type: "boolean",
|
|
3612
|
+
description: "Output as JSON",
|
|
3613
|
+
default: false
|
|
3614
|
+
},
|
|
3615
|
+
verbose: {
|
|
3616
|
+
type: "boolean",
|
|
3617
|
+
description: "Show full configuration details",
|
|
3618
|
+
default: false
|
|
3619
|
+
}
|
|
3620
|
+
},
|
|
3621
|
+
run({ args }) {
|
|
3622
|
+
const typedArgs = args;
|
|
3623
|
+
const cwd = process.cwd();
|
|
3624
|
+
const manifestPath = join7(cwd, "fornix.json");
|
|
3625
|
+
if (!existsSync4(manifestPath)) {
|
|
3626
|
+
console.error(
|
|
3627
|
+
pc7.red("\u2717 No fornix.json found in the current directory.")
|
|
3628
|
+
);
|
|
3629
|
+
console.error(
|
|
3630
|
+
pc7.dim(
|
|
3631
|
+
" Run this command from inside a Fornix project, or create one with: npx create-fornix"
|
|
3632
|
+
)
|
|
3633
|
+
);
|
|
3634
|
+
process.exit(1);
|
|
3635
|
+
}
|
|
3636
|
+
let manifest2;
|
|
3637
|
+
try {
|
|
3638
|
+
const raw = readFileSync5(manifestPath, "utf-8");
|
|
3639
|
+
manifest2 = JSON.parse(raw);
|
|
3640
|
+
} catch {
|
|
3641
|
+
console.error(pc7.red("\u2717 Failed to parse fornix.json."));
|
|
3642
|
+
process.exit(1);
|
|
3643
|
+
}
|
|
3644
|
+
if (typedArgs.json) {
|
|
3645
|
+
console.log(JSON.stringify(manifest2, null, 2));
|
|
3646
|
+
return;
|
|
3647
|
+
}
|
|
3648
|
+
printStatus(manifest2, typedArgs.verbose);
|
|
3649
|
+
}
|
|
3650
|
+
});
|
|
3651
|
+
function printStatus(manifest2, verbose) {
|
|
3652
|
+
console.log();
|
|
3653
|
+
console.log(pc7.bold(" \u{1F4CB} Fornix Project Status"));
|
|
3654
|
+
console.log();
|
|
3655
|
+
console.log(
|
|
3656
|
+
` ${pc7.dim("Render mode:")} ${pc7.bold(pc7.cyan(manifest2.renderMode))}`
|
|
3657
|
+
);
|
|
3658
|
+
console.log(
|
|
3659
|
+
` ${pc7.dim("Deploy target:")} ${pc7.bold(pc7.cyan(manifest2.deployTarget))}`
|
|
3660
|
+
);
|
|
3661
|
+
if (manifest2.palette) {
|
|
3662
|
+
console.log(
|
|
3663
|
+
` ${pc7.dim("Palette:")} ${pc7.bold(manifest2.palette)}`
|
|
3664
|
+
);
|
|
3665
|
+
}
|
|
3666
|
+
if (manifest2.locales && manifest2.locales.length > 0) {
|
|
3667
|
+
const localeStr = manifest2.locales.join(", ");
|
|
3668
|
+
const defaultStr = manifest2.defaultLocale ? ` ${pc7.dim(`(default: ${manifest2.defaultLocale})`)}` : "";
|
|
3669
|
+
console.log(
|
|
3670
|
+
` ${pc7.dim("Locales:")} ${localeStr}${defaultStr}`
|
|
3671
|
+
);
|
|
3672
|
+
}
|
|
3673
|
+
if (manifest2.database && manifest2.database !== "none") {
|
|
3674
|
+
console.log(
|
|
3675
|
+
` ${pc7.dim("Database:")} ${manifest2.database}`
|
|
3676
|
+
);
|
|
3677
|
+
}
|
|
3678
|
+
if (manifest2.themeSwitcher !== void 0) {
|
|
3679
|
+
console.log(
|
|
3680
|
+
` ${pc7.dim("Theme switcher:")} ${manifest2.themeSwitcher ? "enabled" : "disabled"}`
|
|
3681
|
+
);
|
|
3682
|
+
}
|
|
3683
|
+
if (verbose && manifest2.createdWith) {
|
|
3684
|
+
console.log(
|
|
3685
|
+
` ${pc7.dim("Created with:")} ${manifest2.createdWith}`
|
|
3686
|
+
);
|
|
3687
|
+
console.log(
|
|
3688
|
+
` ${pc7.dim("Created at:")} ${manifest2.createdAt}`
|
|
3689
|
+
);
|
|
3690
|
+
}
|
|
3691
|
+
console.log();
|
|
3692
|
+
console.log(
|
|
3693
|
+
` ${pc7.bold("Installed blocks")} ${pc7.dim(`(${manifest2.blocks.length})`)}`
|
|
3694
|
+
);
|
|
3695
|
+
console.log();
|
|
3696
|
+
if (manifest2.blocks.length === 0) {
|
|
3697
|
+
console.log(
|
|
3698
|
+
pc7.dim(" No blocks installed. Add one with: fornix add <block>")
|
|
3699
|
+
);
|
|
3700
|
+
} else {
|
|
3701
|
+
for (const block of manifest2.blocks) {
|
|
3702
|
+
const variant = block.variant !== "default" ? pc7.dim(` [${block.variant}]`) : "";
|
|
3703
|
+
console.log(
|
|
3704
|
+
` ${pc7.green("\u25CF")} ${pc7.bold(block.name)}${variant} ${pc7.dim(`v${block.version}`)}`
|
|
3705
|
+
);
|
|
3706
|
+
if (verbose) {
|
|
3707
|
+
console.log(
|
|
3708
|
+
` ${pc7.dim("Installed:")} ${block.installedAt}`
|
|
3709
|
+
);
|
|
3710
|
+
}
|
|
3711
|
+
}
|
|
3712
|
+
}
|
|
3713
|
+
console.log();
|
|
3714
|
+
}
|
|
3715
|
+
|
|
3716
|
+
// src/cli/commands/doctor.ts
|
|
3717
|
+
import { defineCommand as defineCommand6 } from "citty";
|
|
3718
|
+
import pc8 from "picocolors";
|
|
3719
|
+
import { readFileSync as readFileSync6, existsSync as existsSync5 } from "fs";
|
|
3720
|
+
import { join as join8 } from "path";
|
|
3721
|
+
var doctorCommand = defineCommand6({
|
|
3722
|
+
meta: {
|
|
3723
|
+
name: "doctor",
|
|
3724
|
+
description: "Diagnose common issues in a Fornix project"
|
|
3725
|
+
},
|
|
3726
|
+
args: {
|
|
3727
|
+
json: {
|
|
3728
|
+
type: "boolean",
|
|
3729
|
+
description: "Output as JSON",
|
|
3730
|
+
default: false
|
|
3731
|
+
}
|
|
3732
|
+
},
|
|
3733
|
+
run({ args }) {
|
|
3734
|
+
const cwd = process.cwd();
|
|
3735
|
+
const manifestPath = join8(cwd, "fornix.json");
|
|
3736
|
+
let hasErrors = false;
|
|
3737
|
+
const errors = [];
|
|
3738
|
+
function reportError(msg) {
|
|
3739
|
+
hasErrors = true;
|
|
3740
|
+
errors.push(msg);
|
|
3741
|
+
if (!args.json) {
|
|
3742
|
+
console.error(pc8.red(`\u2717 ${msg}`));
|
|
3743
|
+
}
|
|
3744
|
+
}
|
|
3745
|
+
if (!existsSync5(manifestPath)) {
|
|
3746
|
+
reportError("No fornix.json found in the current directory.");
|
|
3747
|
+
if (args.json) {
|
|
3748
|
+
console.log(JSON.stringify({ healthy: false, errors }));
|
|
3749
|
+
}
|
|
3750
|
+
process.exit(1);
|
|
3751
|
+
}
|
|
3752
|
+
let manifest2;
|
|
3753
|
+
try {
|
|
3754
|
+
const raw = readFileSync6(manifestPath, "utf-8");
|
|
3755
|
+
manifest2 = JSON.parse(raw);
|
|
3756
|
+
} catch {
|
|
3757
|
+
reportError("Failed to parse fornix.json.");
|
|
3758
|
+
if (args.json) {
|
|
3759
|
+
console.log(JSON.stringify({ healthy: false, errors }));
|
|
3760
|
+
}
|
|
3761
|
+
process.exit(1);
|
|
3762
|
+
}
|
|
3763
|
+
const isMultiLocale = manifest2.locales && manifest2.locales.length >= 2;
|
|
3764
|
+
const locales = isMultiLocale ? manifest2.locales : [""];
|
|
3765
|
+
const installedBlocks = new Set(manifest2.blocks.map((b) => b.name));
|
|
3766
|
+
for (const block of manifest2.blocks) {
|
|
3767
|
+
const bManifest = FIXTURE_MANIFESTS[block.name];
|
|
3768
|
+
if (bManifest) {
|
|
3769
|
+
for (const file of bManifest.files) {
|
|
3770
|
+
const filePath = join8(cwd, file.destination);
|
|
3771
|
+
if (!existsSync5(filePath)) {
|
|
3772
|
+
reportError(`Missing expected file for installed block '${block.name}': ${file.destination}`);
|
|
3773
|
+
}
|
|
3774
|
+
}
|
|
3775
|
+
}
|
|
3776
|
+
}
|
|
3777
|
+
for (const [name, bManifest] of Object.entries(FIXTURE_MANIFESTS)) {
|
|
3778
|
+
if (!installedBlocks.has(name)) {
|
|
3779
|
+
const foundOrphaned = bManifest.files.some((file) => {
|
|
3780
|
+
return existsSync5(join8(cwd, file.destination));
|
|
3781
|
+
});
|
|
3782
|
+
if (foundOrphaned) {
|
|
3783
|
+
reportError(`Orphaned block files detected for '${name}'. The block is not in fornix.json.`);
|
|
3784
|
+
}
|
|
3785
|
+
}
|
|
3786
|
+
}
|
|
3787
|
+
let requiresContentConfig = false;
|
|
3788
|
+
const missingContentFiles = [];
|
|
3789
|
+
for (const block of manifest2.blocks) {
|
|
3790
|
+
const bManifest = FIXTURE_MANIFESTS[block.name];
|
|
3791
|
+
if (!bManifest) continue;
|
|
3792
|
+
const hasContentSlots = bManifest.ai?.contentSlots && Object.keys(bManifest.ai.contentSlots).length > 0;
|
|
3793
|
+
const hasCollections = bManifest.collections && bManifest.collections.length > 0;
|
|
3794
|
+
if (hasContentSlots || hasCollections) {
|
|
3795
|
+
requiresContentConfig = true;
|
|
3796
|
+
}
|
|
3797
|
+
if (hasContentSlots) {
|
|
3798
|
+
const subdirectory = getCategory(bManifest.type);
|
|
3799
|
+
for (const locale of locales) {
|
|
3800
|
+
let pathFragment = `src/content/${subdirectory}/${bManifest.name}.json`;
|
|
3801
|
+
if (locale !== "") {
|
|
3802
|
+
pathFragment = `src/content/${locale}/${subdirectory}/${bManifest.name}.json`;
|
|
3803
|
+
}
|
|
3804
|
+
if (!existsSync5(join8(cwd, pathFragment))) {
|
|
3805
|
+
missingContentFiles.push(pathFragment);
|
|
3806
|
+
}
|
|
3807
|
+
}
|
|
3808
|
+
}
|
|
3809
|
+
}
|
|
3810
|
+
if (requiresContentConfig) {
|
|
3811
|
+
if (!existsSync5(join8(cwd, "src/content/config.ts"))) {
|
|
3812
|
+
reportError("Missing expected file: src/content/config.ts");
|
|
3813
|
+
}
|
|
3814
|
+
}
|
|
3815
|
+
for (const missing of missingContentFiles) {
|
|
3816
|
+
reportError(`Broken content reference: missing ${missing}`);
|
|
3817
|
+
}
|
|
3818
|
+
if (hasErrors) {
|
|
3819
|
+
if (args.json) {
|
|
3820
|
+
console.log(JSON.stringify({ healthy: false, errors }));
|
|
3821
|
+
} else {
|
|
3822
|
+
console.log();
|
|
3823
|
+
console.log(pc8.yellow("Run 'fornix add <block>' to repair missing blocks or 'fornix remove <block>' to clean orphaned files."));
|
|
3824
|
+
}
|
|
3825
|
+
process.exit(1);
|
|
3826
|
+
} else {
|
|
3827
|
+
if (args.json) {
|
|
3828
|
+
console.log(JSON.stringify({ healthy: true, errors: [] }));
|
|
3829
|
+
} else {
|
|
3830
|
+
console.log(pc8.green("\u2713 Project is healthy!"));
|
|
3831
|
+
}
|
|
3832
|
+
}
|
|
3833
|
+
}
|
|
3834
|
+
});
|
|
3835
|
+
function getCategory(type) {
|
|
3836
|
+
switch (type) {
|
|
3837
|
+
case "section":
|
|
3838
|
+
return "sections";
|
|
3839
|
+
case "integration":
|
|
3840
|
+
return "integrations";
|
|
3841
|
+
case "feature":
|
|
3842
|
+
return "features";
|
|
3843
|
+
case "layout":
|
|
3844
|
+
return "layouts";
|
|
3845
|
+
default:
|
|
3846
|
+
return type;
|
|
3847
|
+
}
|
|
3848
|
+
}
|
|
3849
|
+
|
|
3850
|
+
// src/cli/commands/mcp.ts
|
|
3851
|
+
import { defineCommand as defineCommand7 } from "citty";
|
|
3852
|
+
|
|
3853
|
+
// src/mcp/server.ts
|
|
3854
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3855
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3856
|
+
import {
|
|
3857
|
+
CallToolRequestSchema,
|
|
3858
|
+
ListResourcesRequestSchema,
|
|
3859
|
+
ListToolsRequestSchema,
|
|
3860
|
+
ReadResourceRequestSchema
|
|
3861
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
3862
|
+
|
|
3863
|
+
// src/mcp/tools/list-blocks.ts
|
|
3864
|
+
function listBlocks(input) {
|
|
3865
|
+
let blocks = Object.values(FIXTURE_MANIFESTS);
|
|
3866
|
+
if (input.type) {
|
|
3867
|
+
const filterType = input.type.toLowerCase();
|
|
3868
|
+
blocks = blocks.filter((block) => block.type === filterType);
|
|
3869
|
+
}
|
|
3870
|
+
if (input.category) {
|
|
3871
|
+
const filterCategory = input.category.toLowerCase();
|
|
3872
|
+
blocks = blocks.filter((block) => block.category === filterCategory);
|
|
3873
|
+
}
|
|
3874
|
+
if (input.search) {
|
|
3875
|
+
const searchTerm = input.search.toLowerCase();
|
|
3876
|
+
blocks = blocks.filter(
|
|
3877
|
+
(block) => block.name.includes(searchTerm) || block.description.toLowerCase().includes(searchTerm) || block.tags.some((tag) => tag.includes(searchTerm))
|
|
3878
|
+
);
|
|
3879
|
+
}
|
|
3880
|
+
const entries = blocks.map((block) => ({
|
|
3881
|
+
name: block.name,
|
|
3882
|
+
type: block.type,
|
|
3883
|
+
category: block.category,
|
|
3884
|
+
description: block.description,
|
|
3885
|
+
tags: block.tags,
|
|
3886
|
+
requiredMode: block.requiredMode,
|
|
3887
|
+
requires: block.requires
|
|
3888
|
+
})).sort((a, b) => a.name.localeCompare(b.name));
|
|
3889
|
+
return ok(entries);
|
|
3890
|
+
}
|
|
3891
|
+
|
|
3892
|
+
// src/mcp/tools/add-block.ts
|
|
3893
|
+
import { readFileSync as readFileSync7, writeFileSync as writeFileSync5, existsSync as existsSync6, mkdirSync as mkdirSync4 } from "fs";
|
|
3894
|
+
import { join as join9, dirname as dirname5 } from "path";
|
|
3895
|
+
function addBlock2(input) {
|
|
3896
|
+
const { name, variant = "default", projectDirectory } = input;
|
|
3897
|
+
const manifestPath = join9(projectDirectory, "fornix.json");
|
|
3898
|
+
if (!existsSync6(manifestPath)) {
|
|
3899
|
+
return err(
|
|
3900
|
+
new Error("No fornix.json found. Not a Fornix project directory.")
|
|
3901
|
+
);
|
|
3902
|
+
}
|
|
3903
|
+
let manifest2;
|
|
3904
|
+
try {
|
|
3905
|
+
const raw = readFileSync7(manifestPath, "utf-8");
|
|
3906
|
+
manifest2 = JSON.parse(raw);
|
|
3907
|
+
} catch {
|
|
3908
|
+
return err(new Error("Failed to parse fornix.json."));
|
|
3909
|
+
}
|
|
3910
|
+
const blockManifest = FIXTURE_MANIFESTS[name];
|
|
3911
|
+
if (!blockManifest) {
|
|
3912
|
+
return err(
|
|
3913
|
+
new Error(
|
|
3914
|
+
`Block '${name}' not found in registry. Available: ${Object.keys(FIXTURE_MANIFESTS).join(", ")}`
|
|
3915
|
+
)
|
|
3916
|
+
);
|
|
3917
|
+
}
|
|
3918
|
+
const installedNames = new Set(manifest2.blocks.map((block) => block.name));
|
|
3919
|
+
if (installedNames.has(name)) {
|
|
3920
|
+
return ok({ addedBlocks: [], filesCreated: 0 });
|
|
3921
|
+
}
|
|
3922
|
+
const blocksToAdd = resolveDependencies3(name, installedNames);
|
|
3923
|
+
for (const blockName of blocksToAdd) {
|
|
3924
|
+
const dependencyManifest = FIXTURE_MANIFESTS[blockName];
|
|
3925
|
+
if (dependencyManifest?.requiredMode && manifest2.renderMode !== dependencyManifest.requiredMode) {
|
|
3926
|
+
return err(
|
|
3927
|
+
new Error(
|
|
3928
|
+
`Block '${blockName}' requires '${dependencyManifest.requiredMode}' mode, but project uses '${manifest2.renderMode}'.`
|
|
3929
|
+
)
|
|
3930
|
+
);
|
|
3931
|
+
}
|
|
3932
|
+
}
|
|
3933
|
+
let filesCreated = 0;
|
|
3934
|
+
for (const blockName of blocksToAdd) {
|
|
3935
|
+
const blockDef = FIXTURE_MANIFESTS[blockName];
|
|
3936
|
+
const sources = FIXTURE_BLOCK_SOURCES[blockName];
|
|
3937
|
+
if (!blockDef || !sources) {
|
|
3938
|
+
return err(
|
|
3939
|
+
new Error(`Source files not found for block '${blockName}'.`)
|
|
3940
|
+
);
|
|
3941
|
+
}
|
|
3942
|
+
for (const file of blockDef.files) {
|
|
3943
|
+
const content = sources[file.source];
|
|
3944
|
+
if (content === void 0) {
|
|
3945
|
+
return err(
|
|
3946
|
+
new Error(
|
|
3947
|
+
`Source file '${file.source}' not found for block '${blockName}'.`
|
|
3948
|
+
)
|
|
3949
|
+
);
|
|
3950
|
+
}
|
|
3951
|
+
const filePath = join9(projectDirectory, file.destination);
|
|
3952
|
+
mkdirSync4(dirname5(filePath), { recursive: true });
|
|
3953
|
+
writeFileSync5(filePath, content);
|
|
3954
|
+
filesCreated++;
|
|
3955
|
+
}
|
|
3956
|
+
}
|
|
3957
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3958
|
+
for (const blockName of blocksToAdd) {
|
|
3959
|
+
const blockDef = FIXTURE_MANIFESTS[blockName];
|
|
3960
|
+
if (!blockDef) continue;
|
|
3961
|
+
manifest2.blocks.push({
|
|
3962
|
+
name: blockName,
|
|
3963
|
+
version: blockDef.version,
|
|
3964
|
+
variant: blockName === name ? variant : "default",
|
|
3965
|
+
installedAt: now
|
|
3966
|
+
});
|
|
3967
|
+
}
|
|
3968
|
+
writeFileSync5(manifestPath, JSON.stringify(manifest2, null, 2) + "\n");
|
|
3969
|
+
return ok({ addedBlocks: blocksToAdd, filesCreated });
|
|
3970
|
+
}
|
|
3971
|
+
function resolveDependencies3(blockName, installedNames) {
|
|
3972
|
+
const result = [];
|
|
3973
|
+
const visited = /* @__PURE__ */ new Set();
|
|
3974
|
+
function walk(currentName) {
|
|
3975
|
+
if (visited.has(currentName) || installedNames.has(currentName)) return;
|
|
3976
|
+
visited.add(currentName);
|
|
3977
|
+
const manifest2 = FIXTURE_MANIFESTS[currentName];
|
|
3978
|
+
if (!manifest2) return;
|
|
3979
|
+
for (const dependency of manifest2.requires) {
|
|
3980
|
+
walk(dependency);
|
|
3981
|
+
}
|
|
3982
|
+
result.push(currentName);
|
|
3983
|
+
}
|
|
3984
|
+
walk(blockName);
|
|
3985
|
+
return result;
|
|
3986
|
+
}
|
|
3987
|
+
|
|
3988
|
+
// src/mcp/tools/remove-block.ts
|
|
3989
|
+
import {
|
|
3990
|
+
readFileSync as readFileSync8,
|
|
3991
|
+
writeFileSync as writeFileSync6,
|
|
3992
|
+
existsSync as existsSync7,
|
|
3993
|
+
unlinkSync as unlinkSync2,
|
|
3994
|
+
readdirSync as readdirSync3,
|
|
3995
|
+
rmdirSync as rmdirSync2
|
|
3996
|
+
} from "fs";
|
|
3997
|
+
import { join as join10, dirname as dirname6 } from "path";
|
|
3998
|
+
function removeBlock(input) {
|
|
3999
|
+
const { name, force = false, projectDirectory } = input;
|
|
4000
|
+
const manifestPath = join10(projectDirectory, "fornix.json");
|
|
4001
|
+
if (!existsSync7(manifestPath)) {
|
|
4002
|
+
return err(
|
|
4003
|
+
new Error("No fornix.json found. Not a Fornix project directory.")
|
|
4004
|
+
);
|
|
4005
|
+
}
|
|
4006
|
+
let manifest2;
|
|
4007
|
+
try {
|
|
4008
|
+
const raw = readFileSync8(manifestPath, "utf-8");
|
|
4009
|
+
manifest2 = JSON.parse(raw);
|
|
4010
|
+
} catch {
|
|
4011
|
+
return err(new Error("Failed to parse fornix.json."));
|
|
4012
|
+
}
|
|
4013
|
+
const installedNames = new Set(manifest2.blocks.map((block) => block.name));
|
|
4014
|
+
if (!installedNames.has(name)) {
|
|
4015
|
+
return err(new Error(`Block '${name}' is not installed.`));
|
|
4016
|
+
}
|
|
4017
|
+
const dependents = findDependents2(name, installedNames);
|
|
4018
|
+
if (dependents.length > 0 && !force) {
|
|
4019
|
+
return err(
|
|
4020
|
+
new Error(
|
|
4021
|
+
`Block '${name}' is required by: ${dependents.join(", ")}. Use force to remove anyway.`
|
|
4022
|
+
)
|
|
4023
|
+
);
|
|
4024
|
+
}
|
|
4025
|
+
const blockManifest = FIXTURE_MANIFESTS[name];
|
|
4026
|
+
const filesToRemove = [];
|
|
4027
|
+
if (blockManifest) {
|
|
4028
|
+
for (const file of blockManifest.files) {
|
|
4029
|
+
const filePath = join10(projectDirectory, file.destination);
|
|
4030
|
+
if (existsSync7(filePath)) {
|
|
4031
|
+
filesToRemove.push(filePath);
|
|
4032
|
+
}
|
|
4033
|
+
}
|
|
4034
|
+
}
|
|
4035
|
+
for (const filePath of filesToRemove) {
|
|
4036
|
+
unlinkSync2(filePath);
|
|
4037
|
+
tryRemoveEmptyDirectory(dirname6(filePath), projectDirectory);
|
|
4038
|
+
}
|
|
4039
|
+
manifest2.blocks = manifest2.blocks.filter((block) => block.name !== name);
|
|
4040
|
+
writeFileSync6(manifestPath, JSON.stringify(manifest2, null, 2) + "\n");
|
|
4041
|
+
return ok({
|
|
4042
|
+
removedBlock: name,
|
|
4043
|
+
filesRemoved: filesToRemove.length,
|
|
4044
|
+
dependentsWarning: dependents
|
|
4045
|
+
});
|
|
4046
|
+
}
|
|
4047
|
+
function findDependents2(blockName, installedNames) {
|
|
4048
|
+
const dependents = [];
|
|
4049
|
+
for (const installedName of installedNames) {
|
|
4050
|
+
const manifest2 = FIXTURE_MANIFESTS[installedName];
|
|
4051
|
+
if (manifest2 && manifest2.requires.includes(blockName)) {
|
|
4052
|
+
dependents.push(installedName);
|
|
4053
|
+
}
|
|
4054
|
+
}
|
|
4055
|
+
return dependents;
|
|
4056
|
+
}
|
|
4057
|
+
function tryRemoveEmptyDirectory(directoryPath, rootPath) {
|
|
4058
|
+
if (directoryPath === rootPath || !directoryPath.startsWith(rootPath)) return;
|
|
4059
|
+
try {
|
|
4060
|
+
const entries = readdirSync3(directoryPath);
|
|
4061
|
+
if (entries.length === 0) {
|
|
4062
|
+
rmdirSync2(directoryPath);
|
|
4063
|
+
tryRemoveEmptyDirectory(dirname6(directoryPath), rootPath);
|
|
4064
|
+
}
|
|
4065
|
+
} catch {
|
|
4066
|
+
}
|
|
4067
|
+
}
|
|
4068
|
+
|
|
4069
|
+
// src/mcp/tools/get-content-schema.ts
|
|
4070
|
+
function getContentSchema(input) {
|
|
4071
|
+
const { collection } = input;
|
|
4072
|
+
const manifest2 = FIXTURE_MANIFESTS[collection];
|
|
4073
|
+
if (!manifest2) {
|
|
4074
|
+
return err(
|
|
4075
|
+
new Error(`Collection '${collection}' not found in registry.`)
|
|
4076
|
+
);
|
|
4077
|
+
}
|
|
4078
|
+
const contentSlots = manifest2.ai?.contentSlots;
|
|
4079
|
+
if (!contentSlots || Object.keys(contentSlots).length === 0) {
|
|
4080
|
+
return err(
|
|
4081
|
+
new Error(
|
|
4082
|
+
`Block '${collection}' does not define content slots.`
|
|
4083
|
+
)
|
|
4084
|
+
);
|
|
4085
|
+
}
|
|
4086
|
+
return ok({
|
|
4087
|
+
collection,
|
|
4088
|
+
slots: contentSlots
|
|
4089
|
+
});
|
|
4090
|
+
}
|
|
4091
|
+
|
|
4092
|
+
// src/mcp/tools/validate-content.ts
|
|
4093
|
+
import { z as z4 } from "zod";
|
|
4094
|
+
function validateContent(input) {
|
|
4095
|
+
const { collection, data } = input;
|
|
4096
|
+
const manifest2 = FIXTURE_MANIFESTS[collection];
|
|
4097
|
+
if (!manifest2) {
|
|
4098
|
+
return err(
|
|
4099
|
+
new Error(
|
|
4100
|
+
`Collection '${collection}' not found in registry.`
|
|
4101
|
+
)
|
|
4102
|
+
);
|
|
4103
|
+
}
|
|
4104
|
+
const contentSlots = manifest2.ai?.contentSlots;
|
|
4105
|
+
if (!contentSlots || Object.keys(contentSlots).length === 0) {
|
|
4106
|
+
return err(
|
|
4107
|
+
new Error(
|
|
4108
|
+
`Block '${collection}' does not define content slots.`
|
|
4109
|
+
)
|
|
4110
|
+
);
|
|
4111
|
+
}
|
|
4112
|
+
const schemaShape = {};
|
|
4113
|
+
for (const [slotName, slot] of Object.entries(contentSlots)) {
|
|
4114
|
+
schemaShape[slotName] = zodTypeForSlot2(slot.type);
|
|
4115
|
+
}
|
|
4116
|
+
const schema = z4.object(schemaShape);
|
|
4117
|
+
const parseResult = schema.safeParse(data);
|
|
4118
|
+
if (parseResult.success) {
|
|
4119
|
+
return ok({ valid: true, errors: [] });
|
|
4120
|
+
}
|
|
4121
|
+
const errors = parseResult.error.issues.map(
|
|
4122
|
+
(issue) => `${issue.path.join(".")}: ${issue.message}`
|
|
4123
|
+
);
|
|
4124
|
+
return ok({ valid: false, errors });
|
|
4125
|
+
}
|
|
4126
|
+
function zodTypeForSlot2(slotType) {
|
|
4127
|
+
switch (slotType) {
|
|
4128
|
+
case "string":
|
|
4129
|
+
return z4.string();
|
|
4130
|
+
case "number":
|
|
4131
|
+
return z4.number();
|
|
4132
|
+
case "boolean":
|
|
4133
|
+
return z4.boolean();
|
|
4134
|
+
case "array":
|
|
4135
|
+
return z4.array(z4.unknown());
|
|
4136
|
+
case "object":
|
|
4137
|
+
return z4.record(z4.unknown());
|
|
4138
|
+
default:
|
|
4139
|
+
return z4.unknown();
|
|
4140
|
+
}
|
|
4141
|
+
}
|
|
4142
|
+
|
|
4143
|
+
// src/mcp/tools/get-project-status.ts
|
|
4144
|
+
import { readFileSync as readFileSync9, existsSync as existsSync8 } from "fs";
|
|
4145
|
+
import { join as join11 } from "path";
|
|
4146
|
+
function getProjectStatus(input) {
|
|
4147
|
+
const manifestPath = join11(input.projectDirectory, "fornix.json");
|
|
4148
|
+
if (!existsSync8(manifestPath)) {
|
|
4149
|
+
return err(
|
|
4150
|
+
new Error("No fornix.json found. Not a Fornix project directory.")
|
|
4151
|
+
);
|
|
4152
|
+
}
|
|
4153
|
+
try {
|
|
4154
|
+
const raw = readFileSync9(manifestPath, "utf-8");
|
|
4155
|
+
const manifest2 = JSON.parse(raw);
|
|
4156
|
+
const blocks = manifest2.blocks;
|
|
4157
|
+
return ok({
|
|
4158
|
+
version: manifest2.version,
|
|
4159
|
+
createdAt: manifest2.createdAt,
|
|
4160
|
+
createdWith: manifest2.createdWith,
|
|
4161
|
+
renderMode: manifest2.renderMode,
|
|
4162
|
+
deployTarget: manifest2.deployTarget,
|
|
4163
|
+
database: manifest2.database ?? "none",
|
|
4164
|
+
locales: manifest2.locales ?? ["en"],
|
|
4165
|
+
defaultLocale: manifest2.defaultLocale ?? "en",
|
|
4166
|
+
palette: manifest2.palette,
|
|
4167
|
+
themeSwitcher: manifest2.themeSwitcher ?? false,
|
|
4168
|
+
blocks: blocks ?? []
|
|
4169
|
+
});
|
|
4170
|
+
} catch {
|
|
4171
|
+
return err(new Error("Failed to parse fornix.json."));
|
|
4172
|
+
}
|
|
4173
|
+
}
|
|
4174
|
+
|
|
4175
|
+
// src/mcp/tools/scaffold-project.ts
|
|
4176
|
+
import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync7 } from "fs";
|
|
4177
|
+
import { join as join12, basename as basename3 } from "path";
|
|
4178
|
+
var DEFAULT_COLORS2 = {
|
|
4179
|
+
primary: "#6366f1",
|
|
4180
|
+
secondary: "#818cf8",
|
|
4181
|
+
accent: "#c084fc",
|
|
4182
|
+
background: "#0f172a",
|
|
4183
|
+
foreground: "#f8fafc"
|
|
4184
|
+
};
|
|
4185
|
+
function scaffoldProject(input) {
|
|
4186
|
+
const {
|
|
4187
|
+
projectDirectory,
|
|
4188
|
+
renderMode = "static",
|
|
4189
|
+
deployTarget = "cloudflare",
|
|
4190
|
+
blocks = [],
|
|
4191
|
+
locales = ["en"]
|
|
4192
|
+
} = input;
|
|
4193
|
+
const projectName = basename3(projectDirectory);
|
|
4194
|
+
const blockSelections = blocks.map((name) => ({ name, variant: "default" }));
|
|
4195
|
+
const allPalettes = loadAllPalettes();
|
|
4196
|
+
const config = {
|
|
4197
|
+
projectName,
|
|
4198
|
+
projectDir: projectDirectory,
|
|
4199
|
+
renderMode,
|
|
4200
|
+
deployTarget,
|
|
4201
|
+
database: "none",
|
|
4202
|
+
cssEngine: "tailwind",
|
|
4203
|
+
packageManager: "pnpm",
|
|
4204
|
+
blocks: blockSelections,
|
|
4205
|
+
locales: locales.length > 0 ? [...locales] : ["en"],
|
|
4206
|
+
defaultLocale: locales[0] ?? "en",
|
|
4207
|
+
palette: {
|
|
4208
|
+
colors: { ...DEFAULT_COLORS2 }
|
|
4209
|
+
},
|
|
4210
|
+
themeSwitcher: false,
|
|
4211
|
+
createdWith: "mcp"
|
|
4212
|
+
};
|
|
4213
|
+
const scaffoldInput = {
|
|
4214
|
+
config,
|
|
4215
|
+
manifests: FIXTURE_MANIFESTS,
|
|
4216
|
+
blockSources: FIXTURE_BLOCK_SOURCES,
|
|
4217
|
+
blockDefaultContent: FIXTURE_DEFAULT_CONTENT,
|
|
4218
|
+
allPalettes
|
|
4219
|
+
};
|
|
4220
|
+
const result = scaffold(scaffoldInput);
|
|
4221
|
+
if (!isOk(result)) {
|
|
4222
|
+
return err(result.error);
|
|
4223
|
+
}
|
|
4224
|
+
const files = result.value.files;
|
|
4225
|
+
let filesCreated = 0;
|
|
4226
|
+
for (const [relativePath, content] of Object.entries(files)) {
|
|
4227
|
+
const fullPath = join12(projectDirectory, relativePath);
|
|
4228
|
+
const parentDirectory = join12(fullPath, "..");
|
|
4229
|
+
mkdirSync5(parentDirectory, { recursive: true });
|
|
4230
|
+
writeFileSync7(fullPath, content, "utf-8");
|
|
4231
|
+
filesCreated++;
|
|
4232
|
+
}
|
|
4233
|
+
return ok({
|
|
4234
|
+
projectDirectory,
|
|
4235
|
+
filesCreated,
|
|
4236
|
+
blocks: result.value.resolvedBlockNames
|
|
4237
|
+
});
|
|
4238
|
+
}
|
|
4239
|
+
|
|
4240
|
+
// src/mcp/server.ts
|
|
4241
|
+
var TOOL_DEFINITIONS = [
|
|
4242
|
+
{
|
|
4243
|
+
name: "list_blocks",
|
|
4244
|
+
description: "List available blocks from the Fornix registry",
|
|
4245
|
+
inputSchema: {
|
|
4246
|
+
type: "object",
|
|
4247
|
+
properties: {
|
|
4248
|
+
type: { type: "string", description: "Filter by block type (section, integration, feature, layout)" },
|
|
4249
|
+
category: { type: "string", description: "Filter by category" },
|
|
4250
|
+
search: { type: "string", description: "Search term to filter blocks" }
|
|
4251
|
+
}
|
|
4252
|
+
}
|
|
4253
|
+
},
|
|
4254
|
+
{
|
|
4255
|
+
name: "add_block",
|
|
4256
|
+
description: "Add a block to an existing Fornix project",
|
|
4257
|
+
inputSchema: {
|
|
4258
|
+
type: "object",
|
|
4259
|
+
properties: {
|
|
4260
|
+
name: { type: "string", description: "Block name to add" },
|
|
4261
|
+
variant: { type: "string", description: "Block variant (default: 'default')" },
|
|
4262
|
+
projectDirectory: { type: "string", description: "Path to the Fornix project directory" }
|
|
4263
|
+
},
|
|
4264
|
+
required: ["name", "projectDirectory"]
|
|
4265
|
+
}
|
|
4266
|
+
},
|
|
4267
|
+
{
|
|
4268
|
+
name: "remove_block",
|
|
4269
|
+
description: "Remove a block from an existing Fornix project",
|
|
4270
|
+
inputSchema: {
|
|
4271
|
+
type: "object",
|
|
4272
|
+
properties: {
|
|
4273
|
+
name: { type: "string", description: "Block name to remove" },
|
|
4274
|
+
force: { type: "boolean", description: "Force removal even if other blocks depend on it" },
|
|
4275
|
+
projectDirectory: { type: "string", description: "Path to the Fornix project directory" }
|
|
4276
|
+
},
|
|
4277
|
+
required: ["name", "projectDirectory"]
|
|
4278
|
+
}
|
|
4279
|
+
},
|
|
4280
|
+
{
|
|
4281
|
+
name: "get_content_schema",
|
|
4282
|
+
description: "Get the content slot schema for a block collection",
|
|
4283
|
+
inputSchema: {
|
|
4284
|
+
type: "object",
|
|
4285
|
+
properties: {
|
|
4286
|
+
collection: { type: "string", description: "Block/collection name" }
|
|
4287
|
+
},
|
|
4288
|
+
required: ["collection"]
|
|
4289
|
+
}
|
|
4290
|
+
},
|
|
4291
|
+
{
|
|
4292
|
+
name: "update_content",
|
|
4293
|
+
description: "Modify content entries for a block collection",
|
|
4294
|
+
inputSchema: {
|
|
4295
|
+
type: "object",
|
|
4296
|
+
properties: {
|
|
4297
|
+
collection: { type: "string", description: "Block/collection name" },
|
|
4298
|
+
entry: { type: "string", description: "Entry identifier" },
|
|
4299
|
+
data: { type: "object", description: "Content data to set" }
|
|
4300
|
+
},
|
|
4301
|
+
required: ["collection", "data"]
|
|
4302
|
+
}
|
|
4303
|
+
},
|
|
4304
|
+
{
|
|
4305
|
+
name: "validate_content",
|
|
4306
|
+
description: "Validate content data against a block's content schema",
|
|
4307
|
+
inputSchema: {
|
|
4308
|
+
type: "object",
|
|
4309
|
+
properties: {
|
|
4310
|
+
collection: { type: "string", description: "Block/collection name" },
|
|
4311
|
+
data: { type: "object", description: "Content data to validate" }
|
|
4312
|
+
},
|
|
4313
|
+
required: ["collection", "data"]
|
|
4314
|
+
}
|
|
4315
|
+
},
|
|
4316
|
+
{
|
|
4317
|
+
name: "get_project_status",
|
|
4318
|
+
description: "Get the current Fornix project configuration and installed blocks",
|
|
4319
|
+
inputSchema: {
|
|
4320
|
+
type: "object",
|
|
4321
|
+
properties: {
|
|
4322
|
+
projectDirectory: { type: "string", description: "Path to the Fornix project directory" }
|
|
4323
|
+
},
|
|
4324
|
+
required: ["projectDirectory"]
|
|
4325
|
+
}
|
|
4326
|
+
},
|
|
4327
|
+
{
|
|
4328
|
+
name: "scaffold_project",
|
|
4329
|
+
description: "Scaffold a complete Fornix project from a natural language description",
|
|
4330
|
+
inputSchema: {
|
|
4331
|
+
type: "object",
|
|
4332
|
+
properties: {
|
|
4333
|
+
description: { type: "string", description: "Natural language description of the project" },
|
|
4334
|
+
projectDirectory: { type: "string", description: "Path to create the project in" },
|
|
4335
|
+
renderMode: { type: "string", description: "Render mode: static, hybrid, server" },
|
|
4336
|
+
deployTarget: { type: "string", description: "Deploy target: cloudflare, vercel, netlify, static" },
|
|
4337
|
+
blocks: { type: "array", items: { type: "string" }, description: "Block names to include" },
|
|
4338
|
+
locales: { type: "array", items: { type: "string" }, description: "Locale codes" }
|
|
4339
|
+
},
|
|
4340
|
+
required: ["description", "projectDirectory"]
|
|
4341
|
+
}
|
|
4342
|
+
}
|
|
4343
|
+
];
|
|
4344
|
+
var FornixMCPServer = class {
|
|
4345
|
+
server;
|
|
4346
|
+
registeredTools = [];
|
|
4347
|
+
registeredResources = [];
|
|
4348
|
+
constructor() {
|
|
4349
|
+
this.server = new Server(
|
|
4350
|
+
{
|
|
4351
|
+
name: "fornix-mcp",
|
|
4352
|
+
version: "0.0.1"
|
|
4353
|
+
},
|
|
4354
|
+
{
|
|
4355
|
+
capabilities: {
|
|
4356
|
+
resources: {},
|
|
4357
|
+
tools: {}
|
|
4358
|
+
}
|
|
4359
|
+
}
|
|
4360
|
+
);
|
|
4361
|
+
this.registerTools();
|
|
4362
|
+
this.registerResources();
|
|
4363
|
+
this.setupHandlers();
|
|
4364
|
+
}
|
|
4365
|
+
registerTools() {
|
|
4366
|
+
this.registeredTools = TOOL_DEFINITIONS.map(
|
|
4367
|
+
(definition) => definition.name
|
|
4368
|
+
);
|
|
4369
|
+
}
|
|
4370
|
+
registerResources() {
|
|
4371
|
+
this.registeredResources = [
|
|
4372
|
+
"fornix://registry",
|
|
4373
|
+
"fornix://project/config"
|
|
4374
|
+
];
|
|
4375
|
+
}
|
|
4376
|
+
setupHandlers() {
|
|
4377
|
+
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
4378
|
+
return {
|
|
4379
|
+
tools: TOOL_DEFINITIONS.map((definition) => ({
|
|
4380
|
+
name: definition.name,
|
|
4381
|
+
description: definition.description,
|
|
4382
|
+
inputSchema: definition.inputSchema
|
|
4383
|
+
}))
|
|
4384
|
+
};
|
|
4385
|
+
});
|
|
4386
|
+
this.server.setRequestHandler(
|
|
4387
|
+
CallToolRequestSchema,
|
|
4388
|
+
async (request) => {
|
|
4389
|
+
const toolName = request.params.name;
|
|
4390
|
+
const args = request.params.arguments ?? {};
|
|
4391
|
+
const result = await this.executeTool(toolName, args);
|
|
4392
|
+
if (!result.ok) {
|
|
4393
|
+
return {
|
|
4394
|
+
content: [
|
|
4395
|
+
{
|
|
4396
|
+
type: "text",
|
|
4397
|
+
text: JSON.stringify({ error: result.error.message })
|
|
4398
|
+
}
|
|
4399
|
+
],
|
|
4400
|
+
isError: true
|
|
4401
|
+
};
|
|
4402
|
+
}
|
|
4403
|
+
return {
|
|
4404
|
+
content: [
|
|
4405
|
+
{
|
|
4406
|
+
type: "text",
|
|
4407
|
+
text: result.value
|
|
4408
|
+
}
|
|
4409
|
+
]
|
|
4410
|
+
};
|
|
4411
|
+
}
|
|
4412
|
+
);
|
|
4413
|
+
this.server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
4414
|
+
return {
|
|
4415
|
+
resources: this.registeredResources.map((uri) => ({
|
|
4416
|
+
uri,
|
|
4417
|
+
name: uri,
|
|
4418
|
+
description: `Fornix MCP Resource: ${uri}`
|
|
4419
|
+
}))
|
|
4420
|
+
};
|
|
4421
|
+
});
|
|
4422
|
+
this.server.setRequestHandler(
|
|
4423
|
+
ReadResourceRequestSchema,
|
|
4424
|
+
async (request) => {
|
|
4425
|
+
const uri = request.params.uri;
|
|
4426
|
+
if (uri === "fornix://registry") {
|
|
4427
|
+
const blocks = Object.values(FIXTURE_MANIFESTS).map((manifest2) => ({
|
|
4428
|
+
name: manifest2.name,
|
|
4429
|
+
type: manifest2.type,
|
|
4430
|
+
category: manifest2.category,
|
|
4431
|
+
description: manifest2.description
|
|
4432
|
+
}));
|
|
4433
|
+
return {
|
|
4434
|
+
contents: [
|
|
4435
|
+
{
|
|
4436
|
+
uri,
|
|
4437
|
+
mimeType: "application/json",
|
|
4438
|
+
text: JSON.stringify(blocks, null, 2)
|
|
4439
|
+
}
|
|
4440
|
+
]
|
|
4441
|
+
};
|
|
4442
|
+
}
|
|
4443
|
+
if (uri === "fornix://project/config") {
|
|
4444
|
+
return {
|
|
4445
|
+
contents: [
|
|
4446
|
+
{
|
|
4447
|
+
uri,
|
|
4448
|
+
mimeType: "application/json",
|
|
4449
|
+
text: JSON.stringify({
|
|
4450
|
+
message: "Use get_project_status tool with a projectDirectory to read project config."
|
|
4451
|
+
})
|
|
4452
|
+
}
|
|
4453
|
+
]
|
|
4454
|
+
};
|
|
4455
|
+
}
|
|
4456
|
+
throw new Error(`Unknown resource: ${uri}`);
|
|
4457
|
+
}
|
|
4458
|
+
);
|
|
4459
|
+
}
|
|
4460
|
+
/**
|
|
4461
|
+
* Execute a tool by name with the given arguments.
|
|
4462
|
+
* Exposed as a public method for testing without MCP transport.
|
|
4463
|
+
*/
|
|
4464
|
+
async callTool(toolName, args) {
|
|
4465
|
+
return this.executeTool(toolName, args);
|
|
4466
|
+
}
|
|
4467
|
+
async executeTool(toolName, args) {
|
|
4468
|
+
switch (toolName) {
|
|
4469
|
+
case "list_blocks": {
|
|
4470
|
+
const result = listBlocks({
|
|
4471
|
+
type: args.type,
|
|
4472
|
+
category: args.category,
|
|
4473
|
+
search: args.search
|
|
4474
|
+
});
|
|
4475
|
+
if (!result.ok) return err(result.error);
|
|
4476
|
+
return ok(JSON.stringify(result.value, null, 2));
|
|
4477
|
+
}
|
|
4478
|
+
case "add_block": {
|
|
4479
|
+
const result = addBlock2({
|
|
4480
|
+
name: args.name,
|
|
4481
|
+
variant: args.variant,
|
|
4482
|
+
projectDirectory: args.projectDirectory
|
|
4483
|
+
});
|
|
4484
|
+
if (!result.ok) return err(result.error);
|
|
4485
|
+
return ok(JSON.stringify(result.value, null, 2));
|
|
4486
|
+
}
|
|
4487
|
+
case "remove_block": {
|
|
4488
|
+
const result = removeBlock({
|
|
4489
|
+
name: args.name,
|
|
4490
|
+
force: args.force,
|
|
4491
|
+
projectDirectory: args.projectDirectory
|
|
4492
|
+
});
|
|
4493
|
+
if (!result.ok) return err(result.error);
|
|
4494
|
+
return ok(JSON.stringify(result.value, null, 2));
|
|
4495
|
+
}
|
|
4496
|
+
case "get_content_schema": {
|
|
4497
|
+
const result = getContentSchema({
|
|
4498
|
+
collection: args.collection
|
|
4499
|
+
});
|
|
4500
|
+
if (!result.ok) return err(result.error);
|
|
4501
|
+
return ok(JSON.stringify(result.value, null, 2));
|
|
4502
|
+
}
|
|
4503
|
+
case "update_content": {
|
|
4504
|
+
const validationResult = validateContent({
|
|
4505
|
+
collection: args.collection,
|
|
4506
|
+
data: args.data
|
|
4507
|
+
});
|
|
4508
|
+
if (!validationResult.ok) return err(validationResult.error);
|
|
4509
|
+
if (!validationResult.value.valid) {
|
|
4510
|
+
return ok(
|
|
4511
|
+
JSON.stringify({
|
|
4512
|
+
updated: false,
|
|
4513
|
+
errors: validationResult.value.errors
|
|
4514
|
+
})
|
|
4515
|
+
);
|
|
4516
|
+
}
|
|
4517
|
+
return ok(
|
|
4518
|
+
JSON.stringify({
|
|
4519
|
+
updated: true,
|
|
4520
|
+
collection: args.collection,
|
|
4521
|
+
data: args.data
|
|
4522
|
+
})
|
|
4523
|
+
);
|
|
4524
|
+
}
|
|
4525
|
+
case "validate_content": {
|
|
4526
|
+
const result = validateContent({
|
|
4527
|
+
collection: args.collection,
|
|
4528
|
+
data: args.data
|
|
4529
|
+
});
|
|
4530
|
+
if (!result.ok) return err(result.error);
|
|
4531
|
+
return ok(JSON.stringify(result.value, null, 2));
|
|
4532
|
+
}
|
|
4533
|
+
case "get_project_status": {
|
|
4534
|
+
const result = getProjectStatus({
|
|
4535
|
+
projectDirectory: args.projectDirectory
|
|
4536
|
+
});
|
|
4537
|
+
if (!result.ok) return err(result.error);
|
|
4538
|
+
return ok(JSON.stringify(result.value, null, 2));
|
|
4539
|
+
}
|
|
4540
|
+
case "scaffold_project": {
|
|
4541
|
+
const result = scaffoldProject({
|
|
4542
|
+
description: args.description,
|
|
4543
|
+
projectDirectory: args.projectDirectory,
|
|
4544
|
+
renderMode: args.renderMode,
|
|
4545
|
+
deployTarget: args.deployTarget,
|
|
4546
|
+
blocks: args.blocks,
|
|
4547
|
+
locales: args.locales
|
|
4548
|
+
});
|
|
4549
|
+
if (!result.ok) return err(result.error);
|
|
4550
|
+
return ok(JSON.stringify(result.value, null, 2));
|
|
4551
|
+
}
|
|
4552
|
+
default:
|
|
4553
|
+
return err(new Error(`Unknown tool: ${toolName}`));
|
|
4554
|
+
}
|
|
4555
|
+
}
|
|
4556
|
+
// Exposed for TDD checks
|
|
4557
|
+
getRegisteredTools() {
|
|
4558
|
+
return this.registeredTools;
|
|
4559
|
+
}
|
|
4560
|
+
getRegisteredResources() {
|
|
4561
|
+
return this.registeredResources;
|
|
4562
|
+
}
|
|
4563
|
+
async start() {
|
|
4564
|
+
const transport = new StdioServerTransport();
|
|
4565
|
+
await this.server.connect(transport);
|
|
4566
|
+
}
|
|
4567
|
+
};
|
|
4568
|
+
|
|
4569
|
+
// src/cli/commands/mcp.ts
|
|
4570
|
+
var serveCommand = defineCommand7({
|
|
4571
|
+
meta: {
|
|
4572
|
+
name: "serve",
|
|
4573
|
+
description: "Start the Fornix MCP Server over stdio"
|
|
4574
|
+
},
|
|
4575
|
+
async run() {
|
|
4576
|
+
console.error("Starting Fornix MCP Server...");
|
|
4577
|
+
const mcpServer = new FornixMCPServer();
|
|
4578
|
+
await mcpServer.start();
|
|
4579
|
+
console.error("Fornix MCP Server running on stdio.");
|
|
4580
|
+
}
|
|
4581
|
+
});
|
|
4582
|
+
var mcpCommand = defineCommand7({
|
|
4583
|
+
meta: {
|
|
4584
|
+
name: "mcp",
|
|
4585
|
+
description: "Manage and run the Model Context Protocol (MCP) server"
|
|
4586
|
+
},
|
|
4587
|
+
subCommands: {
|
|
4588
|
+
serve: serveCommand
|
|
4589
|
+
}
|
|
4590
|
+
});
|
|
4591
|
+
|
|
4592
|
+
// src/cli/index.ts
|
|
4593
|
+
var main = defineCommand8({
|
|
4594
|
+
meta: {
|
|
4595
|
+
name: "fornix",
|
|
4596
|
+
version: "0.0.1",
|
|
4597
|
+
description: "Fornix \u2014 CLI-first Astro + Cloudflare project generator"
|
|
4598
|
+
},
|
|
4599
|
+
subCommands: {
|
|
4600
|
+
create: createCommand,
|
|
4601
|
+
add: addCommand,
|
|
4602
|
+
remove: removeCommand,
|
|
4603
|
+
list: listCommand,
|
|
4604
|
+
status: statusCommand,
|
|
4605
|
+
doctor: doctorCommand,
|
|
4606
|
+
mcp: mcpCommand
|
|
4607
|
+
}
|
|
4608
|
+
});
|
|
4609
|
+
|
|
4610
|
+
// src/index.ts
|
|
4611
|
+
runMain(main);
|
|
4612
|
+
//# sourceMappingURL=index.js.map
|