create-fornix 0.0.9 → 0.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -216,6 +216,7 @@ function topologicalSort(blocks, manifests) {
216
216
  function generateStructure(config, manifests = []) {
217
217
  const files = {};
218
218
  const adapterDeps = getAdapterDependencies(config);
219
+ const blockDeps = getBlockDependencies(manifests);
219
220
  const pkg = {
220
221
  name: config.projectName,
221
222
  type: "module",
@@ -234,10 +235,14 @@ function generateStructure(config, manifests = []) {
234
235
  tailwindcss: "^4.0.0",
235
236
  "@tailwindcss/vite": "^4.0.0"
236
237
  },
237
- ...adapterDeps
238
+ ...adapterDeps,
239
+ ...blockDeps
238
240
  },
239
241
  devDependencies: {
240
- typescript: "^5.7.0"
242
+ typescript: "^5.7.0",
243
+ ...config.deployTarget === "cloudflare" && {
244
+ "@cloudflare/workers-types": "^4.0.0"
245
+ }
241
246
  }
242
247
  };
243
248
  files["package.json"] = JSON.stringify(pkg, null, 2) + "\n";
@@ -246,9 +251,11 @@ function generateStructure(config, manifests = []) {
246
251
  compilerOptions: {
247
252
  jsx: "preserve",
248
253
  jsxImportSource: "react",
249
- // Even if not using React yet, good default for UI frameworks
250
254
  strictNullChecks: true,
251
255
  baseUrl: ".",
256
+ ...config.deployTarget === "cloudflare" && {
257
+ types: ["@cloudflare/workers-types"]
258
+ },
252
259
  paths: {
253
260
  "@/*": ["src/*"]
254
261
  }
@@ -278,42 +285,59 @@ pnpm-debug.log*
278
285
  .DS_Store
279
286
  Thumbs.db
280
287
  `.trim() + "\n";
281
- const blockImports = [];
282
- const blockComponents = [];
288
+ const LAYOUT_CATEGORIES2 = /* @__PURE__ */ new Set(["header", "footer"]);
289
+ const headerImports = [];
290
+ const headerComponents = [];
291
+ const contentImports = [];
292
+ const contentComponents = [];
293
+ const footerImports = [];
294
+ const footerComponents = [];
283
295
  if (manifests.length > 0) {
284
296
  for (const manifest2 of manifests) {
285
297
  if (manifest2.type !== "section") continue;
286
298
  const mainFile = manifest2.files.find((f) => f.destination.endsWith(".astro") || f.destination.endsWith(".tsx"));
287
- if (mainFile) {
288
- const componentName = manifest2.name.split("-").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
289
- let importPath = mainFile.destination;
290
- if (importPath.startsWith("src/")) {
291
- importPath = "../" + importPath.substring(4);
292
- }
293
- blockImports.push(`import ${componentName} from '${importPath}';`);
294
- blockComponents.push(` <${componentName} />`);
299
+ if (!mainFile) continue;
300
+ const componentName = manifest2.name.split("-").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
301
+ let importPath = mainFile.destination;
302
+ if (importPath.startsWith("src/")) {
303
+ importPath = "../" + importPath.substring(4);
304
+ }
305
+ const category = manifest2.category ?? "other";
306
+ if (category === "header") {
307
+ headerImports.push(`import ${componentName} from '${importPath}';`);
308
+ headerComponents.push(` <${componentName} />`);
309
+ } else if (category === "footer") {
310
+ footerImports.push(`import ${componentName} from '${importPath}';`);
311
+ footerComponents.push(` <${componentName} />`);
312
+ } else {
313
+ contentImports.push(`import ${componentName} from '${importPath}';`);
314
+ contentComponents.push(` <${componentName} />`);
295
315
  }
296
316
  }
297
317
  }
298
318
  const indexAstroContent = `
299
319
  ---
300
320
  import Layout from '../layouts/Layout.astro';
301
- ${blockImports.join("\n")}
321
+ ${contentImports.join("\n")}
302
322
  ---
303
323
  <Layout title="Welcome to ${config.projectName}.">
304
324
  <main>
305
- ${blockComponents.length > 0 ? blockComponents.join("\n") : ` <h1>Welcome to <span class="text-gradient">${config.projectName}</span></h1>`}
325
+ ${contentComponents.length > 0 ? contentComponents.join("\n") : ` <h1>Welcome to <span class="text-gradient">${config.projectName}</span></h1>`}
306
326
  </main>
307
327
  </Layout>
308
328
  `.trim() + "\n";
309
329
  files["src/pages/index.astro"] = indexAstroContent;
310
330
  const tailwindImport = config.cssEngine === "tailwind" ? '\nimport "../../tailwind.css";' : "";
331
+ const layoutImportsStr = [...headerImports, ...footerImports].join("\n");
332
+ const layoutImportSection = layoutImportsStr ? "\n" + layoutImportsStr : "";
333
+ const headerSection = headerComponents.length > 0 ? "\n" + headerComponents.join("\n") + "\n" : "";
334
+ const footerSection = footerComponents.length > 0 ? "\n" + footerComponents.join("\n") : "";
311
335
  files["src/layouts/Layout.astro"] = `
312
336
  ---
313
337
  interface Props {
314
338
  title: string;
315
339
  }
316
- const { title } = Astro.props;${tailwindImport}
340
+ const { title } = Astro.props;${tailwindImport}${layoutImportSection}
317
341
  ---
318
342
  <!doctype html>
319
343
  <html lang="en">
@@ -325,8 +349,8 @@ const { title } = Astro.props;${tailwindImport}
325
349
  <meta name="generator" content={Astro.generator} />
326
350
  <title>{title}</title>
327
351
  </head>
328
- <body>
329
- <slot />
352
+ <body>${headerSection}
353
+ <slot />${footerSection}
330
354
  </body>
331
355
  </html>
332
356
 
@@ -378,6 +402,15 @@ const { title } = Astro.props;${tailwindImport}
378
402
  }
379
403
  return files;
380
404
  }
405
+ function getBlockDependencies(manifests) {
406
+ const merged = {};
407
+ for (const manifest2 of manifests) {
408
+ if (manifest2.dependencies) {
409
+ Object.assign(merged, manifest2.dependencies);
410
+ }
411
+ }
412
+ return merged;
413
+ }
381
414
  function getAdapterDependencies(config) {
382
415
  if (config.renderMode === "static" && config.deployTarget === "static") {
383
416
  return {};
@@ -502,11 +535,35 @@ function buildPaletteFile(colors) {
502
535
  const properties = COLOR_TOKENS.map(
503
536
  (token) => ` --color-${token}: ${colors[token]};`
504
537
  ).join("\n");
538
+ const surface = blendHex(colors.background, colors.foreground, 0.08);
539
+ const muted = blendHex(colors.foreground, colors.background, 0.4);
540
+ const derived = [
541
+ ` --color-surface: ${surface};`,
542
+ ` --color-muted: ${muted};`
543
+ ].join("\n");
505
544
  return `:root {
506
545
  ${properties}
546
+ ${derived}
507
547
  }
508
548
  `;
509
549
  }
550
+ function blendHex(colorA, colorB, ratio) {
551
+ const a = parseHex(colorA);
552
+ const b = parseHex(colorB);
553
+ const r = Math.round(a.r + (b.r - a.r) * ratio);
554
+ const g = Math.round(a.g + (b.g - a.g) * ratio);
555
+ const bl = Math.round(a.b + (b.b - a.b) * ratio);
556
+ return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${bl.toString(16).padStart(2, "0")}`;
557
+ }
558
+ function parseHex(hex) {
559
+ const h = hex.replace("#", "");
560
+ const full = h.length === 3 ? h.split("").map((c) => c + c).join("") : h;
561
+ return {
562
+ r: parseInt(full.slice(0, 2), 16),
563
+ g: parseInt(full.slice(2, 4), 16),
564
+ b: parseInt(full.slice(4, 6), 16)
565
+ };
566
+ }
510
567
  function buildSwitcherScript(paletteNames) {
511
568
  return `(function () {
512
569
  const PALETTES = ${JSON.stringify(paletteNames)};
@@ -706,8 +763,17 @@ function wireI18n(config, manifests) {
706
763
  }
707
764
  files["src/i18n/utils.ts"] = generateI18nUtils(config);
708
765
  files["src/pages/[locale]/index.astro"] = generateLocaleIndexPage(config, manifests ?? []);
766
+ files["src/pages/index.astro"] = generateRootRedirect(config);
709
767
  return ok(files);
710
768
  }
769
+ function generateRootRedirect(config) {
770
+ const needsPrerender = config.renderMode === "server" || config.renderMode === "hybrid";
771
+ const prerenderLine = needsPrerender ? "export const prerender = true;\n" : "";
772
+ return `---
773
+ ${prerenderLine}return Astro.redirect("/${config.defaultLocale}/");
774
+ ---
775
+ `;
776
+ }
711
777
  function generateI18nUtils(config) {
712
778
  const localesArray = config.locales.map((locale) => `"${locale}"`).join(", ");
713
779
  return `export const locales = [${localesArray}] as const;
@@ -741,22 +807,27 @@ export function t<T>(
741
807
  `;
742
808
  }
743
809
  function generateLocaleIndexPage(config, manifests) {
744
- const sectionBlocks = manifests.filter((m) => m.type === "section");
810
+ const LAYOUT_CATEGORIES2 = /* @__PURE__ */ new Set(["header", "footer"]);
811
+ const contentBlocks = manifests.filter(
812
+ (m) => m.type === "section" && !LAYOUT_CATEGORIES2.has(m.category ?? "")
813
+ );
745
814
  const imports = [];
746
815
  const tags = [];
747
- for (const block of sectionBlocks) {
816
+ for (const block of contentBlocks) {
748
817
  const componentName = block.name.split("-").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
749
- imports.push(`import ${componentName} from '../../../components/sections/${block.name}.astro';`);
818
+ imports.push(`import ${componentName} from '../../components/sections/${block.name}.astro';`);
750
819
  tags.push(` <${componentName} />`);
751
820
  }
752
821
  const importSection = imports.length > 0 ? imports.join("\n") + "\n" : "";
753
822
  const blockSection = tags.length > 0 ? "\n" + tags.join("\n") + "\n " : "\n <h1>" + config.projectName + "</h1>\n <p>Locale: {locale}</p>\n ";
823
+ const needsPrerender = config.renderMode === "server" || config.renderMode === "hybrid";
824
+ const prerenderLine = needsPrerender ? "export const prerender = true;\n" : "";
754
825
  return `---
755
- import { locales } from "../../../i18n/utils";
756
- import Layout from "../../../layouts/Layout.astro";
757
- ${importSection}
826
+ import { locales } from "../../i18n/utils";
827
+ import Layout from "../../layouts/Layout.astro";
828
+ ${importSection}${prerenderLine}
758
829
  export function getStaticPaths() {
759
- return locales.map((locale) => ({ params: { locale } }));
830
+ return locales.map((locale: string) => ({ params: { locale } }));
760
831
  }
761
832
 
762
833
  const { locale } = Astro.params;
@@ -2193,16 +2264,38 @@ async function runManualFlow(input) {
2193
2264
  ]
2194
2265
  });
2195
2266
  if (p.isCancel(cssEngine)) return handleCancel();
2196
- const blockOptions = buildBlockOptions(input.manifests);
2197
- let selectedBlocks = [];
2198
- if (blockOptions.length > 0) {
2267
+ const headerOptions = buildCategoryOptions(input.manifests, "header");
2268
+ const footerOptions = buildCategoryOptions(input.manifests, "footer");
2269
+ const contentOptions = buildContentBlockOptions(input.manifests);
2270
+ let selectedHeader;
2271
+ let selectedFooter;
2272
+ let selectedContentBlocks = [];
2273
+ if (headerOptions.length > 0) {
2274
+ const noneOption = { value: "__none__", label: "None", hint: "No header" };
2275
+ const headerChoice = await p.select({
2276
+ message: "Choose a header (appears on every page)",
2277
+ options: [noneOption, ...headerOptions]
2278
+ });
2279
+ if (p.isCancel(headerChoice)) return handleCancel();
2280
+ if (headerChoice !== "__none__") selectedHeader = headerChoice;
2281
+ }
2282
+ if (contentOptions.length > 0) {
2199
2283
  const blocks = await p.multiselect({
2200
- message: "Select blocks to include (space to toggle, enter to confirm)",
2201
- options: blockOptions,
2284
+ message: "Select content blocks (space to toggle, enter to confirm)",
2285
+ options: contentOptions,
2202
2286
  required: false
2203
2287
  });
2204
2288
  if (p.isCancel(blocks)) return handleCancel();
2205
- selectedBlocks = blocks;
2289
+ selectedContentBlocks = blocks;
2290
+ }
2291
+ if (footerOptions.length > 0) {
2292
+ const noneOption = { value: "__none__", label: "None", hint: "No footer" };
2293
+ const footerChoice = await p.select({
2294
+ message: "Choose a footer (appears on every page)",
2295
+ options: [noneOption, ...footerOptions]
2296
+ });
2297
+ if (p.isCancel(footerChoice)) return handleCancel();
2298
+ if (footerChoice !== "__none__") selectedFooter = footerChoice;
2206
2299
  }
2207
2300
  const localesInput = await p.text({
2208
2301
  message: "Locales (comma-separated, e.g. en,es,ar)",
@@ -2232,6 +2325,24 @@ async function runManualFlow(input) {
2232
2325
  if (p.isCancel(switcherChoice)) return handleCancel();
2233
2326
  themeSwitcher = switcherChoice;
2234
2327
  }
2328
+ if (!selectedHeader && headerOptions.length > 0) {
2329
+ const needsNav = locales.length >= 2 || themeSwitcher;
2330
+ if (needsNav) {
2331
+ const autoHeader = await p.confirm({
2332
+ message: `You enabled ${locales.length >= 2 ? "multiple locales" : "theme switching"} \u2014 add a header for navigation?`,
2333
+ initialValue: true
2334
+ });
2335
+ if (p.isCancel(autoHeader)) return handleCancel();
2336
+ if (autoHeader) {
2337
+ selectedHeader = headerOptions[0].value;
2338
+ console.log(pc.dim(` Adding ${selectedHeader} for navigation.`));
2339
+ }
2340
+ }
2341
+ }
2342
+ const selectedBlocks = [];
2343
+ if (selectedHeader) selectedBlocks.push(selectedHeader);
2344
+ selectedBlocks.push(...selectedContentBlocks);
2345
+ if (selectedFooter) selectedBlocks.push(selectedFooter);
2235
2346
  const config = {
2236
2347
  projectName: projectName.trim(),
2237
2348
  projectDir: `./${projectName.trim()}`,
@@ -2271,8 +2382,18 @@ function handleCancel() {
2271
2382
  p.cancel("Operation cancelled.");
2272
2383
  return null;
2273
2384
  }
2274
- function buildBlockOptions(manifests) {
2275
- const blocks = Object.values(manifests);
2385
+ function buildCategoryOptions(manifests, category) {
2386
+ return Object.values(manifests).filter((block) => (block.category ?? "other") === category).map((block) => ({
2387
+ value: block.name,
2388
+ label: block.name,
2389
+ hint: block.description
2390
+ }));
2391
+ }
2392
+ function buildContentBlockOptions(manifests) {
2393
+ const LAYOUT_CATEGORIES2 = /* @__PURE__ */ new Set(["header", "footer"]);
2394
+ const blocks = Object.values(manifests).filter(
2395
+ (block) => !LAYOUT_CATEGORIES2.has(block.category ?? "other")
2396
+ );
2276
2397
  const categories = /* @__PURE__ */ new Map();
2277
2398
  for (const block of blocks) {
2278
2399
  const category = block.category ?? "other";
@@ -2286,7 +2407,7 @@ function buildBlockOptions(manifests) {
2286
2407
  for (const block of categoryBlocks) {
2287
2408
  options.push({
2288
2409
  value: block.name,
2289
- label: `${block.name}`,
2410
+ label: block.name,
2290
2411
  hint: `${category} \u2014 ${block.description}`
2291
2412
  });
2292
2413
  }
@@ -4104,17 +4225,60 @@ function preResolveDependencies(selected, manifests) {
4104
4225
  // src/cli/commands/add.ts
4105
4226
  import { defineCommand as defineCommand2 } from "citty";
4106
4227
  import pc4 from "picocolors";
4107
- import { readFileSync as readFileSync5, writeFileSync as writeFileSync5, existsSync as existsSync4, mkdirSync as mkdirSync5 } from "fs";
4108
- import { join as join7, dirname as dirname3 } from "path";
4228
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync5, existsSync as existsSync5, mkdirSync as mkdirSync5 } from "fs";
4229
+ import { join as join8, dirname as dirname3 } from "path";
4109
4230
 
4110
4231
  // src/scaffold/page-updater.ts
4232
+ import { readFileSync as readFileSync5, existsSync as existsSync4 } from "fs";
4233
+ import { join as join7 } from "path";
4234
+ var CATEGORY_ORDER = {
4235
+ header: 0,
4236
+ hero: 1,
4237
+ features: 2,
4238
+ pricing: 3,
4239
+ testimonials: 4,
4240
+ faq: 5,
4241
+ cta: 6,
4242
+ contact: 7,
4243
+ theme: 8,
4244
+ footer: 9
4245
+ };
4246
+ var DEFAULT_PRIORITY = 5;
4247
+ var LAYOUT_CATEGORIES = /* @__PURE__ */ new Set(["header", "footer"]);
4111
4248
  function blockNameToComponentName(blockName) {
4112
4249
  return blockName.split("-").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
4113
4250
  }
4114
4251
  function blockNameToImportPath(blockName) {
4115
4252
  return `../components/sections/${blockName}.astro`;
4116
4253
  }
4117
- function addBlockToPage(pageContent, blockName) {
4254
+ function blockNameToLayoutImportPath(blockName) {
4255
+ return `../components/sections/${blockName}.astro`;
4256
+ }
4257
+ function getBlockCategory(blockName, projectDir) {
4258
+ if (projectDir) {
4259
+ try {
4260
+ const fornixPath = join7(projectDir, "fornix.json");
4261
+ if (existsSync4(fornixPath)) {
4262
+ const fornix = JSON.parse(readFileSync5(fornixPath, "utf-8"));
4263
+ const block = fornix.blocks?.find((b) => b.name === blockName);
4264
+ if (block?.category) return block.category;
4265
+ }
4266
+ } catch {
4267
+ }
4268
+ }
4269
+ for (const prefix of Object.keys(CATEGORY_ORDER)) {
4270
+ if (blockName.startsWith(prefix)) return prefix;
4271
+ }
4272
+ return "other";
4273
+ }
4274
+ function getPriority(category) {
4275
+ return CATEGORY_ORDER[category] ?? DEFAULT_PRIORITY;
4276
+ }
4277
+ function isLayoutBlock(blockName, projectDir) {
4278
+ const category = getBlockCategory(blockName, projectDir);
4279
+ return LAYOUT_CATEGORIES.has(category);
4280
+ }
4281
+ function addBlockToPage(pageContent, blockName, projectDir) {
4118
4282
  const componentName = blockNameToComponentName(blockName);
4119
4283
  const importPath = blockNameToImportPath(blockName);
4120
4284
  if (pageContent.includes(importPath) || pageContent.includes(`import ${componentName}`)) {
@@ -4122,14 +4286,67 @@ function addBlockToPage(pageContent, blockName) {
4122
4286
  }
4123
4287
  const importLine = `import ${componentName} from '${importPath}';`;
4124
4288
  const componentTag = ` <${componentName} />`;
4289
+ const newCategory = getBlockCategory(blockName, projectDir);
4290
+ const newPriority = getPriority(newCategory);
4125
4291
  const frontmatterEnd = pageContent.indexOf("---", pageContent.indexOf("---") + 3);
4126
4292
  if (frontmatterEnd === -1) {
4127
4293
  return pageContent;
4128
4294
  }
4129
4295
  let updated = pageContent.slice(0, frontmatterEnd) + importLine + "\n" + pageContent.slice(frontmatterEnd);
4296
+ const mainOpenMatch = updated.match(/<main[^>]*>/);
4130
4297
  const mainCloseIndex = updated.lastIndexOf("</main>");
4131
- if (mainCloseIndex !== -1) {
4132
- updated = updated.slice(0, mainCloseIndex) + componentTag + "\n " + updated.slice(mainCloseIndex);
4298
+ if (!mainOpenMatch || mainCloseIndex === -1) {
4299
+ return updated;
4300
+ }
4301
+ const mainOpenEnd = mainOpenMatch.index + mainOpenMatch[0].length;
4302
+ const mainContent = updated.slice(mainOpenEnd, mainCloseIndex);
4303
+ const tagPattern = /^(\s*<([A-Z][A-Za-z]*)\s*\/>)\s*$/gm;
4304
+ let insertOffset = mainCloseIndex;
4305
+ let match;
4306
+ while ((match = tagPattern.exec(mainContent)) !== null) {
4307
+ const existingComponentName = match[2];
4308
+ const existingBlockName = componentNameToBlockName(existingComponentName);
4309
+ const existingCategory = getBlockCategory(existingBlockName, projectDir);
4310
+ const existingPriority = getPriority(existingCategory);
4311
+ if (existingPriority > newPriority) {
4312
+ insertOffset = mainOpenEnd + match.index;
4313
+ break;
4314
+ }
4315
+ }
4316
+ updated = updated.slice(0, insertOffset) + componentTag + "\n" + updated.slice(insertOffset);
4317
+ return updated;
4318
+ }
4319
+ function addBlockToLayout(layoutContent, blockName, projectDir) {
4320
+ const componentName = blockNameToComponentName(blockName);
4321
+ const importPath = blockNameToLayoutImportPath(blockName);
4322
+ if (layoutContent.includes(importPath) || layoutContent.includes(`import ${componentName}`)) {
4323
+ return layoutContent;
4324
+ }
4325
+ const importLine = `import ${componentName} from '${importPath}';`;
4326
+ const componentTag = ` <${componentName} />`;
4327
+ const category = getBlockCategory(blockName, projectDir);
4328
+ const frontmatterEnd = layoutContent.indexOf("---", layoutContent.indexOf("---") + 3);
4329
+ if (frontmatterEnd === -1) {
4330
+ return layoutContent;
4331
+ }
4332
+ let updated = layoutContent.slice(0, frontmatterEnd) + importLine + "\n" + layoutContent.slice(frontmatterEnd);
4333
+ if (category === "header") {
4334
+ const slotIndex = updated.indexOf("<slot");
4335
+ const mainIndex = updated.indexOf("<main");
4336
+ const insertBefore = mainIndex !== -1 ? mainIndex : slotIndex;
4337
+ if (insertBefore !== -1) {
4338
+ updated = updated.slice(0, insertBefore) + componentTag + "\n" + updated.slice(insertBefore);
4339
+ }
4340
+ } else if (category === "footer") {
4341
+ const mainCloseIndex = updated.indexOf("</main>");
4342
+ const slotMatch = updated.match(/<slot\s*\/>/);
4343
+ if (mainCloseIndex !== -1) {
4344
+ const afterMain = mainCloseIndex + "</main>".length;
4345
+ updated = updated.slice(0, afterMain) + "\n" + componentTag + updated.slice(afterMain);
4346
+ } else if (slotMatch) {
4347
+ const afterSlot = slotMatch.index + slotMatch[0].length;
4348
+ updated = updated.slice(0, afterSlot) + "\n" + componentTag + updated.slice(afterSlot);
4349
+ }
4133
4350
  }
4134
4351
  return updated;
4135
4352
  }
@@ -4152,6 +4369,9 @@ function removeBlockFromPage(pageContent, blockName) {
4152
4369
  function escapeRegex(str) {
4153
4370
  return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4154
4371
  }
4372
+ function componentNameToBlockName(componentName) {
4373
+ return componentName.replace(/([A-Z])/g, "-$1").toLowerCase().replace(/^-/, "");
4374
+ }
4155
4375
 
4156
4376
  // src/cli/commands/add.ts
4157
4377
  var addCommand = defineCommand2({
@@ -4184,14 +4404,14 @@ var addCommand = defineCommand2({
4184
4404
  async run({ args: args2 }) {
4185
4405
  const typedArgs = args2;
4186
4406
  const cwd = process.cwd();
4187
- const manifestPath = join7(cwd, "fornix.json");
4188
- if (!existsSync4(manifestPath)) {
4407
+ const manifestPath = join8(cwd, "fornix.json");
4408
+ if (!existsSync5(manifestPath)) {
4189
4409
  console.error(
4190
4410
  pc4.red("\u2717 No fornix.json found. Are you in a Fornix project?")
4191
4411
  );
4192
4412
  process.exit(1);
4193
4413
  }
4194
- const manifestRaw = readFileSync5(manifestPath, "utf-8");
4414
+ const manifestRaw = readFileSync6(manifestPath, "utf-8");
4195
4415
  const manifest2 = JSON.parse(manifestRaw);
4196
4416
  const registryResult = await fetchRegistryIndex();
4197
4417
  if (!isOk(registryResult)) {
@@ -4220,6 +4440,27 @@ var addCommand = defineCommand2({
4220
4440
  return;
4221
4441
  }
4222
4442
  const blocksToAdd = resolveDependencies2(blockName, installedNames, manifests);
4443
+ for (const name of blocksToAdd) {
4444
+ const m = manifests[name];
4445
+ if (m?.conflicts && m.conflicts.length > 0) {
4446
+ for (const conflictName of m.conflicts) {
4447
+ if (installedNames.has(conflictName)) {
4448
+ console.error(
4449
+ pc4.red(
4450
+ `\u2717 Block '${name}' conflicts with installed block '${conflictName}'.`
4451
+ )
4452
+ );
4453
+ console.log(
4454
+ pc4.dim(
4455
+ ` Remove '${conflictName}' first: npx create-fornix remove ${conflictName}`
4456
+ )
4457
+ );
4458
+ process.exitCode = 1;
4459
+ return;
4460
+ }
4461
+ }
4462
+ }
4463
+ }
4223
4464
  for (const name of blocksToAdd) {
4224
4465
  const m = manifests[name];
4225
4466
  if (m?.requiredMode && manifest2.renderMode !== m.requiredMode) {
@@ -4257,7 +4498,7 @@ var addCommand = defineCommand2({
4257
4498
  return;
4258
4499
  }
4259
4500
  filesToWrite.push({
4260
- path: join7(cwd, file.destination),
4501
+ path: join8(cwd, file.destination),
4261
4502
  content
4262
4503
  });
4263
4504
  }
@@ -4296,18 +4537,31 @@ var addCommand = defineCommand2({
4296
4537
  });
4297
4538
  }
4298
4539
  writeFileSync5(manifestPath, JSON.stringify(manifest2, null, 2) + "\n");
4299
- const indexPath = join7(cwd, "src/pages/index.astro");
4300
- if (existsSync4(indexPath)) {
4301
- let pageContent = readFileSync5(indexPath, "utf-8");
4302
- for (const name of blocksToAdd) {
4303
- const bManifest = manifests[name];
4304
- if (bManifest && bManifest.type === "section") {
4305
- pageContent = addBlockToPage(pageContent, name);
4540
+ const indexPath = join8(cwd, "src/pages/index.astro");
4541
+ const layoutPath = join8(cwd, "src/layouts/Layout.astro");
4542
+ for (const name of blocksToAdd) {
4543
+ const bManifest = manifests[name];
4544
+ if (!bManifest || bManifest.type !== "section") continue;
4545
+ if (isLayoutBlock(name, cwd)) {
4546
+ if (existsSync5(layoutPath)) {
4547
+ const layoutContent = readFileSync6(layoutPath, "utf-8");
4548
+ const updated = addBlockToLayout(layoutContent, name, cwd);
4549
+ if (updated !== layoutContent) {
4550
+ writeFileSync5(layoutPath, updated);
4551
+ if (typedArgs.verbose) {
4552
+ console.log(` ${pc4.dim("\u270E")} updated Layout.astro (${name})`);
4553
+ }
4554
+ }
4555
+ }
4556
+ } else {
4557
+ if (existsSync5(indexPath)) {
4558
+ let pageContent = readFileSync6(indexPath, "utf-8");
4559
+ pageContent = addBlockToPage(pageContent, name, cwd);
4560
+ writeFileSync5(indexPath, pageContent);
4561
+ if (typedArgs.verbose) {
4562
+ console.log(` ${pc4.dim("\u270E")} updated index.astro (${name})`);
4563
+ }
4306
4564
  }
4307
- }
4308
- writeFileSync5(indexPath, pageContent);
4309
- if (typedArgs.verbose) {
4310
- console.log(` ${pc4.dim("\u270E")} updated index.astro`);
4311
4565
  }
4312
4566
  }
4313
4567
  console.log();
@@ -4350,8 +4604,8 @@ function resolveDependencies2(blockName, installedNames, manifests) {
4350
4604
  // src/cli/commands/remove.ts
4351
4605
  import { defineCommand as defineCommand3 } from "citty";
4352
4606
  import pc5 from "picocolors";
4353
- import { readFileSync as readFileSync6, writeFileSync as writeFileSync6, existsSync as existsSync5, unlinkSync, readdirSync as readdirSync3, rmdirSync } from "fs";
4354
- import { join as join8, dirname as dirname4 } from "path";
4607
+ import { readFileSync as readFileSync7, writeFileSync as writeFileSync6, existsSync as existsSync6, unlinkSync, readdirSync as readdirSync3, rmdirSync } from "fs";
4608
+ import { join as join9, dirname as dirname4 } from "path";
4355
4609
  var removeCommand = defineCommand3({
4356
4610
  meta: {
4357
4611
  name: "remove",
@@ -4382,14 +4636,14 @@ var removeCommand = defineCommand3({
4382
4636
  async run({ args: args2 }) {
4383
4637
  const typedArgs = args2;
4384
4638
  const cwd = process.cwd();
4385
- const manifestPath = join8(cwd, "fornix.json");
4386
- if (!existsSync5(manifestPath)) {
4639
+ const manifestPath = join9(cwd, "fornix.json");
4640
+ if (!existsSync6(manifestPath)) {
4387
4641
  console.error(
4388
4642
  pc5.red("\u2717 No fornix.json found. Are you in a Fornix project?")
4389
4643
  );
4390
4644
  process.exit(1);
4391
4645
  }
4392
- const manifestRaw = readFileSync6(manifestPath, "utf-8");
4646
+ const manifestRaw = readFileSync7(manifestPath, "utf-8");
4393
4647
  const manifest2 = JSON.parse(manifestRaw);
4394
4648
  const registryResult = await fetchRegistryIndex();
4395
4649
  if (!isOk(registryResult)) {
@@ -4422,8 +4676,8 @@ var removeCommand = defineCommand3({
4422
4676
  const filesToRemove = [];
4423
4677
  if (blockManifest) {
4424
4678
  for (const file of blockManifest.files) {
4425
- const filePath = join8(cwd, file.destination);
4426
- if (existsSync5(filePath)) {
4679
+ const filePath = join9(cwd, file.destination);
4680
+ if (existsSync6(filePath)) {
4427
4681
  filesToRemove.push(filePath);
4428
4682
  }
4429
4683
  }
@@ -4446,14 +4700,15 @@ var removeCommand = defineCommand3({
4446
4700
  }
4447
4701
  manifest2.blocks = manifest2.blocks.filter((b) => b.name !== blockName);
4448
4702
  writeFileSync6(manifestPath, JSON.stringify(manifest2, null, 2) + "\n");
4449
- const indexPath = join8(cwd, "src/pages/index.astro");
4450
- if (existsSync5(indexPath)) {
4451
- const original = readFileSync6(indexPath, "utf-8");
4703
+ const targetFile = isLayoutBlock(blockName, cwd) ? join9(cwd, "src/layouts/Layout.astro") : join9(cwd, "src/pages/index.astro");
4704
+ const targetLabel = isLayoutBlock(blockName, cwd) ? "Layout.astro" : "index.astro";
4705
+ if (existsSync6(targetFile)) {
4706
+ const original = readFileSync7(targetFile, "utf-8");
4452
4707
  const updated = removeBlockFromPage(original, blockName);
4453
4708
  if (updated !== original) {
4454
- writeFileSync6(indexPath, updated);
4709
+ writeFileSync6(targetFile, updated);
4455
4710
  if (typedArgs.verbose) {
4456
- console.log(` ${pc5.dim("\u270E")} updated index.astro`);
4711
+ console.log(` ${pc5.dim("\u270E")} updated ${targetLabel}`);
4457
4712
  }
4458
4713
  }
4459
4714
  }
@@ -4628,8 +4883,8 @@ function printFormatted(blocks, verbose) {
4628
4883
  // src/cli/commands/status.ts
4629
4884
  import { defineCommand as defineCommand5 } from "citty";
4630
4885
  import pc7 from "picocolors";
4631
- import { readFileSync as readFileSync7, existsSync as existsSync6 } from "fs";
4632
- import { join as join9 } from "path";
4886
+ import { readFileSync as readFileSync8, existsSync as existsSync7 } from "fs";
4887
+ import { join as join10 } from "path";
4633
4888
  var statusCommand = defineCommand5({
4634
4889
  meta: {
4635
4890
  name: "status",
@@ -4650,8 +4905,8 @@ var statusCommand = defineCommand5({
4650
4905
  run({ args: args2 }) {
4651
4906
  const typedArgs = args2;
4652
4907
  const cwd = process.cwd();
4653
- const manifestPath = join9(cwd, "fornix.json");
4654
- if (!existsSync6(manifestPath)) {
4908
+ const manifestPath = join10(cwd, "fornix.json");
4909
+ if (!existsSync7(manifestPath)) {
4655
4910
  console.error(
4656
4911
  pc7.red("\u2717 No fornix.json found in the current directory.")
4657
4912
  );
@@ -4664,7 +4919,7 @@ var statusCommand = defineCommand5({
4664
4919
  }
4665
4920
  let manifest2;
4666
4921
  try {
4667
- const raw = readFileSync7(manifestPath, "utf-8");
4922
+ const raw = readFileSync8(manifestPath, "utf-8");
4668
4923
  manifest2 = JSON.parse(raw);
4669
4924
  } catch {
4670
4925
  console.error(pc7.red("\u2717 Failed to parse fornix.json."));
@@ -4745,8 +5000,8 @@ function printStatus(manifest2, verbose) {
4745
5000
  // src/cli/commands/doctor.ts
4746
5001
  import { defineCommand as defineCommand6 } from "citty";
4747
5002
  import pc8 from "picocolors";
4748
- import { readFileSync as readFileSync8, existsSync as existsSync7 } from "fs";
4749
- import { join as join10 } from "path";
5003
+ import { readFileSync as readFileSync9, existsSync as existsSync8 } from "fs";
5004
+ import { join as join11 } from "path";
4750
5005
  var doctorCommand = defineCommand6({
4751
5006
  meta: {
4752
5007
  name: "doctor",
@@ -4761,7 +5016,7 @@ var doctorCommand = defineCommand6({
4761
5016
  },
4762
5017
  async run({ args: args2 }) {
4763
5018
  const cwd = process.cwd();
4764
- const manifestPath = join10(cwd, "fornix.json");
5019
+ const manifestPath = join11(cwd, "fornix.json");
4765
5020
  let hasErrors = false;
4766
5021
  const errors = [];
4767
5022
  function reportError(msg) {
@@ -4771,7 +5026,7 @@ var doctorCommand = defineCommand6({
4771
5026
  console.error(pc8.red(`\u2717 ${msg}`));
4772
5027
  }
4773
5028
  }
4774
- if (!existsSync7(manifestPath)) {
5029
+ if (!existsSync8(manifestPath)) {
4775
5030
  reportError("No fornix.json found in the current directory.");
4776
5031
  if (args2.json) {
4777
5032
  console.log(JSON.stringify({ healthy: false, errors }));
@@ -4780,7 +5035,7 @@ var doctorCommand = defineCommand6({
4780
5035
  }
4781
5036
  let manifest2;
4782
5037
  try {
4783
- const raw = readFileSync8(manifestPath, "utf-8");
5038
+ const raw = readFileSync9(manifestPath, "utf-8");
4784
5039
  manifest2 = JSON.parse(raw);
4785
5040
  } catch {
4786
5041
  reportError("Failed to parse fornix.json.");
@@ -4806,8 +5061,8 @@ var doctorCommand = defineCommand6({
4806
5061
  const bManifest = manifests[block.name];
4807
5062
  if (bManifest) {
4808
5063
  for (const file of bManifest.files) {
4809
- const filePath = join10(cwd, file.destination);
4810
- if (!existsSync7(filePath)) {
5064
+ const filePath = join11(cwd, file.destination);
5065
+ if (!existsSync8(filePath)) {
4811
5066
  reportError(`Missing expected file for installed block '${block.name}': ${file.destination}`);
4812
5067
  }
4813
5068
  }
@@ -4816,7 +5071,7 @@ var doctorCommand = defineCommand6({
4816
5071
  for (const [name, bManifest] of Object.entries(manifests)) {
4817
5072
  if (!installedBlocks.has(name)) {
4818
5073
  const foundOrphaned = bManifest.files.some((file) => {
4819
- return existsSync7(join10(cwd, file.destination));
5074
+ return existsSync8(join11(cwd, file.destination));
4820
5075
  });
4821
5076
  if (foundOrphaned) {
4822
5077
  reportError(`Orphaned block files detected for '${name}'. The block is not in fornix.json.`);
@@ -4840,14 +5095,14 @@ var doctorCommand = defineCommand6({
4840
5095
  if (locale !== "") {
4841
5096
  pathFragment = `src/content/${locale}/${subdirectory}/${bManifest.name}.json`;
4842
5097
  }
4843
- if (!existsSync7(join10(cwd, pathFragment))) {
5098
+ if (!existsSync8(join11(cwd, pathFragment))) {
4844
5099
  missingContentFiles.push(pathFragment);
4845
5100
  }
4846
5101
  }
4847
5102
  }
4848
5103
  }
4849
5104
  if (requiresContentConfig) {
4850
- if (!existsSync7(join10(cwd, "src/content/config.ts"))) {
5105
+ if (!existsSync8(join11(cwd, "src/content/config.ts"))) {
4851
5106
  reportError("Missing expected file: src/content/config.ts");
4852
5107
  }
4853
5108
  }
@@ -4938,19 +5193,19 @@ async function listBlocksHandler(args2) {
4938
5193
  }
4939
5194
 
4940
5195
  // src/mcp/tools/add-block.ts
4941
- import { readFileSync as readFileSync9, writeFileSync as writeFileSync7, existsSync as existsSync8, mkdirSync as mkdirSync6 } from "fs";
4942
- import { join as join11, dirname as dirname5 } from "path";
5196
+ import { readFileSync as readFileSync10, writeFileSync as writeFileSync7, existsSync as existsSync9, mkdirSync as mkdirSync6 } from "fs";
5197
+ import { join as join12, dirname as dirname5 } from "path";
4943
5198
  async function addBlock2(input) {
4944
5199
  const { name, variant = "default", projectDirectory } = input;
4945
- const manifestPath = join11(projectDirectory, "fornix.json");
4946
- if (!existsSync8(manifestPath)) {
5200
+ const manifestPath = join12(projectDirectory, "fornix.json");
5201
+ if (!existsSync9(manifestPath)) {
4947
5202
  return err(
4948
5203
  new Error("No fornix.json found. Not a Fornix project directory.")
4949
5204
  );
4950
5205
  }
4951
5206
  let manifest2;
4952
5207
  try {
4953
- const raw = readFileSync9(manifestPath, "utf-8");
5208
+ const raw = readFileSync10(manifestPath, "utf-8");
4954
5209
  manifest2 = JSON.parse(raw);
4955
5210
  } catch {
4956
5211
  return err(new Error("Failed to parse fornix.json."));
@@ -5002,7 +5257,7 @@ async function addBlock2(input) {
5002
5257
  )
5003
5258
  );
5004
5259
  }
5005
- const filePath = join11(projectDirectory, file.destination);
5260
+ const filePath = join12(projectDirectory, file.destination);
5006
5261
  mkdirSync6(dirname5(filePath), { recursive: true });
5007
5262
  writeFileSync7(filePath, content);
5008
5263
  filesCreated++;
@@ -5041,25 +5296,25 @@ function resolveDependencies3(blockName, installedNames, manifests) {
5041
5296
 
5042
5297
  // src/mcp/tools/remove-block.ts
5043
5298
  import {
5044
- readFileSync as readFileSync10,
5299
+ readFileSync as readFileSync11,
5045
5300
  writeFileSync as writeFileSync8,
5046
- existsSync as existsSync9,
5301
+ existsSync as existsSync10,
5047
5302
  unlinkSync as unlinkSync2,
5048
5303
  readdirSync as readdirSync4,
5049
5304
  rmdirSync as rmdirSync2
5050
5305
  } from "fs";
5051
- import { join as join12, dirname as dirname6 } from "path";
5306
+ import { join as join13, dirname as dirname6 } from "path";
5052
5307
  async function removeBlock(input) {
5053
5308
  const { name, force = false, projectDirectory } = input;
5054
- const manifestPath = join12(projectDirectory, "fornix.json");
5055
- if (!existsSync9(manifestPath)) {
5309
+ const manifestPath = join13(projectDirectory, "fornix.json");
5310
+ if (!existsSync10(manifestPath)) {
5056
5311
  return err(
5057
5312
  new Error("No fornix.json found. Not a Fornix project directory.")
5058
5313
  );
5059
5314
  }
5060
5315
  let manifest2;
5061
5316
  try {
5062
- const raw = readFileSync10(manifestPath, "utf-8");
5317
+ const raw = readFileSync11(manifestPath, "utf-8");
5063
5318
  manifest2 = JSON.parse(raw);
5064
5319
  } catch {
5065
5320
  return err(new Error("Failed to parse fornix.json."));
@@ -5085,8 +5340,8 @@ async function removeBlock(input) {
5085
5340
  const filesToRemove = [];
5086
5341
  if (blockManifest) {
5087
5342
  for (const file of blockManifest.files) {
5088
- const filePath = join12(projectDirectory, file.destination);
5089
- if (existsSync9(filePath)) {
5343
+ const filePath = join13(projectDirectory, file.destination);
5344
+ if (existsSync10(filePath)) {
5090
5345
  filesToRemove.push(filePath);
5091
5346
  }
5092
5347
  }
@@ -5218,17 +5473,17 @@ function zodTypeForSlot(slotType) {
5218
5473
  }
5219
5474
 
5220
5475
  // src/mcp/tools/get-project-status.ts
5221
- import { readFileSync as readFileSync11, existsSync as existsSync10 } from "fs";
5222
- import { join as join13 } from "path";
5476
+ import { readFileSync as readFileSync12, existsSync as existsSync11 } from "fs";
5477
+ import { join as join14 } from "path";
5223
5478
  function getProjectStatus(input) {
5224
- const manifestPath = join13(input.projectDirectory, "fornix.json");
5225
- if (!existsSync10(manifestPath)) {
5479
+ const manifestPath = join14(input.projectDirectory, "fornix.json");
5480
+ if (!existsSync11(manifestPath)) {
5226
5481
  return err(
5227
5482
  new Error("No fornix.json found. Not a Fornix project directory.")
5228
5483
  );
5229
5484
  }
5230
5485
  try {
5231
- const raw = readFileSync11(manifestPath, "utf-8");
5486
+ const raw = readFileSync12(manifestPath, "utf-8");
5232
5487
  const manifest2 = JSON.parse(raw);
5233
5488
  const blocks = manifest2.blocks;
5234
5489
  return ok({
@@ -5251,7 +5506,7 @@ function getProjectStatus(input) {
5251
5506
 
5252
5507
  // src/mcp/tools/scaffold-project.ts
5253
5508
  import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync9 } from "fs";
5254
- import { join as join14, basename as basename3 } from "path";
5509
+ import { join as join15, basename as basename3 } from "path";
5255
5510
  var DEFAULT_COLORS2 = {
5256
5511
  primary: "#6366f1",
5257
5512
  secondary: "#818cf8",
@@ -5322,8 +5577,8 @@ async function scaffoldProject(input) {
5322
5577
  const files = result.value.files;
5323
5578
  let filesCreated = 0;
5324
5579
  for (const [relativePath, content] of Object.entries(files)) {
5325
- const fullPath = join14(projectDirectory, relativePath);
5326
- const parentDirectory = join14(fullPath, "..");
5580
+ const fullPath = join15(projectDirectory, relativePath);
5581
+ const parentDirectory = join15(fullPath, "..");
5327
5582
  mkdirSync7(parentDirectory, { recursive: true });
5328
5583
  writeFileSync9(fullPath, content, "utf-8");
5329
5584
  filesCreated++;