create-fornix 0.0.9 → 0.0.11

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;
@@ -2003,9 +2074,10 @@ import {
2003
2074
  mkdirSync as mkdirSync2,
2004
2075
  statSync as statSync2,
2005
2076
  writeFileSync as writeFileSync2,
2006
- rmSync
2077
+ rmSync,
2078
+ cpSync
2007
2079
  } from "fs";
2008
- import { join as join3 } from "path";
2080
+ import { join as join3, resolve } from "path";
2009
2081
  import { homedir as homedir2 } from "os";
2010
2082
  import { createRequire } from "module";
2011
2083
  var DEFAULT_CONFIG2 = {
@@ -2061,6 +2133,15 @@ async function fetchBlock(blockName, config = {}) {
2061
2133
  if (!cfg.force && isCacheValid2(blockCacheDir, cfg.maxCacheAge)) {
2062
2134
  return loadFromCache2(blockName, blockCacheDir);
2063
2135
  }
2136
+ const localBlocksDir = process.env.FORNIX_LOCAL_BLOCKS_DIR;
2137
+ if (localBlocksDir) {
2138
+ const localBlockPath = resolve(process.cwd(), localBlocksDir, blockName);
2139
+ if (existsSync2(localBlockPath)) {
2140
+ mkdirSync2(cfg.cacheDir, { recursive: true });
2141
+ cpSync(localBlockPath, blockCacheDir, { recursive: true, force: true });
2142
+ return loadFromCache2(blockName, blockCacheDir);
2143
+ }
2144
+ }
2064
2145
  try {
2065
2146
  const source = `gh:${cfg.repo}/${cfg.blocksPrefix}/${blockName}#${cfg.ref}`;
2066
2147
  mkdirSync2(cfg.cacheDir, { recursive: true });
@@ -2075,10 +2156,14 @@ async function fetchBlock(blockName, config = {}) {
2075
2156
  return loadFromCache2(blockName, blockCacheDir);
2076
2157
  }
2077
2158
  const message = error instanceof Error ? error.message : String(error);
2159
+ let refinedMessage = message;
2160
+ if (message.includes("404") || message.includes("zlib: invalid distance code")) {
2161
+ refinedMessage += ` (This usually means the block doesn't exist on the remote branch. If developing locally, set FORNIX_LOCAL_BLOCKS_DIR or push your block to GitHub.)`;
2162
+ }
2078
2163
  return err({
2079
2164
  kind: "FetchError",
2080
2165
  blockName,
2081
- message: `Failed to fetch block '${blockName}': ${message}`
2166
+ message: `Failed to fetch block '${blockName}': ${refinedMessage}`
2082
2167
  });
2083
2168
  }
2084
2169
  }
@@ -2193,16 +2278,38 @@ async function runManualFlow(input) {
2193
2278
  ]
2194
2279
  });
2195
2280
  if (p.isCancel(cssEngine)) return handleCancel();
