create-fornix 0.0.8 → 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,10 +349,43 @@ 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>
356
+
357
+ <style is:global>
358
+ *, *::before, *::after {
359
+ box-sizing: border-box;
360
+ margin: 0;
361
+ padding: 0;
362
+ }
363
+
364
+ html {
365
+ scroll-behavior: smooth;
366
+ -webkit-font-smoothing: antialiased;
367
+ -moz-osx-font-smoothing: grayscale;
368
+ }
369
+
370
+ body {
371
+ font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
372
+ background: var(--color-background, #0f172a);
373
+ color: var(--color-foreground, #f8fafc);
374
+ line-height: 1.6;
375
+ min-height: 100vh;
376
+ }
377
+
378
+ img, video {
379
+ max-width: 100%;
380
+ height: auto;
381
+ display: block;
382
+ }
383
+
384
+ a {
385
+ color: inherit;
386
+ text-decoration: none;
387
+ }
388
+ </style>
332
389
  `.trim() + "\n";
333
390
  if (config.deployTarget === "cloudflare") {
334
391
  const wrangler = {
@@ -345,6 +402,15 @@ const { title } = Astro.props;${tailwindImport}
345
402
  }
346
403
  return files;
347
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
+ }
348
414
  function getAdapterDependencies(config) {
349
415
  if (config.renderMode === "static" && config.deployTarget === "static") {
350
416
  return {};
@@ -373,7 +439,7 @@ function generateAstroConfig(config, blocks = []) {
373
439
  local: "defineConfig"
374
440
  });
375
441
  const configObject = module.exports.default.$args[0];
376
- if (config.renderMode === "server") {
442
+ if (config.renderMode === "server" || config.renderMode === "hybrid") {
377
443
  configObject.output = "server";
378
444
  }
379
445
  const adapter = ADAPTER_MAP[config.deployTarget];
@@ -427,22 +493,11 @@ function generateTailwindConfig(config) {
427
493
  if (config.cssEngine !== "tailwind") {
428
494
  return ok(null);
429
495
  }
430
- const themeBlock = [
431
- "@theme {",
432
- " --color-primary: var(--color-primary);",
433
- " --color-secondary: var(--color-secondary);",
434
- " --color-accent: var(--color-accent);",
435
- " --color-background: var(--color-background);",
436
- " --color-foreground: var(--color-foreground);",
437
- "}"
438
- ].join("\n");
439
496
  const lines = [
440
497
  '@import "tailwindcss";',
441
498
  '@import "./src/styles/palettes/_current.css";',
442
499
  "",
443
500
  `@source "./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}";`,
444
- "",
445
- themeBlock,
446
501
  ""
447
502
  ];
448
503
  return ok(lines.join("\n"));
@@ -480,11 +535,35 @@ function buildPaletteFile(colors) {
480
535
  const properties = COLOR_TOKENS.map(
481
536
  (token) => ` --color-${token}: ${colors[token]};`
482
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");
483
544
  return `:root {
484
545
  ${properties}
546
+ ${derived}
485
547
  }
486
548
  `;
487
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
+ }
488
567
  function buildSwitcherScript(paletteNames) {
489
568
  return `(function () {
490
569
  const PALETTES = ${JSON.stringify(paletteNames)};
@@ -615,7 +694,7 @@ function generateContentConfig(blocks) {
615
694
  'import { defineCollection, z } from "astro:content";'
616
695
  ];
617
696
  const collections = [];
618
- const dataCollections = /* @__PURE__ */ new Map();
697
+ const dataCollectionNames = /* @__PURE__ */ new Set();
619
698
  for (const block of blocks) {
620
699
  if (block.collections && block.collections.length > 0) {
621
700
  for (const col of block.collections) {
@@ -632,27 +711,15 @@ function generateContentConfig(blocks) {
632
711
  }
633
712
  const slots = block.ai?.contentSlots;
634
713
  if (slots && Object.keys(slots).length > 0) {
635
- const schemaFields = Object.entries(slots).map(([name, slot]) => ` ${name}: ${zodTypeForSlot(slot)}.optional(),`).join("\n");
636
714
  const subdirectory = TYPE_DIRECTORY[block.type] ?? block.type;
637
- if (!dataCollections.has(subdirectory)) {
638
- dataCollections.set(subdirectory, []);
639
- }
640
- dataCollections.get(subdirectory).push(
641
- ` // ${block.name}
642
- z.object({
643
- ${schemaFields}
644
- })`
645
- );
715
+ dataCollectionNames.add(subdirectory);
646
716
  }
647
717
  }
648
- for (const [colName, schemas] of dataCollections.entries()) {
649
- const schemaStr = schemas.length === 1 ? schemas[0] : `z.union([
650
- ${schemas.join(",\n")}
651
- ])`;
718
+ for (const colName of dataCollectionNames) {
652
719
  collections.push(
653
720
  ` "${colName}": defineCollection({
654
721
  type: "data",
655
- schema: ${schemaStr},
722
+ schema: z.record(z.unknown()),
656
723
  })`
657
724
  );
658
725
  }
@@ -666,20 +733,6 @@ ${schemas.join(",\n")}
666
733
  ];
667
734
  return lines.join("\n");
668
735
  }
669
- function zodTypeForSlot(slot) {
670
- switch (slot.type) {
671
- case "string":
672
- return "z.string()";
673
- case "number":
674
- return "z.number()";
675
- case "boolean":
676
- return "z.boolean()";
677
- case "array":
678
- return "z.array(z.unknown())";
679
- case "object":
680
- return "z.record(z.unknown())";
681
- }
682
- }
683
736
  function buildDefaultFromSlots(slots) {
684
737
  const content = {};
685
738
  for (const [name, slot] of Object.entries(slots)) {
@@ -703,15 +756,24 @@ function defaultValueForType(type) {
703
756
  }
704
757
 
705
758
  // src/scaffold/i18n-wiring.ts
706
- function wireI18n(config) {
759
+ function wireI18n(config, manifests) {
707
760
  const files = {};
708
761
  if (config.locales.length < 2) {
709
762
  return ok(files);
710
763
  }
711
764
  files["src/i18n/utils.ts"] = generateI18nUtils(config);
712
- files["src/pages/[locale]/index.astro"] = generateLocaleIndexPage(config);
765
+ files["src/pages/[locale]/index.astro"] = generateLocaleIndexPage(config, manifests ?? []);
766
+ files["src/pages/index.astro"] = generateRootRedirect(config);
713
767
  return ok(files);
714
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
+ }
715
777
  function generateI18nUtils(config) {
716
778
  const localesArray = config.locales.map((locale) => `"${locale}"`).join(", ");
717
779
  return `export const locales = [${localesArray}] as const;
@@ -744,22 +806,34 @@ export function t<T>(
744
806
  }
745
807
  `;
746
808
  }
747
- function generateLocaleIndexPage(config) {
809
+ function generateLocaleIndexPage(config, manifests) {
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
+ );
814
+ const imports = [];
815
+ const tags = [];
816
+ for (const block of contentBlocks) {
817
+ const componentName = block.name.split("-").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
818
+ imports.push(`import ${componentName} from '../../components/sections/${block.name}.astro';`);
819
+ tags.push(` <${componentName} />`);
820
+ }
821
+ const importSection = imports.length > 0 ? imports.join("\n") + "\n" : "";
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" : "";
748
825
  return `---
749
826
  import { locales } from "../../i18n/utils";
750
827
  import Layout from "../../layouts/Layout.astro";
751
-
828
+ ${importSection}${prerenderLine}
752
829
  export function getStaticPaths() {
753
- return locales.map((locale) => ({ params: { locale } }));
830
+ return locales.map((locale: string) => ({ params: { locale } }));
754
831
  }
755
832
 
756
833
  const { locale } = Astro.params;
757
834
  ---
758
835
  <Layout title="${config.projectName}">
759
- <main>
760
- <h1>${config.projectName}</h1>
761
- <p>Locale: {locale}</p>
762
- </main>
836
+ <main>${blockSection}</main>
763
837
  </Layout>
764
838
  `;
765
839
  }
@@ -875,7 +949,7 @@ function scaffold(input) {
875
949
  return err(contentResult.error);
876
950
  }
877
951
  Object.assign(files, contentResult.value);
878
- const i18nResult = wireI18n(config);
952
+ const i18nResult = wireI18n(config, resolvedManifests);
879
953
  if (!isOk(i18nResult)) {
880
954
  return err(i18nResult.error);
881
955
  }
@@ -1503,7 +1577,7 @@ function manifest(name, overrides = {}) {
1503
1577
  var FIXTURE_MANIFESTS = {
1504
1578
  "hero-gradient": manifest("hero-gradient", {
1505
1579
  category: "hero",
1506
- conflicts: ["hero-video"],
1580
+ conflicts: ["hero-video", "hero-split"],
1507
1581
  ai: {
1508
1582
  whenToUse: "Landing page hero with gradient background",
1509
1583
  whenNotToUse: "Internal pages",
@@ -1515,7 +1589,8 @@ var FIXTURE_MANIFESTS = {
1515
1589
  }
1516
1590
  }),
1517
1591
  "footer-minimal": manifest("footer-minimal", {
1518
- category: "footer"
1592
+ category: "footer",
1593
+ conflicts: ["footer-rich"]
1519
1594
  }),
1520
1595
  "cta-simple": manifest("cta-simple", {
1521
1596
  category: "cta"
@@ -1626,17 +1701,96 @@ var FIXTURE_MANIFESTS = {
1626
1701
  }
1627
1702
  }
1628
1703
  }),
1629
- "hero-video": manifest("hero-video"),
1630
- "features-bento": manifest("features-bento"),
1631
- "pricing-table": manifest("pricing-table"),
1632
- "faq-accordion": manifest("faq-accordion"),
1633
- "footer-rich": manifest("footer-rich"),
1704
+ "hero-video": manifest("hero-video", {
1705
+ category: "hero",
1706
+ conflicts: ["hero-gradient", "hero-split"],
1707
+ files: [
1708
+ { source: "hero-video.astro", destination: "src/components/sections/hero-video.astro" },
1709
+ { source: "hero-video.css", destination: "src/styles/sections/hero-video.css" }
1710
+ ],
1711
+ ai: {
1712
+ whenToUse: "Hero with background video",
1713
+ whenNotToUse: "Static sites",
1714
+ pairsWith: [],
1715
+ contentSlots: {
1716
+ headline: { type: "string" },
1717
+ subheadline: { type: "string" },
1718
+ ctaText: { type: "string" },
1719
+ ctaHref: { type: "string" },
1720
+ videoUrl: { type: "string" },
1721
+ posterUrl: { type: "string" }
1722
+ }
1723
+ }
1724
+ }),
1725
+ "features-bento": manifest("features-bento", {
1726
+ category: "features",
1727
+ ai: {
1728
+ whenToUse: "Feature showcase in bento grid",
1729
+ whenNotToUse: "Simple pages",
1730
+ pairsWith: [],
1731
+ contentSlots: {
1732
+ headline: { type: "string" },
1733
+ subheadline: { type: "string" },
1734
+ items: { type: "array" }
1735
+ }
1736
+ }
1737
+ }),
1738
+ "pricing-table": manifest("pricing-table", {
1739
+ category: "pricing",
1740
+ conflicts: ["pricing-comparison"],
1741
+ ai: {
1742
+ whenToUse: "Pricing plans display",
1743
+ whenNotToUse: "Free products",
1744
+ pairsWith: [],
1745
+ contentSlots: {
1746
+ headline: { type: "string" },
1747
+ subheadline: { type: "string" },
1748
+ plans: { type: "array" }
1749
+ }
1750
+ }
1751
+ }),
1752
+ "faq-accordion": manifest("faq-accordion", {
1753
+ category: "faq",
1754
+ ai: {
1755
+ whenToUse: "Frequently asked questions",
1756
+ whenNotToUse: "Simple pages",
1757
+ pairsWith: [],
1758
+ contentSlots: {
1759
+ headline: { type: "string" },
1760
+ items: { type: "array" }
1761
+ }
1762
+ }
1763
+ }),
1764
+ "footer-rich": manifest("footer-rich", {
1765
+ category: "footer",
1766
+ conflicts: ["footer-minimal"],
1767
+ ai: {
1768
+ whenToUse: "Multi-column footer with links",
1769
+ whenNotToUse: "Minimal sites",
1770
+ pairsWith: [],
1771
+ contentSlots: {
1772
+ brand: { type: "string" },
1773
+ description: { type: "string" },
1774
+ columns: { type: "array" },
1775
+ copyright: { type: "string" }
1776
+ }
1777
+ }
1778
+ }),
1634
1779
  "testimonials-carousel": manifest("testimonials-carousel"),
1635
1780
  "contact-form": manifest("contact-form"),
1636
- "hero-split": manifest("hero-split"),
1637
- "header-transparent": manifest("header-transparent"),
1781
+ "hero-split": manifest("hero-split", {
1782
+ category: "hero",
1783
+ conflicts: ["hero-gradient", "hero-video"]
1784
+ }),
1785
+ "header-transparent": manifest("header-transparent", {
1786
+ category: "header",
1787
+ conflicts: ["header-sticky"]
1788
+ }),
1638
1789
  "cta-newsletter": manifest("cta-newsletter"),
1639
- "header-sticky": manifest("header-sticky")
1790
+ "header-sticky": manifest("header-sticky", {
1791
+ category: "header",
1792
+ conflicts: ["header-transparent"]
1793
+ })
1640
1794
  };
1641
1795
  var FIXTURE_BLOCK_SOURCES = {
1642
1796
  "hero-gradient": {
@@ -1769,11 +1923,31 @@ export function getStaticPaths() { return [{ params: { slug: '1' } }]; }
1769
1923
  <slot />`,
1770
1924
  "default-content.json": `{ "sidebarLinks": [], "logoutText": "" }`
1771
1925
  },
1772
- "hero-video": { "hero-video.astro": "<section>Hero Video</section>\n" },
1773
- "features-bento": { "features-bento.astro": "<section>Features Bento</section>\n" },
1774
- "pricing-table": { "pricing-table.astro": "<section>Pricing</section>\n" },
1775
- "faq-accordion": { "faq-accordion.astro": "<section>FAQ</section>\n" },
1776
- "footer-rich": { "footer-rich.astro": "<section>Footer</section>\n" },
1926
+ "hero-video": {
1927
+ "hero-video.astro": "<section>Hero Video</section>\n",
1928
+ "hero-video.css": ".hero-video { min-height: 80vh; }\n",
1929
+ "default-content.json": '{"headline":"Experience the Future","subheadline":"Immersive experiences.","ctaText":"Get Started","ctaHref":"#","videoUrl":"","posterUrl":""}'
1930
+ },
1931
+ "features-bento": {
1932
+ "features-bento.astro": "<section>Features Bento</section>\n",
1933
+ "features-bento.css": ".features-bento { padding: 4rem 2rem; }\n",
1934
+ "default-content.json": '{"headline":"Features","subheadline":"Everything you need.","items":[{"title":"Fast","description":"Lightning speed."}]}'
1935
+ },
1936
+ "pricing-table": {
1937
+ "pricing-table.astro": "<section>Pricing</section>\n",
1938
+ "pricing-table.css": ".pricing-table { padding: 4rem 2rem; }\n",
1939
+ "default-content.json": '{"headline":"Pricing","subheadline":"Simple pricing.","plans":[{"name":"Free","price":"$0","features":["Basic"]}]}'
1940
+ },
1941
+ "faq-accordion": {
1942
+ "faq-accordion.astro": "<section>FAQ</section>\n",
1943
+ "faq-accordion.css": ".faq-accordion { padding: 4rem 2rem; }\n",
1944
+ "default-content.json": '{"headline":"FAQ","items":[{"question":"How does it work?","answer":"It just works."}]}'
1945
+ },
1946
+ "footer-rich": {
1947
+ "footer-rich.astro": "<section>Footer</section>\n",
1948
+ "footer-rich.css": ".footer-rich { padding: 3rem 2rem; }\n",
1949
+ "default-content.json": '{"brand":"Acme","description":"Building the future.","columns":[],"copyright":"\xA9 2025"}'
1950
+ },
1777
1951
  "testimonials-carousel": { "testimonials-carousel.astro": "<section>Testimonials</section>\n" },
1778
1952
  "contact-form": { "contact-form.astro": "<section>Contact</section>\n" },
1779
1953
  "hero-split": { "hero-split.astro": "<section>Hero Split</section>\n" },
@@ -2067,7 +2241,7 @@ async function runManualFlow(input) {
2067
2241
  message: "Choose a render mode",
2068
2242
  options: [
2069
2243
  { value: "static", label: "Static (SSG)", hint: "Pre-built HTML, fastest" },
2070
- { value: "hybrid", label: "Hybrid", hint: "Static by default, opt into SSR per page" },
2244
+ { value: "hybrid", label: "Hybrid", hint: "Static + per-page SSR opt-in" },
2071
2245
  { value: "server", label: "Server (SSR)", hint: "Server-rendered on every request" }
2072
2246
  ]
2073
2247
  });
@@ -2090,16 +2264,38 @@ async function runManualFlow(input) {
2090
2264
  ]
2091
2265
  });
2092
2266
  if (p.isCancel(cssEngine)) return handleCancel();
2093
- const blockOptions = buildBlockOptions(input.manifests);
2094
- let selectedBlocks = [];
2095
- 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) {
2096
2283
  const blocks = await p.multiselect({
2097
- message: "Select blocks to include (space to toggle, enter to confirm)",
2098
- options: blockOptions,
2284
+ message: "Select content blocks (space to toggle, enter to confirm)",
2285
+ options: contentOptions,
2099
2286
  required: false
2100
2287
  });
2101
2288
  if (p.isCancel(blocks)) return handleCancel();
2102
- 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;
2103
2299
  }
2104
2300
  const localesInput = await p.text({
2105
2301
  message: "Locales (comma-separated, e.g. en,es,ar)",
@@ -2129,6 +2325,24 @@ async function runManualFlow(input) {
2129
2325
  if (p.isCancel(switcherChoice)) return handleCancel();
2130
2326
  themeSwitcher = switcherChoice;
2131
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);
2132
2346
  const config = {
2133
2347
  projectName: projectName.trim(),
2134
2348
  projectDir: `./${projectName.trim()}`,
@@ -2168,8 +2382,18 @@ function handleCancel() {
2168
2382
  p.cancel("Operation cancelled.");
2169
2383
  return null;
2170
2384
  }
2171
- function buildBlockOptions(manifests) {
2172
- 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
+ );
2173
2397
  const categories = /* @__PURE__ */ new Map();
2174
2398
  for (const block of blocks) {
2175
2399
  const category = block.category ?? "other";
@@ -2183,7 +2407,7 @@ function buildBlockOptions(manifests) {
2183
2407
  for (const block of categoryBlocks) {
2184
2408
  options.push({
2185
2409
  value: block.name,
2186
- label: `${block.name}`,
2410
+ label: block.name,
2187
2411
  hint: `${category} \u2014 ${block.description}`
2188
2412
  });
2189
2413
  }
@@ -2285,7 +2509,7 @@ function runPostScaffold(input, callbacks) {
2285
2509
  log(pc2.dim(` ${config.packageManager} install`));
2286
2510
  }
2287
2511
  log(pc2.dim(` ${config.packageManager} dev`));
2288
- log(pc2.dim(` fornix add <block>`));
2512
+ log(pc2.dim(` npx create-fornix add <block>`));
2289
2513
  log("");
2290
2514
  }
2291
2515
  function installDependencies(projectDir, packageManager, verbose, log, warn) {
@@ -2378,9 +2602,9 @@ function generateClaudeMd(projectDir, config, blockNames) {
2378
2602
  lines.push("");
2379
2603
  lines.push(`- \`${config.packageManager} dev\` \u2014 start development server`);
2380
2604
  lines.push(`- \`${config.packageManager} build\` \u2014 build for production`);
2381
- lines.push("- `fornix add <block>` \u2014 add a new block");
2382
- lines.push("- `fornix remove <block>` \u2014 remove a block");
2383
- lines.push("- `fornix status` \u2014 show project configuration");
2605
+ lines.push("- `npx create-fornix add <block>` \u2014 add a new block");
2606
+ lines.push("- `npx create-fornix remove <block>` \u2014 remove a block");
2607
+ lines.push("- `npx create-fornix status` \u2014 show project configuration");
2384
2608
  lines.push("");
2385
2609
  lines.push("## File Structure");
2386
2610
  lines.push("");
@@ -3829,8 +4053,9 @@ async function runFlagDrivenMode(args2, manifests, allPalettes) {
3829
4053
  async function runScaffold(config, manifests, allPalettes, dryRun, verbose, skipInstall, skipGit) {
3830
4054
  const spinner2 = p2.spinner();
3831
4055
  spinner2.start("Fetching blocks from registry...");
3832
- const blockNames = config.blocks.map((b) => b.name);
3833
- const blockResults = await fetchBlocks(blockNames);
4056
+ const selectedBlockNames = config.blocks.map((b) => b.name);
4057
+ const allBlockNames = preResolveDependencies(selectedBlockNames, manifests);
4058
+ const blockResults = await fetchBlocks(allBlockNames);
3834
4059
  const blockSources = {};
3835
4060
  const blockDefaultContent = {};
3836
4061
  for (const result2 of blockResults) {
@@ -3977,12 +4202,178 @@ function showNoProviderGuide() {
3977
4202
  console.error(" export CLOUDFLARE_ACCOUNT_ID=... CLOUDFLARE_API_TOKEN=...\n");
3978
4203
  console.error(pc3.dim(" Or use manual mode: npx create-fornix --manual\n"));
3979
4204
  }
4205
+ function preResolveDependencies(selected, manifests) {
4206
+ const result = /* @__PURE__ */ new Set();
4207
+ function walk(name) {
4208
+ if (result.has(name)) return;
4209
+ const manifest2 = manifests[name];
4210
+ if (!manifest2) {
4211
+ result.add(name);
4212
+ return;
4213
+ }
4214
+ for (const dep of manifest2.requires) {
4215
+ walk(dep);
4216
+ }
4217
+ result.add(name);
4218
+ }
4219
+ for (const name of selected) {
4220
+ walk(name);
4221
+ }
4222
+ return [...result];
4223
+ }
3980
4224
 
3981
4225
  // src/cli/commands/add.ts
3982
4226
  import { defineCommand as defineCommand2 } from "citty";
3983
4227
  import pc4 from "picocolors";
3984
- import { readFileSync as readFileSync5, writeFileSync as writeFileSync5, existsSync as existsSync4, mkdirSync as mkdirSync5 } from "fs";
3985
- 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";
4230
+
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"]);
4248
+ function blockNameToComponentName(blockName) {
4249
+ return blockName.split("-").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
4250
+ }
4251
+ function blockNameToImportPath(blockName) {
4252
+ return `../components/sections/${blockName}.astro`;
4253
+ }
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) {
4282
+ const componentName = blockNameToComponentName(blockName);
4283
+ const importPath = blockNameToImportPath(blockName);
4284
+ if (pageContent.includes(importPath) || pageContent.includes(`import ${componentName}`)) {
4285
+ return pageContent;
4286
+ }
4287
+ const importLine = `import ${componentName} from '${importPath}';`;
4288
+ const componentTag = ` <${componentName} />`;
4289
+ const newCategory = getBlockCategory(blockName, projectDir);
4290
+ const newPriority = getPriority(newCategory);
4291
+ const frontmatterEnd = pageContent.indexOf("---", pageContent.indexOf("---") + 3);
4292
+ if (frontmatterEnd === -1) {
4293
+ return pageContent;
4294
+ }
4295
+ let updated = pageContent.slice(0, frontmatterEnd) + importLine + "\n" + pageContent.slice(frontmatterEnd);
4296
+ const mainOpenMatch = updated.match(/<main[^>]*>/);
4297
+ const mainCloseIndex = updated.lastIndexOf("</main>");
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
+ }
4350
+ }
4351
+ return updated;
4352
+ }
4353
+ function removeBlockFromPage(pageContent, blockName) {
4354
+ const componentName = blockNameToComponentName(blockName);
4355
+ const importPath = blockNameToImportPath(blockName);
4356
+ let updated = pageContent;
4357
+ const importRegex = new RegExp(
4358
+ `^\\s*import\\s+${componentName}\\s+from\\s+['"]${escapeRegex(importPath)}['"];?\\s*\\n?`,
4359
+ "m"
4360
+ );
4361
+ updated = updated.replace(importRegex, "");
4362
+ const tagRegex = new RegExp(
4363
+ `^\\s*<${componentName}\\s*/?>\\s*\\n?`,
4364
+ "m"
4365
+ );
4366
+ updated = updated.replace(tagRegex, "");
4367
+ return updated;
4368
+ }
4369
+ function escapeRegex(str) {
4370
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4371
+ }
4372
+ function componentNameToBlockName(componentName) {
4373
+ return componentName.replace(/([A-Z])/g, "-$1").toLowerCase().replace(/^-/, "");
4374
+ }
4375
+
4376
+ // src/cli/commands/add.ts
3986
4377
  var addCommand = defineCommand2({
3987
4378
  meta: {
3988
4379
  name: "add",
@@ -4013,14 +4404,14 @@ var addCommand = defineCommand2({
4013
4404
  async run({ args: args2 }) {
4014
4405
  const typedArgs = args2;
4015
4406
  const cwd = process.cwd();
4016
- const manifestPath = join7(cwd, "fornix.json");
4017
- if (!existsSync4(manifestPath)) {
4407
+ const manifestPath = join8(cwd, "fornix.json");
4408
+ if (!existsSync5(manifestPath)) {
4018
4409
  console.error(
4019
4410
  pc4.red("\u2717 No fornix.json found. Are you in a Fornix project?")
4020
4411
  );
4021
4412
  process.exit(1);
4022
4413
  }
4023
- const manifestRaw = readFileSync5(manifestPath, "utf-8");
4414
+ const manifestRaw = readFileSync6(manifestPath, "utf-8");
4024
4415
  const manifest2 = JSON.parse(manifestRaw);
4025
4416
  const registryResult = await fetchRegistryIndex();
4026
4417
  if (!isOk(registryResult)) {
@@ -4049,6 +4440,27 @@ var addCommand = defineCommand2({
4049
4440
  return;
4050
4441
  }
4051
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
+ }
4052
4464
  for (const name of blocksToAdd) {
4053
4465
  const m = manifests[name];
4054
4466
  if (m?.requiredMode && manifest2.renderMode !== m.requiredMode) {
@@ -4086,7 +4498,7 @@ var addCommand = defineCommand2({
4086
4498
  return;
4087
4499
  }
4088
4500
  filesToWrite.push({
4089
- path: join7(cwd, file.destination),
4501
+ path: join8(cwd, file.destination),
4090
4502
  content
4091
4503
  });
4092
4504
  }
@@ -4125,6 +4537,33 @@ var addCommand = defineCommand2({
4125
4537
  });
4126
4538
  }
4127
4539
  writeFileSync5(manifestPath, JSON.stringify(manifest2, null, 2) + "\n");
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
+ }
4564
+ }
4565
+ }
4566
+ }
4128
4567
  console.log();
4129
4568
  for (const name of blocksToAdd) {
4130
4569
  const isDep = name !== blockName;
@@ -4165,8 +4604,8 @@ function resolveDependencies2(blockName, installedNames, manifests) {
4165
4604
  // src/cli/commands/remove.ts
4166
4605
  import { defineCommand as defineCommand3 } from "citty";
4167
4606
  import pc5 from "picocolors";
4168
- import { readFileSync as readFileSync6, writeFileSync as writeFileSync6, existsSync as existsSync5, unlinkSync, readdirSync as readdirSync3, rmdirSync } from "fs";
4169
- 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";
4170
4609
  var removeCommand = defineCommand3({
4171
4610
  meta: {
4172
4611
  name: "remove",
@@ -4197,14 +4636,14 @@ var removeCommand = defineCommand3({
4197
4636
  async run({ args: args2 }) {
4198
4637
  const typedArgs = args2;
4199
4638
  const cwd = process.cwd();
4200
- const manifestPath = join8(cwd, "fornix.json");
4201
- if (!existsSync5(manifestPath)) {
4639
+ const manifestPath = join9(cwd, "fornix.json");
4640
+ if (!existsSync6(manifestPath)) {
4202
4641
  console.error(
4203
4642
  pc5.red("\u2717 No fornix.json found. Are you in a Fornix project?")
4204
4643
  );
4205
4644
  process.exit(1);
4206
4645
  }
4207
- const manifestRaw = readFileSync6(manifestPath, "utf-8");
4646
+ const manifestRaw = readFileSync7(manifestPath, "utf-8");
4208
4647
  const manifest2 = JSON.parse(manifestRaw);
4209
4648
  const registryResult = await fetchRegistryIndex();
4210
4649
  if (!isOk(registryResult)) {
@@ -4237,8 +4676,8 @@ var removeCommand = defineCommand3({
4237
4676
  const filesToRemove = [];
4238
4677
  if (blockManifest) {
4239
4678
  for (const file of blockManifest.files) {
4240
- const filePath = join8(cwd, file.destination);
4241
- if (existsSync5(filePath)) {
4679
+ const filePath = join9(cwd, file.destination);
4680
+ if (existsSync6(filePath)) {
4242
4681
  filesToRemove.push(filePath);
4243
4682
  }
4244
4683
  }
@@ -4261,6 +4700,18 @@ var removeCommand = defineCommand3({
4261
4700
  }
4262
4701
  manifest2.blocks = manifest2.blocks.filter((b) => b.name !== blockName);
4263
4702
  writeFileSync6(manifestPath, JSON.stringify(manifest2, null, 2) + "\n");
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");
4707
+ const updated = removeBlockFromPage(original, blockName);
4708
+ if (updated !== original) {
4709
+ writeFileSync6(targetFile, updated);
4710
+ if (typedArgs.verbose) {
4711
+ console.log(` ${pc5.dim("\u270E")} updated ${targetLabel}`);
4712
+ }
4713
+ }
4714
+ }
4264
4715
  console.log();
4265
4716
  console.log(` ${pc5.red("-")} ${pc5.bold(blockName)} removed`);
4266
4717
  if (dependents.length > 0) {
@@ -4432,8 +4883,8 @@ function printFormatted(blocks, verbose) {
4432
4883
  // src/cli/commands/status.ts
4433
4884
  import { defineCommand as defineCommand5 } from "citty";
4434
4885
  import pc7 from "picocolors";
4435
- import { readFileSync as readFileSync7, existsSync as existsSync6 } from "fs";
4436
- import { join as join9 } from "path";
4886
+ import { readFileSync as readFileSync8, existsSync as existsSync7 } from "fs";
4887
+ import { join as join10 } from "path";
4437
4888
  var statusCommand = defineCommand5({
4438
4889
  meta: {
4439
4890
  name: "status",
@@ -4454,8 +4905,8 @@ var statusCommand = defineCommand5({
4454
4905
  run({ args: args2 }) {
4455
4906
  const typedArgs = args2;
4456
4907
  const cwd = process.cwd();
4457
- const manifestPath = join9(cwd, "fornix.json");
4458
- if (!existsSync6(manifestPath)) {
4908
+ const manifestPath = join10(cwd, "fornix.json");
4909
+ if (!existsSync7(manifestPath)) {
4459
4910
  console.error(
4460
4911
  pc7.red("\u2717 No fornix.json found in the current directory.")
4461
4912
  );
@@ -4468,7 +4919,7 @@ var statusCommand = defineCommand5({
4468
4919
  }
4469
4920
  let manifest2;
4470
4921
  try {
4471
- const raw = readFileSync7(manifestPath, "utf-8");
4922
+ const raw = readFileSync8(manifestPath, "utf-8");
4472
4923
  manifest2 = JSON.parse(raw);
4473
4924
  } catch {
4474
4925
  console.error(pc7.red("\u2717 Failed to parse fornix.json."));
@@ -4549,8 +5000,8 @@ function printStatus(manifest2, verbose) {
4549
5000
  // src/cli/commands/doctor.ts
4550
5001
  import { defineCommand as defineCommand6 } from "citty";
4551
5002
  import pc8 from "picocolors";
4552
- import { readFileSync as readFileSync8, existsSync as existsSync7 } from "fs";
4553
- import { join as join10 } from "path";
5003
+ import { readFileSync as readFileSync9, existsSync as existsSync8 } from "fs";
5004
+ import { join as join11 } from "path";
4554
5005
  var doctorCommand = defineCommand6({
4555
5006
  meta: {
4556
5007
  name: "doctor",
@@ -4565,7 +5016,7 @@ var doctorCommand = defineCommand6({
4565
5016
  },
4566
5017
  async run({ args: args2 }) {
4567
5018
  const cwd = process.cwd();
4568
- const manifestPath = join10(cwd, "fornix.json");
5019
+ const manifestPath = join11(cwd, "fornix.json");
4569
5020
  let hasErrors = false;
4570
5021
  const errors = [];
4571
5022
  function reportError(msg) {
@@ -4575,7 +5026,7 @@ var doctorCommand = defineCommand6({
4575
5026
  console.error(pc8.red(`\u2717 ${msg}`));
4576
5027
  }
4577
5028
  }
4578
- if (!existsSync7(manifestPath)) {
5029
+ if (!existsSync8(manifestPath)) {
4579
5030
  reportError("No fornix.json found in the current directory.");
4580
5031
  if (args2.json) {
4581
5032
  console.log(JSON.stringify({ healthy: false, errors }));
@@ -4584,7 +5035,7 @@ var doctorCommand = defineCommand6({
4584
5035
  }
4585
5036
  let manifest2;
4586
5037
  try {
4587
- const raw = readFileSync8(manifestPath, "utf-8");
5038
+ const raw = readFileSync9(manifestPath, "utf-8");
4588
5039
  manifest2 = JSON.parse(raw);
4589
5040
  } catch {
4590
5041
  reportError("Failed to parse fornix.json.");
@@ -4610,8 +5061,8 @@ var doctorCommand = defineCommand6({
4610
5061
  const bManifest = manifests[block.name];
4611
5062
  if (bManifest) {
4612
5063
  for (const file of bManifest.files) {
4613
- const filePath = join10(cwd, file.destination);
4614
- if (!existsSync7(filePath)) {
5064
+ const filePath = join11(cwd, file.destination);
5065
+ if (!existsSync8(filePath)) {
4615
5066
  reportError(`Missing expected file for installed block '${block.name}': ${file.destination}`);
4616
5067
  }
4617
5068
  }
@@ -4620,7 +5071,7 @@ var doctorCommand = defineCommand6({
4620
5071
  for (const [name, bManifest] of Object.entries(manifests)) {
4621
5072
  if (!installedBlocks.has(name)) {
4622
5073
  const foundOrphaned = bManifest.files.some((file) => {
4623
- return existsSync7(join10(cwd, file.destination));
5074
+ return existsSync8(join11(cwd, file.destination));
4624
5075
  });
4625
5076
  if (foundOrphaned) {
4626
5077
  reportError(`Orphaned block files detected for '${name}'. The block is not in fornix.json.`);
@@ -4644,14 +5095,14 @@ var doctorCommand = defineCommand6({
4644
5095
  if (locale !== "") {
4645
5096
  pathFragment = `src/content/${locale}/${subdirectory}/${bManifest.name}.json`;
4646
5097
  }
4647
- if (!existsSync7(join10(cwd, pathFragment))) {
5098
+ if (!existsSync8(join11(cwd, pathFragment))) {
4648
5099
  missingContentFiles.push(pathFragment);
4649
5100
  }
4650
5101
  }
4651
5102
  }
4652
5103
  }
4653
5104
  if (requiresContentConfig) {
4654
- if (!existsSync7(join10(cwd, "src/content/config.ts"))) {
5105
+ if (!existsSync8(join11(cwd, "src/content/config.ts"))) {
4655
5106
  reportError("Missing expected file: src/content/config.ts");
4656
5107
  }
4657
5108
  }
@@ -4742,19 +5193,19 @@ async function listBlocksHandler(args2) {
4742
5193
  }
4743
5194
 
4744
5195
  // src/mcp/tools/add-block.ts
4745
- import { readFileSync as readFileSync9, writeFileSync as writeFileSync7, existsSync as existsSync8, mkdirSync as mkdirSync6 } from "fs";
4746
- 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";
4747
5198
  async function addBlock2(input) {
4748
5199
  const { name, variant = "default", projectDirectory } = input;
4749
- const manifestPath = join11(projectDirectory, "fornix.json");
4750
- if (!existsSync8(manifestPath)) {
5200
+ const manifestPath = join12(projectDirectory, "fornix.json");
5201
+ if (!existsSync9(manifestPath)) {
4751
5202
  return err(
4752
5203
  new Error("No fornix.json found. Not a Fornix project directory.")
4753
5204
  );
4754
5205
  }
4755
5206
  let manifest2;
4756
5207
  try {
4757
- const raw = readFileSync9(manifestPath, "utf-8");
5208
+ const raw = readFileSync10(manifestPath, "utf-8");
4758
5209
  manifest2 = JSON.parse(raw);
4759
5210
  } catch {
4760
5211
  return err(new Error("Failed to parse fornix.json."));
@@ -4806,7 +5257,7 @@ async function addBlock2(input) {
4806
5257
  )
4807
5258
  );
4808
5259
  }
4809
- const filePath = join11(projectDirectory, file.destination);
5260
+ const filePath = join12(projectDirectory, file.destination);
4810
5261
  mkdirSync6(dirname5(filePath), { recursive: true });
4811
5262
  writeFileSync7(filePath, content);
4812
5263
  filesCreated++;
@@ -4845,25 +5296,25 @@ function resolveDependencies3(blockName, installedNames, manifests) {
4845
5296
 
4846
5297
  // src/mcp/tools/remove-block.ts
4847
5298
  import {
4848
- readFileSync as readFileSync10,
5299
+ readFileSync as readFileSync11,
4849
5300
  writeFileSync as writeFileSync8,
4850
- existsSync as existsSync9,
5301
+ existsSync as existsSync10,
4851
5302
  unlinkSync as unlinkSync2,
4852
5303
  readdirSync as readdirSync4,
4853
5304
  rmdirSync as rmdirSync2
4854
5305
  } from "fs";
4855
- import { join as join12, dirname as dirname6 } from "path";
5306
+ import { join as join13, dirname as dirname6 } from "path";
4856
5307
  async function removeBlock(input) {
4857
5308
  const { name, force = false, projectDirectory } = input;
4858
- const manifestPath = join12(projectDirectory, "fornix.json");
4859
- if (!existsSync9(manifestPath)) {
5309
+ const manifestPath = join13(projectDirectory, "fornix.json");
5310
+ if (!existsSync10(manifestPath)) {
4860
5311
  return err(
4861
5312
  new Error("No fornix.json found. Not a Fornix project directory.")
4862
5313
  );
4863
5314
  }
4864
5315
  let manifest2;
4865
5316
  try {
4866
- const raw = readFileSync10(manifestPath, "utf-8");
5317
+ const raw = readFileSync11(manifestPath, "utf-8");
4867
5318
  manifest2 = JSON.parse(raw);
4868
5319
  } catch {
4869
5320
  return err(new Error("Failed to parse fornix.json."));
@@ -4889,8 +5340,8 @@ async function removeBlock(input) {
4889
5340
  const filesToRemove = [];
4890
5341
  if (blockManifest) {
4891
5342
  for (const file of blockManifest.files) {
4892
- const filePath = join12(projectDirectory, file.destination);
4893
- if (existsSync9(filePath)) {
5343
+ const filePath = join13(projectDirectory, file.destination);
5344
+ if (existsSync10(filePath)) {
4894
5345
  filesToRemove.push(filePath);
4895
5346
  }
4896
5347
  }
@@ -4992,7 +5443,7 @@ async function validateContent(input) {
4992
5443
  }
4993
5444
  const schemaShape = {};
4994
5445
  for (const [slotName, slot] of Object.entries(contentSlots)) {
4995
- schemaShape[slotName] = zodTypeForSlot2(slot.type);
5446
+ schemaShape[slotName] = zodTypeForSlot(slot.type);
4996
5447
  }
4997
5448
  const schema = z5.object(schemaShape);
4998
5449
  const parseResult = schema.safeParse(data);
@@ -5004,7 +5455,7 @@ async function validateContent(input) {
5004
5455
  );
5005
5456
  return ok({ valid: false, errors });
5006
5457
  }
5007
- function zodTypeForSlot2(slotType) {
5458
+ function zodTypeForSlot(slotType) {
5008
5459
  switch (slotType) {
5009
5460
  case "string":
5010
5461
  return z5.string();
@@ -5022,17 +5473,17 @@ function zodTypeForSlot2(slotType) {
5022
5473
  }
5023
5474
 
5024
5475
  // src/mcp/tools/get-project-status.ts
5025
- import { readFileSync as readFileSync11, existsSync as existsSync10 } from "fs";
5026
- import { join as join13 } from "path";
5476
+ import { readFileSync as readFileSync12, existsSync as existsSync11 } from "fs";
5477
+ import { join as join14 } from "path";
5027
5478
  function getProjectStatus(input) {
5028
- const manifestPath = join13(input.projectDirectory, "fornix.json");
5029
- if (!existsSync10(manifestPath)) {
5479
+ const manifestPath = join14(input.projectDirectory, "fornix.json");
5480
+ if (!existsSync11(manifestPath)) {
5030
5481
  return err(
5031
5482
  new Error("No fornix.json found. Not a Fornix project directory.")
5032
5483
  );
5033
5484
  }
5034
5485
  try {
5035
- const raw = readFileSync11(manifestPath, "utf-8");
5486
+ const raw = readFileSync12(manifestPath, "utf-8");
5036
5487
  const manifest2 = JSON.parse(raw);
5037
5488
  const blocks = manifest2.blocks;
5038
5489
  return ok({
@@ -5055,7 +5506,7 @@ function getProjectStatus(input) {
5055
5506
 
5056
5507
  // src/mcp/tools/scaffold-project.ts
5057
5508
  import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync9 } from "fs";
5058
- import { join as join14, basename as basename3 } from "path";
5509
+ import { join as join15, basename as basename3 } from "path";
5059
5510
  var DEFAULT_COLORS2 = {
5060
5511
  primary: "#6366f1",
5061
5512
  secondary: "#818cf8",
@@ -5126,8 +5577,8 @@ async function scaffoldProject(input) {
5126
5577
  const files = result.value.files;
5127
5578
  let filesCreated = 0;
5128
5579
  for (const [relativePath, content] of Object.entries(files)) {
5129
- const fullPath = join14(projectDirectory, relativePath);
5130
- const parentDirectory = join14(fullPath, "..");
5580
+ const fullPath = join15(projectDirectory, relativePath);
5581
+ const parentDirectory = join15(fullPath, "..");
5131
5582
  mkdirSync7(parentDirectory, { recursive: true });
5132
5583
  writeFileSync9(fullPath, content, "utf-8");
5133
5584
  filesCreated++;