@cmssy/cli 0.10.1 → 0.12.0

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.
Files changed (101) hide show
  1. package/config.d.ts +2 -2
  2. package/config.js +1 -1
  3. package/dist/cli.js +84 -32
  4. package/dist/cli.js.map +1 -1
  5. package/dist/commands/add-source.d.ts +6 -0
  6. package/dist/commands/add-source.d.ts.map +1 -1
  7. package/dist/commands/add-source.js +37 -2
  8. package/dist/commands/add-source.js.map +1 -1
  9. package/dist/commands/build.d.ts.map +1 -1
  10. package/dist/commands/build.js +23 -1
  11. package/dist/commands/build.js.map +1 -1
  12. package/dist/commands/configure.d.ts.map +1 -1
  13. package/dist/commands/configure.js +0 -3
  14. package/dist/commands/configure.js.map +1 -1
  15. package/dist/commands/dev.d.ts.map +1 -1
  16. package/dist/commands/dev.js +20 -0
  17. package/dist/commands/dev.js.map +1 -1
  18. package/dist/commands/doctor.d.ts +2 -0
  19. package/dist/commands/doctor.d.ts.map +1 -0
  20. package/dist/commands/doctor.js +223 -0
  21. package/dist/commands/doctor.js.map +1 -0
  22. package/dist/commands/init.d.ts.map +1 -1
  23. package/dist/commands/init.js +5 -12
  24. package/dist/commands/init.js.map +1 -1
  25. package/dist/commands/link.d.ts +8 -0
  26. package/dist/commands/link.d.ts.map +1 -0
  27. package/dist/commands/link.js +121 -0
  28. package/dist/commands/link.js.map +1 -0
  29. package/dist/commands/package.d.ts +7 -0
  30. package/dist/commands/package.d.ts.map +1 -1
  31. package/dist/commands/package.js +2 -2
  32. package/dist/commands/package.js.map +1 -1
  33. package/dist/commands/publish.d.ts +3 -0
  34. package/dist/commands/publish.d.ts.map +1 -1
  35. package/dist/commands/publish.js +222 -4
  36. package/dist/commands/publish.js.map +1 -1
  37. package/dist/commands/sync.js +1 -1
  38. package/dist/commands/sync.js.map +1 -1
  39. package/dist/commands/test.d.ts +8 -0
  40. package/dist/commands/test.d.ts.map +1 -0
  41. package/dist/commands/test.js +60 -0
  42. package/dist/commands/test.js.map +1 -0
  43. package/dist/commands/upload.d.ts +4 -0
  44. package/dist/commands/upload.d.ts.map +1 -1
  45. package/dist/commands/upload.js +6 -4
  46. package/dist/commands/upload.js.map +1 -1
  47. package/dist/commands/workspaces.d.ts.map +1 -1
  48. package/dist/commands/workspaces.js +1 -1
  49. package/dist/commands/workspaces.js.map +1 -1
  50. package/dist/utils/block-config.d.ts +7 -1
  51. package/dist/utils/block-config.d.ts.map +1 -1
  52. package/dist/utils/block-config.js +5 -3
  53. package/dist/utils/block-config.js.map +1 -1
  54. package/dist/utils/config.js +17 -12
  55. package/dist/utils/config.js.map +1 -1
  56. package/dist/utils/dependency-check.d.ts +21 -0
  57. package/dist/utils/dependency-check.d.ts.map +1 -0
  58. package/dist/utils/dependency-check.js +56 -0
  59. package/dist/utils/dependency-check.js.map +1 -0
  60. package/dist/utils/dev-generator/home-page.js +2 -2
  61. package/dist/utils/dev-generator/index.d.ts +1 -2
  62. package/dist/utils/dev-generator/index.d.ts.map +1 -1
  63. package/dist/utils/dev-generator/index.js +53 -21
  64. package/dist/utils/dev-generator/index.js.map +1 -1
  65. package/dist/utils/dev-generator/preview-pages.js +8 -2
  66. package/dist/utils/dev-generator/preview-pages.js.map +1 -1
  67. package/dist/utils/field-schema.d.ts.map +1 -1
  68. package/dist/utils/field-schema.js +0 -16
  69. package/dist/utils/field-schema.js.map +1 -1
  70. package/dist/utils/graphql.d.ts +3 -1
  71. package/dist/utils/graphql.d.ts.map +1 -1
  72. package/dist/utils/graphql.js +52 -1
  73. package/dist/utils/graphql.js.map +1 -1
  74. package/dist/utils/publish-helpers.js +1 -1
  75. package/dist/utils/publish-helpers.js.map +1 -1
  76. package/dist/utils/scanner.d.ts +6 -0
  77. package/dist/utils/scanner.d.ts.map +1 -1
  78. package/dist/utils/scanner.js +13 -0
  79. package/dist/utils/scanner.js.map +1 -1
  80. package/dist/utils/schema-diff.d.ts +22 -0
  81. package/dist/utils/schema-diff.d.ts.map +1 -0
  82. package/dist/utils/schema-diff.js +107 -0
  83. package/dist/utils/schema-diff.js.map +1 -0
  84. package/dist/utils/theme-builder.d.ts +17 -0
  85. package/dist/utils/theme-builder.d.ts.map +1 -0
  86. package/dist/utils/theme-builder.js +160 -0
  87. package/dist/utils/theme-builder.js.map +1 -0
  88. package/dist/utils/type-generator.d.ts.map +1 -1
  89. package/dist/utils/type-generator.js +0 -2
  90. package/dist/utils/type-generator.js.map +1 -1
  91. package/package.json +7 -3
  92. package/src/dev-app/app/api/blocks/[name]/config/route.ts +140 -0
  93. package/src/dev-app/app/api/blocks/route.ts +61 -0
  94. package/src/dev-app/app/api/config/route.ts +98 -0
  95. package/src/dev-app/app/api/context/route.ts +126 -0
  96. package/src/dev-app/app/api/preview/[name]/route.ts +137 -0
  97. package/src/dev-app/app/api/workspaces/route.ts +124 -0
  98. package/src/dev-app/app/layout.tsx +18 -0
  99. package/src/dev-app/app/page.tsx +1785 -0
  100. package/test.d.ts +1 -0
  101. package/test.js +2 -0
