cmx-sdk 0.2.2 → 0.2.4

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/cli.js CHANGED
@@ -384,10 +384,10 @@ async function confirm(message) {
384
384
  input: process.stdin,
385
385
  output: process.stdout
386
386
  });
387
- return new Promise((resolve2) => {
387
+ return new Promise((resolve5) => {
388
388
  rl.question(`${message} (y/N): `, (answer) => {
389
389
  rl.close();
390
- resolve2(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
390
+ resolve5(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
391
391
  });
392
392
  });
393
393
  }
@@ -429,10 +429,10 @@ async function confirm2(message) {
429
429
  input: process.stdin,
430
430
  output: process.stdout
431
431
  });
432
- return new Promise((resolve2) => {
432
+ return new Promise((resolve5) => {
433
433
  rl.question(`${message} (y/N): `, (answer) => {
434
434
  rl.close();
435
- resolve2(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
435
+ resolve5(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
436
436
  });
437
437
  });
438
438
  }
@@ -474,10 +474,10 @@ async function confirm3(message) {
474
474
  input: process.stdin,
475
475
  output: process.stdout
476
476
  });
477
- return new Promise((resolve2) => {
477
+ return new Promise((resolve5) => {
478
478
  rl.question(`${message} (y/N): `, (answer) => {
479
479
  rl.close();
480
- resolve2(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
480
+ resolve5(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
481
481
  });
482
482
  });
483
483
  }
@@ -516,78 +516,199 @@ async function deleteForm(options) {
516
516
  // src/commands/sync-components.ts
517
517
  import { readdirSync, readFileSync as readFileSync2, existsSync } from "fs";
518
518
  import { join } from "path";
519
- import { execSync } from "child_process";
520
- function detectEnvironment() {
521
- try {
522
- const branch = execSync("git branch --show-current", { encoding: "utf-8" }).trim();
523
- if (branch === "main") return "production";
524
- if (branch === "develop") return "staging";
525
- const prMatch = branch.match(/^(?:feature|fix)\/(.+)$/);
526
- if (prMatch) {
527
- try {
528
- const prNumber = execSync("gh pr view --json number -q .number", {
529
- encoding: "utf-8",
530
- stdio: ["pipe", "pipe", "ignore"]
531
- }).trim();
532
- if (prNumber) return `preview/pr-${prNumber}`;
533
- } catch {
534
- }
535
- }
536
- return `preview/${branch.replace(/[^a-zA-Z0-9-]/g, "-")}`;
537
- } catch {
538
- return "production";
539
- }
519
+ import { z as z2 } from "zod";
520
+
521
+ // src/mdx/component-catalog.ts
522
+ import { z } from "zod";
523
+ var componentSchemas = {
524
+ BlogCard: z.object({
525
+ contentId: z.string().uuid().describe("\u53C2\u7167\u5148\u30B3\u30F3\u30C6\u30F3\u30C4\u306EUUID")
526
+ }),
527
+ Image: z.object({
528
+ assetId: z.string().uuid().describe("\u30A2\u30BB\u30C3\u30C8\u306EUUID"),
529
+ alt: z.string().optional().describe("\u4EE3\u66FF\u30C6\u30AD\u30B9\u30C8"),
530
+ size: z.enum(["thumbnail", "medium", "large", "original"]).default("large").describe("\u8868\u793A\u30B5\u30A4\u30BA"),
531
+ caption: z.string().optional().describe("\u30AD\u30E3\u30D7\u30B7\u30E7\u30F3")
532
+ }),
533
+ Callout: z.object({
534
+ type: z.enum(["info", "warning", "error", "success", "tip"]).default("info").describe("\u30BF\u30A4\u30D7"),
535
+ title: z.string().optional().describe("\u30BF\u30A4\u30C8\u30EB"),
536
+ children: z.string().describe("\u672C\u6587\uFF08Markdown\u53EF\uFF09")
537
+ }),
538
+ Embed: z.object({
539
+ url: z.string().url().describe("\u57CB\u3081\u8FBC\u307FURL"),
540
+ type: z.enum(["youtube", "twitter", "generic"]).default("generic").describe("\u30BF\u30A4\u30D7")
541
+ }),
542
+ Button: z.object({
543
+ href: z.string().describe("\u30EA\u30F3\u30AF\u5148URL"),
544
+ children: z.string().describe("\u30DC\u30BF\u30F3\u30C6\u30AD\u30B9\u30C8"),
545
+ variant: z.enum(["primary", "secondary", "outline"]).default("primary").describe("\u30B9\u30BF\u30A4\u30EB")
546
+ })
547
+ };
548
+ var componentCatalog = {
549
+ BlogCard: {
550
+ name: "BlogCard",
551
+ displayName: "\u8A18\u4E8B\u30AB\u30FC\u30C9",
552
+ description: "\u4ED6\u306E\u8A18\u4E8B\u3078\u306E\u30EA\u30F3\u30AF\u30AB\u30FC\u30C9\u3092\u8868\u793A\u3057\u307E\u3059",
553
+ category: "reference",
554
+ schema: componentSchemas.BlogCard,
555
+ examples: ['<BlogCard contentId="123e4567-e89b-12d3-a456-426614174000" />'],
556
+ hasReferences: true,
557
+ source: "standard",
558
+ kind: "data-bound",
559
+ locked: true,
560
+ editable: false,
561
+ bindings: [{ prop: "contentId", target: "content", strategy: "by-id" }]
562
+ },
563
+ Image: {
564
+ name: "Image",
565
+ displayName: "\u753B\u50CF",
566
+ description: "\u30A2\u30C3\u30D7\u30ED\u30FC\u30C9\u6E08\u307F\u753B\u50CF\u3092\u8868\u793A\u3057\u307E\u3059",
567
+ category: "media",
568
+ schema: componentSchemas.Image,
569
+ examples: [
570
+ '<Image assetId="123e4567-e89b-12d3-a456-426614174000" alt="\u8AAC\u660E" />',
571
+ '<Image assetId="123e4567-e89b-12d3-a456-426614174000" size="medium" caption="\u30AD\u30E3\u30D7\u30B7\u30E7\u30F3" />'
572
+ ],
573
+ hasReferences: true,
574
+ source: "standard",
575
+ kind: "data-bound",
576
+ locked: true,
577
+ editable: false,
578
+ bindings: [{ prop: "assetId", target: "asset", strategy: "by-id" }]
579
+ },
580
+ Callout: {
581
+ name: "Callout",
582
+ displayName: "\u30B3\u30FC\u30EB\u30A2\u30A6\u30C8",
583
+ description: "\u6CE8\u610F\u66F8\u304D\u3084\u88DC\u8DB3\u60C5\u5831\u3092\u76EE\u7ACB\u305F\u305B\u308B\u30DC\u30C3\u30AF\u30B9",
584
+ category: "content",
585
+ schema: componentSchemas.Callout,
586
+ examples: [
587
+ '<Callout type="info">\u3053\u308C\u306F\u60C5\u5831\u3067\u3059</Callout>',
588
+ '<Callout type="warning" title="\u6CE8\u610F">\u91CD\u8981\u306A\u6CE8\u610F\u4E8B\u9805\u3067\u3059</Callout>'
589
+ ],
590
+ hasReferences: false,
591
+ source: "standard",
592
+ kind: "presentational",
593
+ locked: true,
594
+ editable: false
595
+ },
596
+ Embed: {
597
+ name: "Embed",
598
+ displayName: "\u57CB\u3081\u8FBC\u307F",
599
+ description: "YouTube\u52D5\u753B\u3084\u30C4\u30A4\u30FC\u30C8\u3092\u57CB\u3081\u8FBC\u307F\u307E\u3059",
600
+ category: "media",
601
+ schema: componentSchemas.Embed,
602
+ examples: [
603
+ '<Embed url="https://www.youtube.com/watch?v=dQw4w9WgXcQ" type="youtube" />',
604
+ '<Embed url="https://twitter.com/example/status/123456789" type="twitter" />'
605
+ ],
606
+ hasReferences: false,
607
+ source: "standard",
608
+ kind: "presentational",
609
+ locked: true,
610
+ editable: false
611
+ },
612
+ Button: {
613
+ name: "Button",
614
+ displayName: "\u30DC\u30BF\u30F3",
615
+ description: "\u30A2\u30AF\u30B7\u30E7\u30F3\u3092\u4FC3\u3059\u30DC\u30BF\u30F3\u30EA\u30F3\u30AF",
616
+ category: "content",
617
+ schema: componentSchemas.Button,
618
+ examples: [
619
+ '<Button href="/contact">\u304A\u554F\u3044\u5408\u308F\u305B</Button>',
620
+ '<Button href="https://example.com" variant="secondary">\u8A73\u7D30\u3092\u898B\u308B</Button>'
621
+ ],
622
+ hasReferences: false,
623
+ source: "standard",
624
+ kind: "presentational",
625
+ locked: true,
626
+ editable: false
627
+ }
628
+ };
629
+ function isValidComponent(name) {
630
+ return name in componentCatalog;
631
+ }
632
+ function validateComponentProps(name, props) {
633
+ const schema = componentSchemas[name];
634
+ const result = schema.safeParse(props);
635
+ if (result.success) {
636
+ return { success: true, data: result.data };
637
+ }
638
+ return { success: false, error: result.error };
639
+ }
640
+ function isStandardComponentName(name) {
641
+ return isValidComponent(name);
540
642
  }
541
- function readComponentDefinitions(componentsDir, environment) {
643
+
644
+ // src/commands/sync-components.ts
645
+ var ComponentPropSchema = z2.object({
646
+ type: z2.enum(["string", "number", "boolean", "enum", "asset", "content"]),
647
+ description: z2.string().optional(),
648
+ required: z2.boolean().optional().default(false),
649
+ values: z2.array(z2.string()).optional(),
650
+ default: z2.union([z2.string(), z2.number(), z2.boolean()]).optional()
651
+ });
652
+ var ComponentDefinitionSchema = z2.object({
653
+ name: z2.string().min(1).max(100).regex(/^[A-Z][a-zA-Z0-9]*$/),
654
+ displayName: z2.string().min(1).max(255),
655
+ description: z2.string().optional(),
656
+ propsSchema: z2.record(ComponentPropSchema).default({}),
657
+ examples: z2.array(z2.string()).default([])
658
+ });
659
+ function readComponentDefinitions(componentsDir) {
542
660
  if (!existsSync(componentsDir)) {
543
661
  console.log("No components directory found at cmx/components/");
544
- return [];
662
+ return { exists: false, components: [] };
545
663
  }
546
664
  const files = readdirSync(componentsDir).filter((f) => f.endsWith(".json"));
547
- if (files.length === 0) {
548
- console.log("No component definition files found");
549
- return [];
550
- }
551
665
  const components = [];
552
666
  for (const file of files) {
553
667
  try {
554
668
  const filePath = join(componentsDir, file);
555
669
  const content = readFileSync2(filePath, "utf-8");
556
- const component = JSON.parse(content);
557
- if (!component.environment) {
558
- component.environment = environment;
670
+ const parsed = JSON.parse(content);
671
+ const result = ComponentDefinitionSchema.safeParse(parsed);
672
+ if (!result.success) {
673
+ console.error(` Invalid format in ${file}`);
674
+ for (const issue of result.error.issues) {
675
+ const path = issue.path.length > 0 ? issue.path.join(".") : "(root)";
676
+ console.error(` - ${path}: ${issue.message}`);
677
+ }
678
+ process.exit(1);
559
679
  }
560
- if (!component.name || !component.displayName) {
561
- console.warn(` Skipping ${file}: missing required fields (name, displayName)`);
562
- continue;
680
+ if (isValidComponent(result.data.name)) {
681
+ console.error(` ${file}: '${result.data.name}' \u306F\u6A19\u6E96\u30B3\u30F3\u30DD\u30FC\u30CD\u30F3\u30C8\u540D\u306E\u305F\u3081\u4F7F\u7528\u3067\u304D\u307E\u305B\u3093`);
682
+ process.exit(1);
563
683
  }
564
- component.propsSchema = component.propsSchema || {};
565
- component.examples = component.examples || [];
566
- components.push(component);
684
+ components.push(result.data);
567
685
  } catch (error) {
568
686
  console.error(` Error reading ${file}:`, error instanceof Error ? error.message : error);
687
+ process.exit(1);
569
688
  }
570
689
  }
571
- return components;
690
+ return { exists: true, components };
572
691
  }
573
- async function syncComponents(options) {
692
+ async function syncComponents() {
574
693
  const apiUrl = process.env.CMX_API_URL;
575
694
  const apiKey = process.env.CMX_API_KEY;
576
695
  if (!apiUrl || !apiKey) {
577
696
  console.error("Error: CMX_API_URL and CMX_API_KEY environment variables are required");
578
697
  process.exit(1);
579
698
  }
580
- const environment = options.environment || detectEnvironment();
581
699
  const componentsDir = join(process.cwd(), "cmx/components");
582
- console.log(`Environment: ${environment}`);
583
- const components = readComponentDefinitions(componentsDir, environment);
584
- if (components.length === 0) {
700
+ const { exists, components } = readComponentDefinitions(componentsDir);
701
+ if (!exists) {
585
702
  console.log("No components to sync");
586
703
  return;
587
704
  }
588
- console.log(`Found ${components.length} components:`);
589
- for (const c of components) {
590
- console.log(` - ${c.name} (${c.displayName})`);
705
+ if (components.length === 0) {
706
+ console.log("No component definition files found, syncing empty list (will delete existing custom components)");
707
+ } else {
708
+ console.log(`Found ${components.length} components:`);
709
+ for (const c of components) {
710
+ console.log(` - ${c.name} (${c.displayName})`);
711
+ }
591
712
  }
592
713
  console.log(`Syncing to ${apiUrl}...`);
593
714
  const result = await manageApiFetch(
@@ -600,6 +721,7 @@ async function syncComponents(options) {
600
721
  console.log(`Success: ${result.message || "Components synced"}`);
601
722
  if (result.created) console.log(` Created: ${result.created}`);
602
723
  if (result.updated) console.log(` Updated: ${result.updated}`);
724
+ if (result.deleted) console.log(` Deleted: ${result.deleted}`);
603
725
  }
604
726
 
605
727
  // src/codegen/generator.ts
@@ -722,7 +844,7 @@ function generateDataTypeFile(dataType) {
722
844
  const interfaceName = singularize(pascalPlural);
723
845
  const slug = dataType.slug;
724
846
  const lines = [];
725
- lines.push(`// Auto-generated by cmx-sdk generate`);
847
+ lines.push(`// Auto-generated by cmx-sdk codegen types`);
726
848
  lines.push(`// Data Type: ${dataType.name} (${slug})`);
727
849
  lines.push(`// Do not edit manually.`);
728
850
  lines.push(``);
@@ -777,7 +899,7 @@ function generateCollectionFile(collection) {
777
899
  const slug = collection.slug;
778
900
  const lines = [];
779
901
  const escapedName = escapeTs(collection.name);
780
- lines.push(`// Auto-generated by cmx-sdk generate`);
902
+ lines.push(`// Auto-generated by cmx-sdk codegen types`);
781
903
  lines.push(`// Collection: ${escapedName} (${slug})`);
782
904
  lines.push(`// Do not edit manually.`);
783
905
  lines.push(``);
@@ -863,7 +985,7 @@ function generateFormFile(form) {
863
985
  const slug = form.slug;
864
986
  const lines = [];
865
987
  const escapedName = escapeTs(form.name);
866
- lines.push(`// Auto-generated by cmx-sdk generate`);
988
+ lines.push(`// Auto-generated by cmx-sdk codegen types`);
867
989
  lines.push(`// Form: ${escapedName} (${slug})`);
868
990
  lines.push(`// Do not edit manually.`);
869
991
  lines.push(``);
@@ -895,7 +1017,7 @@ function generateFormFile(form) {
895
1017
  // src/codegen/generate-index.ts
896
1018
  function generateDataTypesIndex(slugs) {
897
1019
  const lines = [
898
- `// Auto-generated by cmx-sdk generate`,
1020
+ `// Auto-generated by cmx-sdk codegen types`,
899
1021
  `// Do not edit manually.`,
900
1022
  ``
901
1023
  ];
@@ -907,7 +1029,7 @@ function generateDataTypesIndex(slugs) {
907
1029
  }
908
1030
  function generateCollectionsIndex(slugs) {
909
1031
  const lines = [
910
- `// Auto-generated by cmx-sdk generate`,
1032
+ `// Auto-generated by cmx-sdk codegen types`,
911
1033
  `// Do not edit manually.`,
912
1034
  ``
913
1035
  ];
@@ -919,7 +1041,7 @@ function generateCollectionsIndex(slugs) {
919
1041
  }
920
1042
  function generateFormsIndex(slugs) {
921
1043
  const lines = [
922
- `// Auto-generated by cmx-sdk generate`,
1044
+ `// Auto-generated by cmx-sdk codegen types`,
923
1045
  `// Do not edit manually.`,
924
1046
  ``
925
1047
  ];
@@ -931,7 +1053,7 @@ function generateFormsIndex(slugs) {
931
1053
  }
932
1054
  function generateRootIndex(hasDataTypes, hasCollections, hasForms) {
933
1055
  const lines = [
934
- `// Auto-generated by cmx-sdk generate`,
1056
+ `// Auto-generated by cmx-sdk codegen types`,
935
1057
  `// Do not edit manually.`,
936
1058
  ``
937
1059
  ];
@@ -1256,7 +1378,7 @@ async function linkCollectionDataType(opts) {
1256
1378
  // src/codegen/scaffolder.ts
1257
1379
  import { existsSync as existsSync2 } from "fs";
1258
1380
  import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
1259
- import { join as join3, dirname, resolve } from "path";
1381
+ import { dirname, join as join3, resolve } from "path";
1260
1382
 
1261
1383
  // src/codegen/scaffold-collection.ts
1262
1384
  function scaffoldCollectionListPage(collection) {
@@ -1264,7 +1386,7 @@ function scaffoldCollectionListPage(collection) {
1264
1386
  const pascal = slugToPascalCase(safeSlug);
1265
1387
  const escapedName = escapeTs(collection.name);
1266
1388
  const escapedDescription = escapeTs(collection.description ?? `${collection.name}\u306E\u4E00\u89A7`);
1267
- return `import { get${pascal}Contents } from "@/cmx/generated"
1389
+ return `import { get${pascal}Contents } from "@cmx/generated"
1268
1390
  import Link from "next/link"
1269
1391
  import type { Metadata } from "next"
1270
1392
 
@@ -1297,7 +1419,7 @@ export default async function ${pascal}Page() {
1297
1419
  function scaffoldCollectionDetailPage(collection) {
1298
1420
  const safeSlug = sanitizeIdentifier(collection.slug);
1299
1421
  const pascal = slugToPascalCase(safeSlug);
1300
- return `import { get${pascal}Contents, get${pascal}ContentDetail } from "@/cmx/generated"
1422
+ return `import { get${pascal}Contents, get${pascal}ContentDetail } from "@cmx/generated"
1301
1423
  import { renderMdx } from "cmx-sdk"
1302
1424
  import type { Metadata } from "next"
1303
1425
 
@@ -1343,8 +1465,8 @@ function scaffoldDataTypePage(dataType) {
1343
1465
  const interfaceName = singularize(pascalPlural);
1344
1466
  const slug = dataType.slug;
1345
1467
  const fieldComments = dataType.fields.map((f) => `// ${f.key}: ${f.type}${f.required ? "" : " (optional)"}`).join("\n");
1346
- return `import { get${pascalPlural} } from "@/cmx/generated"
1347
- import type { ${interfaceName} } from "@/cmx/generated"
1468
+ return `import { get${pascalPlural} } from "@cmx/generated"
1469
+ import type { ${interfaceName} } from "@cmx/generated"
1348
1470
  import type { Metadata } from "next"
1349
1471
 
1350
1472
  export const metadata: Metadata = {
@@ -1380,7 +1502,7 @@ function scaffoldFormPage(form) {
1380
1502
  const pascal = slugToPascalCase(safeSlug);
1381
1503
  const camel = slugToCamelCase(safeSlug);
1382
1504
  return `import { ${pascal}Form } from "./_components/${safeSlug}-form"
1383
- import { ${camel}Schema, submit${pascal} } from "@/cmx/generated"
1505
+ import { ${camel}Schema, submit${pascal} } from "@cmx/generated"
1384
1506
  import type { Metadata } from "next"
1385
1507
 
1386
1508
  export const metadata: Metadata = {
@@ -1427,7 +1549,7 @@ function scaffoldFormComponent(form) {
1427
1549
 
1428
1550
  import { useForm } from "react-hook-form"
1429
1551
  import { zodResolver } from "@hookform/resolvers/zod"
1430
- import { ${camel}Schema, type ${pascal}FormData } from "@/cmx/generated"
1552
+ import { ${camel}Schema, type ${pascal}FormData } from "@cmx/generated"
1431
1553
  import { useState } from "react"
1432
1554
 
1433
1555
  type Props = {
@@ -1500,12 +1622,393 @@ function getInputType(fieldType) {
1500
1622
  }
1501
1623
  }
1502
1624
 
1625
+ // src/codegen/scaffold-layered.ts
1626
+ function scaffoldLayeredCollectionListRoute(collection, featureImportBase = "@/features") {
1627
+ const safeSlug = sanitizeIdentifier(collection.slug);
1628
+ const pascal = slugToPascalCase(safeSlug);
1629
+ return `import { ${pascal}ListView } from "${featureImportBase}/collections/${safeSlug}/list/view"
1630
+ import { get${pascal}ListData } from "${featureImportBase}/collections/${safeSlug}/list/resolver"
1631
+ export { metadata } from "${featureImportBase}/collections/${safeSlug}/list/meta"
1632
+
1633
+ export default async function ${pascal}Page() {
1634
+ const data = await get${pascal}ListData()
1635
+ return <${pascal}ListView data={data} />
1636
+ }
1637
+ `;
1638
+ }
1639
+ function scaffoldLayeredCollectionListResolver(collection) {
1640
+ const safeSlug = sanitizeIdentifier(collection.slug);
1641
+ const pascal = slugToPascalCase(safeSlug);
1642
+ return `import { get${pascal}Contents } from "@cmx/generated"
1643
+
1644
+ export async function get${pascal}ListData() {
1645
+ return get${pascal}Contents()
1646
+ }
1647
+
1648
+ export type ${pascal}ListData = Awaited<ReturnType<typeof get${pascal}ListData>>
1649
+ `;
1650
+ }
1651
+ function scaffoldLayeredCollectionListMeta(collection) {
1652
+ const escapedName = escapeTs(collection.name);
1653
+ const escapedDescription = escapeTs(collection.description ?? `${collection.name}\u306E\u4E00\u89A7`);
1654
+ return `import type { Metadata } from "next"
1655
+
1656
+ export const metadata: Metadata = {
1657
+ title: "${escapedName}",
1658
+ description: "${escapedDescription}",
1659
+ }
1660
+ `;
1661
+ }
1662
+ function scaffoldLayeredCollectionListView(collection) {
1663
+ const safeSlug = sanitizeIdentifier(collection.slug);
1664
+ const pascal = slugToPascalCase(safeSlug);
1665
+ const escapedName = escapeTs(collection.name);
1666
+ return `import Link from "next/link"
1667
+ import type { ${pascal}ListData } from "./resolver"
1668
+
1669
+ type Props = {
1670
+ data: ${pascal}ListData
1671
+ }
1672
+
1673
+ export function ${pascal}ListView({ data }: Props) {
1674
+ const { contents } = data
1675
+
1676
+ return (
1677
+ <main>
1678
+ <h1>${escapedName}</h1>
1679
+ {contents.length === 0 ? (
1680
+ <p>No content yet.</p>
1681
+ ) : (
1682
+ <ul>
1683
+ {contents.map((item) => (
1684
+ <li key={item.id}>
1685
+ <Link href="/${safeSlug}/\${item.slug}">
1686
+ <h2>{item.title}</h2>
1687
+ {item.description && <p>{item.description}</p>}
1688
+ </Link>
1689
+ </li>
1690
+ ))}
1691
+ </ul>
1692
+ )}
1693
+ </main>
1694
+ )
1695
+ }
1696
+ `;
1697
+ }
1698
+ function scaffoldLayeredCollectionDetailRoute(collection, featureImportBase = "@/features") {
1699
+ const safeSlug = sanitizeIdentifier(collection.slug);
1700
+ const pascal = slugToPascalCase(safeSlug);
1701
+ return `import type { Metadata } from "next"
1702
+ import {
1703
+ get${pascal}DetailData,
1704
+ get${pascal}DetailParams,
1705
+ } from "${featureImportBase}/collections/${safeSlug}/detail/resolver"
1706
+ import { get${pascal}DetailMetadata } from "${featureImportBase}/collections/${safeSlug}/detail/meta"
1707
+ import { ${pascal}DetailView } from "${featureImportBase}/collections/${safeSlug}/detail/view"
1708
+
1709
+ type Props = {
1710
+ params: Promise<{ slug: string }>
1711
+ }
1712
+
1713
+ export async function generateStaticParams() {
1714
+ return get${pascal}DetailParams()
1715
+ }
1716
+
1717
+ export async function generateMetadata({ params }: Props): Promise<Metadata> {
1718
+ const { slug } = await params
1719
+ return get${pascal}DetailMetadata(slug)
1720
+ }
1721
+
1722
+ export default async function ${pascal}DetailPage({ params }: Props) {
1723
+ const { slug } = await params
1724
+ const data = await get${pascal}DetailData(slug)
1725
+
1726
+ return <${pascal}DetailView data={data} />
1727
+ }
1728
+ `;
1729
+ }
1730
+ function scaffoldLayeredCollectionDetailResolver(collection) {
1731
+ const safeSlug = sanitizeIdentifier(collection.slug);
1732
+ const pascal = slugToPascalCase(safeSlug);
1733
+ return `import { get${pascal}Contents, get${pascal}ContentDetail } from "@cmx/generated"
1734
+ import { renderMdx } from "cmx-sdk"
1735
+
1736
+ export async function get${pascal}DetailParams() {
1737
+ const { contents } = await get${pascal}Contents()
1738
+ return contents.map((item) => ({ slug: item.slug }))
1739
+ }
1740
+
1741
+ export async function get${pascal}DetailData(slug: string) {
1742
+ const { content, references } = await get${pascal}ContentDetail(slug)
1743
+ const { content: rendered } = await renderMdx(content.mdx, references)
1744
+
1745
+ return {
1746
+ content,
1747
+ rendered,
1748
+ }
1749
+ }
1750
+
1751
+ export type ${pascal}DetailData = Awaited<ReturnType<typeof get${pascal}DetailData>>
1752
+ `;
1753
+ }
1754
+ function scaffoldLayeredCollectionDetailMeta(collection) {
1755
+ const safeSlug = sanitizeIdentifier(collection.slug);
1756
+ const pascal = slugToPascalCase(safeSlug);
1757
+ return `import type { Metadata } from "next"
1758
+ import { get${pascal}ContentDetail } from "@cmx/generated"
1759
+
1760
+ export async function get${pascal}DetailMetadata(slug: string): Promise<Metadata> {
1761
+ const { content } = await get${pascal}ContentDetail(slug)
1762
+
1763
+ return {
1764
+ title: content.title,
1765
+ description: content.description ?? undefined,
1766
+ }
1767
+ }
1768
+ `;
1769
+ }
1770
+ function scaffoldLayeredCollectionDetailView(collection) {
1771
+ const pascal = slugToPascalCase(sanitizeIdentifier(collection.slug));
1772
+ return `import type { ${pascal}DetailData } from "./resolver"
1773
+
1774
+ type Props = {
1775
+ data: ${pascal}DetailData
1776
+ }
1777
+
1778
+ export function ${pascal}DetailView({ data }: Props) {
1779
+ const { content, rendered } = data
1780
+
1781
+ return (
1782
+ <main>
1783
+ <article>
1784
+ <h1>{content.title}</h1>
1785
+ {rendered}
1786
+ </article>
1787
+ </main>
1788
+ )
1789
+ }
1790
+ `;
1791
+ }
1792
+ function scaffoldLayeredDataTypeRoute(dataType, featureImportBase = "@/features") {
1793
+ const safeSlug = sanitizeIdentifier(dataType.slug);
1794
+ const pascalPlural = slugToPascalCase(safeSlug);
1795
+ return `import { ${pascalPlural}ListView } from "${featureImportBase}/data-types/${safeSlug}/list/view"
1796
+ import { get${pascalPlural}ListData } from "${featureImportBase}/data-types/${safeSlug}/list/resolver"
1797
+ export { metadata } from "${featureImportBase}/data-types/${safeSlug}/list/meta"
1798
+
1799
+ export default async function ${pascalPlural}Page() {
1800
+ const data = await get${pascalPlural}ListData()
1801
+ return <${pascalPlural}ListView data={data} />
1802
+ }
1803
+ `;
1804
+ }
1805
+ function scaffoldLayeredDataTypeResolver(dataType) {
1806
+ const safeSlug = sanitizeIdentifier(dataType.slug);
1807
+ const pascalPlural = slugToPascalCase(safeSlug);
1808
+ return `import { get${pascalPlural} } from "@cmx/generated"
1809
+
1810
+ export async function get${pascalPlural}ListData() {
1811
+ return get${pascalPlural}()
1812
+ }
1813
+
1814
+ export type ${pascalPlural}ListData = Awaited<ReturnType<typeof get${pascalPlural}ListData>>
1815
+ `;
1816
+ }
1817
+ function scaffoldLayeredDataTypeMeta(dataType) {
1818
+ return `import type { Metadata } from "next"
1819
+
1820
+ export const metadata: Metadata = {
1821
+ title: "${escapeTs(dataType.name)}",
1822
+ }
1823
+ `;
1824
+ }
1825
+ function scaffoldLayeredDataTypeView(dataType) {
1826
+ const safeSlug = sanitizeIdentifier(dataType.slug);
1827
+ const pascalPlural = slugToPascalCase(safeSlug);
1828
+ const interfaceName = singularize(pascalPlural);
1829
+ const fieldComments = dataType.fields.map((f) => `// ${f.key}: ${f.type}${f.required ? "" : " (optional)"}`).join("\n");
1830
+ return `import type { ${pascalPlural}ListData } from "./resolver"
1831
+ import type { ${interfaceName} } from "@cmx/generated"
1832
+
1833
+ type Props = {
1834
+ data: ${pascalPlural}ListData
1835
+ }
1836
+
1837
+ // Available fields:
1838
+ ${fieldComments}
1839
+
1840
+ export function ${pascalPlural}ListView({ data }: Props) {
1841
+ const { items } = data
1842
+
1843
+ return (
1844
+ <main>
1845
+ <h1>${escapeTs(dataType.name)}</h1>
1846
+ <ul>
1847
+ {items.map((item: ${interfaceName}, index: number) => (
1848
+ <li key={index}>
1849
+ <pre>{JSON.stringify(item, null, 2)}</pre>
1850
+ </li>
1851
+ ))}
1852
+ </ul>
1853
+ </main>
1854
+ )
1855
+ }
1856
+ `;
1857
+ }
1858
+ function scaffoldLayeredFormRoute(form, featureImportBase = "@/features") {
1859
+ const safeSlug = sanitizeIdentifier(form.slug);
1860
+ const pascal = slugToPascalCase(safeSlug);
1861
+ return `import { ${pascal}PageView } from "${featureImportBase}/forms/${safeSlug}/view"
1862
+ import { submit${pascal}FormAction } from "${featureImportBase}/forms/${safeSlug}/resolver"
1863
+ export { metadata } from "${featureImportBase}/forms/${safeSlug}/meta"
1864
+
1865
+ export default function ${pascal}Page() {
1866
+ return <${pascal}PageView action={submit${pascal}FormAction} />
1867
+ }
1868
+ `;
1869
+ }
1870
+ function scaffoldLayeredFormResolver(form) {
1871
+ const safeSlug = sanitizeIdentifier(form.slug);
1872
+ const pascal = slugToPascalCase(safeSlug);
1873
+ const camel = slugToCamelCase(safeSlug);
1874
+ return `import { ${camel}Schema, submit${pascal}, type ${pascal}FormData } from "@cmx/generated"
1875
+
1876
+ export async function submit${pascal}FormAction(data: Record<string, unknown>) {
1877
+ "use server"
1878
+ const validated = ${camel}Schema.parse(data)
1879
+ return submit${pascal}(validated as ${pascal}FormData)
1880
+ }
1881
+ `;
1882
+ }
1883
+ function scaffoldLayeredFormMeta(form) {
1884
+ return `import type { Metadata } from "next"
1885
+
1886
+ export const metadata: Metadata = {
1887
+ title: "${escapeTs(form.name)}",
1888
+ }
1889
+ `;
1890
+ }
1891
+ function scaffoldLayeredFormView(form) {
1892
+ const safeSlug = sanitizeIdentifier(form.slug);
1893
+ const pascal = slugToPascalCase(safeSlug);
1894
+ return `import { ${pascal}FormView } from "./form-view"
1895
+
1896
+ type Props = {
1897
+ action: (data: Record<string, unknown>) => Promise<{ success: true; id: string }>
1898
+ }
1899
+
1900
+ export function ${pascal}PageView({ action }: Props) {
1901
+ return (
1902
+ <main>
1903
+ <h1>${escapeTs(form.name)}</h1>
1904
+ <${pascal}FormView action={action} />
1905
+ </main>
1906
+ )
1907
+ }
1908
+ `;
1909
+ }
1910
+ function scaffoldLayeredFormClientView(form) {
1911
+ const safeSlug = sanitizeIdentifier(form.slug);
1912
+ const pascal = slugToPascalCase(safeSlug);
1913
+ const camel = slugToCamelCase(safeSlug);
1914
+ const inputFields = form.fields.map((field) => {
1915
+ const inputType = getInputType2(field.type);
1916
+ const required = field.required ? " required" : "";
1917
+ const escapedLabel = escapeTs(field.label);
1918
+ return ` <div>
1919
+ <label htmlFor="${field.key}">${escapedLabel}</label>
1920
+ <input
1921
+ id="${field.key}"
1922
+ type="${inputType}"
1923
+ {...register("${field.key}")}${required}
1924
+ />
1925
+ {errors.${field.key} && (
1926
+ <p role="alert">{errors.${field.key}?.message}</p>
1927
+ )}
1928
+ </div>`;
1929
+ }).join("\n");
1930
+ return `"use client"
1931
+
1932
+ import { useState } from "react"
1933
+ import { useForm } from "react-hook-form"
1934
+ import { zodResolver } from "@hookform/resolvers/zod"
1935
+ import { ${camel}Schema, type ${pascal}FormData } from "@cmx/generated"
1936
+
1937
+ type Props = {
1938
+ action: (data: Record<string, unknown>) => Promise<{ success: true; id: string }>
1939
+ }
1940
+
1941
+ export function ${pascal}FormView({ action }: Props) {
1942
+ const [status, setStatus] = useState<"idle" | "submitting" | "success" | "error">("idle")
1943
+
1944
+ const {
1945
+ register,
1946
+ handleSubmit,
1947
+ reset,
1948
+ formState: { errors },
1949
+ } = useForm<${pascal}FormData>({
1950
+ resolver: zodResolver(${camel}Schema),
1951
+ })
1952
+
1953
+ const onSubmit = async (data: ${pascal}FormData) => {
1954
+ setStatus("submitting")
1955
+ try {
1956
+ await action(data)
1957
+ setStatus("success")
1958
+ reset()
1959
+ } catch {
1960
+ setStatus("error")
1961
+ }
1962
+ }
1963
+
1964
+ if (status === "success") {
1965
+ return <p>\u9001\u4FE1\u304C\u5B8C\u4E86\u3057\u307E\u3057\u305F\u3002\u3042\u308A\u304C\u3068\u3046\u3054\u3056\u3044\u307E\u3059\u3002</p>
1966
+ }
1967
+
1968
+ return (
1969
+ <form onSubmit={handleSubmit(onSubmit)}>
1970
+ <div style={{ position: "absolute", left: "-9999px" }} aria-hidden="true">
1971
+ <input type="text" name="_hp" tabIndex={-1} autoComplete="off" />
1972
+ </div>
1973
+
1974
+ ${inputFields}
1975
+
1976
+ {status === "error" && (
1977
+ <p role="alert">\u9001\u4FE1\u306B\u5931\u6557\u3057\u307E\u3057\u305F\u3002\u3082\u3046\u4E00\u5EA6\u304A\u8A66\u3057\u304F\u3060\u3055\u3044\u3002</p>
1978
+ )}
1979
+
1980
+ <button type="submit" disabled={status === "submitting"}>
1981
+ {status === "submitting" ? "\u9001\u4FE1\u4E2D..." : "\u9001\u4FE1"}
1982
+ </button>
1983
+ </form>
1984
+ )
1985
+ }
1986
+ `;
1987
+ }
1988
+ function getInputType2(fieldType) {
1989
+ switch (fieldType) {
1990
+ case "email":
1991
+ return "email";
1992
+ case "url":
1993
+ return "url";
1994
+ case "number":
1995
+ return "number";
1996
+ case "boolean":
1997
+ return "checkbox";
1998
+ case "textarea":
1999
+ case "richtext":
2000
+ return "text";
2001
+ default:
2002
+ return "text";
2003
+ }
2004
+ }
2005
+
1503
2006
  // src/codegen/scaffold-seo.ts
1504
2007
  function scaffoldSitemap(collections, siteUrl) {
1505
2008
  const imports = collections.map((col) => {
1506
2009
  const safeSlug = sanitizeIdentifier(col.slug);
1507
2010
  const pascal = slugToPascalCase(safeSlug);
1508
- return `import { get${pascal}Contents } from "@/cmx/generated"`;
2011
+ return `import { get${pascal}Contents } from "@cmx/generated"`;
1509
2012
  }).join("\n");
1510
2013
  const fetchBlocks = collections.map((col) => {
1511
2014
  const safeSlug = sanitizeIdentifier(col.slug);
@@ -1538,9 +2041,11 @@ ${fetchBlocks}
1538
2041
  // src/codegen/scaffolder.ts
1539
2042
  async function writeScaffoldFile(filePath, content, result, options) {
1540
2043
  const resolvedPath = resolve(filePath);
1541
- const resolvedAppDir = resolve(options.appDir);
1542
- if (!resolvedPath.startsWith(resolvedAppDir + "/") && resolvedPath !== resolvedAppDir) {
1543
- throw new Error(`Path traversal detected: "${filePath}" resolves outside of appDir "${options.appDir}"`);
2044
+ const resolvedBaseDir = resolve(options.baseDir);
2045
+ if (!resolvedPath.startsWith(resolvedBaseDir + "/") && resolvedPath !== resolvedBaseDir) {
2046
+ throw new Error(
2047
+ `Path traversal detected: "${filePath}" resolves outside of base dir "${options.baseDir}"`
2048
+ );
1544
2049
  }
1545
2050
  if (!options.force && existsSync2(filePath)) {
1546
2051
  result.skipped.push(filePath);
@@ -1555,12 +2060,49 @@ async function writeScaffoldFile(filePath, content, result, options) {
1555
2060
  result.created.push(filePath);
1556
2061
  }
1557
2062
  function parseOnly(only) {
1558
- const [category, slug] = only.split(":");
1559
- if (!category || !slug) return null;
1560
- return { category, slug };
2063
+ const [categoryRaw, slug] = only.split(":");
2064
+ if (!categoryRaw || !slug) return null;
2065
+ if (categoryRaw !== "collections" && categoryRaw !== "data-types" && categoryRaw !== "forms") {
2066
+ return null;
2067
+ }
2068
+ return { category: categoryRaw, slug };
2069
+ }
2070
+ function parseTemplate(template) {
2071
+ if (!template || template === "layered") {
2072
+ return "layered";
2073
+ }
2074
+ if (template === "legacy") {
2075
+ return "legacy";
2076
+ }
2077
+ throw new Error(`Invalid template: "${template}". Use "legacy" or "layered".`);
2078
+ }
2079
+ function inferFeaturesDir(appDir) {
2080
+ const normalized = appDir.replace(/\\/g, "/");
2081
+ if (normalized.startsWith("src/")) {
2082
+ return "src/features";
2083
+ }
2084
+ if (normalized.includes("/src/")) {
2085
+ return "src/features";
2086
+ }
2087
+ return "features";
2088
+ }
2089
+ function inferFeatureImportBase(featuresDir) {
2090
+ const normalized = featuresDir.replace(/\\/g, "/").replace(/^\.\//, "");
2091
+ if (normalized === "src/features") {
2092
+ return "@/features";
2093
+ }
2094
+ if (normalized.startsWith("src/")) {
2095
+ return `@/${normalized.slice("src/".length)}`;
2096
+ }
2097
+ throw new Error(
2098
+ `Layered template requires --features-dir to be under src/ (received: "${featuresDir}")`
2099
+ );
1561
2100
  }
1562
2101
  async function scaffold(options) {
1563
2102
  const { apiUrl, apiKey, appDir, dryRun, force, only } = options;
2103
+ const template = parseTemplate(options.template);
2104
+ const featuresDir = template === "layered" ? options.featuresDir ?? inferFeaturesDir(appDir) : null;
2105
+ const featureImportBase = template === "layered" ? inferFeatureImportBase(featuresDir ?? inferFeaturesDir(appDir)) : null;
1564
2106
  console.log(`Fetching schema from ${apiUrl} ...`);
1565
2107
  const schema = await fetchSchema(apiUrl, apiKey);
1566
2108
  const forms = schema.forms ?? [];
@@ -1576,10 +2118,77 @@ async function scaffold(options) {
1576
2118
  for (const col of collections) {
1577
2119
  const safeSlug = sanitizeIdentifier(col.slug);
1578
2120
  if (!safeSlug) continue;
1579
- const listPath = join3(appDir, safeSlug, "page.tsx");
1580
- await writeScaffoldFile(listPath, scaffoldCollectionListPage(col), result, { dryRun, force, appDir });
1581
- const detailPath = join3(appDir, safeSlug, "[slug]", "page.tsx");
1582
- await writeScaffoldFile(detailPath, scaffoldCollectionDetailPage(col), result, { dryRun, force, appDir });
2121
+ if (template === "legacy") {
2122
+ const listPath = join3(appDir, safeSlug, "page.tsx");
2123
+ await writeScaffoldFile(listPath, scaffoldCollectionListPage(col), result, { dryRun, force, baseDir: appDir });
2124
+ const detailPath = join3(appDir, safeSlug, "[slug]", "page.tsx");
2125
+ await writeScaffoldFile(detailPath, scaffoldCollectionDetailPage(col), result, { dryRun, force, baseDir: appDir });
2126
+ } else {
2127
+ const listRoutePath = join3(appDir, safeSlug, "page.tsx");
2128
+ await writeScaffoldFile(
2129
+ listRoutePath,
2130
+ scaffoldLayeredCollectionListRoute(col, featureImportBase ?? "@/features"),
2131
+ result,
2132
+ {
2133
+ dryRun,
2134
+ force,
2135
+ baseDir: appDir
2136
+ }
2137
+ );
2138
+ const detailRoutePath = join3(appDir, safeSlug, "[slug]", "page.tsx");
2139
+ await writeScaffoldFile(
2140
+ detailRoutePath,
2141
+ scaffoldLayeredCollectionDetailRoute(col, featureImportBase ?? "@/features"),
2142
+ result,
2143
+ {
2144
+ dryRun,
2145
+ force,
2146
+ baseDir: appDir
2147
+ }
2148
+ );
2149
+ const listFeatureBase = join3(featuresDir, "collections", safeSlug, "list");
2150
+ await writeScaffoldFile(
2151
+ join3(listFeatureBase, "resolver.ts"),
2152
+ scaffoldLayeredCollectionListResolver(col),
2153
+ result,
2154
+ {
2155
+ dryRun,
2156
+ force,
2157
+ baseDir: featuresDir
2158
+ }
2159
+ );
2160
+ await writeScaffoldFile(join3(listFeatureBase, "meta.ts"), scaffoldLayeredCollectionListMeta(col), result, {
2161
+ dryRun,
2162
+ force,
2163
+ baseDir: featuresDir
2164
+ });
2165
+ await writeScaffoldFile(join3(listFeatureBase, "view.tsx"), scaffoldLayeredCollectionListView(col), result, {
2166
+ dryRun,
2167
+ force,
2168
+ baseDir: featuresDir
2169
+ });
2170
+ const detailFeatureBase = join3(featuresDir, "collections", safeSlug, "detail");
2171
+ await writeScaffoldFile(
2172
+ join3(detailFeatureBase, "resolver.ts"),
2173
+ scaffoldLayeredCollectionDetailResolver(col),
2174
+ result,
2175
+ {
2176
+ dryRun,
2177
+ force,
2178
+ baseDir: featuresDir
2179
+ }
2180
+ );
2181
+ await writeScaffoldFile(join3(detailFeatureBase, "meta.ts"), scaffoldLayeredCollectionDetailMeta(col), result, {
2182
+ dryRun,
2183
+ force,
2184
+ baseDir: featuresDir
2185
+ });
2186
+ await writeScaffoldFile(join3(detailFeatureBase, "view.tsx"), scaffoldLayeredCollectionDetailView(col), result, {
2187
+ dryRun,
2188
+ force,
2189
+ baseDir: featuresDir
2190
+ });
2191
+ }
1583
2192
  }
1584
2193
  }
1585
2194
  if (!filter || filter.category === "data-types") {
@@ -1587,8 +2196,38 @@ async function scaffold(options) {
1587
2196
  for (const dt of dataTypes) {
1588
2197
  const safeSlug = sanitizeIdentifier(dt.slug);
1589
2198
  if (!safeSlug) continue;
1590
- const pagePath = join3(appDir, safeSlug, "page.tsx");
1591
- await writeScaffoldFile(pagePath, scaffoldDataTypePage(dt), result, { dryRun, force, appDir });
2199
+ if (template === "legacy") {
2200
+ const pagePath = join3(appDir, safeSlug, "page.tsx");
2201
+ await writeScaffoldFile(pagePath, scaffoldDataTypePage(dt), result, { dryRun, force, baseDir: appDir });
2202
+ } else {
2203
+ const pagePath = join3(appDir, safeSlug, "page.tsx");
2204
+ await writeScaffoldFile(
2205
+ pagePath,
2206
+ scaffoldLayeredDataTypeRoute(dt, featureImportBase ?? "@/features"),
2207
+ result,
2208
+ {
2209
+ dryRun,
2210
+ force,
2211
+ baseDir: appDir
2212
+ }
2213
+ );
2214
+ const featureBase = join3(featuresDir, "data-types", safeSlug, "list");
2215
+ await writeScaffoldFile(join3(featureBase, "resolver.ts"), scaffoldLayeredDataTypeResolver(dt), result, {
2216
+ dryRun,
2217
+ force,
2218
+ baseDir: featuresDir
2219
+ });
2220
+ await writeScaffoldFile(join3(featureBase, "meta.ts"), scaffoldLayeredDataTypeMeta(dt), result, {
2221
+ dryRun,
2222
+ force,
2223
+ baseDir: featuresDir
2224
+ });
2225
+ await writeScaffoldFile(join3(featureBase, "view.tsx"), scaffoldLayeredDataTypeView(dt), result, {
2226
+ dryRun,
2227
+ force,
2228
+ baseDir: featuresDir
2229
+ });
2230
+ }
1592
2231
  }
1593
2232
  }
1594
2233
  if (!filter || filter.category === "forms") {
@@ -1596,17 +2235,56 @@ async function scaffold(options) {
1596
2235
  for (const form of filteredForms) {
1597
2236
  const safeSlug = sanitizeIdentifier(form.slug);
1598
2237
  if (!safeSlug) continue;
1599
- const pagePath = join3(appDir, safeSlug, "page.tsx");
1600
- await writeScaffoldFile(pagePath, scaffoldFormPage(form), result, { dryRun, force, appDir });
1601
- const componentPath = join3(appDir, safeSlug, "_components", `${safeSlug}-form.tsx`);
1602
- await writeScaffoldFile(componentPath, scaffoldFormComponent(form), result, { dryRun, force, appDir });
2238
+ if (template === "legacy") {
2239
+ const pagePath = join3(appDir, safeSlug, "page.tsx");
2240
+ await writeScaffoldFile(pagePath, scaffoldFormPage(form), result, { dryRun, force, baseDir: appDir });
2241
+ const componentPath = join3(appDir, safeSlug, "_components", `${safeSlug}-form.tsx`);
2242
+ await writeScaffoldFile(componentPath, scaffoldFormComponent(form), result, {
2243
+ dryRun,
2244
+ force,
2245
+ baseDir: appDir
2246
+ });
2247
+ } else {
2248
+ const pagePath = join3(appDir, safeSlug, "page.tsx");
2249
+ await writeScaffoldFile(
2250
+ pagePath,
2251
+ scaffoldLayeredFormRoute(form, featureImportBase ?? "@/features"),
2252
+ result,
2253
+ { dryRun, force, baseDir: appDir }
2254
+ );
2255
+ const featureBase = join3(featuresDir, "forms", safeSlug);
2256
+ await writeScaffoldFile(join3(featureBase, "resolver.ts"), scaffoldLayeredFormResolver(form), result, {
2257
+ dryRun,
2258
+ force,
2259
+ baseDir: featuresDir
2260
+ });
2261
+ await writeScaffoldFile(join3(featureBase, "meta.ts"), scaffoldLayeredFormMeta(form), result, {
2262
+ dryRun,
2263
+ force,
2264
+ baseDir: featuresDir
2265
+ });
2266
+ await writeScaffoldFile(join3(featureBase, "view.tsx"), scaffoldLayeredFormView(form), result, {
2267
+ dryRun,
2268
+ force,
2269
+ baseDir: featuresDir
2270
+ });
2271
+ await writeScaffoldFile(join3(featureBase, "form-view.tsx"), scaffoldLayeredFormClientView(form), result, {
2272
+ dryRun,
2273
+ force,
2274
+ baseDir: featuresDir
2275
+ });
2276
+ }
1603
2277
  }
1604
2278
  }
1605
2279
  if (!filter) {
1606
2280
  const sitemapPath = join3(appDir, "sitemap.ts");
1607
2281
  if (schema.collections.length > 0) {
1608
2282
  const siteUrl = apiUrl.replace(/\/api\/.*$/, "");
1609
- await writeScaffoldFile(sitemapPath, scaffoldSitemap(schema.collections, siteUrl), result, { dryRun, force, appDir });
2283
+ await writeScaffoldFile(sitemapPath, scaffoldSitemap(schema.collections, siteUrl), result, {
2284
+ dryRun,
2285
+ force,
2286
+ baseDir: appDir
2287
+ });
1610
2288
  }
1611
2289
  }
1612
2290
  console.log("");
@@ -1628,8 +2306,13 @@ Skipped ${result.skipped.length} file(s) (already exist):`);
1628
2306
  }
1629
2307
  console.log("");
1630
2308
  console.log("Next steps:");
1631
- console.log(" 1. Customize the generated pages' design (HTML/CSS)");
1632
- console.log(" 2. Add navigation links to your layout");
2309
+ if (template === "layered") {
2310
+ console.log(" 1. Customize view files under the generated features directory");
2311
+ console.log(` 2. Keep data fetching in resolver files (${featuresDir})`);
2312
+ } else {
2313
+ console.log(" 1. Customize the generated pages' design (HTML/CSS)");
2314
+ console.log(" 2. Add navigation links to your layout");
2315
+ }
1633
2316
  console.log(" 3. Run `npm run dev` to preview your site");
1634
2317
  }
1635
2318
 
@@ -1910,10 +2593,10 @@ async function deleteDataEntry(options) {
1910
2593
  input: process.stdin,
1911
2594
  output: process.stdout
1912
2595
  });
1913
- const answer = await new Promise((resolve2) => {
2596
+ const answer = await new Promise((resolve5) => {
1914
2597
  rl.question(
1915
2598
  `Are you sure you want to delete data entry ${options.id}? (yes/no): `,
1916
- resolve2
2599
+ resolve5
1917
2600
  );
1918
2601
  });
1919
2602
  rl.close();
@@ -1933,12 +2616,682 @@ async function deleteDataEntry(options) {
1933
2616
  console.log(JSON.stringify(result, null, 2));
1934
2617
  }
1935
2618
 
2619
+ // src/commands/scaffold-component.ts
2620
+ import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync3, writeFileSync } from "fs";
2621
+ import { join as join4 } from "path";
2622
+ function toPascalCase(input) {
2623
+ const cleaned = input.replace(/[^a-zA-Z0-9_-]/g, "-");
2624
+ return cleaned.split(/[-_\s]+/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase()).join("");
2625
+ }
2626
+ function toKebabCase(input) {
2627
+ return input.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/[^a-zA-Z0-9-]/g, "-").toLowerCase();
2628
+ }
2629
+ function ensureDir(path) {
2630
+ if (!existsSync3(path)) {
2631
+ mkdirSync(path, { recursive: true });
2632
+ }
2633
+ }
2634
+ function ensureExport(indexPath, exportName) {
2635
+ const exportLine = `export { ${exportName} } from "./${exportName}"
2636
+ `;
2637
+ if (!existsSync3(indexPath)) {
2638
+ writeFileSync(indexPath, exportLine, "utf-8");
2639
+ return;
2640
+ }
2641
+ const current = readFileSync3(indexPath, "utf-8");
2642
+ if (current.includes(exportLine.trim())) {
2643
+ return;
2644
+ }
2645
+ const needsNewline = current.length > 0 && !current.endsWith("\n");
2646
+ writeFileSync(indexPath, `${current}${needsNewline ? "\n" : ""}${exportLine}`, "utf-8");
2647
+ }
2648
+ async function scaffoldComponent(options) {
2649
+ const pascalName = toPascalCase(options.name);
2650
+ if (!/^[A-Z][a-zA-Z0-9]*$/.test(pascalName)) {
2651
+ console.error("Error: name must resolve to PascalCase (e.g. FeatureCard)");
2652
+ process.exit(1);
2653
+ }
2654
+ const kebabName = toKebabCase(pascalName);
2655
+ const componentsDir = options.componentsDir || join4(process.cwd(), "src/components/custom");
2656
+ const definitionsDir = options.definitionsDir || join4(process.cwd(), "cmx/components");
2657
+ ensureDir(componentsDir);
2658
+ ensureDir(definitionsDir);
2659
+ const componentPath = join4(componentsDir, `${pascalName}.tsx`);
2660
+ const definitionPath = join4(definitionsDir, `${kebabName}.json`);
2661
+ const indexPath = join4(componentsDir, "index.ts");
2662
+ if (!options.force && (existsSync3(componentPath) || existsSync3(definitionPath))) {
2663
+ console.error("Error: target files already exist. Use --force to overwrite");
2664
+ process.exit(1);
2665
+ }
2666
+ const definition = {
2667
+ name: pascalName,
2668
+ displayName: pascalName,
2669
+ description: `Custom component: ${pascalName}`,
2670
+ propsSchema: options.dataBound ? {
2671
+ contentId: {
2672
+ type: "content",
2673
+ description: "Target content ID",
2674
+ required: true
2675
+ },
2676
+ title: {
2677
+ type: "string",
2678
+ description: "Title",
2679
+ required: false
2680
+ }
2681
+ } : {
2682
+ title: {
2683
+ type: "string",
2684
+ description: "Title",
2685
+ required: false
2686
+ },
2687
+ children: {
2688
+ type: "string",
2689
+ description: "Body text",
2690
+ required: false
2691
+ }
2692
+ },
2693
+ examples: options.dataBound ? [`<${pascalName} contentId="123e4567-e89b-12d3-a456-426614174000" />`] : [`<${pascalName} title="Hello">Body</${pascalName}>`]
2694
+ };
2695
+ const componentCode = options.dataBound ? `interface ${pascalName}Props {
2696
+ contentId: string
2697
+ title?: string
2698
+ }
2699
+
2700
+ export function ${pascalName}({ contentId, title }: ${pascalName}Props) {
2701
+ // TODO: Resolve data by contentId and map it to presentational props.
2702
+ return (
2703
+ <section className="rounded-lg border p-4">
2704
+ {title ? <h3 className="mb-2 text-lg font-semibold">{title}</h3> : null}
2705
+ <p className="text-sm text-muted-foreground">Resolve content by ID: {contentId}</p>
2706
+ </section>
2707
+ )
2708
+ }
2709
+ ` : `interface ${pascalName}Props {
2710
+ title?: string
2711
+ children?: string
2712
+ }
2713
+
2714
+ export function ${pascalName}({ title, children }: ${pascalName}Props) {
2715
+ return (
2716
+ <section className="rounded-lg border p-4">
2717
+ {title ? <h3 className="mb-2 text-lg font-semibold">{title}</h3> : null}
2718
+ {children ? <p className="text-sm text-muted-foreground">{children}</p> : null}
2719
+ </section>
2720
+ )
2721
+ }
2722
+ `;
2723
+ writeFileSync(definitionPath, `${JSON.stringify(definition, null, 2)}
2724
+ `, "utf-8");
2725
+ writeFileSync(componentPath, componentCode, "utf-8");
2726
+ ensureExport(indexPath, pascalName);
2727
+ console.log(`Created: ${definitionPath}`);
2728
+ console.log(`Created: ${componentPath}`);
2729
+ console.log(`Updated: ${indexPath}`);
2730
+ console.log("Next: run `cmx-sdk sync-components` to register the new definition");
2731
+ }
2732
+
2733
+ // src/commands/codegen-check.ts
2734
+ import { existsSync as existsSync4, readFileSync as readFileSync4, readdirSync as readdirSync2, statSync } from "fs";
2735
+ import { extname, join as join5, relative, resolve as resolve2 } from "path";
2736
+ import stripJsonComments from "strip-json-comments";
2737
+ function stripTrailingCommas(input) {
2738
+ return input.replace(/,\s*([}\]])/g, "$1");
2739
+ }
2740
+ function normalizePathValue(input) {
2741
+ return input.replace(/\\/g, "/").replace(/^\.\//, "");
2742
+ }
2743
+ function collectFiles(rootDir, extensions) {
2744
+ const files = [];
2745
+ function walk(dir) {
2746
+ const entries = readdirSync2(dir);
2747
+ for (const entry of entries) {
2748
+ const fullPath = join5(dir, entry);
2749
+ const stat = statSync(fullPath);
2750
+ if (stat.isDirectory()) {
2751
+ if (entry === "node_modules" || entry === ".next" || entry === "dist") {
2752
+ continue;
2753
+ }
2754
+ walk(fullPath);
2755
+ } else if (extensions.has(extname(entry))) {
2756
+ files.push(fullPath);
2757
+ }
2758
+ }
2759
+ }
2760
+ if (existsSync4(rootDir)) {
2761
+ walk(rootDir);
2762
+ }
2763
+ return files;
2764
+ }
2765
+ function extractImports(code) {
2766
+ const imports = /* @__PURE__ */ new Set();
2767
+ const fromPattern = /from\s+["']([^"']+)["']/g;
2768
+ let match;
2769
+ while ((match = fromPattern.exec(code)) !== null) {
2770
+ imports.add(match[1]);
2771
+ }
2772
+ const dynamicImportPattern = /import\(\s*["']([^"']+)["']\s*\)/g;
2773
+ while ((match = dynamicImportPattern.exec(code)) !== null) {
2774
+ imports.add(match[1]);
2775
+ }
2776
+ return Array.from(imports);
2777
+ }
2778
+ function resolveGeneratedImport(outputDirAbs, importPath) {
2779
+ if (importPath === "@cmx/generated") {
2780
+ return [join5(outputDirAbs, "index.ts")];
2781
+ }
2782
+ const subPath = importPath.replace(/^@cmx\/generated\//, "");
2783
+ return [
2784
+ join5(outputDirAbs, `${subPath}.ts`),
2785
+ join5(outputDirAbs, subPath, "index.ts")
2786
+ ];
2787
+ }
2788
+ async function runCodegenCheck(options = {}) {
2789
+ const projectRoot = resolve2(options.projectRoot ?? process.cwd());
2790
+ const outputDirAbs = resolve2(projectRoot, options.outputDir ?? "cmx/generated");
2791
+ const srcDirAbs = resolve2(projectRoot, options.srcDir ?? "src");
2792
+ const tsconfigAbs = resolve2(projectRoot, options.tsconfigPath ?? "tsconfig.json");
2793
+ const errors = [];
2794
+ const warnings = [];
2795
+ const checks = [];
2796
+ if (!existsSync4(outputDirAbs)) {
2797
+ errors.push(`Generated output directory not found: ${relative(projectRoot, outputDirAbs)}`);
2798
+ } else {
2799
+ checks.push(`Generated directory exists: ${relative(projectRoot, outputDirAbs)}`);
2800
+ const indexPath = join5(outputDirAbs, "index.ts");
2801
+ if (!existsSync4(indexPath)) {
2802
+ errors.push(`Generated index not found: ${relative(projectRoot, indexPath)}`);
2803
+ } else {
2804
+ checks.push(`Generated index exists: ${relative(projectRoot, indexPath)}`);
2805
+ }
2806
+ }
2807
+ if (!existsSync4(tsconfigAbs)) {
2808
+ errors.push(`tsconfig not found: ${relative(projectRoot, tsconfigAbs)}`);
2809
+ } else {
2810
+ try {
2811
+ const raw = readFileSync4(tsconfigAbs, "utf-8");
2812
+ const parsed = JSON.parse(stripTrailingCommas(stripJsonComments(raw)));
2813
+ const aliasValues = parsed.compilerOptions?.paths?.["@cmx/*"] ?? [];
2814
+ if (aliasValues.length === 0) {
2815
+ errors.push("tsconfig compilerOptions.paths['@cmx/*'] is missing");
2816
+ } else {
2817
+ const normalizedAliasValues = aliasValues.map(normalizePathValue);
2818
+ if (!normalizedAliasValues.includes("cmx/*")) {
2819
+ errors.push(
2820
+ "tsconfig compilerOptions.paths['@cmx/*'] must include './cmx/*' (or equivalent)"
2821
+ );
2822
+ } else {
2823
+ checks.push("tsconfig '@cmx/*' alias is configured");
2824
+ }
2825
+ }
2826
+ const includeValues = parsed.include ?? [];
2827
+ const normalizedIncludeValues = includeValues.map(normalizePathValue);
2828
+ if (!normalizedIncludeValues.includes("cmx/**/*.ts")) {
2829
+ warnings.push("tsconfig include does not contain 'cmx/**/*.ts'");
2830
+ } else {
2831
+ checks.push("tsconfig include contains 'cmx/**/*.ts'");
2832
+ }
2833
+ } catch (error) {
2834
+ errors.push(`Failed to parse tsconfig: ${error instanceof Error ? error.message : String(error)}`);
2835
+ }
2836
+ }
2837
+ if (!existsSync4(srcDirAbs)) {
2838
+ warnings.push(`Source directory not found: ${relative(projectRoot, srcDirAbs)}`);
2839
+ } else {
2840
+ const sourceFiles = collectFiles(srcDirAbs, /* @__PURE__ */ new Set([".ts", ".tsx"]));
2841
+ const legacyImportHits = [];
2842
+ const generatedImportHits = [];
2843
+ for (const sourceFile of sourceFiles) {
2844
+ const code = readFileSync4(sourceFile, "utf-8");
2845
+ const imports = extractImports(code);
2846
+ for (const importPath of imports) {
2847
+ if (importPath.startsWith("@/cmx/generated")) {
2848
+ legacyImportHits.push(`${relative(projectRoot, sourceFile)} -> ${importPath}`);
2849
+ }
2850
+ if (importPath === "@cmx/generated" || importPath.startsWith("@cmx/generated/")) {
2851
+ generatedImportHits.push(`${relative(projectRoot, sourceFile)} -> ${importPath}`);
2852
+ const candidates = resolveGeneratedImport(outputDirAbs, importPath);
2853
+ for (const candidate of candidates) {
2854
+ if (existsSync4(candidate)) {
2855
+ break;
2856
+ }
2857
+ const isLast = candidate === candidates.at(-1);
2858
+ if (isLast) {
2859
+ errors.push(
2860
+ `Missing generated target for import '${importPath}' referenced in ${relative(projectRoot, sourceFile)}`
2861
+ );
2862
+ }
2863
+ }
2864
+ }
2865
+ }
2866
+ }
2867
+ if (legacyImportHits.length > 0) {
2868
+ errors.push("Legacy '@/cmx/generated' imports were found:");
2869
+ for (const hit of legacyImportHits) {
2870
+ errors.push(` - ${hit}`);
2871
+ }
2872
+ } else {
2873
+ checks.push("No legacy '@/cmx/generated' imports found");
2874
+ }
2875
+ if (generatedImportHits.length === 0) {
2876
+ warnings.push("No '@cmx/generated' imports found under src/ (this may be expected for empty projects)");
2877
+ } else {
2878
+ checks.push(`Found ${generatedImportHits.length} '@cmx/generated' import(s) under src/`);
2879
+ }
2880
+ }
2881
+ return {
2882
+ success: errors.length === 0,
2883
+ errors,
2884
+ warnings,
2885
+ checks
2886
+ };
2887
+ }
2888
+ async function codegenCheck(options = {}) {
2889
+ const projectRoot = resolve2(options.projectRoot ?? process.cwd());
2890
+ const result = await runCodegenCheck(options);
2891
+ console.log("Codegen check:");
2892
+ for (const check of result.checks) {
2893
+ console.log(` \u2713 ${check}`);
2894
+ }
2895
+ for (const warning of result.warnings) {
2896
+ console.warn(` \u26A0 ${warning}`);
2897
+ }
2898
+ for (const error of result.errors) {
2899
+ console.error(` \u2717 ${error}`);
2900
+ }
2901
+ if (!result.success) {
2902
+ console.error(`
2903
+ Codegen check failed in ${projectRoot}`);
2904
+ process.exit(1);
2905
+ }
2906
+ console.log("\nCodegen check passed");
2907
+ }
2908
+
2909
+ // src/commands/mdx-validate.ts
2910
+ import { existsSync as existsSync5, readFileSync as readFileSync5, readdirSync as readdirSync3, statSync as statSync2 } from "fs";
2911
+ import { extname as extname2, join as join6, relative as relative2, resolve as resolve3 } from "path";
2912
+
2913
+ // src/mdx/validator.ts
2914
+ import { compile } from "@mdx-js/mdx";
2915
+ var FORBIDDEN_PATTERNS = [
2916
+ // import/export statements
2917
+ { pattern: /^\s*import\s+/m, message: "import\u6587\u306F\u4F7F\u7528\u3067\u304D\u307E\u305B\u3093" },
2918
+ { pattern: /^\s*export\s+/m, message: "export\u6587\u306F\u4F7F\u7528\u3067\u304D\u307E\u305B\u3093" },
2919
+ // JS expressions in curly braces (except component props)
2920
+ { pattern: /\{[^}]*(?:=>|function|new\s+|typeof|instanceof)[^}]*\}/m, message: "JavaScript\u5F0F\u306F\u4F7F\u7528\u3067\u304D\u307E\u305B\u3093" },
2921
+ // eval, Function constructor
2922
+ { pattern: /\beval\s*\(/m, message: "eval\u306F\u4F7F\u7528\u3067\u304D\u307E\u305B\u3093" },
2923
+ { pattern: /\bnew\s+Function\s*\(/m, message: "Function constructor\u306F\u4F7F\u7528\u3067\u304D\u307E\u305B\u3093" },
2924
+ // script tags
2925
+ { pattern: /<script[\s>]/i, message: "script\u30BF\u30B0\u306F\u4F7F\u7528\u3067\u304D\u307E\u305B\u3093" },
2926
+ // on* event handlers
2927
+ { pattern: /\bon\w+\s*=/i, message: "\u30A4\u30D9\u30F3\u30C8\u30CF\u30F3\u30C9\u30E9\u5C5E\u6027\u306F\u4F7F\u7528\u3067\u304D\u307E\u305B\u3093" },
2928
+ // javascript: URLs
2929
+ { pattern: /javascript\s*:/i, message: "javascript: URL\u306F\u4F7F\u7528\u3067\u304D\u307E\u305B\u3093" },
2930
+ // data: URLs (for images - can be XSS vector)
2931
+ { pattern: /src\s*=\s*["']?\s*data:/i, message: "data: URL\u306F\u4F7F\u7528\u3067\u304D\u307E\u305B\u3093" }
2932
+ ];
2933
+ function extractComponents(mdx) {
2934
+ const components = [];
2935
+ const lines = mdx.split("\n");
2936
+ const componentPattern = /<([A-Z][a-zA-Z0-9]*)\s*([^>]*?)\s*\/?>/g;
2937
+ for (let i = 0; i < lines.length; i++) {
2938
+ const line = lines[i];
2939
+ let match;
2940
+ while ((match = componentPattern.exec(line)) !== null) {
2941
+ const name = match[1];
2942
+ const propsString = match[2];
2943
+ const props = parseProps(propsString);
2944
+ components.push({
2945
+ name,
2946
+ props,
2947
+ line: i + 1
2948
+ });
2949
+ }
2950
+ }
2951
+ return components;
2952
+ }
2953
+ function parseProps(propsString) {
2954
+ const props = {};
2955
+ const propPattern = /(\w+)\s*=\s*(?:"([^"]*)"|'([^']*)'|\{([^}]*)\})/g;
2956
+ let match;
2957
+ while ((match = propPattern.exec(propsString)) !== null) {
2958
+ const key = match[1];
2959
+ const stringValue = match[2] ?? match[3];
2960
+ const expressionValue = match[4];
2961
+ if (stringValue !== void 0) {
2962
+ props[key] = stringValue;
2963
+ } else if (expressionValue !== void 0) {
2964
+ const trimmed = expressionValue.trim();
2965
+ if (trimmed === "true") {
2966
+ props[key] = true;
2967
+ } else if (trimmed === "false") {
2968
+ props[key] = false;
2969
+ } else if (/^\d+$/.test(trimmed)) {
2970
+ props[key] = parseInt(trimmed, 10);
2971
+ } else if (/^\d+\.\d+$/.test(trimmed)) {
2972
+ props[key] = parseFloat(trimmed);
2973
+ } else if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
2974
+ try {
2975
+ props[key] = JSON.parse(trimmed.replace(/'/g, '"'));
2976
+ } catch {
2977
+ props[key] = trimmed;
2978
+ }
2979
+ } else {
2980
+ props[key] = trimmed;
2981
+ }
2982
+ }
2983
+ }
2984
+ const booleanPattern = /(?:^|\s)(\w+)(?=\s|\/|>|$)/g;
2985
+ while ((match = booleanPattern.exec(propsString)) !== null) {
2986
+ const key = match[1];
2987
+ if (!(key in props)) {
2988
+ props[key] = true;
2989
+ }
2990
+ }
2991
+ return props;
2992
+ }
2993
+ function extractReferences(components) {
2994
+ const contentIds = [];
2995
+ const assetIds = [];
2996
+ for (const component of components) {
2997
+ if (component.name === "BlogCard" && typeof component.props.contentId === "string") {
2998
+ contentIds.push(component.props.contentId);
2999
+ }
3000
+ if (component.name === "Image" && typeof component.props.assetId === "string") {
3001
+ assetIds.push(component.props.assetId);
3002
+ }
3003
+ }
3004
+ return {
3005
+ contentIds: [...new Set(contentIds)],
3006
+ assetIds: [...new Set(assetIds)]
3007
+ };
3008
+ }
3009
+ async function validateMdx(mdx) {
3010
+ const errors = [];
3011
+ const warnings = [];
3012
+ for (const { pattern, message } of FORBIDDEN_PATTERNS) {
3013
+ if (pattern.test(mdx)) {
3014
+ const lines = mdx.split("\n");
3015
+ for (let i = 0; i < lines.length; i++) {
3016
+ if (pattern.test(lines[i])) {
3017
+ errors.push({
3018
+ type: "forbidden",
3019
+ message,
3020
+ line: i + 1
3021
+ });
3022
+ break;
3023
+ }
3024
+ }
3025
+ }
3026
+ }
3027
+ try {
3028
+ await compile(mdx, {
3029
+ development: false
3030
+ // Minimal compilation to check syntax
3031
+ });
3032
+ } catch (error) {
3033
+ const err = error;
3034
+ errors.push({
3035
+ type: "syntax",
3036
+ message: err.message,
3037
+ line: err.line,
3038
+ column: err.column
3039
+ });
3040
+ }
3041
+ const components = extractComponents(mdx);
3042
+ for (const component of components) {
3043
+ if (!isValidComponent(component.name)) {
3044
+ errors.push({
3045
+ type: "component",
3046
+ message: `\u672A\u77E5\u306E\u30B3\u30F3\u30DD\u30FC\u30CD\u30F3\u30C8: ${component.name}`,
3047
+ line: component.line,
3048
+ component: component.name
3049
+ });
3050
+ continue;
3051
+ }
3052
+ const result = validateComponentProps(component.name, component.props);
3053
+ if (!result.success) {
3054
+ for (const issue of result.error.issues) {
3055
+ errors.push({
3056
+ type: "props",
3057
+ message: `${component.name}: ${issue.path.join(".")} - ${issue.message}`,
3058
+ line: component.line,
3059
+ component: component.name
3060
+ });
3061
+ }
3062
+ }
3063
+ }
3064
+ const references = extractReferences(components);
3065
+ return {
3066
+ valid: errors.length === 0,
3067
+ errors,
3068
+ warnings,
3069
+ references
3070
+ };
3071
+ }
3072
+
3073
+ // src/commands/mdx-validate.ts
3074
+ function collectMdxFiles(rootDir) {
3075
+ const files = [];
3076
+ function walk(dir) {
3077
+ const entries = readdirSync3(dir);
3078
+ for (const entry of entries) {
3079
+ const fullPath = join6(dir, entry);
3080
+ const stat = statSync2(fullPath);
3081
+ if (stat.isDirectory()) {
3082
+ if (entry === "node_modules" || entry === ".git" || entry === ".next" || entry === "dist") {
3083
+ continue;
3084
+ }
3085
+ walk(fullPath);
3086
+ } else if (extname2(entry) === ".mdx") {
3087
+ files.push(fullPath);
3088
+ }
3089
+ }
3090
+ }
3091
+ if (existsSync5(rootDir)) {
3092
+ walk(rootDir);
3093
+ }
3094
+ return files;
3095
+ }
3096
+ async function mdxValidate(options) {
3097
+ const projectRoot = process.cwd();
3098
+ const inputFile = options.file ? resolve3(projectRoot, options.file) : null;
3099
+ const inputDir = resolve3(projectRoot, options.dir ?? "src");
3100
+ const targets = [];
3101
+ if (inputFile) {
3102
+ if (!existsSync5(inputFile)) {
3103
+ console.error(`Error: file not found: ${options.file}`);
3104
+ process.exit(1);
3105
+ }
3106
+ targets.push(inputFile);
3107
+ } else {
3108
+ const dirFiles = collectMdxFiles(inputDir);
3109
+ targets.push(...dirFiles);
3110
+ }
3111
+ if (targets.length === 0) {
3112
+ console.error("No MDX files found to validate");
3113
+ process.exit(1);
3114
+ }
3115
+ let invalidCount = 0;
3116
+ let warningCount = 0;
3117
+ for (const filePath of targets) {
3118
+ const mdx = readFileSync5(filePath, "utf-8");
3119
+ const result = await validateMdx(mdx);
3120
+ const relPath = relative2(projectRoot, filePath);
3121
+ if (result.valid) {
3122
+ console.log(`\u2713 ${relPath}`);
3123
+ } else {
3124
+ invalidCount++;
3125
+ console.log(`\u2717 ${relPath}`);
3126
+ for (const err of result.errors) {
3127
+ const location = err.line ? `:${err.line}${err.column ? `:${err.column}` : ""}` : "";
3128
+ console.log(` - [${err.type}]${location} ${err.message}`);
3129
+ }
3130
+ }
3131
+ if (result.warnings.length > 0) {
3132
+ warningCount += result.warnings.length;
3133
+ for (const warning of result.warnings) {
3134
+ console.log(` ! [warning] ${warning}`);
3135
+ }
3136
+ }
3137
+ }
3138
+ console.log("");
3139
+ console.log(`Validated ${targets.length} file(s)`);
3140
+ console.log(`Invalid: ${invalidCount}`);
3141
+ console.log(`Warnings: ${warningCount}`);
3142
+ if (invalidCount > 0) {
3143
+ process.exit(1);
3144
+ }
3145
+ }
3146
+
3147
+ // src/commands/mdx-doctor.ts
3148
+ import { existsSync as existsSync6, readFileSync as readFileSync6, readdirSync as readdirSync4, statSync as statSync3 } from "fs";
3149
+ import { basename, extname as extname3, join as join7, relative as relative3, resolve as resolve4 } from "path";
3150
+ function collectFiles2(rootDir, extension) {
3151
+ const files = [];
3152
+ function walk(dir) {
3153
+ const entries = readdirSync4(dir);
3154
+ for (const entry of entries) {
3155
+ const fullPath = join7(dir, entry);
3156
+ const stat = statSync3(fullPath);
3157
+ if (stat.isDirectory()) {
3158
+ if (entry === "node_modules" || entry === ".git" || entry === ".next" || entry === "dist") {
3159
+ continue;
3160
+ }
3161
+ walk(fullPath);
3162
+ } else if (extname3(entry) === extension) {
3163
+ files.push(fullPath);
3164
+ }
3165
+ }
3166
+ }
3167
+ if (existsSync6(rootDir)) {
3168
+ walk(rootDir);
3169
+ }
3170
+ return files;
3171
+ }
3172
+ function parseDefinitionFiles(projectRoot, files) {
3173
+ const definitions = [];
3174
+ const errors = [];
3175
+ for (const file of files) {
3176
+ try {
3177
+ const raw = readFileSync6(file, "utf-8");
3178
+ const parsed = JSON.parse(raw);
3179
+ if (typeof parsed.name !== "string" || parsed.name.length === 0) {
3180
+ errors.push(`${relative3(projectRoot, file)}: missing string 'name'`);
3181
+ continue;
3182
+ }
3183
+ definitions.push({ name: parsed.name, file });
3184
+ } catch (error) {
3185
+ errors.push(
3186
+ `${relative3(projectRoot, file)}: invalid JSON (${error instanceof Error ? error.message : String(error)})`
3187
+ );
3188
+ }
3189
+ }
3190
+ return { definitions, errors };
3191
+ }
3192
+ function collectExportedComponentNames(indexContent) {
3193
+ const exported = /* @__PURE__ */ new Set();
3194
+ const namedExportPattern = /export\s+\{\s*([A-Za-z0-9_]+)(?:\s+as\s+[A-Za-z0-9_]+)?\s*\}\s+from\s+["']\.\/?([^"']+)["']/g;
3195
+ let match;
3196
+ while ((match = namedExportPattern.exec(indexContent)) !== null) {
3197
+ exported.add(match[1]);
3198
+ const pathBase = basename(match[2]);
3199
+ exported.add(pathBase);
3200
+ }
3201
+ const exportAllPattern = /export\s+\*\s+from\s+["']\.\/?([^"']+)["']/g;
3202
+ while ((match = exportAllPattern.exec(indexContent)) !== null) {
3203
+ exported.add(basename(match[1]));
3204
+ }
3205
+ return exported;
3206
+ }
3207
+ async function mdxDoctor(options = {}) {
3208
+ const projectRoot = process.cwd();
3209
+ const definitionsDir = resolve4(projectRoot, options.definitionsDir ?? "cmx/components");
3210
+ const componentsDir = resolve4(projectRoot, options.componentsDir ?? "src/components/custom");
3211
+ const indexFile = resolve4(projectRoot, options.indexFile ?? join7(options.componentsDir ?? "src/components/custom", "index.ts"));
3212
+ const issues = [];
3213
+ const checks = [];
3214
+ if (!existsSync6(definitionsDir)) {
3215
+ issues.push(`Definitions directory not found: ${relative3(projectRoot, definitionsDir)}`);
3216
+ }
3217
+ if (!existsSync6(componentsDir)) {
3218
+ issues.push(`Components directory not found: ${relative3(projectRoot, componentsDir)}`);
3219
+ }
3220
+ if (issues.length > 0) {
3221
+ for (const issue of issues) {
3222
+ console.error(`\u2717 ${issue}`);
3223
+ }
3224
+ process.exit(1);
3225
+ }
3226
+ const definitionFiles = collectFiles2(definitionsDir, ".json");
3227
+ const implFiles = collectFiles2(componentsDir, ".tsx").filter((f) => basename(f) !== "index.tsx");
3228
+ checks.push(`Found ${definitionFiles.length} definition file(s)`);
3229
+ checks.push(`Found ${implFiles.length} implementation file(s)`);
3230
+ const { definitions, errors } = parseDefinitionFiles(projectRoot, definitionFiles);
3231
+ for (const error of errors) {
3232
+ issues.push(error);
3233
+ }
3234
+ const definitionNames = new Set(definitions.map((d) => d.name));
3235
+ const implementationNames = new Set(
3236
+ implFiles.map((f) => basename(f, ".tsx")).filter((name) => name !== "index")
3237
+ );
3238
+ for (const def of definitions) {
3239
+ if (isStandardComponentName(def.name)) {
3240
+ issues.push(`${relative3(projectRoot, def.file)}: '${def.name}' is reserved as a standard component`);
3241
+ }
3242
+ }
3243
+ for (const defName of definitionNames) {
3244
+ if (!implementationNames.has(defName)) {
3245
+ issues.push(`Definition exists but implementation is missing: ${defName}.tsx`);
3246
+ }
3247
+ }
3248
+ for (const implName of implementationNames) {
3249
+ if (!definitionNames.has(implName)) {
3250
+ issues.push(`Implementation exists but definition is missing: ${implName}.json`);
3251
+ }
3252
+ }
3253
+ if (!existsSync6(indexFile)) {
3254
+ issues.push(`Export index not found: ${relative3(projectRoot, indexFile)}`);
3255
+ } else {
3256
+ const indexContent = readFileSync6(indexFile, "utf-8");
3257
+ const exportedNames = collectExportedComponentNames(indexContent);
3258
+ for (const implName of implementationNames) {
3259
+ if (!exportedNames.has(implName)) {
3260
+ issues.push(`Implementation is not exported in ${relative3(projectRoot, indexFile)}: ${implName}`);
3261
+ }
3262
+ }
3263
+ checks.push(`Checked exports in ${relative3(projectRoot, indexFile)}`);
3264
+ }
3265
+ console.log("MDX doctor:");
3266
+ for (const check of checks) {
3267
+ console.log(` \u2713 ${check}`);
3268
+ }
3269
+ if (issues.length > 0) {
3270
+ for (const issue of issues) {
3271
+ console.error(` \u2717 ${issue}`);
3272
+ }
3273
+ console.error(`
3274
+ Doctor found ${issues.length} issue(s)`);
3275
+ process.exit(1);
3276
+ }
3277
+ console.log("\nDoctor checks passed");
3278
+ }
3279
+
1936
3280
  // src/cli.ts
1937
3281
  config();
1938
3282
  var program = new Command();
1939
- program.name("cmx-sdk").description("CMX SDK - CLI tool for managing CMX schemas and content").version("0.1.13");
3283
+ program.name("cmx-sdk").description("CMX SDK - CLI tool for managing CMX schemas and content").version("0.2.3").allowExcessArguments(false);
3284
+ function requireApiCredentials() {
3285
+ const apiUrl = process.env.CMX_API_URL;
3286
+ const apiKey = process.env.CMX_API_KEY;
3287
+ if (!apiUrl || !apiKey) {
3288
+ console.error("Error: CMX_API_URL and CMX_API_KEY environment variables are required");
3289
+ process.exit(1);
3290
+ }
3291
+ return { apiUrl, apiKey };
3292
+ }
1940
3293
  program.action(async () => {
1941
- const { interactiveMenu } = await import("./interactive-menu-UQHS5FLW.js");
3294
+ const { interactiveMenu } = await import("./interactive-menu-BDZOOGQH.js");
1942
3295
  await interactiveMenu();
1943
3296
  });
1944
3297
  program.command("init [project-name]").description("\u65B0\u898F CMX \u30B5\u30A4\u30C8\u3092\u4F5C\u6210").option("--no-studio", "CMX Studio \u3092\u30B9\u30AD\u30C3\u30D7").option("--pm <manager>", "\u30D1\u30C3\u30B1\u30FC\u30B8\u30DE\u30CD\u30FC\u30B8\u30E3\u30FC (npm, pnpm, yarn)").action(async (projectName, options) => {
@@ -1957,20 +3310,75 @@ program.command("list-components").description("List all custom components").act
1957
3310
  program.command("create-collection").description("Create a new collection").option("--json <json>", "JSON string with collection data").option("--type <type>", "Collection type (post, page, doc, news)").option("--slug <slug>", "Collection slug").option("--name <name>", "Collection name").option("--description <description>", "Collection description").action(createCollection);
1958
3311
  program.command("create-data-type").description("Create a new data type").option("--json <json>", "JSON string with data type data (recommended)").option("--slug <slug>", "Data type slug").option("--name <name>", "Data type name").option("--description <description>", "Data type description").action(createDataType);
1959
3312
  program.command("create-form").description("Create a new form definition").option("--json <json>", "JSON string with form data (recommended)").option("--slug <slug>", "Form slug").option("--name <name>", "Form name").option("--description <description>", "Form description").action(createForm);
1960
- program.command("sync-components").description("Sync custom components").option("--file <file>", "Path to JSON file with components").option("--json <json>", "JSON string with components data").option("--environment <env>", "Deployment environment (auto-detected if omitted)").action(syncComponents);
1961
- program.command("generate").description("Generate TypeScript types and functions from CMX schema").option("--output <dir>", "Output directory (default: cmx/generated)").action((options) => {
1962
- const apiUrl = process.env.CMX_API_URL;
1963
- const apiKey = process.env.CMX_API_KEY;
1964
- if (!apiUrl || !apiKey) {
1965
- console.error("Error: CMX_API_URL and CMX_API_KEY environment variables are required");
1966
- process.exit(1);
1967
- }
1968
- return generate({
3313
+ program.command("sync-components").description("Sync custom components").action(syncComponents);
3314
+ program.command("components").description("Manage custom component definitions and templates").command("scaffold").description("Generate component definition + implementation scaffold").requiredOption("--name <name>", "Component name (PascalCase)").option("--data-bound", "Generate data-bound template with ID resolver props").option("--force", "Overwrite existing files").option("--components-dir <dir>", "Custom components directory (default: src/components/custom)").option("--definitions-dir <dir>", "Component definition directory (default: cmx/components)").action((options) => scaffoldComponent(options));
3315
+ var codegenCommand = program.command("codegen").description("Generate typed code, page scaffolds, and run compatibility checks");
3316
+ codegenCommand.command("types").description("Generate TypeScript types and fetch helpers from CMX schema").option("--output <dir>", "Output directory", "cmx/generated").action(async (options) => {
3317
+ const { apiUrl, apiKey } = requireApiCredentials();
3318
+ await generate({
1969
3319
  apiUrl,
1970
3320
  apiKey,
1971
- outputDir: options.output || "cmx/generated"
3321
+ outputDir: options.output
1972
3322
  });
1973
3323
  });
3324
+ codegenCommand.command("pages").description("Generate Next.js pages from CMX schema").option("--app-dir <dir>", "App directory path", "src/app").option("--features-dir <dir>", "Features directory path (layered template only)", "src/features").option("--template <template>", "Template style: layered or legacy", "layered").option("--dry-run", "Preview without writing files").option("--force", "Overwrite existing files").option("--only <filter>", "Scaffold only specific items (e.g. collections:blog)").action(
3325
+ async (options) => {
3326
+ const { apiUrl, apiKey } = requireApiCredentials();
3327
+ await scaffold({
3328
+ apiUrl,
3329
+ apiKey,
3330
+ appDir: options.appDir,
3331
+ featuresDir: options.featuresDir,
3332
+ template: options.template,
3333
+ dryRun: options.dryRun,
3334
+ force: options.force,
3335
+ only: options.only
3336
+ });
3337
+ }
3338
+ );
3339
+ codegenCommand.command("check").description("Check generated code and project wiring consistency").option("--project-root <dir>", "Project root directory").option("--output-dir <dir>", "Generated output directory", "cmx/generated").option("--src-dir <dir>", "Source directory to scan imports", "src").option("--tsconfig <path>", "Path to tsconfig", "tsconfig.json").action(
3340
+ async (options) => {
3341
+ await codegenCheck({
3342
+ projectRoot: options.projectRoot,
3343
+ outputDir: options.outputDir,
3344
+ srcDir: options.srcDir,
3345
+ tsconfigPath: options.tsconfig
3346
+ });
3347
+ }
3348
+ );
3349
+ codegenCommand.command("all").description("Run `codegen types`, `codegen pages`, and `codegen check` in sequence").option("--output <dir>", "Output directory for generated types", "cmx/generated").option("--app-dir <dir>", "App directory path", "src/app").option("--features-dir <dir>", "Features directory path (layered template only)", "src/features").option("--template <template>", "Template style: layered or legacy", "layered").option("--dry-run", "Preview page generation without writing files").option("--force", "Overwrite existing files when generating pages").option("--only <filter>", "Scaffold only specific items (e.g. collections:blog)").option("--src-dir <dir>", "Source directory to scan imports", "src").option("--tsconfig <path>", "Path to tsconfig", "tsconfig.json").action(
3350
+ async (options) => {
3351
+ const { apiUrl, apiKey } = requireApiCredentials();
3352
+ console.log("\n[1/3] codegen types");
3353
+ await generate({
3354
+ apiUrl,
3355
+ apiKey,
3356
+ outputDir: options.output
3357
+ });
3358
+ console.log("\n[2/3] codegen pages");
3359
+ await scaffold({
3360
+ apiUrl,
3361
+ apiKey,
3362
+ appDir: options.appDir,
3363
+ featuresDir: options.featuresDir,
3364
+ template: options.template,
3365
+ dryRun: options.dryRun,
3366
+ force: options.force,
3367
+ only: options.only
3368
+ });
3369
+ console.log("\n[3/3] codegen check");
3370
+ await codegenCheck({
3371
+ outputDir: options.output,
3372
+ srcDir: options.srcDir,
3373
+ tsconfigPath: options.tsconfig
3374
+ });
3375
+ }
3376
+ );
3377
+ var mdxCommand = program.command("mdx").description("MDX developer tooling");
3378
+ mdxCommand.command("validate").description("Validate MDX files for syntax, forbidden patterns, and component usage").option("--file <path>", "Validate a single file").option("--dir <dir>", "Validate all .mdx files under a directory", "src").action((options) => mdxValidate(options));
3379
+ mdxCommand.command("doctor").description("Check consistency between MDX component definitions and implementations").option("--definitions-dir <dir>", "Definitions directory", "cmx/components").option("--components-dir <dir>", "Component implementation directory", "src/components/custom").option("--index-file <path>", "Component export index file", "src/components/custom/index.ts").action(
3380
+ (options) => mdxDoctor(options)
3381
+ );
1974
3382
  program.command("update-collection").description("Update an existing collection").requiredOption("--slug <slug>", "Current slug of the collection").option("--json <json>", "JSON string with update data").option("--new-slug <slug>", "New slug").option("--name <name>", "New name").option("--description <description>", "New description").action(updateCollection);
1975
3383
  program.command("delete-collection").description("Delete a collection").requiredOption("--slug <slug>", "Collection slug").option("--force", "Skip confirmation prompt").option("--cascade", "Also delete associated content").action(deleteCollection);
1976
3384
  program.command("update-data-type").description("Update an existing data type").requiredOption("--slug <slug>", "Current slug of the data type").option("--json <json>", "JSON string with update data").option("--new-slug <slug>", "New slug").option("--name <name>", "New name").option("--description <description>", "New description").action(updateDataType);
@@ -1982,22 +3390,6 @@ program.command("add-collection-data-type").description("Add a data type to a co
1982
3390
  program.command("remove-collection-data-type").description("Remove a data type from a collection").requiredOption("--collection <slug>", "Collection slug").requiredOption("--data-type <slug>", "Data type slug to remove").action(removeCollectionDataType);
1983
3391
  program.command("link-collection-data-type").description("Link an existing global data type to a collection").requiredOption("--collection <slug>", "Collection slug").requiredOption("--data-type <slug>", "Global data type slug to link").requiredOption("--field-slug <slug>", "Reference field slug").requiredOption("--label <label>", "Reference field display label").option("--reference-type <type>", "Reference type: single or multiple (default: single)", "single").action(linkCollectionDataType);
1984
3392
  program.command("list-collection-presets").description("List available presets for collection data types").option("--type <type>", "Collection type (post, news, doc, page)").action(listCollectionPresets);
1985
- program.command("scaffold").description("Generate Next.js pages from CMX schema").option("--app-dir <dir>", "App directory path (default: app/(public))").option("--dry-run", "Preview without writing files").option("--force", "Overwrite existing files").option("--only <filter>", "Scaffold only specific items (e.g. collections:blog)").action((options) => {
1986
- const apiUrl = process.env.CMX_API_URL;
1987
- const apiKey = process.env.CMX_API_KEY;
1988
- if (!apiUrl || !apiKey) {
1989
- console.error("Error: CMX_API_URL and CMX_API_KEY environment variables are required");
1990
- process.exit(1);
1991
- }
1992
- return scaffold({
1993
- apiUrl,
1994
- apiKey,
1995
- appDir: options.appDir || "app/(public)",
1996
- dryRun: options.dryRun,
1997
- force: options.force,
1998
- only: options.only
1999
- });
2000
- });
2001
3393
  program.command("create-data-entry").description("Create a new data entry").requiredOption("--type-slug <slug>", "Data type slug").option("--json <json>", "JSON string with entry data").option("--file <file>", "Path to JSON file with entry data").action(createDataEntry);
2002
3394
  program.command("list-data-entries").description("List data entries for a data type").requiredOption("--type-slug <slug>", "Data type slug").option("--sort-by <field>", "Sort by field").option("--sort-order <order>", "Sort order (asc or desc)").option("--limit <number>", "Maximum number of entries").option("--offset <number>", "Number of entries to skip").option("--status <status>", "Filter by status (draft or published)").action(listDataEntries);
2003
3395
  program.command("get-data-entry").description("Get a single data entry").requiredOption("--type-slug <slug>", "Data type slug").requiredOption("--id <id>", "Data entry ID").action(getDataEntry);