2196
- const blockOptions = buildBlockOptions(input.manifests);
2197
- let selectedBlocks = [];
2198
- if (blockOptions.length > 0) {
2281
+ const headerOptions = buildCategoryOptions(input.manifests, "header");
2282
+ const footerOptions = buildCategoryOptions(input.manifests, "footer");
2283
+ const contentOptions = buildContentBlockOptions(input.manifests);
2284
+ let selectedHeader;
2285
+ let selectedFooter;
2286
+ let selectedContentBlocks = [];
2287
+ if (headerOptions.length > 0) {
2288
+ const noneOption = { value: "__none__", label: "None", hint: "No header" };
2289
+ const headerChoice = await p.select({
2290
+ message: "Choose a header (appears on every page)",
2291
+ options: [noneOption, ...headerOptions]
2292
+ });
2293
+ if (p.isCancel(headerChoice)) return handleCancel();
2294
+ if (headerChoice !== "__none__") selectedHeader = headerChoice;
2295
+ }
2296
+ if (contentOptions.length > 0) {
2199
2297
  const blocks = await p.multiselect({
2200
- message: "Select blocks to include (space to toggle, enter to confirm)",
2201
- options: blockOptions,
2298
+ message: "Select content blocks (space to toggle, enter to confirm)",
2299
+ options: contentOptions,
2202
2300
  required: false
2203
2301
  });
2204
2302
  if (p.isCancel(blocks)) return handleCancel();
2205
- selectedBlocks = blocks;
2303
+ selectedContentBlocks = blocks;
2304
+ }
2305
+ if (footerOptions.length > 0) {
2306
+ const noneOption = { value: "__none__", label: "None", hint: "No footer" };
2307
+ const footerChoice = await p.select({
2308
+ message: "Choose a footer (appears on every page)",
2309
+ options: [noneOption, ...footerOptions]
2310
+ });
2311
+ if (p.isCancel(footerChoice)) return handleCancel();
2312
+ if (footerChoice !== "__none__") selectedFooter = footerChoice;
2206
2313
  }
2207
2314
  const localesInput = await p.text({
2208
2315
  message: "Locales (comma-separated, e.g. en,es,ar)",
@@ -2232,6 +2339,24 @@ async function runManualFlow(input) {
2232
2339
  if (p.isCancel(switcherChoice)) return handleCancel();
2233
2340
  themeSwitcher = switcherChoice;
2234
2341
  }
2342
+ if (!selectedHeader && headerOptions.length > 0) {
2343
+ const needsNav = locales.length >= 2 || themeSwitcher;
2344
+ if (needsNav) {
2345
+ const autoHeader = await p.confirm({
2346
+ message: `You enabled ${locales.length >= 2 ? "multiple locales" : "theme switching"} \u2014 add a header for navigation?`,
2347
+ initialValue: true
2348
+ });
2349
+ if (p.isCancel(autoHeader)) return handleCancel();
2350
+ if (autoHeader) {
2351
+ selectedHeader = headerOptions[0].value;
2352
+ console.log(pc.dim(` Adding ${selectedHeader} for navigation.`));
2353
+ }
2354
+ }
2355
+ }
2356
+ const selectedBlocks = [];
2357
+ if (selectedHeader) selectedBlocks.push(selectedHeader);
2358
+ selectedBlocks.push(...selectedContentBlocks);
2359
+ if (selectedFooter) selectedBlocks.push(selectedFooter);
2235
2360
  const config = {
2236
2361
  projectName: projectName.trim(),
2237
2362
  projectDir: `./${projectName.trim()}`,
@@ -2271,8 +2396,18 @@ function handleCancel() {
2271
2396
  p.cancel("Operation cancelled.");
2272
2397
  return null;
2273
2398
  }
2274
- function buildBlockOptions(manifests) {
2275
- const blocks = Object.values(manifests);
2399
+ function buildCategoryOptions(manifests, category) {
2400
+ return Object.values(manifests).filter((block) => (block.category ?? "other") === category).map((block) => ({
2401
+ value: block.name,
2402
+ label: block.name,
2403
+ hint: block.description
2404
+ }));
2405
+ }
2406
+ function buildContentBlockOptions(manifests) {
2407
+ const LAYOUT_CATEGORIES2 = /* @__PURE__ */ new Set(["header", "footer"]);
2408
+ const blocks = Object.values(manifests).filter(
2409
+ (block) => !LAYOUT_CATEGORIES2.has(block.category ?? "other")
2410
+ );
2276
2411
  const categories = /* @__PURE__ */ new Map();
2277
2412
  for (const block of blocks) {
2278
2413
  const category = block.category ?? "other";
@@ -2286,7 +2421,7 @@ function buildBlockOptions(manifests) {
2286
2421
  for (const block of categoryBlocks) {
2287
2422
  options.push({
2288
2423
  value: block.name,
2289
- label: `${block.name}`,
2424
+ label: block.name,
2290
2425
  hint: `${category} \u2014 ${block.description}`
2291
2426
  });
2292
2427
  }
@@ -4104,17 +4239,60 @@ function preResolveDependencies(selected, manifests) {
4104
4239
  // src/cli/commands/add.ts
4105
4240
  import { defineCommand as defineCommand2 } from "citty";
4106
4241
  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";
4242
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync5, existsSync as existsSync5, mkdirSync as mkdirSync5 } from "fs";
4243
+ import { join as join8, dirname as dirname3 } from "path";
4109
4244
 
4110
4245
  // src/scaffold/page-updater.ts
4246
+ import { readFileSync as readFileSync5, existsSync as existsSync4 } from "fs";
4247
+ import { join as join7 } from "path";
4248
+ var CATEGORY_ORDER = {
4249
+ header: 0,
4250
+ hero: 1,
4251
+ features: 2,
4252
+ pricing: 3,
4253
+ testimonials: 4,
4254
+ faq: 5,
4255
+ cta: 6,
4256
+ contact: 7,
4257
+ theme: 8,
4258
+ footer: 9
4259
+ };
4260
+ var DEFAULT_PRIORITY = 5;
4261
+ var LAYOUT_CATEGORIES = /* @__PURE__ */ new Set(["header", "footer"]);
4111
4262
  function blockNameToComponentName(blockName) {
4112
4263
  return blockName.split("-").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
4113
4264
  }
4114
4265
  function blockNameToImportPath(blockName) {
4115
4266
  return `../components/sections/${blockName}.astro`;
4116
4267
  }
4117
- function addBlockToPage(pageContent, blockName) {
4268
+ function blockNameToLayoutImportPath(blockName) {
4269
+ return `../components/sections/${blockName}.astro`;
4270
+ }
4271
+ function getBlockCategory(blockName, projectDir) {
4272
+ if (projectDir) {
4273
+ try {
4274
+ const fornixPath = join7(projectDir, "fornix.json");
4275
+ if (existsSync4(fornixPath)) {
4276
+ const fornix = JSON.parse(readFileSync5(fornixPath, "utf-8"));
4277
+ const block = fornix.blocks?.find((b) => b.name === blockName);
4278
+ if (block?.category) return block.category;
4279
+ }
4280
+ } catch {
4281
+ }
4282
+ }
4283
+ for (const prefix of Object.keys(CATEGORY_ORDER)) {
4284
+ if (blockName.startsWith(prefix)) return prefix;
4285
+ }
4286
+ return "other";
4287
+ }
4288
+ function getPriority(category) {
4289
+ return CATEGORY_ORDER[category] ?? DEFAULT_PRIORITY;
4290
+ }
4291
+ function isLayoutBlock(blockName, projectDir) {
4292
+ const category = getBlockCategory(blockName, projectDir);
4293
+ return LAYOUT_CATEGORIES.has(category);
4294
+ }
4295
+ function addBlockToPage(pageContent, blockName, projectDir) {
4118
4296
  const componentName = blockNameToComponentName(blockName);
4119
4297
  const importPath = blockNameToImportPath(blockName);
4120
4298
  if (pageContent.includes(importPath) || pageContent.includes(`import ${componentName}`)) {
@@ -4122,14 +4300,67 @@ function addBlockToPage(pageContent, blockName) {
4122
4300
  }
4123
4301
  const importLine = `import ${componentName} from '${importPath}';`;
4124
4302
  const componentTag = ` <${componentName} />`;
4303
+ const newCategory = getBlockCategory(blockName, projectDir);
4304
+ const newPriority = getPriority(newCategory);
4125
4305
  const frontmatterEnd = pageContent.indexOf("---", pageContent.indexOf("---") + 3);
4126
4306
  if (frontmatterEnd === -1) {
4127
4307
  return pageContent;
4128
4308
  }
4129
4309
  let updated = pageContent.slice(0, frontmatterEnd) + importLine + "\n" + pageContent.slice(frontmatterEnd);
4310
+ const mainOpenMatch = updated.match(/<main[^>]*>/);
4130
4311
  const mainCloseIndex = updated.lastIndexOf("</main>");
4131
- if (mainCloseIndex !== -1) {
4132
- updated = updated.slice(0, mainCloseIndex) + componentTag + "\n " + updated.slice(mainCloseIndex);
4312
+ if (!mainOpenMatch || mainCloseIndex === -1) {
4313
+ return updated;
4314
+ }
4315
+ const mainOpenEnd = mainOpenMatch.index + mainOpenMatch[0].length;
4316
+ const mainContent = updated.slice(mainOpenEnd, mainCloseIndex);
4317
+ const tagPattern = /^(\s*<([A-Z][A-Za-z]*)\s*\/>)\s*$/gm;
4318
+ let insertOffset = mainCloseIndex;
4319
+ let match;
4320
+ while ((match = tagPattern.exec(mainContent)) !== null) {
4321
+ const existingComponentName = match[2];
4322
+ const existingBlockName = componentNameToBlockName(existingComponentName);
4323
+ const existingCategory = getBlockCategory(existingBlockName, projectDir);
4324
+ const existingPriority = getPriority(existingCategory);
4325
+ if (existingPriority > newPriority) {
4326
+ insertOffset = mainOpenEnd + match.index;
4327
+ break;
4328
+ }
4329
+ }
4330
+ updated = updated.slice(0, insertOffset) + componentTag + "\n" + updated.slice(insertOffset);
4331
+ return updated;
4332
+ }
4333
+ function addBlockToLayout(layoutContent, blockName, projectDir) {
4334
+ const componentName = blockNameToComponentName(blockName);
4335
+ const importPath = blockNameToLayoutImportPath(blockName);
4336
+ if (layoutContent.includes(importPath) || layoutContent.includes(`import ${componentName}`)) {
4337
+ return layoutContent;
4338
+ }
4339
+ const importLine = `import ${componentName} from '${importPath}';`;
4340
+ const componentTag = ` <${componentName} />`;
4341
+ const category = getBlockCategory(blockName, projectDir);
4342
+ const frontmatterEnd = layoutContent.indexOf("---", layoutContent.indexOf("---") + 3);
4343
+ if (frontmatterEnd === -1) {
4344
+ return layoutContent;
4345
+ }
4346
+ let updated = layoutContent.slice(0, frontmatterEnd) + importLine + "\n" + layoutContent.slice(frontmatterEnd);
4347
+ if (category === "header") {
4348
+ const slotIndex = updated.indexOf("<slot");
4349
+ const mainIndex = updated.indexOf("<main");
4350
+ const insertBefore = mainIndex !== -1 ? mainIndex : slotIndex;
4351
+ if (insertBefore !== -1) {
4352
+ updated = updated.slice(0, insertBefore) + componentTag + "\n" + updated.slice(insertBefore);
4353
+ }
4354
+ } else if (category === "footer") {
4355
+ const mainCloseIndex = updated.indexOf("</main>");
4356
+ const slotMatch = updated.match(/<slot\s*\/>/);
4357
+ if (mainCloseIndex !== -1) {
4358
+ const afterMain = mainCloseIndex + "</main>".length;
4359
+ updated = updated.slice(0, afterMain) + "\n" + componentTag + updated.slice(afterMain);
4360
+ } else if (slotMatch) {
4361
+ const afterSlot = slotMatch.index + slotMatch[0].length;
4362
+ updated = updated.slice(0, afterSlot) + "\n" + componentTag + updated.slice(afterSlot);
4363
+ }
4133
4364
  }
4134
4365
  return updated;
4135
4366
  }
@@ -4152,6 +4383,9 @@ function removeBlockFromPage(pageContent, blockName) {
4152
4383
  function escapeRegex(str) {
4153
4384
  return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4154
4385
  }
4386
+ function componentNameToBlockName(componentName) {
4387
+ return componentName.replace(/([A-Z])/g, "-$1").toLowerCase().replace(/^-/, "");
4388
+ }
4155
4389
 
4156
4390
  // src/cli/commands/add.ts
4157
4391
  var addCommand = defineCommand2({
@@ -4184,14 +4418,14 @@ var addCommand = defineCommand2({
4184
4418
  async run({ args: args2 }) {
4185
4419
  const typedArgs = args2;
4186
4420
  const cwd = process.cwd();
4187
- const manifestPath = join7(cwd, "fornix.json");
4188
- if (!existsSync4(manifestPath)) {
4421
+ const manifestPath = join8(cwd, "fornix.json");
4422
+ if (!existsSync5(manifestPath)) {
4189
4423
  console.error(
4190
4424
  pc4.red("\u2717 No fornix.json found. Are you in a Fornix project?")
4191
4425
  );
4192
4426
  process.exit(1);
4193
4427
  }
4194
- const manifestRaw = readFileSync5(manifestPath, "utf-8");
4428
+ const manifestRaw = readFileSync6(manifestPath, "utf-8");
4195
4429
  const manifest2 = JSON.parse(manifestRaw);
4196
4430
  const registryResult = await fetchRegistryIndex();
4197
4431
  if (!isOk(registryResult)) {
@@ -4220,6 +4454,27 @@ var addCommand = defineCommand2({
4220
4454
  return;
4221
4455
  }
4222
4456
  const blocksToAdd = resolveDependencies2(blockName, installedNames, manifests);
4457
+ for (const name of blocksToAdd) {
4458
+ const m = manifests[name];
4459
+ if (m?.conflicts && m.conflicts.length > 0) {
4460
+ for (const conflictName of m.conflicts) {
4461
+ if (installedNames.has(conflictName)) {
4462
+ console.error(
4463
+ pc4.red(
4464
+ `\u2717 Block '${name}' conflicts with installed block '${conflictName}'.`
4465
+ )
4466
+ );
4467
+ console.log(
4468
+ pc4.dim(
4469
+ ` Remove '${conflictName}' first: npx create-fornix remove ${conflictName}`
4470
+ )
4471
+ );
4472
+ process.exitCode = 1;
4473
+ return;
4474
+ }
4475
+ }
4476
+ }
4477
+ }
4223
4478
  for (const name of blocksToAdd) {
4224
4479
  const m = manifests[name];
4225
4480
  if (m?.requiredMode && manifest2.renderMode !== m.requiredMode) {
@@ -4257,10 +4512,17 @@ var addCommand = defineCommand2({
4257
4512
  return;
4258
4513
  }
4259
4514
  filesToWrite.push({
4260
- path: join7(cwd, file.destination),
4515
+ path: join8(cwd, file.destination),
4261
4516
  content
4262
4517
  });
4263
4518
  }
4519
+ if (sources["default-content.json"]) {
4520
+ const collection = bManifest.type === "section" ? "sections" : bManifest.type + "s";
4521
+ filesToWrite.push({
4522
+ path: join8(cwd, `src/content/${collection}/${name}.json`),
4523
+ content: sources["default-content.json"]
4524
+ });
4525
+ }
4264
4526
  }
4265
4527
  if (typedArgs["dry-run"]) {
4266
4528
  console.log(pc4.bold("\n Dry run \u2014 no files written\n"));
@@ -4296,18 +4558,31 @@ var addCommand = defineCommand2({
4296
4558
  });
4297
4559
  }
4298
4560
  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);
4561
+ const indexPath = join8(cwd, "src/pages/index.astro");
4562
+ const layoutPath = join8(cwd, "src/layouts/Layout.astro");
4563
+ for (const name of blocksToAdd) {
4564
+ const bManifest = manifests[name];
4565
+ if (!bManifest || bManifest.type !== "section") continue;
4566
+ if (isLayoutBlock(name, cwd)) {
4567
+ if (existsSync5(layoutPath)) {
4568
+ const layoutContent = readFileSync6(layoutPath, "utf-8");
4569
+ const updated = addBlockToLayout(layoutContent, name, cwd);
4570
+ if (updated !== layoutContent) {
4571
+ writeFileSync5(layoutPath, updated);
4572
+ if (typedArgs.verbose) {
4573
+ console.log(` ${pc4.dim("\u270E")} updated Layout.astro (${name})`);
4574
+ }
4575
+ }
4576
+ }
4577
+ } else {
4578
+ if (existsSync5(indexPath)) {
4579
+ let pageContent = readFileSync6(indexPath, "utf-8");
4580
+ pageContent = addBlockToPage(pageContent, name, cwd);
4581
+ writeFileSync5(indexPath, pageContent);
4582
+ if (typedArgs.verbose) {
4583
+ console.log(` ${pc4.dim("\u270E")} updated index.astro (${name})`);
4584
+ }
4306
4585
  }
4307
- }
4308
- writeFileSync5(indexPath, pageContent);
4309
- if (typedArgs.verbose) {
4310
- console.log(` ${pc4.dim("\u270E")} updated index.astro`);
4311
4586
  }
4312
4587
  }
4313
4588
  console.log();
@@ -4350,8 +4625,8 @@ function resolveDependencies2(blockName, installedNames, manifests) {
4350
4625
  // src/cli/commands/remove.ts
4351
4626
  import { defineCommand as defineCommand3 } from "citty";
4352
4627
  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";
4628
+ import { readFileSync as readFileSync7, writeFileSync as writeFileSync6, existsSync as existsSync6, unlinkSync, readdirSync as readdirSync3, rmdirSync } from "fs";
4629
+ import { join as join9, dirname as dirname4 } from "path";
4355
4630
  var removeCommand = defineCommand3({
4356
4631
  meta: {
4357
4632
  name: "remove",
@@ -4382,14 +4657,14 @@ var removeCommand = defineCommand3({
4382
4657
  async run({ args: args2 }) {
4383
4658
  const typedArgs = args2;
4384
4659
  const cwd = process.cwd();
4385
- const manifestPath = join8(cwd, "fornix.json");
4386
- if (!existsSync5(manifestPath)) {
4660
+ const manifestPath = join9(cwd, "fornix.json");
4661
+ if (!existsSync6(manifestPath)) {
4387
4662
  console.error(
4388
4663
  pc5.red("\u2717 No fornix.json found. Are you in a Fornix project?")
4389
4664
  );
4390
4665
  process.exit(1);
4391
4666
  }
4392
- const manifestRaw = readFileSync6(manifestPath, "utf-8");
4667
+ const manifestRaw = readFileSync7(manifestPath, "utf-8");
4393
4668
  const manifest2 = JSON.parse(manifestRaw);
4394
4669
  const registryResult = await fetchRegistryIndex();
4395
4670
  if (!isOk(registryResult)) {
@@ -4422,8 +4697,8 @@ var removeCommand = defineCommand3({
4422
4697
  const filesToRemove = [];
4423
4698
  if (blockManifest) {
4424
4699
  for (const file of blockManifest.files) {
4425
- const filePath = join8(cwd, file.destination);
4426
- if (existsSync5(filePath)) {
4700
+ const filePath = join9(cwd, file.destination);
4701
+ if (existsSync6(filePath)) {
4427
4702
  filesToRemove.push(filePath);
4428
4703
  }
4429
4704
  }
@@ -4446,14 +4721,15 @@ var removeCommand = defineCommand3({
4446
4721
  }
4447
4722
  manifest2.blocks = manifest2.blocks.filter((b) => b.name !== blockName);
4448
4723
  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");
4724
+ const targetFile = isLayoutBlock(blockName, cwd) ? join9(cwd, "src/layouts/Layout.astro") : join9(cwd, "src/pages/index.astro");
4725
+ const targetLabel = isLayoutBlock(blockName, cwd) ? "Layout.astro" : "index.astro";
4726
+ if (existsSync6(targetFile)) {
4727
+ const original = readFileSync7(targetFile, "utf-8");
4452
4728
  const updated = removeBlockFromPage(original, blockName);
4453
4729
  if (updated !== original) {
4454
- writeFileSync6(indexPath, updated);
4730
+ writeFileSync6(targetFile, updated);
4455
4731
  if (typedArgs.verbose) {
4456
- console.log(` ${pc5.dim("\u270E")} updated index.astro`);
4732
+ console.log(` ${pc5.dim("\u270E")} updated ${targetLabel}`);
4457
4733
  }
4458
4734
  }
4459
4735
  }
@@ -4628,8 +4904,8 @@ function printFormatted(blocks, verbose) {
4628
4904
  // src/cli/commands/status.ts
4629
4905
  import { defineCommand as defineCommand5 } from "citty";
4630
4906
  import pc7 from "picocolors";
4631
- import { readFileSync as readFileSync7, existsSync as existsSync6 } from "fs";
4632
- import { join as join9 } from "path";
4907
+ import { readFileSync as readFileSync8, existsSync as existsSync7 } from "fs";
4908
+ import { join as join10 } from "path";
4633
4909
  var statusCommand = defineCommand5({
4634
4910
  meta: {
4635
4911
  name: "status",
@@ -4650,8 +4926,8 @@ var statusCommand = defineCommand5({
4650
4926
  run({ args: args2 }) {
4651
4927
  const typedArgs = args2;
4652
4928
  const cwd = process.cwd();
4653
- const manifestPath = join9(cwd, "fornix.json");
4654
- if (!existsSync6(manifestPath)) {
4929
+ const manifestPath = join10(cwd, "fornix.json");
4930
+ if (!existsSync7(manifestPath)) {
4655
4931
  console.error(
4656
4932
  pc7.red("\u2717 No fornix.json found in the current directory.")
4657
4933
  );
@@ -4664,7 +4940,7 @@ var statusCommand = defineCommand5({
4664
4940
  }
4665
4941
  let manifest2;
4666
4942
  try {
4667
- const raw = readFileSync7(manifestPath, "utf-8");
4943
+ const raw = readFileSync8(manifestPath, "utf-8");
4668
4944
  manifest2 = JSON.parse(raw);
4669
4945
  } catch {
4670
4946
  console.error(pc7.red("\u2717 Failed to parse fornix.json."));
@@ -4745,8 +5021,8 @@ function printStatus(manifest2, verbose) {
4745
5021
  // src/cli/commands/doctor.ts
4746
5022
  import { defineCommand as defineCommand6 } from "citty";
4747
5023
  import pc8 from "picocolors";
4748
- import { readFileSync as readFileSync8, existsSync as existsSync7 } from "fs";
4749
- import { join as join10 } from "path";
5024
+ import { readFileSync as readFileSync9, existsSync as existsSync8 } from "fs";
5025
+ import { join as join11 } from "path";
4750
5026
  var doctorCommand = defineCommand6({
4751
5027
  meta: {
4752
5028
  name: "doctor",
@@ -4761,7 +5037,7 @@ var doctorCommand = defineCommand6({
4761
5037
  },
4762
5038
  async run({ args: args2 }) {
4763
5039
  const cwd = process.cwd();
4764
- const manifestPath = join10(cwd, "fornix.json");
5040
+ const manifestPath = join11(cwd, "fornix.json");
4765
5041
  let hasErrors = false;
4766
5042
  const errors = [];
4767
5043
  function reportError(msg) {
@@ -4771,7 +5047,7 @@ var doctorCommand = defineCommand6({
4771
5047
  console.error(pc8.red(`\u2717 ${msg}`));
4772
5048
  }
4773
5049
  }
4774
- if (!existsSync7(manifestPath)) {
5050
+ if (!existsSync8(manifestPath)) {
4775
5051
  reportError("No fornix.json found in the current directory.");
4776
5052
  if (args2.json) {
4777
5053
  console.log(JSON.stringify({ healthy: false, errors }));
@@ -4780,7 +5056,7 @@ var doctorCommand = defineCommand6({
4780
5056
  }
4781
5057
  let manifest2;
4782
5058
  try {
4783
- const raw = readFileSync8(manifestPath, "utf-8");
5059
+ const raw = readFileSync9(manifestPath, "utf-8");
4784
5060
  manifest2 = JSON.parse(raw);
4785
5061
  } catch {
4786
5062
  reportError("Failed to parse fornix.json.");
@@ -4806,8 +5082,8 @@ var doctorCommand = defineCommand6({
4806
5082
  const bManifest = manifests[block.name];
4807
5083
  if (bManifest) {
4808
5084
  for (const file of bManifest.files) {
4809
- const filePath = join10(cwd, file.destination);
4810
- if (!existsSync7(filePath)) {
5085
+ const filePath = join11(cwd, file.destination);
5086
+ if (!existsSync8(filePath)) {
4811
5087
  reportError(`Missing expected file for installed block '${block.name}': ${file.destination}`);
4812
5088
  }
4813
5089
  }
@@ -4816,7 +5092,7 @@ var doctorCommand = defineCommand6({
4816
5092
  for (const [name, bManifest] of Object.entries(manifests)) {
4817
5093
  if (!installedBlocks.has(name)) {
4818
5094
  const foundOrphaned = bManifest.files.some((file) => {
4819
- return existsSync7(join10(cwd, file.destination));
5095
+ return existsSync8(join11(cwd, file.destination));
4820
5096
  });
4821
5097
  if (foundOrphaned) {
4822
5098
  reportError(`Orphaned block files detected for '${name}'. The block is not in fornix.json.`);
@@ -4840,14 +5116,14 @@ var doctorCommand = defineCommand6({
4840
5116
  if (locale !== "") {
4841
5117
  pathFragment = `src/content/${locale}/${subdirectory}/${bManifest.name}.json`;
4842
5118
  }
4843
- if (!existsSync7(join10(cwd, pathFragment))) {
5119
+ if (!existsSync8(join11(cwd, pathFragment))) {
4844
5120
  missingContentFiles.push(pathFragment);
4845
5121
  }
4846
5122
  }
4847
5123
  }
4848
5124
  }
4849
5125
  if (requiresContentConfig) {
4850
- if (!existsSync7(join10(cwd, "src/content/config.ts"))) {
5126
+ if (!existsSync8(join11(cwd, "src/content/config.ts"))) {
4851
5127
  reportError("Missing expected file: src/content/config.ts");
4852
5128
  }
4853
5129
  }
@@ -4938,19 +5214,19 @@ async function listBlocksHandler(args2) {
4938
5214
  }
4939
5215
 
4940
5216
  // 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";
5217
+ import { readFileSync as readFileSync10, writeFileSync as writeFileSync7, existsSync as existsSync9, mkdirSync as mkdirSync6 } from "fs";
5218
+ import { join as join12, dirname as dirname5 } from "path";
4943
5219
  async function addBlock2(input) {
4944
5220
  const { name, variant = "default", projectDirectory } = input;
4945
- const manifestPath = join11(projectDirectory, "fornix.json");
4946
- if (!existsSync8(manifestPath)) {
5221
+ const manifestPath = join12(projectDirectory, "fornix.json");
5222
+ if (!existsSync9(manifestPath)) {
4947
5223
  return err(
4948
5224
  new Error("No fornix.json found. Not a Fornix project directory.")
4949
5225
  );
4950
5226
  }
4951
5227
  let manifest2;
4952
5228
  try {
4953
- const raw = readFileSync9(manifestPath, "utf-8");
5229
+ const raw = readFileSync10(manifestPath, "utf-8");
4954
5230
  manifest2 = JSON.parse(raw);
4955
5231
  } catch {
4956
5232
  return err(new Error("Failed to parse fornix.json."));
@@ -5002,7 +5278,7 @@ async function addBlock2(input) {
5002
5278
  )
5003
5279
  );
5004
5280
  }
5005
- const filePath = join11(projectDirectory, file.destination);
5281
+ const filePath = join12(projectDirectory, file.destination);
5006
5282
  mkdirSync6(dirname5(filePath), { recursive: true });
5007
5283
  writeFileSync7(filePath, content);
5008
5284
  filesCreated++;
@@ -5041,25 +5317,25 @@ function resolveDependencies3(blockName, installedNames, manifests) {
5041
5317
 
5042
5318
  // src/mcp/tools/remove-block.ts
5043
5319
  import {
5044
- readFileSync as readFileSync10,
5320
+ readFileSync as readFileSync11,
5045
5321
  writeFileSync as writeFileSync8,
5046
- existsSync as existsSync9,
5322
+ existsSync as existsSync10,
5047
5323
  unlinkSync as unlinkSync2,
5048
5324
  readdirSync as readdirSync4,
5049
5325
  rmdirSync as rmdirSync2
5050
5326
  } from "fs";
5051
- import { join as join12, dirname as dirname6 } from "path";
5327
+ import { join as join13, dirname as dirname6 } from "path";
5052
5328
  async function removeBlock(input) {
5053
5329
  const { name, force = false, projectDirectory } = input;
5054
- const manifestPath = join12(projectDirectory, "fornix.json");
5055
- if (!existsSync9(manifestPath)) {
5330
+ const manifestPath = join13(projectDirectory, "fornix.json");
5331
+ if (!existsSync10(manifestPath)) {
5056
5332
  return err(
5057
5333
  new Error("No fornix.json found. Not a Fornix project directory.")
5058
5334
  );
5059
5335
  }
5060
5336
  let manifest2;
5061
5337
  try {
5062
- const raw = readFileSync10(manifestPath, "utf-8");
5338
+ const raw = readFileSync11(manifestPath, "utf-8");
5063
5339
  manifest2 = JSON.parse(raw);
5064
5340
  } catch {
5065
5341
  return err(new Error("Failed to parse fornix.json."));
@@ -5085,8 +5361,8 @@ async function removeBlock(input) {
5085
5361
  const filesToRemove = [];
5086
5362
  if (blockManifest) {
5087
5363
  for (const file of blockManifest.files) {
5088
- const filePath = join12(projectDirectory, file.destination);
5089
- if (existsSync9(filePath)) {
5364
+ const filePath = join13(projectDirectory, file.destination);
5365
+ if (existsSync10(filePath)) {
5090
5366
  filesToRemove.push(filePath);
5091
5367
  }
5092
5368
  }
@@ -5218,17 +5494,17 @@ function zodTypeForSlot(slotType) {
5218
5494
  }
5219
5495
 
5220
5496
  // src/mcp/tools/get-project-status.ts
5221
- import { readFileSync as readFileSync11, existsSync as existsSync10 } from "fs";
5222
- import { join as join13 } from "path";
5497
+ import { readFileSync as readFileSync12, existsSync as existsSync11 } from "fs";
5498
+ import { join as join14 } from "path";
5223
5499
  function getProjectStatus(input) {
5224
- const manifestPath = join13(input.projectDirectory, "fornix.json");
5225
- if (!existsSync10(manifestPath)) {
5500
+ const manifestPath = join14(input.projectDirectory, "fornix.json");
5501
+ if (!existsSync11(manifestPath)) {
5226
5502
  return err(
5227
5503
  new Error("No fornix.json found. Not a Fornix project directory.")
5228
5504
  );
5229
5505
  }
5230
5506
  try {
5231
- const raw = readFileSync11(manifestPath, "utf-8");
5507
+ const raw = readFileSync12(manifestPath, "utf-8");
5232
5508
  const manifest2 = JSON.parse(raw);
5233
5509
  const blocks = manifest2.blocks;
5234
5510
  return ok({
@@ -5251,7 +5527,7 @@ function getProjectStatus(input) {
5251
5527
 
5252
5528
  // src/mcp/tools/scaffold-project.ts
5253
5529
  import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync9 } from "fs";
5254
- import { join as join14, basename as basename3 } from "path";
5530
+ import { join as join15, basename as basename3 } from "path";
5255
5531
  var DEFAULT_COLORS2 = {
5256
5532
  primary: "#6366f1",
5257
5533
  secondary: "#818cf8",
@@ -5322,8 +5598,8 @@ async function scaffoldProject(input) {
5322
5598
  const files = result.value.files;
5323
5599
  let filesCreated = 0;
5324
5600
  for (const [relativePath, content] of Object.entries(files)) {
5325
- const fullPath = join14(projectDirectory, relativePath);
5326
- const parentDirectory = join14(fullPath, "..");
5601
+ const fullPath = join15(projectDirectory, relativePath);
5602
+ const parentDirectory = join15(fullPath, "..");
5327
5603
  mkdirSync7(parentDirectory, { recursive: true });
5328
5604
  writeFileSync9(fullPath, content, "utf-8");
5329
5605
  filesCreated++;