cmx-sdk 0.2.1 → 0.2.3

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
@@ -1,4 +1,7 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ studio
4
+ } from "./chunk-XPP5MZKG.js";
2
5
 
3
6
  // src/cli.ts
4
7
  import { Command } from "commander";
@@ -381,10 +384,10 @@ async function confirm(message) {
381
384
  input: process.stdin,
382
385
  output: process.stdout
383
386
  });
384
- return new Promise((resolve2) => {
387
+ return new Promise((resolve5) => {
385
388
  rl.question(`${message} (y/N): `, (answer) => {
386
389
  rl.close();
387
- resolve2(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
390
+ resolve5(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
388
391
  });
389
392
  });
390
393
  }
@@ -426,10 +429,10 @@ async function confirm2(message) {
426
429
  input: process.stdin,
427
430
  output: process.stdout
428
431
  });
429
- return new Promise((resolve2) => {
432
+ return new Promise((resolve5) => {
430
433
  rl.question(`${message} (y/N): `, (answer) => {
431
434
  rl.close();
432
- resolve2(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
435
+ resolve5(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
433
436
  });
434
437
  });
435
438
  }
@@ -471,10 +474,10 @@ async function confirm3(message) {
471
474
  input: process.stdin,
472
475
  output: process.stdout
473
476
  });
474
- return new Promise((resolve2) => {
477
+ return new Promise((resolve5) => {
475
478
  rl.question(`${message} (y/N): `, (answer) => {
476
479
  rl.close();
477
- resolve2(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
480
+ resolve5(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
478
481
  });
479
482
  });
480
483
  }
@@ -513,78 +516,199 @@ async function deleteForm(options) {
513
516
  // src/commands/sync-components.ts
514
517
  import { readdirSync, readFileSync as readFileSync2, existsSync } from "fs";
515
518
  import { join } from "path";
516
- import { execSync } from "child_process";
517
- function detectEnvironment() {
518
- try {
519
- const branch = execSync("git branch --show-current", { encoding: "utf-8" }).trim();
520
- if (branch === "main") return "production";
521
- if (branch === "develop") return "staging";
522
- const prMatch = branch.match(/^(?:feature|fix)\/(.+)$/);
523
- if (prMatch) {
524
- try {
525
- const prNumber = execSync("gh pr view --json number -q .number", {
526
- encoding: "utf-8",
527
- stdio: ["pipe", "pipe", "ignore"]
528
- }).trim();
529
- if (prNumber) return `preview/pr-${prNumber}`;
530
- } catch {
531
- }
532
- }
533
- return `preview/${branch.replace(/[^a-zA-Z0-9-]/g, "-")}`;
534
- } catch {
535
- return "production";
536
- }
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);
537
642
  }
538
- 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) {
539
660
  if (!existsSync(componentsDir)) {
540
661
  console.log("No components directory found at cmx/components/");
541
- return [];
662
+ return { exists: false, components: [] };
542
663
  }
543
664
  const files = readdirSync(componentsDir).filter((f) => f.endsWith(".json"));
544
- if (files.length === 0) {
545
- console.log("No component definition files found");
546
- return [];
547
- }
548
665
  const components = [];
549
666
  for (const file of files) {
550
667
  try {
551
668
  const filePath = join(componentsDir, file);
552
669
  const content = readFileSync2(filePath, "utf-8");
553
- const component = JSON.parse(content);
554
- if (!component.environment) {
555
- 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);
556
679
  }
557
- if (!component.name || !component.displayName) {
558
- console.warn(` Skipping ${file}: missing required fields (name, displayName)`);
559
- 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);
560
683
  }
561
- component.propsSchema = component.propsSchema || {};
562
- component.examples = component.examples || [];
563
- components.push(component);
684
+ components.push(result.data);
564
685
  } catch (error) {
565
686
  console.error(` Error reading ${file}:`, error instanceof Error ? error.message : error);
687
+ process.exit(1);
566
688
  }
567
689
  }
568
- return components;
690
+ return { exists: true, components };
569
691
  }
570
- async function syncComponents(options) {
692
+ async function syncComponents() {
571
693
  const apiUrl = process.env.CMX_API_URL;
572
694
  const apiKey = process.env.CMX_API_KEY;
573
695
  if (!apiUrl || !apiKey) {
574
696
  console.error("Error: CMX_API_URL and CMX_API_KEY environment variables are required");
575
697
  process.exit(1);
576
698
  }
577
- const environment = options.environment || detectEnvironment();
578
699
  const componentsDir = join(process.cwd(), "cmx/components");
579
- console.log(`Environment: ${environment}`);
580
- const components = readComponentDefinitions(componentsDir, environment);
581
- if (components.length === 0) {
700
+ const { exists, components } = readComponentDefinitions(componentsDir);
701
+ if (!exists) {
582
702
  console.log("No components to sync");
583
703
  return;
584
704
  }
585
- console.log(`Found ${components.length} components:`);
586
- for (const c of components) {
587
- 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
+ }
588
712
  }
589
713
  console.log(`Syncing to ${apiUrl}...`);
590
714
  const result = await manageApiFetch(
@@ -597,6 +721,7 @@ async function syncComponents(options) {
597
721
  console.log(`Success: ${result.message || "Components synced"}`);
598
722
  if (result.created) console.log(` Created: ${result.created}`);
599
723
  if (result.updated) console.log(` Updated: ${result.updated}`);
724
+ if (result.deleted) console.log(` Deleted: ${result.deleted}`);
600
725
  }
601
726
 
602
727
  // src/codegen/generator.ts
@@ -719,7 +844,7 @@ function generateDataTypeFile(dataType) {
719
844
  const interfaceName = singularize(pascalPlural);
720
845
  const slug = dataType.slug;
721
846
  const lines = [];
722
- lines.push(`// Auto-generated by cmx-sdk generate`);
847
+ lines.push(`// Auto-generated by cmx-sdk codegen types`);
723
848
  lines.push(`// Data Type: ${dataType.name} (${slug})`);
724
849
  lines.push(`// Do not edit manually.`);
725
850
  lines.push(``);
@@ -774,7 +899,7 @@ function generateCollectionFile(collection) {
774
899
  const slug = collection.slug;
775
900
  const lines = [];
776
901
  const escapedName = escapeTs(collection.name);
777
- lines.push(`// Auto-generated by cmx-sdk generate`);
902
+ lines.push(`// Auto-generated by cmx-sdk codegen types`);
778
903
  lines.push(`// Collection: ${escapedName} (${slug})`);
779
904
  lines.push(`// Do not edit manually.`);
780
905
  lines.push(``);
@@ -860,7 +985,7 @@ function generateFormFile(form) {
860
985
  const slug = form.slug;
861
986
  const lines = [];
862
987
  const escapedName = escapeTs(form.name);
863
- lines.push(`// Auto-generated by cmx-sdk generate`);
988
+ lines.push(`// Auto-generated by cmx-sdk codegen types`);
864
989
  lines.push(`// Form: ${escapedName} (${slug})`);
865
990
  lines.push(`// Do not edit manually.`);
866
991
  lines.push(``);
@@ -892,7 +1017,7 @@ function generateFormFile(form) {
892
1017
  // src/codegen/generate-index.ts
893
1018
  function generateDataTypesIndex(slugs) {
894
1019
  const lines = [
895
- `// Auto-generated by cmx-sdk generate`,
1020
+ `// Auto-generated by cmx-sdk codegen types`,
896
1021
  `// Do not edit manually.`,
897
1022
  ``
898
1023
  ];
@@ -904,7 +1029,7 @@ function generateDataTypesIndex(slugs) {
904
1029
  }
905
1030
  function generateCollectionsIndex(slugs) {
906
1031
  const lines = [
907
- `// Auto-generated by cmx-sdk generate`,
1032
+ `// Auto-generated by cmx-sdk codegen types`,
908
1033
  `// Do not edit manually.`,
909
1034
  ``
910
1035
  ];
@@ -916,7 +1041,7 @@ function generateCollectionsIndex(slugs) {
916
1041
  }
917
1042
  function generateFormsIndex(slugs) {
918
1043
  const lines = [
919
- `// Auto-generated by cmx-sdk generate`,
1044
+ `// Auto-generated by cmx-sdk codegen types`,
920
1045
  `// Do not edit manually.`,
921
1046
  ``
922
1047
  ];
@@ -928,7 +1053,7 @@ function generateFormsIndex(slugs) {
928
1053
  }
929
1054
  function generateRootIndex(hasDataTypes, hasCollections, hasForms) {
930
1055
  const lines = [
931
- `// Auto-generated by cmx-sdk generate`,
1056
+ `// Auto-generated by cmx-sdk codegen types`,
932
1057
  `// Do not edit manually.`,
933
1058
  ``
934
1059
  ];
@@ -1253,7 +1378,7 @@ async function linkCollectionDataType(opts) {
1253
1378
  // src/codegen/scaffolder.ts
1254
1379
  import { existsSync as existsSync2 } from "fs";
1255
1380
  import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
1256
- import { join as join3, dirname, resolve } from "path";
1381
+ import { dirname, join as join3, resolve } from "path";
1257
1382
 
1258
1383
  // src/codegen/scaffold-collection.ts
1259
1384
  function scaffoldCollectionListPage(collection) {
@@ -1261,7 +1386,7 @@ function scaffoldCollectionListPage(collection) {
1261
1386
  const pascal = slugToPascalCase(safeSlug);
1262
1387
  const escapedName = escapeTs(collection.name);
1263
1388
  const escapedDescription = escapeTs(collection.description ?? `${collection.name}\u306E\u4E00\u89A7`);
1264
- return `import { get${pascal}Contents } from "@/cmx/generated"
1389
+ return `import { get${pascal}Contents } from "@cmx/generated"
1265
1390
  import Link from "next/link"
1266
1391
  import type { Metadata } from "next"
1267
1392
 
@@ -1294,7 +1419,7 @@ export default async function ${pascal}Page() {
1294
1419
  function scaffoldCollectionDetailPage(collection) {
1295
1420
  const safeSlug = sanitizeIdentifier(collection.slug);
1296
1421
  const pascal = slugToPascalCase(safeSlug);
1297
- return `import { get${pascal}Contents, get${pascal}ContentDetail } from "@/cmx/generated"
1422
+ return `import { get${pascal}Contents, get${pascal}ContentDetail } from "@cmx/generated"
1298
1423
  import { renderMdx } from "cmx-sdk"
1299
1424
  import type { Metadata } from "next"
1300
1425
 
@@ -1340,8 +1465,8 @@ function scaffoldDataTypePage(dataType) {
1340
1465
  const interfaceName = singularize(pascalPlural);
1341
1466
  const slug = dataType.slug;
1342
1467
  const fieldComments = dataType.fields.map((f) => `// ${f.key}: ${f.type}${f.required ? "" : " (optional)"}`).join("\n");
1343
- return `import { get${pascalPlural} } from "@/cmx/generated"
1344
- import type { ${interfaceName} } from "@/cmx/generated"
1468
+ return `import { get${pascalPlural} } from "@cmx/generated"
1469
+ import type { ${interfaceName} } from "@cmx/generated"
1345
1470
  import type { Metadata } from "next"
1346
1471
 
1347
1472
  export const metadata: Metadata = {
@@ -1377,7 +1502,7 @@ function scaffoldFormPage(form) {
1377
1502
  const pascal = slugToPascalCase(safeSlug);
1378
1503
  const camel = slugToCamelCase(safeSlug);
1379
1504
  return `import { ${pascal}Form } from "./_components/${safeSlug}-form"
1380
- import { ${camel}Schema, submit${pascal} } from "@/cmx/generated"
1505
+ import { ${camel}Schema, submit${pascal} } from "@cmx/generated"
1381
1506
  import type { Metadata } from "next"
1382
1507
 
1383
1508
  export const metadata: Metadata = {
@@ -1424,7 +1549,7 @@ function scaffoldFormComponent(form) {
1424
1549
 
1425
1550
  import { useForm } from "react-hook-form"
1426
1551
  import { zodResolver } from "@hookform/resolvers/zod"
1427
- import { ${camel}Schema, type ${pascal}FormData } from "@/cmx/generated"
1552
+ import { ${camel}Schema, type ${pascal}FormData } from "@cmx/generated"
1428
1553
  import { useState } from "react"
1429
1554
 
1430
1555
  type Props = {
@@ -1497,12 +1622,393 @@ function getInputType(fieldType) {
1497
1622
  }
1498
1623
  }
1499
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
+
1500
2006
  // src/codegen/scaffold-seo.ts
1501
2007
  function scaffoldSitemap(collections, siteUrl) {
1502
2008
  const imports = collections.map((col) => {
1503
2009
  const safeSlug = sanitizeIdentifier(col.slug);
1504
2010
  const pascal = slugToPascalCase(safeSlug);
1505
- return `import { get${pascal}Contents } from "@/cmx/generated"`;
2011
+ return `import { get${pascal}Contents } from "@cmx/generated"`;
1506
2012
  }).join("\n");
1507
2013
  const fetchBlocks = collections.map((col) => {
1508
2014
  const safeSlug = sanitizeIdentifier(col.slug);
@@ -1535,9 +2041,11 @@ ${fetchBlocks}
1535
2041
  // src/codegen/scaffolder.ts
1536
2042
  async function writeScaffoldFile(filePath, content, result, options) {
1537
2043
  const resolvedPath = resolve(filePath);
1538
- const resolvedAppDir = resolve(options.appDir);
1539
- if (!resolvedPath.startsWith(resolvedAppDir + "/") && resolvedPath !== resolvedAppDir) {
1540
- 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
+ );
1541
2049
  }
1542
2050
  if (!options.force && existsSync2(filePath)) {
1543
2051
  result.skipped.push(filePath);
@@ -1552,12 +2060,49 @@ async function writeScaffoldFile(filePath, content, result, options) {
1552
2060
  result.created.push(filePath);
1553
2061
  }
1554
2062
  function parseOnly(only) {
1555
- const [category, slug] = only.split(":");
1556
- if (!category || !slug) return null;
1557
- 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
+ );
1558
2100
  }
1559
2101
  async function scaffold(options) {
1560
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;
1561
2106
  console.log(`Fetching schema from ${apiUrl} ...`);
1562
2107
  const schema = await fetchSchema(apiUrl, apiKey);
1563
2108
  const forms = schema.forms ?? [];
@@ -1573,10 +2118,77 @@ async function scaffold(options) {
1573
2118
  for (const col of collections) {
1574
2119
  const safeSlug = sanitizeIdentifier(col.slug);
1575
2120
  if (!safeSlug) continue;
1576
- const listPath = join3(appDir, safeSlug, "page.tsx");
1577
- await writeScaffoldFile(listPath, scaffoldCollectionListPage(col), result, { dryRun, force, appDir });
1578
- const detailPath = join3(appDir, safeSlug, "[slug]", "page.tsx");
1579
- 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
+ }
1580
2192
  }
1581
2193
  }
1582
2194
  if (!filter || filter.category === "data-types") {
@@ -1584,8 +2196,38 @@ async function scaffold(options) {
1584
2196
  for (const dt of dataTypes) {
1585
2197
  const safeSlug = sanitizeIdentifier(dt.slug);
1586
2198
  if (!safeSlug) continue;
1587
- const pagePath = join3(appDir, safeSlug, "page.tsx");
1588
- 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
+ }
1589
2231
  }
1590
2232
  }
1591
2233
  if (!filter || filter.category === "forms") {
@@ -1593,17 +2235,56 @@ async function scaffold(options) {
1593
2235
  for (const form of filteredForms) {
1594
2236
  const safeSlug = sanitizeIdentifier(form.slug);
1595
2237
  if (!safeSlug) continue;
1596
- const pagePath = join3(appDir, safeSlug, "page.tsx");
1597
- await writeScaffoldFile(pagePath, scaffoldFormPage(form), result, { dryRun, force, appDir });
1598
- const componentPath = join3(appDir, safeSlug, "_components", `${safeSlug}-form.tsx`);
1599
- 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
+ }
1600
2277
  }
1601
2278
  }
1602
2279
  if (!filter) {
1603
2280
  const sitemapPath = join3(appDir, "sitemap.ts");
1604
2281
  if (schema.collections.length > 0) {
1605
2282
  const siteUrl = apiUrl.replace(/\/api\/.*$/, "");
1606
- 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
+ });
1607
2288
  }
1608
2289
  }
1609
2290
  console.log("");
@@ -1625,8 +2306,13 @@ Skipped ${result.skipped.length} file(s) (already exist):`);
1625
2306
  }
1626
2307
  console.log("");
1627
2308
  console.log("Next steps:");
1628
- console.log(" 1. Customize the generated pages' design (HTML/CSS)");
1629
- 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
+ }
1630
2316
  console.log(" 3. Run `npm run dev` to preview your site");
1631
2317
  }
1632
2318
 
@@ -1907,10 +2593,10 @@ async function deleteDataEntry(options) {
1907
2593
  input: process.stdin,
1908
2594
  output: process.stdout
1909
2595
  });
1910
- const answer = await new Promise((resolve2) => {
2596
+ const answer = await new Promise((resolve5) => {
1911
2597
  rl.question(
1912
2598
  `Are you sure you want to delete data entry ${options.id}? (yes/no): `,
1913
- resolve2
2599
+ resolve5
1914
2600
  );
1915
2601
  });
1916
2602
  rl.close();
@@ -1930,22 +2616,695 @@ async function deleteDataEntry(options) {
1930
2616
  console.log(JSON.stringify(result, null, 2));
1931
2617
  }
1932
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
+ function stripJsonComments(input) {
2737
+ return input.replace(/\/\*[\s\S]*?\*\//g, "").replace(/^\s*\/\/.*$/gm, "");
2738
+ }
2739
+ function stripTrailingCommas(input) {
2740
+ return input.replace(/,\s*([}\]])/g, "$1");
2741
+ }
2742
+ function normalizePathValue(input) {
2743
+ return input.replace(/\\/g, "/").replace(/^\.\//, "");
2744
+ }
2745
+ function collectFiles(rootDir, extensions) {
2746
+ const files = [];
2747
+ function walk(dir) {
2748
+ const entries = readdirSync2(dir);
2749
+ for (const entry of entries) {
2750
+ const fullPath = join5(dir, entry);
2751
+ const stat = statSync(fullPath);
2752
+ if (stat.isDirectory()) {
2753
+ if (entry === "node_modules" || entry === ".next" || entry === "dist") {
2754
+ continue;
2755
+ }
2756
+ walk(fullPath);
2757
+ } else if (extensions.has(extname(entry))) {
2758
+ files.push(fullPath);
2759
+ }
2760
+ }
2761
+ }
2762
+ if (existsSync4(rootDir)) {
2763
+ walk(rootDir);
2764
+ }
2765
+ return files;
2766
+ }
2767
+ function extractImports(code) {
2768
+ const imports = /* @__PURE__ */ new Set();
2769
+ const fromPattern = /from\s+["']([^"']+)["']/g;
2770
+ let match;
2771
+ while ((match = fromPattern.exec(code)) !== null) {
2772
+ imports.add(match[1]);
2773
+ }
2774
+ const dynamicImportPattern = /import\(\s*["']([^"']+)["']\s*\)/g;
2775
+ while ((match = dynamicImportPattern.exec(code)) !== null) {
2776
+ imports.add(match[1]);
2777
+ }
2778
+ return Array.from(imports);
2779
+ }
2780
+ function resolveGeneratedImport(outputDirAbs, importPath) {
2781
+ if (importPath === "@cmx/generated") {
2782
+ return [join5(outputDirAbs, "index.ts")];
2783
+ }
2784
+ const subPath = importPath.replace(/^@cmx\/generated\//, "");
2785
+ return [
2786
+ join5(outputDirAbs, `${subPath}.ts`),
2787
+ join5(outputDirAbs, subPath, "index.ts")
2788
+ ];
2789
+ }
2790
+ async function runCodegenCheck(options = {}) {
2791
+ const projectRoot = resolve2(options.projectRoot ?? process.cwd());
2792
+ const outputDirAbs = resolve2(projectRoot, options.outputDir ?? "cmx/generated");
2793
+ const srcDirAbs = resolve2(projectRoot, options.srcDir ?? "src");
2794
+ const tsconfigAbs = resolve2(projectRoot, options.tsconfigPath ?? "tsconfig.json");
2795
+ const errors = [];
2796
+ const warnings = [];
2797
+ const checks = [];
2798
+ if (!existsSync4(outputDirAbs)) {
2799
+ errors.push(`Generated output directory not found: ${relative(projectRoot, outputDirAbs)}`);
2800
+ } else {
2801
+ checks.push(`Generated directory exists: ${relative(projectRoot, outputDirAbs)}`);
2802
+ const indexPath = join5(outputDirAbs, "index.ts");
2803
+ if (!existsSync4(indexPath)) {
2804
+ errors.push(`Generated index not found: ${relative(projectRoot, indexPath)}`);
2805
+ } else {
2806
+ checks.push(`Generated index exists: ${relative(projectRoot, indexPath)}`);
2807
+ }
2808
+ }
2809
+ if (!existsSync4(tsconfigAbs)) {
2810
+ errors.push(`tsconfig not found: ${relative(projectRoot, tsconfigAbs)}`);
2811
+ } else {
2812
+ try {
2813
+ const raw = readFileSync4(tsconfigAbs, "utf-8");
2814
+ const parsed = JSON.parse(stripTrailingCommas(stripJsonComments(raw)));
2815
+ const aliasValues = parsed.compilerOptions?.paths?.["@cmx/*"] ?? [];
2816
+ if (aliasValues.length === 0) {
2817
+ errors.push("tsconfig compilerOptions.paths['@cmx/*'] is missing");
2818
+ } else {
2819
+ const normalizedAliasValues = aliasValues.map(normalizePathValue);
2820
+ if (!normalizedAliasValues.includes("cmx/*")) {
2821
+ errors.push(
2822
+ "tsconfig compilerOptions.paths['@cmx/*'] must include './cmx/*' (or equivalent)"
2823
+ );
2824
+ } else {
2825
+ checks.push("tsconfig '@cmx/*' alias is configured");
2826
+ }
2827
+ }
2828
+ const includeValues = parsed.include ?? [];
2829
+ const normalizedIncludeValues = includeValues.map(normalizePathValue);
2830
+ if (!normalizedIncludeValues.includes("cmx/**/*.ts")) {
2831
+ warnings.push("tsconfig include does not contain 'cmx/**/*.ts'");
2832
+ } else {
2833
+ checks.push("tsconfig include contains 'cmx/**/*.ts'");
2834
+ }
2835
+ } catch (error) {
2836
+ errors.push(`Failed to parse tsconfig: ${error instanceof Error ? error.message : String(error)}`);
2837
+ }
2838
+ }
2839
+ if (!existsSync4(srcDirAbs)) {
2840
+ warnings.push(`Source directory not found: ${relative(projectRoot, srcDirAbs)}`);
2841
+ } else {
2842
+ const sourceFiles = collectFiles(srcDirAbs, /* @__PURE__ */ new Set([".ts", ".tsx"]));
2843
+ const legacyImportHits = [];
2844
+ const generatedImportHits = [];
2845
+ for (const sourceFile of sourceFiles) {
2846
+ const code = readFileSync4(sourceFile, "utf-8");
2847
+ const imports = extractImports(code);
2848
+ for (const importPath of imports) {
2849
+ if (importPath.startsWith("@/cmx/generated")) {
2850
+ legacyImportHits.push(`${relative(projectRoot, sourceFile)} -> ${importPath}`);
2851
+ }
2852
+ if (importPath === "@cmx/generated" || importPath.startsWith("@cmx/generated/")) {
2853
+ generatedImportHits.push(`${relative(projectRoot, sourceFile)} -> ${importPath}`);
2854
+ const candidates = resolveGeneratedImport(outputDirAbs, importPath);
2855
+ for (const candidate of candidates) {
2856
+ if (existsSync4(candidate)) {
2857
+ break;
2858
+ }
2859
+ const isLast = candidate === candidates.at(-1);
2860
+ if (isLast) {
2861
+ errors.push(
2862
+ `Missing generated target for import '${importPath}' referenced in ${relative(projectRoot, sourceFile)}`
2863
+ );
2864
+ }
2865
+ }
2866
+ }
2867
+ }
2868
+ }
2869
+ if (legacyImportHits.length > 0) {
2870
+ errors.push("Legacy '@/cmx/generated' imports were found:");
2871
+ for (const hit of legacyImportHits) {
2872
+ errors.push(` - ${hit}`);
2873
+ }
2874
+ } else {
2875
+ checks.push("No legacy '@/cmx/generated' imports found");
2876
+ }
2877
+ if (generatedImportHits.length === 0) {
2878
+ warnings.push("No '@cmx/generated' imports found under src/ (this may be expected for empty projects)");
2879
+ } else {
2880
+ checks.push(`Found ${generatedImportHits.length} '@cmx/generated' import(s) under src/`);
2881
+ }
2882
+ }
2883
+ return {
2884
+ success: errors.length === 0,
2885
+ errors,
2886
+ warnings,
2887
+ checks
2888
+ };
2889
+ }
2890
+ async function codegenCheck(options = {}) {
2891
+ const projectRoot = resolve2(options.projectRoot ?? process.cwd());
2892
+ const result = await runCodegenCheck(options);
2893
+ console.log("Codegen check:");
2894
+ for (const check of result.checks) {
2895
+ console.log(` \u2713 ${check}`);
2896
+ }
2897
+ for (const warning of result.warnings) {
2898
+ console.warn(` \u26A0 ${warning}`);
2899
+ }
2900
+ for (const error of result.errors) {
2901
+ console.error(` \u2717 ${error}`);
2902
+ }
2903
+ if (!result.success) {
2904
+ console.error(`
2905
+ Codegen check failed in ${projectRoot}`);
2906
+ process.exit(1);
2907
+ }
2908
+ console.log("\nCodegen check passed");
2909
+ }
2910
+
2911
+ // src/commands/mdx-validate.ts
2912
+ import { existsSync as existsSync5, readFileSync as readFileSync5, readdirSync as readdirSync3, statSync as statSync2 } from "fs";
2913
+ import { extname as extname2, join as join6, relative as relative2, resolve as resolve3 } from "path";
2914
+
2915
+ // src/mdx/validator.ts
2916
+ import { compile } from "@mdx-js/mdx";
2917
+ var FORBIDDEN_PATTERNS = [
2918
+ // import/export statements
2919
+ { pattern: /^\s*import\s+/m, message: "import\u6587\u306F\u4F7F\u7528\u3067\u304D\u307E\u305B\u3093" },
2920
+ { pattern: /^\s*export\s+/m, message: "export\u6587\u306F\u4F7F\u7528\u3067\u304D\u307E\u305B\u3093" },
2921
+ // JS expressions in curly braces (except component props)
2922
+ { pattern: /\{[^}]*(?:=>|function|new\s+|typeof|instanceof)[^}]*\}/m, message: "JavaScript\u5F0F\u306F\u4F7F\u7528\u3067\u304D\u307E\u305B\u3093" },
2923
+ // eval, Function constructor
2924
+ { pattern: /\beval\s*\(/m, message: "eval\u306F\u4F7F\u7528\u3067\u304D\u307E\u305B\u3093" },
2925
+ { pattern: /\bnew\s+Function\s*\(/m, message: "Function constructor\u306F\u4F7F\u7528\u3067\u304D\u307E\u305B\u3093" },
2926
+ // script tags
2927
+ { pattern: /<script[\s>]/i, message: "script\u30BF\u30B0\u306F\u4F7F\u7528\u3067\u304D\u307E\u305B\u3093" },
2928
+ // on* event handlers
2929
+ { pattern: /\bon\w+\s*=/i, message: "\u30A4\u30D9\u30F3\u30C8\u30CF\u30F3\u30C9\u30E9\u5C5E\u6027\u306F\u4F7F\u7528\u3067\u304D\u307E\u305B\u3093" },
2930
+ // javascript: URLs
2931
+ { pattern: /javascript\s*:/i, message: "javascript: URL\u306F\u4F7F\u7528\u3067\u304D\u307E\u305B\u3093" },
2932
+ // data: URLs (for images - can be XSS vector)
2933
+ { pattern: /src\s*=\s*["']?\s*data:/i, message: "data: URL\u306F\u4F7F\u7528\u3067\u304D\u307E\u305B\u3093" }
2934
+ ];
2935
+ function extractComponents(mdx) {
2936
+ const components = [];
2937
+ const lines = mdx.split("\n");
2938
+ const componentPattern = /<([A-Z][a-zA-Z0-9]*)\s*([^>]*?)\s*\/?>/g;
2939
+ for (let i = 0; i < lines.length; i++) {
2940
+ const line = lines[i];
2941
+ let match;
2942
+ while ((match = componentPattern.exec(line)) !== null) {
2943
+ const name = match[1];
2944
+ const propsString = match[2];
2945
+ const props = parseProps(propsString);
2946
+ components.push({
2947
+ name,
2948
+ props,
2949
+ line: i + 1
2950
+ });
2951
+ }
2952
+ }
2953
+ return components;
2954
+ }
2955
+ function parseProps(propsString) {
2956
+ const props = {};
2957
+ const propPattern = /(\w+)\s*=\s*(?:"([^"]*)"|'([^']*)'|\{([^}]*)\})/g;
2958
+ let match;
2959
+ while ((match = propPattern.exec(propsString)) !== null) {
2960
+ const key = match[1];
2961
+ const stringValue = match[2] ?? match[3];
2962
+ const expressionValue = match[4];
2963
+ if (stringValue !== void 0) {
2964
+ props[key] = stringValue;
2965
+ } else if (expressionValue !== void 0) {
2966
+ const trimmed = expressionValue.trim();
2967
+ if (trimmed === "true") {
2968
+ props[key] = true;
2969
+ } else if (trimmed === "false") {
2970
+ props[key] = false;
2971
+ } else if (/^\d+$/.test(trimmed)) {
2972
+ props[key] = parseInt(trimmed, 10);
2973
+ } else if (/^\d+\.\d+$/.test(trimmed)) {
2974
+ props[key] = parseFloat(trimmed);
2975
+ } else if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
2976
+ try {
2977
+ props[key] = JSON.parse(trimmed.replace(/'/g, '"'));
2978
+ } catch {
2979
+ props[key] = trimmed;
2980
+ }
2981
+ } else {
2982
+ props[key] = trimmed;
2983
+ }
2984
+ }
2985
+ }
2986
+ const booleanPattern = /(?:^|\s)(\w+)(?=\s|\/|>|$)/g;
2987
+ while ((match = booleanPattern.exec(propsString)) !== null) {
2988
+ const key = match[1];
2989
+ if (!(key in props)) {
2990
+ props[key] = true;
2991
+ }
2992
+ }
2993
+ return props;
2994
+ }
2995
+ function extractReferences(components) {
2996
+ const contentIds = [];
2997
+ const assetIds = [];
2998
+ for (const component of components) {
2999
+ if (component.name === "BlogCard" && typeof component.props.contentId === "string") {
3000
+ contentIds.push(component.props.contentId);
3001
+ }
3002
+ if (component.name === "Image" && typeof component.props.assetId === "string") {
3003
+ assetIds.push(component.props.assetId);
3004
+ }
3005
+ }
3006
+ return {
3007
+ contentIds: [...new Set(contentIds)],
3008
+ assetIds: [...new Set(assetIds)]
3009
+ };
3010
+ }
3011
+ async function validateMdx(mdx) {
3012
+ const errors = [];
3013
+ const warnings = [];
3014
+ for (const { pattern, message } of FORBIDDEN_PATTERNS) {
3015
+ if (pattern.test(mdx)) {
3016
+ const lines = mdx.split("\n");
3017
+ for (let i = 0; i < lines.length; i++) {
3018
+ if (pattern.test(lines[i])) {
3019
+ errors.push({
3020
+ type: "forbidden",
3021
+ message,
3022
+ line: i + 1
3023
+ });
3024
+ break;
3025
+ }
3026
+ }
3027
+ }
3028
+ }
3029
+ try {
3030
+ await compile(mdx, {
3031
+ development: false
3032
+ // Minimal compilation to check syntax
3033
+ });
3034
+ } catch (error) {
3035
+ const err = error;
3036
+ errors.push({
3037
+ type: "syntax",
3038
+ message: err.message,
3039
+ line: err.line,
3040
+ column: err.column
3041
+ });
3042
+ }
3043
+ const components = extractComponents(mdx);
3044
+ for (const component of components) {
3045
+ if (!isValidComponent(component.name)) {
3046
+ errors.push({
3047
+ type: "component",
3048
+ message: `\u672A\u77E5\u306E\u30B3\u30F3\u30DD\u30FC\u30CD\u30F3\u30C8: ${component.name}`,
3049
+ line: component.line,
3050
+ component: component.name
3051
+ });
3052
+ continue;
3053
+ }
3054
+ const result = validateComponentProps(component.name, component.props);
3055
+ if (!result.success) {
3056
+ for (const issue of result.error.issues) {
3057
+ errors.push({
3058
+ type: "props",
3059
+ message: `${component.name}: ${issue.path.join(".")} - ${issue.message}`,
3060
+ line: component.line,
3061
+ component: component.name
3062
+ });
3063
+ }
3064
+ }
3065
+ }
3066
+ const references = extractReferences(components);
3067
+ return {
3068
+ valid: errors.length === 0,
3069
+ errors,
3070
+ warnings,
3071
+ references
3072
+ };
3073
+ }
3074
+
3075
+ // src/commands/mdx-validate.ts
3076
+ function collectMdxFiles(rootDir) {
3077
+ const files = [];
3078
+ function walk(dir) {
3079
+ const entries = readdirSync3(dir);
3080
+ for (const entry of entries) {
3081
+ const fullPath = join6(dir, entry);
3082
+ const stat = statSync2(fullPath);
3083
+ if (stat.isDirectory()) {
3084
+ if (entry === "node_modules" || entry === ".git" || entry === ".next" || entry === "dist") {
3085
+ continue;
3086
+ }
3087
+ walk(fullPath);
3088
+ } else if (extname2(entry) === ".mdx") {
3089
+ files.push(fullPath);
3090
+ }
3091
+ }
3092
+ }
3093
+ if (existsSync5(rootDir)) {
3094
+ walk(rootDir);
3095
+ }
3096
+ return files;
3097
+ }
3098
+ async function mdxValidate(options) {
3099
+ const projectRoot = process.cwd();
3100
+ const inputFile = options.file ? resolve3(projectRoot, options.file) : null;
3101
+ const inputDir = resolve3(projectRoot, options.dir ?? "src");
3102
+ const targets = [];
3103
+ if (inputFile) {
3104
+ if (!existsSync5(inputFile)) {
3105
+ console.error(`Error: file not found: ${options.file}`);
3106
+ process.exit(1);
3107
+ }
3108
+ targets.push(inputFile);
3109
+ } else {
3110
+ const dirFiles = collectMdxFiles(inputDir);
3111
+ targets.push(...dirFiles);
3112
+ }
3113
+ if (targets.length === 0) {
3114
+ console.error("No MDX files found to validate");
3115
+ process.exit(1);
3116
+ }
3117
+ let invalidCount = 0;
3118
+ let warningCount = 0;
3119
+ for (const filePath of targets) {
3120
+ const mdx = readFileSync5(filePath, "utf-8");
3121
+ const result = await validateMdx(mdx);
3122
+ const relPath = relative2(projectRoot, filePath);
3123
+ if (result.valid) {
3124
+ console.log(`\u2713 ${relPath}`);
3125
+ } else {
3126
+ invalidCount++;
3127
+ console.log(`\u2717 ${relPath}`);
3128
+ for (const err of result.errors) {
3129
+ const location = err.line ? `:${err.line}${err.column ? `:${err.column}` : ""}` : "";
3130
+ console.log(` - [${err.type}]${location} ${err.message}`);
3131
+ }
3132
+ }
3133
+ if (result.warnings.length > 0) {
3134
+ warningCount += result.warnings.length;
3135
+ for (const warning of result.warnings) {
3136
+ console.log(` ! [warning] ${warning}`);
3137
+ }
3138
+ }
3139
+ }
3140
+ console.log("");
3141
+ console.log(`Validated ${targets.length} file(s)`);
3142
+ console.log(`Invalid: ${invalidCount}`);
3143
+ console.log(`Warnings: ${warningCount}`);
3144
+ if (invalidCount > 0) {
3145
+ process.exit(1);
3146
+ }
3147
+ }
3148
+
3149
+ // src/commands/mdx-doctor.ts
3150
+ import { existsSync as existsSync6, readFileSync as readFileSync6, readdirSync as readdirSync4, statSync as statSync3 } from "fs";
3151
+ import { basename, extname as extname3, join as join7, relative as relative3, resolve as resolve4 } from "path";
3152
+ function collectFiles2(rootDir, extension) {
3153
+ const files = [];
3154
+ function walk(dir) {
3155
+ const entries = readdirSync4(dir);
3156
+ for (const entry of entries) {
3157
+ const fullPath = join7(dir, entry);
3158
+ const stat = statSync3(fullPath);
3159
+ if (stat.isDirectory()) {
3160
+ if (entry === "node_modules" || entry === ".git" || entry === ".next" || entry === "dist") {
3161
+ continue;
3162
+ }
3163
+ walk(fullPath);
3164
+ } else if (extname3(entry) === extension) {
3165
+ files.push(fullPath);
3166
+ }
3167
+ }
3168
+ }
3169
+ if (existsSync6(rootDir)) {
3170
+ walk(rootDir);
3171
+ }
3172
+ return files;
3173
+ }
3174
+ function parseDefinitionFiles(projectRoot, files) {
3175
+ const definitions = [];
3176
+ const errors = [];
3177
+ for (const file of files) {
3178
+ try {
3179
+ const raw = readFileSync6(file, "utf-8");
3180
+ const parsed = JSON.parse(raw);
3181
+ if (typeof parsed.name !== "string" || parsed.name.length === 0) {
3182
+ errors.push(`${relative3(projectRoot, file)}: missing string 'name'`);
3183
+ continue;
3184
+ }
3185
+ definitions.push({ name: parsed.name, file });
3186
+ } catch (error) {
3187
+ errors.push(
3188
+ `${relative3(projectRoot, file)}: invalid JSON (${error instanceof Error ? error.message : String(error)})`
3189
+ );
3190
+ }
3191
+ }
3192
+ return { definitions, errors };
3193
+ }
3194
+ function collectExportedComponentNames(indexContent) {
3195
+ const exported = /* @__PURE__ */ new Set();
3196
+ const namedExportPattern = /export\s+\{\s*([A-Za-z0-9_]+)(?:\s+as\s+[A-Za-z0-9_]+)?\s*\}\s+from\s+["']\.\/?([^"']+)["']/g;
3197
+ let match;
3198
+ while ((match = namedExportPattern.exec(indexContent)) !== null) {
3199
+ exported.add(match[1]);
3200
+ const pathBase = basename(match[2]);
3201
+ exported.add(pathBase);
3202
+ }
3203
+ const exportAllPattern = /export\s+\*\s+from\s+["']\.\/?([^"']+)["']/g;
3204
+ while ((match = exportAllPattern.exec(indexContent)) !== null) {
3205
+ exported.add(basename(match[1]));
3206
+ }
3207
+ return exported;
3208
+ }
3209
+ async function mdxDoctor(options = {}) {
3210
+ const projectRoot = process.cwd();
3211
+ const definitionsDir = resolve4(projectRoot, options.definitionsDir ?? "cmx/components");
3212
+ const componentsDir = resolve4(projectRoot, options.componentsDir ?? "src/components/custom");
3213
+ const indexFile = resolve4(projectRoot, options.indexFile ?? join7(options.componentsDir ?? "src/components/custom", "index.ts"));
3214
+ const issues = [];
3215
+ const checks = [];
3216
+ if (!existsSync6(definitionsDir)) {
3217
+ issues.push(`Definitions directory not found: ${relative3(projectRoot, definitionsDir)}`);
3218
+ }
3219
+ if (!existsSync6(componentsDir)) {
3220
+ issues.push(`Components directory not found: ${relative3(projectRoot, componentsDir)}`);
3221
+ }
3222
+ if (issues.length > 0) {
3223
+ for (const issue of issues) {
3224
+ console.error(`\u2717 ${issue}`);
3225
+ }
3226
+ process.exit(1);
3227
+ }
3228
+ const definitionFiles = collectFiles2(definitionsDir, ".json");
3229
+ const implFiles = collectFiles2(componentsDir, ".tsx").filter((f) => basename(f) !== "index.tsx");
3230
+ checks.push(`Found ${definitionFiles.length} definition file(s)`);
3231
+ checks.push(`Found ${implFiles.length} implementation file(s)`);
3232
+ const { definitions, errors } = parseDefinitionFiles(projectRoot, definitionFiles);
3233
+ for (const error of errors) {
3234
+ issues.push(error);
3235
+ }
3236
+ const definitionNames = new Set(definitions.map((d) => d.name));
3237
+ const implementationNames = new Set(
3238
+ implFiles.map((f) => basename(f, ".tsx")).filter((name) => name !== "index")
3239
+ );
3240
+ for (const def of definitions) {
3241
+ if (isStandardComponentName(def.name)) {
3242
+ issues.push(`${relative3(projectRoot, def.file)}: '${def.name}' is reserved as a standard component`);
3243
+ }
3244
+ }
3245
+ for (const defName of definitionNames) {
3246
+ if (!implementationNames.has(defName)) {
3247
+ issues.push(`Definition exists but implementation is missing: ${defName}.tsx`);
3248
+ }
3249
+ }
3250
+ for (const implName of implementationNames) {
3251
+ if (!definitionNames.has(implName)) {
3252
+ issues.push(`Implementation exists but definition is missing: ${implName}.json`);
3253
+ }
3254
+ }
3255
+ if (!existsSync6(indexFile)) {
3256
+ issues.push(`Export index not found: ${relative3(projectRoot, indexFile)}`);
3257
+ } else {
3258
+ const indexContent = readFileSync6(indexFile, "utf-8");
3259
+ const exportedNames = collectExportedComponentNames(indexContent);
3260
+ for (const implName of implementationNames) {
3261
+ if (!exportedNames.has(implName)) {
3262
+ issues.push(`Implementation is not exported in ${relative3(projectRoot, indexFile)}: ${implName}`);
3263
+ }
3264
+ }
3265
+ checks.push(`Checked exports in ${relative3(projectRoot, indexFile)}`);
3266
+ }
3267
+ console.log("MDX doctor:");
3268
+ for (const check of checks) {
3269
+ console.log(` \u2713 ${check}`);
3270
+ }
3271
+ if (issues.length > 0) {
3272
+ for (const issue of issues) {
3273
+ console.error(` \u2717 ${issue}`);
3274
+ }
3275
+ console.error(`
3276
+ Doctor found ${issues.length} issue(s)`);
3277
+ process.exit(1);
3278
+ }
3279
+ console.log("\nDoctor checks passed");
3280
+ }
3281
+
1933
3282
  // src/cli.ts
1934
3283
  config();
1935
3284
  var program = new Command();
1936
- program.name("cmx-sdk").description("CMX SDK - CLI tool for managing CMX schemas and content").version("0.1.13");
3285
+ program.name("cmx-sdk").description("CMX SDK - CLI tool for managing CMX schemas and content").version("0.2.3").allowExcessArguments(false);
3286
+ function requireApiCredentials() {
3287
+ const apiUrl = process.env.CMX_API_URL;
3288
+ const apiKey = process.env.CMX_API_KEY;
3289
+ if (!apiUrl || !apiKey) {
3290
+ console.error("Error: CMX_API_URL and CMX_API_KEY environment variables are required");
3291
+ process.exit(1);
3292
+ }
3293
+ return { apiUrl, apiKey };
3294
+ }
1937
3295
  program.action(async () => {
1938
- const { interactiveMenu } = await import("./interactive-menu-BAMWXEKP.js");
3296
+ const { interactiveMenu } = await import("./interactive-menu-BDZOOGQH.js");
1939
3297
  await interactiveMenu();
1940
3298
  });
1941
3299
  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) => {
1942
- const { init } = await import("./init-OD2XGZI2.js");
3300
+ const { init } = await import("./init-XUTF5IBZ.js");
1943
3301
  await init(projectName, options);
1944
3302
  });
1945
3303
  program.command("add-studio").description("\u65E2\u5B58\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u306B CMX Studio \u3092\u8FFD\u52A0").action(async () => {
1946
- const { addStudio } = await import("./add-studio-C2L2NYQC.js");
3304
+ const { addStudio } = await import("./add-studio-TLDFTZTX.js");
1947
3305
  await addStudio();
1948
3306
  });
3307
+ program.command("studio").description("\u30B5\u30A4\u30C8 (4000) \u3068 Studio (4001) \u3092\u540C\u6642\u8D77\u52D5").action(studio);
1949
3308
  program.command("list-collections").description("List all collections").action(listCollections);
1950
3309
  program.command("list-data-types").description("List all data types").action(listDataTypes);
1951
3310
  program.command("list-forms").description("List all form definitions").action(listForms);
@@ -1953,20 +3312,75 @@ program.command("list-components").description("List all custom components").act
1953
3312
  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);
1954
3313
  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);
1955
3314
  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);
1956
- 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);
1957
- program.command("generate").description("Generate TypeScript types and functions from CMX schema").option("--output <dir>", "Output directory (default: cmx/generated)").action((options) => {
1958
- const apiUrl = process.env.CMX_API_URL;
1959
- const apiKey = process.env.CMX_API_KEY;
1960
- if (!apiUrl || !apiKey) {
1961
- console.error("Error: CMX_API_URL and CMX_API_KEY environment variables are required");
1962
- process.exit(1);
1963
- }
1964
- return generate({
3315
+ program.command("sync-components").description("Sync custom components").action(syncComponents);
3316
+ 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));
3317
+ var codegenCommand = program.command("codegen").description("Generate typed code, page scaffolds, and run compatibility checks");
3318
+ codegenCommand.command("types").description("Generate TypeScript types and fetch helpers from CMX schema").option("--output <dir>", "Output directory", "cmx/generated").action(async (options) => {
3319
+ const { apiUrl, apiKey } = requireApiCredentials();
3320
+ await generate({
1965
3321
  apiUrl,
1966
3322
  apiKey,
1967
- outputDir: options.output || "cmx/generated"
3323
+ outputDir: options.output
1968
3324
  });
1969
3325
  });
3326
+ 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(
3327
+ async (options) => {
3328
+ const { apiUrl, apiKey } = requireApiCredentials();
3329
+ await scaffold({
3330
+ apiUrl,
3331
+ apiKey,
3332
+ appDir: options.appDir,
3333
+ featuresDir: options.featuresDir,
3334
+ template: options.template,
3335
+ dryRun: options.dryRun,
3336
+ force: options.force,
3337
+ only: options.only
3338
+ });
3339
+ }
3340
+ );
3341
+ 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(
3342
+ async (options) => {
3343
+ await codegenCheck({
3344
+ projectRoot: options.projectRoot,
3345
+ outputDir: options.outputDir,
3346
+ srcDir: options.srcDir,
3347
+ tsconfigPath: options.tsconfig
3348
+ });
3349
+ }
3350
+ );
3351
+ 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(
3352
+ async (options) => {
3353
+ const { apiUrl, apiKey } = requireApiCredentials();
3354
+ console.log("\n[1/3] codegen types");
3355
+ await generate({
3356
+ apiUrl,
3357
+ apiKey,
3358
+ outputDir: options.output
3359
+ });
3360
+ console.log("\n[2/3] codegen pages");
3361
+ await scaffold({
3362
+ apiUrl,
3363
+ apiKey,
3364
+ appDir: options.appDir,
3365
+ featuresDir: options.featuresDir,
3366
+ template: options.template,
3367
+ dryRun: options.dryRun,
3368
+ force: options.force,
3369
+ only: options.only
3370
+ });
3371
+ console.log("\n[3/3] codegen check");
3372
+ await codegenCheck({
3373
+ outputDir: options.output,
3374
+ srcDir: options.srcDir,
3375
+ tsconfigPath: options.tsconfig
3376
+ });
3377
+ }
3378
+ );
3379
+ var mdxCommand = program.command("mdx").description("MDX developer tooling");
3380
+ 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));
3381
+ 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(
3382
+ (options) => mdxDoctor(options)
3383
+ );
1970
3384
  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);
1971
3385
  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);
1972
3386
  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);
@@ -1978,22 +3392,6 @@ program.command("add-collection-data-type").description("Add a data type to a co
1978
3392
  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);
1979
3393
  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);
1980
3394
  program.command("list-collection-presets").description("List available presets for collection data types").option("--type <type>", "Collection type (post, news, doc, page)").action(listCollectionPresets);
1981
- 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) => {
1982
- const apiUrl = process.env.CMX_API_URL;
1983
- const apiKey = process.env.CMX_API_KEY;
1984
- if (!apiUrl || !apiKey) {
1985
- console.error("Error: CMX_API_URL and CMX_API_KEY environment variables are required");
1986
- process.exit(1);
1987
- }
1988
- return scaffold({
1989
- apiUrl,
1990
- apiKey,
1991
- appDir: options.appDir || "app/(public)",
1992
- dryRun: options.dryRun,
1993
- force: options.force,
1994
- only: options.only
1995
- });
1996
- });
1997
3395
  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);
1998
3396
  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);
1999
3397
  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);