@@ -0,0 +1,140 @@
1
+ import { NextResponse } from "next/server";
2
+ import fs from "fs";
3
+ import path from "path";
4
+ import { execSync } from "child_process";
5
+
6
+ const projectRoot = process.env.CMSSY_PROJECT_ROOT || process.cwd();
7
+ const SAFE_NAME_RE = /^[a-zA-Z0-9_-]+$/;
8
+
9
+ function loadBlockConfig(blockPath: string): Record<string, unknown> | null {
10
+ const configPath = path.join(blockPath, "config.ts");
11
+ if (!fs.existsSync(configPath)) return null;
12
+
13
+ try {
14
+ const tsxPaths = [
15
+ path.join(projectRoot, "node_modules", ".bin", "tsx"),
16
+ path.join(
17
+ projectRoot,
18
+ "node_modules",
19
+ "cmssy-cli",
20
+ "node_modules",
21
+ ".bin",
22
+ "tsx",
23
+ ),
24
+ ];
25
+ const tsxBinary = tsxPaths.find((p) => fs.existsSync(p)) || "npx -y tsx";
26
+
27
+ const cacheDir = path.join(projectRoot, ".cmssy", "cache");
28
+ fs.mkdirSync(cacheDir, { recursive: true });
29
+
30
+ const tempDir = fs.mkdtempSync(path.join(cacheDir, "block-config-"));
31
+ try {
32
+ const mockConfigPath = path.join(tempDir, "cmssy-cli-config.mjs");
33
+ fs.writeFileSync(
34
+ mockConfigPath,
35
+ "export const defineBlock = (config) => config;\nexport const defineTemplate = (config) => config;\nexport const field = (config) => config;",
36
+ );
37
+
38
+ const configContent = fs.readFileSync(configPath, "utf-8");
39
+ const modified = configContent.replace(
40
+ /from\s+['"](?:@cmssy\/cli\/config|cmssy-cli\/config)['"]/g,
41
+ `from '${mockConfigPath.replace(/\\\\/g, "/")}'`,
42
+ );
43
+
44
+ const tempPath = path.join(tempDir, "temp-block-config.ts");
45
+ fs.writeFileSync(tempPath, modified);
46
+
47
+ const evalCode = `import cfg from '${tempPath.replace(/\\\\/g, "/")}'; console.log(JSON.stringify(cfg.default || cfg));`;
48
+ const cmd = tsxBinary.includes("npx")
49
+ ? `${tsxBinary} --eval "${evalCode}"`
50
+ : `"${tsxBinary}" --eval "${evalCode}"`;
51
+
52
+ const output = execSync(cmd, {
53
+ encoding: "utf-8",
54
+ cwd: projectRoot,
55
+ stdio: ["pipe", "pipe", "pipe"],
56
+ });
57
+
58
+ const lines = output.trim().split("\n");
59
+ return JSON.parse(lines[lines.length - 1]);
60
+ } finally {
61
+ try {
62
+ fs.rmSync(tempDir, { recursive: true });
63
+ } catch {}
64
+ }
65
+ } catch {
66
+ return null;
67
+ }
68
+ }
69
+
70
+ export async function GET(
71
+ _request: Request,
72
+ { params }: { params: Promise<{ name: string }> },
73
+ ) {
74
+ const { name } = await params;
75
+ if (!SAFE_NAME_RE.test(name)) {
76
+ return NextResponse.json({ error: "Invalid name" }, { status: 400 });
77
+ }
78
+
79
+ let blockPath = path.join(projectRoot, "blocks", name);
80
+ if (!fs.existsSync(blockPath)) {
81
+ blockPath = path.join(projectRoot, "templates", name);
82
+ }
83
+
84
+ if (!fs.existsSync(blockPath)) {
85
+ return NextResponse.json({ error: "Block not found" }, { status: 404 });
86
+ }
87
+
88
+ const previewPath = path.join(blockPath, "preview.json");
89
+ const previewData = fs.existsSync(previewPath)
90
+ ? JSON.parse(fs.readFileSync(previewPath, "utf-8"))
91
+ : {};
92
+
93
+ const config = loadBlockConfig(blockPath);
94
+
95
+ const pagesJsonPath = path.join(blockPath, "pages.json");
96
+ let pagesData = fs.existsSync(pagesJsonPath)
97
+ ? JSON.parse(fs.readFileSync(pagesJsonPath, "utf-8"))
98
+ : null;
99
+
100
+ if (!pagesData && config && (config.pages || config.layoutPositions)) {
101
+ const layoutPositions: Record<string, any> = {};
102
+ if (Array.isArray(config.layoutPositions)) {
103
+ for (const lp of config.layoutPositions as any[]) {
104
+ layoutPositions[lp.position] = {
105
+ type: lp.type,
106
+ content: lp.content || {},
107
+ };
108
+ }
109
+ } else if (
110
+ config.layoutPositions &&
111
+ typeof config.layoutPositions === "object"
112
+ ) {
113
+ Object.assign(layoutPositions, config.layoutPositions);
114
+ }
115
+
116
+ const pages = ((config.pages || []) as any[]).map(
117
+ (page: any, index: number) => ({
118
+ name: page.name,
119
+ slug:
120
+ page.slug === "home" || page.slug === "/" || index === 0
121
+ ? "/"
122
+ : page.slug.startsWith("/")
123
+ ? page.slug
124
+ : "/" + page.slug,
125
+ blocks: page.blocks || [],
126
+ }),
127
+ );
128
+
129
+ pagesData = { layoutPositions, pages };
130
+ }
131
+
132
+ return NextResponse.json({
133
+ name,
134
+ schema: config?.schema || {},
135
+ previewData,
136
+ pages: config?.pages,
137
+ layoutPositions: config?.layoutPositions,
138
+ pagesData,
139
+ });
140
+ }
@@ -0,0 +1,61 @@
1
+ import { NextResponse } from "next/server";
2
+ import fs from "fs";
3
+ import path from "path";
4
+
5
+ const projectRoot = process.env.CMSSY_PROJECT_ROOT || process.cwd();
6
+
7
+ export async function GET() {
8
+ const blocks: any[] = [];
9
+
10
+ const blocksDir = path.join(projectRoot, "blocks");
11
+ if (fs.existsSync(blocksDir)) {
12
+ const dirs = fs
13
+ .readdirSync(blocksDir, { withFileTypes: true })
14
+ .filter((d) => d.isDirectory());
15
+
16
+ for (const dir of dirs) {
17
+ const pkgPath = path.join(blocksDir, dir.name, "package.json");
18
+ const pkg = fs.existsSync(pkgPath)
19
+ ? JSON.parse(fs.readFileSync(pkgPath, "utf-8"))
20
+ : {};
21
+ blocks.push({
22
+ type: "block",
23
+ name: dir.name,
24
+ displayName: pkg.cmssy?.displayName || dir.name,
25
+ version: pkg.version || "1.0.0",
26
+ category: pkg.cmssy?.category || "other",
27
+ tags: pkg.cmssy?.tags || [],
28
+ description: pkg.description || "",
29
+ hasConfig: fs.existsSync(path.join(blocksDir, dir.name, "config.ts")),
30
+ });
31
+ }
32
+ }
33
+
34
+ const templatesDir = path.join(projectRoot, "templates");
35
+ if (fs.existsSync(templatesDir)) {
36
+ const dirs = fs
37
+ .readdirSync(templatesDir, { withFileTypes: true })
38
+ .filter((d) => d.isDirectory());
39
+
40
+ for (const dir of dirs) {
41
+ const pkgPath = path.join(templatesDir, dir.name, "package.json");
42
+ const pkg = fs.existsSync(pkgPath)
43
+ ? JSON.parse(fs.readFileSync(pkgPath, "utf-8"))
44
+ : {};
45
+ blocks.push({
46
+ type: "template",
47
+ name: dir.name,
48
+ displayName: pkg.cmssy?.displayName || dir.name,
49
+ version: pkg.version || "1.0.0",
50
+ category: pkg.cmssy?.category || "pages",
51
+ tags: pkg.cmssy?.tags || [],
52
+ description: pkg.description || "",
53
+ hasConfig: fs.existsSync(
54
+ path.join(templatesDir, dir.name, "config.ts"),
55
+ ),
56
+ });
57
+ }
58
+ }
59
+
60
+ return NextResponse.json(blocks);
61
+ }
@@ -0,0 +1,98 @@
1
+ import { NextResponse } from "next/server";
2
+ import fs from "fs";
3
+ import path from "path";
4
+
5
+ const projectRoot = process.env.CMSSY_PROJECT_ROOT || process.cwd();
6
+
7
+ function getEnvPath() {
8
+ return path.join(projectRoot, ".env");
9
+ }
10
+
11
+ function readEnv(): Record<string, string> {
12
+ const envPath = getEnvPath();
13
+ const result: Record<string, string> = {};
14
+ if (!fs.existsSync(envPath)) return result;
15
+ const content = fs.readFileSync(envPath, "utf-8");
16
+ for (const line of content.split("\n")) {
17
+ const match = line.match(/^([A-Z_]+)=(.*)$/);
18
+ if (match) result[match[1]] = match[2].trim();
19
+ }
20
+ return result;
21
+ }
22
+
23
+ function writeEnv(updates: Record<string, string>) {
24
+ const envPath = getEnvPath();
25
+ let content = fs.existsSync(envPath) ? fs.readFileSync(envPath, "utf-8") : "";
26
+
27
+ for (const [key, value] of Object.entries(updates)) {
28
+ const regex = new RegExp(`^(${key})=.*$`, "m");
29
+ if (regex.test(content)) {
30
+ content = content.replace(regex, `${key}=${value}`);
31
+ } else {
32
+ content = content.trimEnd() + "\n" + `${key}=${value}` + "\n";
33
+ }
34
+ }
35
+
36
+ fs.writeFileSync(envPath, content);
37
+ }
38
+
39
+ function maskToken(token: string | undefined): string | null {
40
+ if (!token || token.length < 8) return token ? "***" : null;
41
+ return token.substring(0, 3) + "***" + token.substring(token.length - 3);
42
+ }
43
+
44
+ function readCmssyConfig(): Record<string, any> | null {
45
+ const configPath = path.join(projectRoot, "cmssy.config.js");
46
+ if (!fs.existsSync(configPath)) return null;
47
+ try {
48
+ const content = fs.readFileSync(configPath, "utf-8");
49
+ const match = content.match(/export\s+default\s+({[\s\S]*});?/);
50
+ if (match)
51
+ return JSON.parse(match[1].replace(/,\s*}/g, "}").replace(/'/g, '"'));
52
+ } catch {}
53
+ return null;
54
+ }
55
+
56
+ export async function GET() {
57
+ const env = readEnv();
58
+ const cmssyConfig = readCmssyConfig();
59
+
60
+ return NextResponse.json({
61
+ env: {
62
+ apiUrl: env.CMSSY_API_URL || "https://api.cmssy.io/graphql",
63
+ hasToken: !!env.CMSSY_API_TOKEN,
64
+ maskedToken: maskToken(env.CMSSY_API_TOKEN),
65
+ workspaceId: env.CMSSY_WORKSPACE_ID || null,
66
+ },
67
+ project: cmssyConfig,
68
+ });
69
+ }
70
+
71
+ export async function PUT(request: Request) {
72
+ try {
73
+ const body = await request.json();
74
+ const updates: Record<string, string> = {};
75
+
76
+ if (body.apiUrl !== undefined) {
77
+ updates.CMSSY_API_URL = body.apiUrl;
78
+ }
79
+ if (body.apiToken !== undefined) {
80
+ updates.CMSSY_API_TOKEN = body.apiToken;
81
+ }
82
+ if (body.workspaceId !== undefined) {
83
+ updates.CMSSY_WORKSPACE_ID = body.workspaceId;
84
+ }
85
+
86
+ if (Object.keys(updates).length === 0) {
87
+ return NextResponse.json(
88
+ { error: "No values to update" },
89
+ { status: 400 },
90
+ );
91
+ }
92
+
93
+ writeEnv(updates);
94
+ return NextResponse.json({ success: true });
95
+ } catch (error: any) {
96
+ return NextResponse.json({ error: error.message }, { status: 500 });
97
+ }
98
+ }
@@ -0,0 +1,126 @@
1
+ import { NextResponse } from "next/server";
2
+ import fs from "fs";
3
+ import path from "path";
4
+
5
+ const projectRoot = process.env.CMSSY_PROJECT_ROOT || process.cwd();
6
+ const CONTEXT_PATH = path.join(projectRoot, ".cmssy", "dev-context.json");
7
+
8
+ const DEFAULT_CONTEXT = {
9
+ locale: {
10
+ current: "en",
11
+ default: "en",
12
+ enabled: ["en"],
13
+ },
14
+ auth: null,
15
+ workspace: {
16
+ id: "dev-workspace",
17
+ slug: "dev",
18
+ name: "Dev Workspace",
19
+ },
20
+ isPreview: true,
21
+ };
22
+
23
+ const PRESETS: Record<string, any> = {
24
+ empty: {
25
+ locale: { current: "en", default: "en", enabled: ["en"] },
26
+ auth: null,
27
+ workspace: null,
28
+ isPreview: true,
29
+ },
30
+ "logged-in": {
31
+ locale: { current: "en", default: "en", enabled: ["en"] },
32
+ auth: {
33
+ isAuthenticated: true,
34
+ member: {
35
+ id: "dev-member-1",
36
+ email: "user@example.com",
37
+ profile: {
38
+ firstName: "Jane",
39
+ lastName: "Doe",
40
+ displayName: "Jane Doe",
41
+ avatarUrl: "",
42
+ },
43
+ role: "member",
44
+ verified: true,
45
+ },
46
+ },
47
+ workspace: { id: "dev-workspace", slug: "dev", name: "Dev Workspace" },
48
+ isPreview: true,
49
+ },
50
+ "multi-language": {
51
+ locale: { current: "en", default: "en", enabled: ["en", "pl", "de"] },
52
+ auth: null,
53
+ workspace: { id: "dev-workspace", slug: "dev", name: "Dev Workspace" },
54
+ isPreview: true,
55
+ },
56
+ "blog-posts": {
57
+ locale: { current: "en", default: "en", enabled: ["en"] },
58
+ auth: null,
59
+ workspace: { id: "dev-workspace", slug: "dev", name: "Dev Workspace" },
60
+ isPreview: true,
61
+ pages: {
62
+ posts: [
63
+ {
64
+ slug: "hello-world",
65
+ fullSlug: "/blog/hello-world",
66
+ displayName: { en: "Hello World" },
67
+ pageType: "post",
68
+ },
69
+ {
70
+ slug: "getting-started",
71
+ fullSlug: "/blog/getting-started",
72
+ displayName: { en: "Getting Started" },
73
+ pageType: "post",
74
+ },
75
+ {
76
+ slug: "advanced-tips",
77
+ fullSlug: "/blog/advanced-tips",
78
+ displayName: { en: "Advanced Tips" },
79
+ pageType: "post",
80
+ },
81
+ ],
82
+ },
83
+ },
84
+ };
85
+
86
+ export async function GET(request: Request) {
87
+ const url = new URL(request.url);
88
+
89
+ // Return presets list
90
+ if (url.searchParams.get("presets") === "true") {
91
+ return NextResponse.json({
92
+ presets: Object.keys(PRESETS),
93
+ });
94
+ }
95
+
96
+ // Return a specific preset
97
+ const preset = url.searchParams.get("preset");
98
+ if (preset && PRESETS[preset]) {
99
+ return NextResponse.json(PRESETS[preset]);
100
+ }
101
+
102
+ // Return saved context
103
+ let context = DEFAULT_CONTEXT;
104
+ if (fs.existsSync(CONTEXT_PATH)) {
105
+ try {
106
+ context = JSON.parse(fs.readFileSync(CONTEXT_PATH, "utf-8"));
107
+ } catch {
108
+ context = DEFAULT_CONTEXT;
109
+ }
110
+ }
111
+
112
+ return NextResponse.json(context);
113
+ }
114
+
115
+ export async function PUT(request: Request) {
116
+ try {
117
+ const body = await request.json();
118
+ const dir = path.dirname(CONTEXT_PATH);
119
+ fs.mkdirSync(dir, { recursive: true });
120
+ fs.writeFileSync(CONTEXT_PATH, JSON.stringify(body, null, 2));
121
+ return NextResponse.json({ success: true });
122
+ } catch (error: any) {
123
+ const status = error?.message?.includes("JSON") ? 400 : 500;
124
+ return NextResponse.json({ error: error.message }, { status });
125
+ }
126
+ }
@@ -0,0 +1,137 @@
1
+ import { NextResponse } from "next/server";
2
+ import fs from "fs";
3
+ import path from "path";
4
+
5
+ const projectRoot = process.env.CMSSY_PROJECT_ROOT || process.cwd();
6
+
7
+ const SAFE_NAME_RE = /^[a-zA-Z0-9_-]+$/;
8
+
9
+ function isValidName(name: string): boolean {
10
+ return SAFE_NAME_RE.test(name);
11
+ }
12
+
13
+ function resolveBlockPath(name: string): string | null {
14
+ if (!isValidName(name)) return null;
15
+ let blockPath = path.join(projectRoot, "blocks", name);
16
+ if (fs.existsSync(blockPath)) return blockPath;
17
+ blockPath = path.join(projectRoot, "templates", name);
18
+ return fs.existsSync(blockPath) ? blockPath : null;
19
+ }
20
+
21
+ function getVariants(blockPath: string): string[] {
22
+ const previewsDir = path.join(blockPath, "previews");
23
+ if (!fs.existsSync(previewsDir)) return [];
24
+ return fs
25
+ .readdirSync(previewsDir)
26
+ .filter((f) => f.endsWith(".json"))
27
+ .map((f) => f.replace(/\.json$/, ""))
28
+ .filter((name) => SAFE_NAME_RE.test(name))
29
+ .sort((a, b) => a.localeCompare(b));
30
+ }
31
+
32
+ function getPreviewPath(blockPath: string, variant?: string): string {
33
+ if (variant && isValidName(variant)) {
34
+ const variantPath = path.join(blockPath, "previews", `${variant}.json`);
35
+ if (fs.existsSync(variantPath)) return variantPath;
36
+ }
37
+ return path.join(blockPath, "preview.json");
38
+ }
39
+
40
+ export async function GET(
41
+ request: Request,
42
+ { params }: { params: Promise<{ name: string }> },
43
+ ) {
44
+ const { name } = await params;
45
+ if (!isValidName(name)) {
46
+ return NextResponse.json({ error: "Invalid name" }, { status: 400 });
47
+ }
48
+
49
+ const blockPath = resolveBlockPath(name);
50
+ if (!blockPath) {
51
+ return NextResponse.json({ error: "Not found" }, { status: 404 });
52
+ }
53
+
54
+ const url = new URL(request.url);
55
+ const variant = url.searchParams.get("variant") || undefined;
56
+
57
+ if (url.searchParams.get("list") === "variants") {
58
+ return NextResponse.json({ variants: getVariants(blockPath) });
59
+ }
60
+
61
+ const previewPath = getPreviewPath(blockPath, variant);
62
+ let data = {};
63
+ if (fs.existsSync(previewPath)) {
64
+ try {
65
+ data = JSON.parse(fs.readFileSync(previewPath, "utf-8"));
66
+ } catch {
67
+ data = {};
68
+ }
69
+ }
70
+
71
+ return NextResponse.json({
72
+ data,
73
+ variants: getVariants(blockPath),
74
+ currentVariant: variant || null,
75
+ });
76
+ }
77
+
78
+ export async function POST(
79
+ request: Request,
80
+ { params }: { params: Promise<{ name: string }> },
81
+ ) {
82
+ const { name } = await params;
83
+ if (!isValidName(name)) {
84
+ return NextResponse.json({ error: "Invalid name" }, { status: 400 });
85
+ }
86
+
87
+ const blockPath = resolveBlockPath(name);
88
+ if (!blockPath) {
89
+ return NextResponse.json({ error: "Not found" }, { status: 404 });
90
+ }
91
+
92
+ const url = new URL(request.url);
93
+ const variant = url.searchParams.get("variant") || undefined;
94
+ const action = url.searchParams.get("action");
95
+
96
+ // Validate variant param if present
97
+ if (variant && !isValidName(variant)) {
98
+ return NextResponse.json({ error: "Invalid variant" }, { status: 400 });
99
+ }
100
+
101
+ // Save as new variant
102
+ if (action === "save-variant") {
103
+ const body = await request.json();
104
+ const variantName = body.variantName;
105
+ if (!variantName || !isValidName(variantName)) {
106
+ return NextResponse.json(
107
+ {
108
+ error:
109
+ "Invalid variant name (use alphanumeric, hyphens, underscores)",
110
+ },
111
+ { status: 400 },
112
+ );
113
+ }
114
+ const previewsDir = path.join(blockPath, "previews");
115
+ fs.mkdirSync(previewsDir, { recursive: true });
116
+ const variantPath = path.join(previewsDir, `${variantName}.json`);
117
+ const dataToWrite = body.data ?? {};
118
+ fs.writeFileSync(variantPath, JSON.stringify(dataToWrite, null, 2));
119
+ return NextResponse.json({ success: true, variant: variantName });
120
+ }
121
+
122
+ // Delete variant
123
+ if (action === "delete-variant" && variant) {
124
+ const variantPath = path.join(blockPath, "previews", `${variant}.json`);
125
+ if (fs.existsSync(variantPath)) {
126
+ fs.unlinkSync(variantPath);
127
+ }
128
+ return NextResponse.json({ success: true });
129
+ }
130
+
131
+ // Regular save
132
+ const body = await request.json();
133
+ const previewPath = getPreviewPath(blockPath, variant);
134
+ fs.writeFileSync(previewPath, JSON.stringify(body, null, 2));
135
+
136
+ return NextResponse.json({ success: true });
137
+ }
@@ -0,0 +1,124 @@
1
+ import { NextResponse } from "next/server";
2
+ import fs from "fs";
3
+ import path from "path";
4
+
5
+ const projectRoot = process.env.CMSSY_PROJECT_ROOT || process.cwd();
6
+
7
+ function loadEnvConfig(): {
8
+ apiUrl: string;
9
+ apiToken: string | null;
10
+ workspaceId: string | null;
11
+ } {
12
+ const envPath = path.join(projectRoot, ".env");
13
+ const config: any = {
14
+ apiUrl: "https://api.cmssy.io/graphql",
15
+ apiToken: null,
16
+ workspaceId: null,
17
+ };
18
+ if (!fs.existsSync(envPath)) return config;
19
+ const content = fs.readFileSync(envPath, "utf-8");
20
+ for (const line of content.split("\n")) {
21
+ const match = line.match(/^(CMSSY_\w+)=(.*)$/);
22
+ if (!match) continue;
23
+ const [, key, val] = match;
24
+ if (key === "CMSSY_API_URL") config.apiUrl = val.trim();
25
+ if (key === "CMSSY_API_TOKEN") config.apiToken = val.trim() || null;
26
+ if (key === "CMSSY_WORKSPACE_ID") config.workspaceId = val.trim() || null;
27
+ }
28
+ return config;
29
+ }
30
+
31
+ const MY_WORKSPACES_QUERY = `
32
+ query MyWorkspaces {
33
+ myWorkspaces {
34
+ id
35
+ slug
36
+ name
37
+ myRole { name slug }
38
+ }
39
+ }
40
+ `;
41
+
42
+ const WORKSPACE_BLOCKS_QUERY = `
43
+ query GetWorkspaceBlocks {
44
+ workspaceBlocks {
45
+ id
46
+ blockType
47
+ name
48
+ version
49
+ }
50
+ }
51
+ `;
52
+
53
+ export async function GET() {
54
+ const config = loadEnvConfig();
55
+
56
+ if (!config.apiToken) {
57
+ return NextResponse.json({ connected: false, reason: "no_token" });
58
+ }
59
+
60
+ try {
61
+ const headers = {
62
+ "Content-Type": "application/json",
63
+ Authorization: `Bearer ${config.apiToken}`,
64
+ };
65
+
66
+ const wsRes = await fetch(config.apiUrl, {
67
+ method: "POST",
68
+ headers,
69
+ body: JSON.stringify({ query: MY_WORKSPACES_QUERY }),
70
+ });
71
+ const wsData = await wsRes.json();
72
+
73
+ if (wsData.errors?.length) {
74
+ return NextResponse.json({
75
+ connected: false,
76
+ reason: "auth_error",
77
+ error: wsData.errors[0].message,
78
+ });
79
+ }
80
+
81
+ const workspaces = wsData.data?.myWorkspaces || [];
82
+
83
+ let targetWorkspace = null;
84
+ if (config.workspaceId) {
85
+ targetWorkspace = workspaces.find(
86
+ (w: any) => w.id === config.workspaceId,
87
+ );
88
+ }
89
+ if (!targetWorkspace && workspaces.length > 0) {
90
+ targetWorkspace = workspaces[0];
91
+ }
92
+
93
+ let publishedBlocks: any[] = [];
94
+ if (targetWorkspace) {
95
+ const blocksRes = await fetch(config.apiUrl, {
96
+ method: "POST",
97
+ headers: {
98
+ ...headers,
99
+ "x-workspace-id": targetWorkspace.id,
100
+ },
101
+ body: JSON.stringify({ query: WORKSPACE_BLOCKS_QUERY }),
102
+ });
103
+ const blocksData = await blocksRes.json();
104
+ publishedBlocks = blocksData.data?.workspaceBlocks || [];
105
+ }
106
+
107
+ return NextResponse.json({
108
+ connected: true,
109
+ workspace: targetWorkspace,
110
+ workspaces,
111
+ publishedBlocks: publishedBlocks.map((b: any) => ({
112
+ blockType: b.blockType,
113
+ name: b.name,
114
+ version: b.version || "1.0.0",
115
+ })),
116
+ });
117
+ } catch (error: any) {
118
+ return NextResponse.json({
119
+ connected: false,
120
+ reason: "network_error",
121
+ error: error.message,
122
+ });
123
+ }
124
+ }
@@ -0,0 +1,18 @@
1
+ import type { Metadata } from "next";
2
+ import type { ReactNode } from "react";
3
+ import "./globals.css";
4
+
5
+ export const metadata: Metadata = {
6
+ title: "Cmssy Dev Server",
7
+ icons: {
8
+ icon: "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Crect width='100' height='100' rx='20' fill='%23667eea'/%3E%3Ctext x='50' y='70' font-size='60' font-weight='bold' text-anchor='middle' fill='white' font-family='system-ui'%3EC%3C/text%3E%3C/svg%3E",
9
+ },
10
+ };
11
+
12
+ export default function RootLayout({ children }: { children: ReactNode }) {
13
+ return (
14
+ <html lang="en">
15
+ <body>{children}</body>
16
+ </html>
17
+ );
18
+ }