@cmssy/cli 0.1.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 (113) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +649 -0
  3. package/config.d.ts +2 -0
  4. package/config.js +2 -0
  5. package/dist/cli.d.ts +3 -0
  6. package/dist/cli.d.ts.map +1 -0
  7. package/dist/cli.js +236 -0
  8. package/dist/cli.js.map +1 -0
  9. package/dist/commands/add-source.d.ts +7 -0
  10. package/dist/commands/add-source.d.ts.map +1 -0
  11. package/dist/commands/add-source.js +238 -0
  12. package/dist/commands/add-source.js.map +1 -0
  13. package/dist/commands/build.d.ts +7 -0
  14. package/dist/commands/build.d.ts.map +1 -0
  15. package/dist/commands/build.js +105 -0
  16. package/dist/commands/build.js.map +1 -0
  17. package/dist/commands/configure.d.ts +6 -0
  18. package/dist/commands/configure.d.ts.map +1 -0
  19. package/dist/commands/configure.js +42 -0
  20. package/dist/commands/configure.js.map +1 -0
  21. package/dist/commands/create.d.ts +18 -0
  22. package/dist/commands/create.d.ts.map +1 -0
  23. package/dist/commands/create.js +444 -0
  24. package/dist/commands/create.js.map +1 -0
  25. package/dist/commands/dev.d.ts +6 -0
  26. package/dist/commands/dev.d.ts.map +1 -0
  27. package/dist/commands/dev.js +962 -0
  28. package/dist/commands/dev.js.map +1 -0
  29. package/dist/commands/init.d.ts +2 -0
  30. package/dist/commands/init.d.ts.map +1 -0
  31. package/dist/commands/init.js +362 -0
  32. package/dist/commands/init.js.map +1 -0
  33. package/dist/commands/migrate.d.ts +2 -0
  34. package/dist/commands/migrate.d.ts.map +1 -0
  35. package/dist/commands/migrate.js +227 -0
  36. package/dist/commands/migrate.js.map +1 -0
  37. package/dist/commands/package.d.ts +7 -0
  38. package/dist/commands/package.d.ts.map +1 -0
  39. package/dist/commands/package.js +136 -0
  40. package/dist/commands/package.js.map +1 -0
  41. package/dist/commands/publish.d.ts +13 -0
  42. package/dist/commands/publish.d.ts.map +1 -0
  43. package/dist/commands/publish.js +910 -0
  44. package/dist/commands/publish.js.map +1 -0
  45. package/dist/commands/sync.d.ts +6 -0
  46. package/dist/commands/sync.d.ts.map +1 -0
  47. package/dist/commands/sync.js +208 -0
  48. package/dist/commands/sync.js.map +1 -0
  49. package/dist/commands/upload.d.ts +7 -0
  50. package/dist/commands/upload.d.ts.map +1 -0
  51. package/dist/commands/upload.js +126 -0
  52. package/dist/commands/upload.js.map +1 -0
  53. package/dist/commands/workspaces.d.ts +2 -0
  54. package/dist/commands/workspaces.d.ts.map +1 -0
  55. package/dist/commands/workspaces.js +67 -0
  56. package/dist/commands/workspaces.js.map +1 -0
  57. package/dist/dev-ui/app.js +1284 -0
  58. package/dist/dev-ui/index.html +1511 -0
  59. package/dist/dev-ui-react/App.tsx +164 -0
  60. package/dist/dev-ui-react/__tests__/previewData.test.ts +193 -0
  61. package/dist/dev-ui-react/components/BlocksList.tsx +232 -0
  62. package/dist/dev-ui-react/components/Editor.tsx +469 -0
  63. package/dist/dev-ui-react/components/Preview.tsx +146 -0
  64. package/dist/dev-ui-react/hooks/useBlocks.ts +80 -0
  65. package/dist/dev-ui-react/index.html +13 -0
  66. package/dist/dev-ui-react/main.tsx +8 -0
  67. package/dist/dev-ui-react/styles.css +856 -0
  68. package/dist/dev-ui-react/types.ts +45 -0
  69. package/dist/types/block-config.d.ts +315 -0
  70. package/dist/types/block-config.d.ts.map +1 -0
  71. package/dist/types/block-config.js +8 -0
  72. package/dist/types/block-config.js.map +1 -0
  73. package/dist/utils/block-config.d.ts +10 -0
  74. package/dist/utils/block-config.d.ts.map +1 -0
  75. package/dist/utils/block-config.js +199 -0
  76. package/dist/utils/block-config.js.map +1 -0
  77. package/dist/utils/blocks-meta-cache.d.ts +28 -0
  78. package/dist/utils/blocks-meta-cache.d.ts.map +1 -0
  79. package/dist/utils/blocks-meta-cache.js +72 -0
  80. package/dist/utils/blocks-meta-cache.js.map +1 -0
  81. package/dist/utils/builder.d.ts +34 -0
  82. package/dist/utils/builder.d.ts.map +1 -0
  83. package/dist/utils/builder.js +140 -0
  84. package/dist/utils/builder.js.map +1 -0
  85. package/dist/utils/cmssy-config.d.ts +16 -0
  86. package/dist/utils/cmssy-config.d.ts.map +1 -0
  87. package/dist/utils/cmssy-config.js +19 -0
  88. package/dist/utils/cmssy-config.js.map +1 -0
  89. package/dist/utils/config.d.ts +9 -0
  90. package/dist/utils/config.d.ts.map +1 -0
  91. package/dist/utils/config.js +46 -0
  92. package/dist/utils/config.js.map +1 -0
  93. package/dist/utils/field-schema.d.ts +12 -0
  94. package/dist/utils/field-schema.d.ts.map +1 -0
  95. package/dist/utils/field-schema.js +202 -0
  96. package/dist/utils/field-schema.js.map +1 -0
  97. package/dist/utils/graphql.d.ts +8 -0
  98. package/dist/utils/graphql.d.ts.map +1 -0
  99. package/dist/utils/graphql.js +118 -0
  100. package/dist/utils/graphql.js.map +1 -0
  101. package/dist/utils/publish-helpers.d.ts +35 -0
  102. package/dist/utils/publish-helpers.d.ts.map +1 -0
  103. package/dist/utils/publish-helpers.js +141 -0
  104. package/dist/utils/publish-helpers.js.map +1 -0
  105. package/dist/utils/scanner.d.ts +36 -0
  106. package/dist/utils/scanner.d.ts.map +1 -0
  107. package/dist/utils/scanner.js +140 -0
  108. package/dist/utils/scanner.js.map +1 -0
  109. package/dist/utils/type-generator.d.ts +9 -0
  110. package/dist/utils/type-generator.d.ts.map +1 -0
  111. package/dist/utils/type-generator.js +85 -0
  112. package/dist/utils/type-generator.js.map +1 -0
  113. package/package.json +88 -0
@@ -0,0 +1,962 @@
1
+ import chalk from "chalk";
2
+ import { exec } from "child_process";
3
+ import express from "express";
4
+ import fs from "fs-extra";
5
+ import { GraphQLClient } from "graphql-request";
6
+ import ora from "ora";
7
+ import path from "path";
8
+ import { fileURLToPath } from "url";
9
+ import { createServer as createViteServer } from "vite";
10
+ import react from "@vitejs/plugin-react";
11
+ import tailwindcss from "@tailwindcss/postcss";
12
+ // Custom Vite plugin to resolve @import "main.css" from styles folder
13
+ // Inlines the imported CSS content to avoid Vite's postcss-import issues
14
+ function cmssyStylesImportPlugin(projectRoot) {
15
+ return {
16
+ name: "cmssy-styles-import",
17
+ enforce: "pre",
18
+ async load(id) {
19
+ // Strip query params for matching
20
+ const cleanId = id.split("?")[0];
21
+ // Only process CSS files in blocks/templates
22
+ if (!cleanId.endsWith(".css"))
23
+ return null;
24
+ if (!cleanId.includes("/blocks/") && !cleanId.includes("/templates/"))
25
+ return null;
26
+ const content = await fs.readFile(cleanId, "utf-8");
27
+ // Check if it has @import "main.css" or similar simple imports
28
+ if (!content.includes('@import "') && !content.includes("@import '")) {
29
+ return null;
30
+ }
31
+ // Replace @import "filename.css" with the actual file content (inline it)
32
+ const stylesDir = path.join(projectRoot, "styles");
33
+ let transformed = content;
34
+ // Match @import "filename.css" or @import 'filename.css' (without path)
35
+ const importRegex = /@import\s+["']([^"'\/]+\.css)["']\s*;/g;
36
+ transformed = transformed.replace(importRegex, (match, filename) => {
37
+ const fullPath = path.join(stylesDir, filename);
38
+ if (fs.existsSync(fullPath)) {
39
+ // Inline the CSS content instead of keeping the import
40
+ const importedContent = fs.readFileSync(fullPath, "utf-8");
41
+ return `/* Inlined from ${filename} */\n${importedContent}`;
42
+ }
43
+ return match; // Keep original if file doesn't exist
44
+ });
45
+ if (transformed !== content) {
46
+ return transformed;
47
+ }
48
+ return null;
49
+ },
50
+ };
51
+ }
52
+ import { loadBlockConfig, validateSchema as validateBlockSchema } from "../utils/block-config.js";
53
+ import { loadMetaCache, updateBlockInCache } from "../utils/blocks-meta-cache.js";
54
+ import { isTemplateConfig } from "../types/block-config.js";
55
+ import { loadConfig } from "../utils/cmssy-config.js";
56
+ import { loadConfig as loadEnvConfig } from "../utils/config.js";
57
+ import { getFieldTypes } from "../utils/field-schema.js";
58
+ import { scanResources } from "../utils/scanner.js";
59
+ import { generateTypes } from "../utils/type-generator.js";
60
+ // Merge default values from schema into preview data
61
+ // Preview data values take precedence over defaults
62
+ function mergeDefaultsWithPreview(schema, previewData) {
63
+ const merged = { ...previewData };
64
+ for (const [key, field] of Object.entries(schema)) {
65
+ // If field is missing or undefined, use defaultValue
66
+ if (merged[key] === undefined || merged[key] === null) {
67
+ if (field.defaultValue !== undefined) {
68
+ merged[key] = field.defaultValue;
69
+ }
70
+ else if (field.type === "repeater") {
71
+ // Repeaters default to empty array if no defaultValue
72
+ merged[key] = [];
73
+ }
74
+ }
75
+ // For repeaters with items, merge nested defaults
76
+ if (field.type === "repeater" && field.schema && Array.isArray(merged[key])) {
77
+ merged[key] = merged[key].map((item) => {
78
+ const mergedItem = { ...item };
79
+ for (const [nestedKey, nestedField] of Object.entries(field.schema)) {
80
+ // Add default value if missing
81
+ if (mergedItem[nestedKey] === undefined && nestedField.defaultValue !== undefined) {
82
+ mergedItem[nestedKey] = nestedField.defaultValue;
83
+ }
84
+ }
85
+ return mergedItem;
86
+ });
87
+ }
88
+ }
89
+ return merged;
90
+ }
91
+ export async function devCommand(options) {
92
+ const spinner = ora("Starting development server...").start();
93
+ try {
94
+ const config = await loadConfig();
95
+ const port = parseInt(options.port, 10);
96
+ const projectRoot = process.cwd();
97
+ // Scan for blocks and templates - FAST: no config loading at startup
98
+ spinner.text = "Scanning blocks...";
99
+ const resources = await scanResources({
100
+ strict: false,
101
+ loadConfig: false, // Lazy load configs when needed
102
+ validateSchema: false,
103
+ loadPreview: false, // Lazy load preview data
104
+ requirePackageJson: false,
105
+ });
106
+ if (resources.length === 0) {
107
+ spinner.warn("No blocks or templates found");
108
+ console.log(chalk.yellow("\nCreate your first block:\n"));
109
+ console.log(chalk.white(" npx cmssy create block my-block\n"));
110
+ process.exit(0);
111
+ }
112
+ // Load metadata cache for instant filters
113
+ spinner.text = "Loading metadata cache...";
114
+ const metaCache = loadMetaCache(projectRoot);
115
+ let cachedCount = 0;
116
+ // Merge cached metadata into resources
117
+ resources.forEach((r) => {
118
+ const cached = metaCache.blocks[r.name];
119
+ if (cached) {
120
+ r.category = cached.category;
121
+ r.displayName = cached.displayName || r.name;
122
+ r.description = cached.description;
123
+ // Store tags in a temp property for API
124
+ r.cachedTags = cached.tags;
125
+ cachedCount++;
126
+ }
127
+ });
128
+ if (cachedCount > 0) {
129
+ spinner.text = `Loaded ${cachedCount} blocks from cache`;
130
+ }
131
+ // Fetch field types from backend (used for type generation)
132
+ spinner.text = "Fetching field types...";
133
+ let fieldTypes = [];
134
+ try {
135
+ fieldTypes = await getFieldTypes();
136
+ }
137
+ catch (error) {
138
+ // Will use fallback types if backend is unreachable
139
+ }
140
+ spinner.text = "Starting Vite server...";
141
+ // Dev UI paths (must be before Vite config)
142
+ const __filename = fileURLToPath(import.meta.url);
143
+ const __dirname = path.dirname(__filename);
144
+ const devUiReactPath = path.join(__dirname, "../dev-ui-react");
145
+ // Create Express app for API routes
146
+ const app = express();
147
+ app.use(express.json());
148
+ // Create Vite server in middleware mode
149
+ const vite = await createViteServer({
150
+ root: projectRoot,
151
+ server: {
152
+ middlewareMode: true,
153
+ hmr: { port: port + 1 },
154
+ fs: {
155
+ // Allow serving files from cmssy-cli package (dev-ui-react)
156
+ allow: [projectRoot, path.dirname(__dirname)],
157
+ },
158
+ },
159
+ appType: "custom",
160
+ plugins: [cmssyStylesImportPlugin(projectRoot), react()],
161
+ resolve: {
162
+ alias: [
163
+ // React packages must resolve from user's project, not cmssy-cli
164
+ { find: "react", replacement: path.join(projectRoot, "node_modules/react") },
165
+ { find: "react-dom", replacement: path.join(projectRoot, "node_modules/react-dom") },
166
+ // Common @ alias for project root (shadcn/ui convention)
167
+ { find: /^@\/(.*)/, replacement: path.join(projectRoot, "$1") },
168
+ { find: "@blocks", replacement: path.join(projectRoot, "blocks") },
169
+ { find: "@templates", replacement: path.join(projectRoot, "templates") },
170
+ { find: "@styles", replacement: path.join(projectRoot, "styles") },
171
+ { find: "@lib", replacement: path.join(projectRoot, "lib") },
172
+ // Handle relative imports to lib from any depth
173
+ { find: /^(\.\.\/)+lib/, replacement: path.join(projectRoot, "lib") },
174
+ // Serve dev UI React files from cmssy-cli package
175
+ { find: /^\/dev-ui-react\/(.*)/, replacement: path.join(devUiReactPath, "$1") },
176
+ ],
177
+ },
178
+ css: {
179
+ postcss: {
180
+ plugins: [tailwindcss()],
181
+ },
182
+ },
183
+ optimizeDeps: {
184
+ include: ["react", "react-dom", "framer-motion"],
185
+ },
186
+ });
187
+ // API: Get all blocks (uses cache for instant filters)
188
+ app.get("/api/blocks", (_req, res) => {
189
+ const blockList = resources.map((r) => ({
190
+ type: r.type,
191
+ name: r.name,
192
+ displayName: r.displayName || r.name,
193
+ version: r.packageJson?.version || "1.0.0",
194
+ // Use cached or loaded metadata
195
+ category: r.blockConfig?.category || r.category || "other",
196
+ tags: r.blockConfig?.tags || r.cachedTags || [],
197
+ description: r.blockConfig?.description || r.description,
198
+ hasConfig: !!r.blockConfig,
199
+ }));
200
+ res.json(blockList);
201
+ });
202
+ // API: Lazy load block config (called when block is selected)
203
+ app.get("/api/blocks/:name/config", async (req, res) => {
204
+ const { name } = req.params;
205
+ const resource = resources.find((r) => r.name === name);
206
+ if (!resource) {
207
+ res.status(404).json({ error: "Block not found" });
208
+ return;
209
+ }
210
+ // Load config if not already loaded
211
+ if (!resource.blockConfig) {
212
+ try {
213
+ const blockConfig = await loadBlockConfig(resource.path);
214
+ if (blockConfig) {
215
+ // Validate schema
216
+ if (blockConfig.schema) {
217
+ const validation = await validateBlockSchema(blockConfig.schema, resource.path);
218
+ if (!validation.valid) {
219
+ console.log(chalk.yellow(`\n⚠️ Schema warnings for ${name}:`));
220
+ validation.errors.forEach((err) => console.log(chalk.yellow(` • ${err}`)));
221
+ }
222
+ }
223
+ resource.blockConfig = blockConfig;
224
+ resource.displayName = blockConfig.name || resource.name;
225
+ resource.description = blockConfig.description;
226
+ resource.category = blockConfig.category;
227
+ // Update metadata cache
228
+ updateBlockInCache(name, resource.type, blockConfig, resource.packageJson?.version, projectRoot);
229
+ }
230
+ }
231
+ catch (error) {
232
+ console.log(chalk.red(`\n❌ Failed to load config for ${name}: ${error.message}`));
233
+ res.status(500).json({ error: error.message });
234
+ return;
235
+ }
236
+ }
237
+ // Always load preview data fresh from file (don't use stale cache)
238
+ const previewPath = path.join(resource.path, "preview.json");
239
+ if (fs.existsSync(previewPath)) {
240
+ resource.previewData = fs.readJsonSync(previewPath);
241
+ }
242
+ else {
243
+ resource.previewData = {};
244
+ }
245
+ const cfg = resource.blockConfig;
246
+ // Merge default values from schema into previewData (preview.json values take precedence)
247
+ const mergedPreviewData = mergeDefaultsWithPreview(cfg?.schema || {}, resource.previewData || {});
248
+ // Build response with template-specific fields if applicable
249
+ const response = {
250
+ name: resource.name,
251
+ displayName: cfg?.name || resource.displayName || resource.name,
252
+ description: cfg?.description || resource.description,
253
+ category: cfg?.category || "other",
254
+ tags: cfg?.tags || [],
255
+ schema: cfg?.schema || {},
256
+ previewData: mergedPreviewData,
257
+ version: resource.packageJson?.version || "1.0.0",
258
+ };
259
+ // Add template-specific fields
260
+ if (cfg && isTemplateConfig(cfg)) {
261
+ response.pages = cfg.pages;
262
+ response.layoutSlots = cfg.layoutSlots || [];
263
+ }
264
+ res.json(response);
265
+ });
266
+ // API: Get user's workspaces
267
+ app.get("/api/workspaces", async (_req, res) => {
268
+ try {
269
+ const envConfig = loadEnvConfig();
270
+ if (!envConfig.apiToken) {
271
+ res.status(401).json({
272
+ error: "API token not configured",
273
+ message: "Run 'cmssy configure' to set up your API credentials",
274
+ });
275
+ return;
276
+ }
277
+ const client = new GraphQLClient(envConfig.apiUrl, {
278
+ headers: {
279
+ "Content-Type": "application/json",
280
+ Authorization: `Bearer ${envConfig.apiToken}`,
281
+ },
282
+ });
283
+ const query = `
284
+ query MyWorkspaces {
285
+ myWorkspaces {
286
+ id
287
+ slug
288
+ name
289
+ myRole { name slug }
290
+ }
291
+ }
292
+ `;
293
+ const data = await client.request(query);
294
+ res.json(data.myWorkspaces || []);
295
+ }
296
+ catch (error) {
297
+ console.error("Failed to fetch workspaces:", error);
298
+ res.status(500).json({
299
+ error: "Failed to fetch workspaces",
300
+ message: error.message || "Unknown error",
301
+ });
302
+ }
303
+ });
304
+ // API: Get preview data for a block (lazy loads if needed)
305
+ app.get("/api/preview/:blockName", (req, res) => {
306
+ const { blockName } = req.params;
307
+ const resource = resources.find((r) => r.name === blockName);
308
+ if (!resource) {
309
+ res.status(404).json({ error: "Block not found" });
310
+ return;
311
+ }
312
+ // Always load preview data fresh from file
313
+ const previewPath = path.join(resource.path, "preview.json");
314
+ if (fs.existsSync(previewPath)) {
315
+ resource.previewData = fs.readJsonSync(previewPath);
316
+ }
317
+ else {
318
+ resource.previewData = {};
319
+ }
320
+ res.json(resource.previewData);
321
+ });
322
+ // API: Save preview data for a block
323
+ app.post("/api/preview/:blockName", (req, res) => {
324
+ const { blockName } = req.params;
325
+ const newPreviewData = req.body;
326
+ const resource = resources.find((r) => r.name === blockName);
327
+ if (!resource) {
328
+ res.status(404).json({ error: "Block not found" });
329
+ return;
330
+ }
331
+ resource.previewData = newPreviewData;
332
+ const previewPath = path.join(resource.path, "preview.json");
333
+ try {
334
+ fs.writeJsonSync(previewPath, newPreviewData, { spaces: 2 });
335
+ res.json({ success: true });
336
+ }
337
+ catch (error) {
338
+ res.status(500).json({ error: error.message });
339
+ }
340
+ });
341
+ // API: Get published version from backend
342
+ app.get("/api/blocks/:name/published-version", async (req, res) => {
343
+ const { name } = req.params;
344
+ const { workspaceId } = req.query;
345
+ const resource = resources.find((r) => r.name === name);
346
+ if (!resource) {
347
+ res.status(404).json({ error: "Block not found" });
348
+ return;
349
+ }
350
+ if (!workspaceId) {
351
+ res.status(400).json({ error: "workspaceId is required" });
352
+ return;
353
+ }
354
+ try {
355
+ const envConfig = loadEnvConfig();
356
+ if (!envConfig.apiToken) {
357
+ res.json({ version: null, published: false });
358
+ return;
359
+ }
360
+ const client = new GraphQLClient(envConfig.apiUrl, {
361
+ headers: {
362
+ Authorization: `Bearer ${envConfig.apiToken}`,
363
+ "x-workspace-id": workspaceId,
364
+ },
365
+ });
366
+ const packageName = resource.packageJson?.name || "";
367
+ const blockType = packageName.split(".").pop() || name;
368
+ const query = `
369
+ query GetPublishedVersion($blockType: String!) {
370
+ workspaceBlockByType(blockType: $blockType) { version }
371
+ }
372
+ `;
373
+ const data = await client.request(query, { blockType });
374
+ const publishedVersion = data.workspaceBlockByType?.version || null;
375
+ res.json({ version: publishedVersion, published: publishedVersion !== null });
376
+ }
377
+ catch (error) {
378
+ res.json({ version: null, published: false, error: error.message });
379
+ }
380
+ });
381
+ // API: Get block publish status
382
+ app.get("/api/blocks/:name/status", (req, res) => {
383
+ const { name } = req.params;
384
+ const resource = resources.find((r) => r.name === name);
385
+ if (!resource) {
386
+ res.status(404).json({ error: "Block not found" });
387
+ return;
388
+ }
389
+ res.json({
390
+ name: resource.name,
391
+ version: resource.packageJson?.version || "1.0.0",
392
+ packageName: resource.packageJson?.name || `@local/${resource.type}s.${resource.name}`,
393
+ published: false,
394
+ lastPublished: null,
395
+ });
396
+ });
397
+ // API: Publish block
398
+ app.post("/api/blocks/:name/publish", async (req, res) => {
399
+ const { name } = req.params;
400
+ const { target, workspaceId, versionBump } = req.body;
401
+ const resource = resources.find((r) => r.name === name);
402
+ if (!resource) {
403
+ res.status(404).json({ error: "Block not found" });
404
+ return;
405
+ }
406
+ if (!target || (target !== "marketplace" && target !== "workspace")) {
407
+ res.status(400).json({ error: "Invalid target" });
408
+ return;
409
+ }
410
+ if (target === "workspace" && !workspaceId) {
411
+ res.status(400).json({ error: "Workspace ID required" });
412
+ return;
413
+ }
414
+ const args = ["publish", resource.name, `--${target}`];
415
+ if (target === "workspace" && workspaceId)
416
+ args.push(workspaceId);
417
+ if (versionBump && versionBump !== "none") {
418
+ args.push(`--${versionBump}`);
419
+ }
420
+ else {
421
+ args.push("--no-bump");
422
+ }
423
+ const command = `cmssy ${args.join(" ")}`;
424
+ console.log("[PUBLISH] Executing:", command);
425
+ exec(command, {
426
+ cwd: projectRoot,
427
+ timeout: 60000,
428
+ maxBuffer: 10 * 1024 * 1024,
429
+ env: { ...process.env, CI: "true", FORCE_COLOR: "0", NO_COLOR: "1" },
430
+ }, (error, stdout, stderr) => {
431
+ const output = `${stdout}\n${stderr}`;
432
+ const success = output.includes("published successfully") ||
433
+ output.includes("published to workspace") ||
434
+ output.includes("submitted for review");
435
+ if (success) {
436
+ const pkgPath = path.join(resource.path, "package.json");
437
+ if (fs.existsSync(pkgPath)) {
438
+ resource.packageJson = fs.readJsonSync(pkgPath);
439
+ }
440
+ res.json({
441
+ success: true,
442
+ message: target === "marketplace" ? "Submitted for review" : "Published to workspace",
443
+ version: resource.packageJson?.version,
444
+ });
445
+ }
446
+ else {
447
+ res.status(500).json({ success: false, error: stderr || error?.message || "Publish failed" });
448
+ }
449
+ });
450
+ });
451
+ // API: List resources (legacy)
452
+ app.get("/api/resources", (_req, res) => {
453
+ res.json(resources.map((r) => ({
454
+ type: r.type,
455
+ name: r.name,
456
+ displayName: r.displayName,
457
+ description: r.description,
458
+ category: r.category,
459
+ })));
460
+ });
461
+ // API: Get template pages (for template preview)
462
+ app.get("/api/templates/:name/pages", async (req, res) => {
463
+ const { name } = req.params;
464
+ const resource = resources.find((r) => r.name === name && r.type === "template");
465
+ if (!resource) {
466
+ res.status(404).json({ error: "Template not found" });
467
+ return;
468
+ }
469
+ // Lazy load config if needed
470
+ if (!resource.blockConfig) {
471
+ try {
472
+ const blockConfig = await loadBlockConfig(resource.path);
473
+ if (blockConfig) {
474
+ resource.blockConfig = blockConfig;
475
+ }
476
+ }
477
+ catch (error) {
478
+ res.status(500).json({ error: error.message });
479
+ return;
480
+ }
481
+ }
482
+ const config = resource.blockConfig;
483
+ if (!config || !isTemplateConfig(config)) {
484
+ res.status(400).json({ error: "Not a valid template (missing pages)" });
485
+ return;
486
+ }
487
+ res.json({
488
+ name: resource.name,
489
+ displayName: config.name || resource.name,
490
+ pages: config.pages.map((p) => ({
491
+ name: p.name,
492
+ slug: p.slug,
493
+ blocksCount: p.blocks.length,
494
+ })),
495
+ layoutSlots: config.layoutSlots || [],
496
+ });
497
+ });
498
+ // Template page preview - renders full page with all blocks
499
+ app.get("/preview/template/:name/:pageSlug?", async (req, res) => {
500
+ const { name, pageSlug } = req.params;
501
+ const resource = resources.find((r) => r.name === name && r.type === "template");
502
+ if (!resource) {
503
+ res.status(404).send("Template not found");
504
+ return;
505
+ }
506
+ // Lazy load config if needed
507
+ if (!resource.blockConfig) {
508
+ try {
509
+ const blockConfig = await loadBlockConfig(resource.path);
510
+ if (blockConfig) {
511
+ resource.blockConfig = blockConfig;
512
+ }
513
+ }
514
+ catch (error) {
515
+ res.status(500).send(`Failed to load template: ${error.message}`);
516
+ return;
517
+ }
518
+ }
519
+ const templateConfig = resource.blockConfig;
520
+ if (!templateConfig || !isTemplateConfig(templateConfig)) {
521
+ res.status(400).send("Not a valid template (missing pages)");
522
+ return;
523
+ }
524
+ // Find page (default to first page)
525
+ const page = pageSlug
526
+ ? templateConfig.pages.find((p) => p.slug === pageSlug)
527
+ : templateConfig.pages[0];
528
+ if (!page) {
529
+ res.status(404).send(`Page "${pageSlug}" not found in template`);
530
+ return;
531
+ }
532
+ const html = generateTemplatePreviewHTML(resource, templateConfig, page, resources, port);
533
+ const transformed = await vite.transformIndexHtml(req.url, html);
534
+ res.send(transformed);
535
+ });
536
+ // Preview page - serves HTML that loads block via Vite
537
+ app.get("/preview/:name", async (req, res) => {
538
+ const { name } = req.params;
539
+ const resource = resources.find((r) => r.name === name);
540
+ if (!resource) {
541
+ res.status(404).send("Resource not found");
542
+ return;
543
+ }
544
+ // Always load preview data fresh from file
545
+ const previewPath = path.join(resource.path, "preview.json");
546
+ if (fs.existsSync(previewPath)) {
547
+ resource.previewData = fs.readJsonSync(previewPath);
548
+ }
549
+ else {
550
+ resource.previewData = {};
551
+ }
552
+ const html = generatePreviewHTML(resource, config, port);
553
+ const transformed = await vite.transformIndexHtml(req.url, html);
554
+ res.send(transformed);
555
+ });
556
+ // Legacy preview route
557
+ app.get("/preview/:type/:name", async (req, res) => {
558
+ const { name } = req.params;
559
+ const resource = resources.find((r) => r.name === name);
560
+ if (!resource) {
561
+ res.status(404).send("Resource not found");
562
+ return;
563
+ }
564
+ // Always load preview data fresh from file
565
+ const previewPath2 = path.join(resource.path, "preview.json");
566
+ if (fs.existsSync(previewPath2)) {
567
+ resource.previewData = fs.readJsonSync(previewPath2);
568
+ }
569
+ else {
570
+ resource.previewData = {};
571
+ }
572
+ const html = generatePreviewHTML(resource, config, port);
573
+ const transformed = await vite.transformIndexHtml(req.url, html);
574
+ res.send(transformed);
575
+ });
576
+ // Home page - serve React dev UI
577
+ app.get("/", async (req, res) => {
578
+ const indexPath = path.join(devUiReactPath, "index.html");
579
+ let html = fs.readFileSync(indexPath, "utf-8");
580
+ // Transform HTML through Vite for HMR support
581
+ html = await vite.transformIndexHtml(req.url, html);
582
+ res.send(html);
583
+ });
584
+ // Use Vite's middleware for JS/TS/CSS transforms (handles /dev-ui-react/ via alias)
585
+ app.use(vite.middlewares);
586
+ // Start server
587
+ const server = app.listen(port, () => {
588
+ spinner.succeed("Development server started (Vite)");
589
+ console.log(chalk.green.bold("\n─────────────────────────────────────────"));
590
+ console.log(chalk.green.bold(" Cmssy Dev Server (Vite HMR)"));
591
+ console.log(chalk.green.bold("─────────────────────────────────────────\n"));
592
+ const blocks = resources.filter((r) => r.type === "block");
593
+ const templates = resources.filter((r) => r.type === "template");
594
+ console.log(chalk.cyan(` ${blocks.length} blocks, ${templates.length} templates`));
595
+ console.log(chalk.green(`\n Local: ${chalk.cyan(`http://localhost:${port}`)}`));
596
+ console.log(chalk.green(" Vite HMR enabled ✓"));
597
+ console.log(chalk.green(" Press Ctrl+C to stop"));
598
+ console.log(chalk.green.bold("\n─────────────────────────────────────────\n"));
599
+ // Listen for Ctrl+C directly on stdin (works even if SIGINT is blocked)
600
+ if (process.stdin.isTTY) {
601
+ process.stdin.setRawMode(true);
602
+ process.stdin.resume();
603
+ process.stdin.on("data", (data) => {
604
+ // Ctrl+C = \x03, Ctrl+D = \x04
605
+ if (data[0] === 0x03 || data[0] === 0x04) {
606
+ console.log(chalk.yellow("\n\nShutting down..."));
607
+ process.exit(0);
608
+ }
609
+ });
610
+ }
611
+ // Also register SIGINT as fallback
612
+ process.removeAllListeners("SIGINT");
613
+ process.on("SIGINT", () => {
614
+ console.log(chalk.yellow("\n\nShutting down..."));
615
+ process.exit(0);
616
+ });
617
+ });
618
+ // Watch for new blocks/config changes
619
+ setupConfigWatcher({ resources, vite, fieldTypes });
620
+ }
621
+ catch (error) {
622
+ spinner.fail("Failed to start development server");
623
+ console.error(chalk.red("Error:"), error);
624
+ process.exit(1);
625
+ }
626
+ }
627
+ function setupConfigWatcher(options) {
628
+ const { resources, vite, fieldTypes } = options;
629
+ const projectRoot = process.cwd();
630
+ // Watch for block.config.ts changes to regenerate types
631
+ vite.watcher.on("change", async (filePath) => {
632
+ if (filePath.endsWith("block.config.ts")) {
633
+ const relativePath = path.relative(projectRoot, filePath);
634
+ const parts = relativePath.split(path.sep);
635
+ const resourceName = parts[1]; // blocks/hero/block.config.ts -> hero
636
+ const resource = resources.find((r) => r.name === resourceName);
637
+ if (resource) {
638
+ console.log(chalk.blue(`\n⚙️ Config changed: ${resourceName}`));
639
+ try {
640
+ const blockConfig = await loadBlockConfig(resource.path);
641
+ if (blockConfig) {
642
+ // Validate schema and show errors
643
+ if (blockConfig.schema) {
644
+ const validation = await validateBlockSchema(blockConfig.schema, resource.path);
645
+ if (!validation.valid) {
646
+ console.log(chalk.red(`\n❌ Schema validation errors in ${resourceName}:`));
647
+ validation.errors.forEach((err) => {
648
+ console.log(chalk.red(` • ${err}`));
649
+ });
650
+ console.log(chalk.yellow(`\nFix the errors above in block.config.ts\n`));
651
+ }
652
+ }
653
+ resource.blockConfig = blockConfig;
654
+ resource.displayName = blockConfig.name || resource.name;
655
+ resource.description = blockConfig.description;
656
+ resource.category = blockConfig.category;
657
+ if (blockConfig.schema) {
658
+ await generateTypes({
659
+ blockPath: resource.path,
660
+ schema: blockConfig.schema,
661
+ fieldTypes,
662
+ });
663
+ }
664
+ // Update metadata cache
665
+ updateBlockInCache(resourceName, resource.type, blockConfig, resource.packageJson?.version);
666
+ console.log(chalk.green(`✓ Types regenerated for ${resourceName}\n`));
667
+ }
668
+ }
669
+ catch (error) {
670
+ console.log(chalk.red(`\n❌ Failed to load config for ${resourceName}:`));
671
+ console.log(chalk.red(` ${error.message}\n`));
672
+ // Show hint for common errors
673
+ if (error.message.includes('SyntaxError') || error.message.includes('Unexpected')) {
674
+ console.log(chalk.yellow(` Hint: Check for syntax errors in block.config.ts\n`));
675
+ }
676
+ }
677
+ }
678
+ }
679
+ // Watch for new package.json (new block detection)
680
+ if (filePath.endsWith("package.json") && !filePath.includes("node_modules")) {
681
+ const relativePath = path.relative(projectRoot, filePath);
682
+ const parts = relativePath.split(path.sep);
683
+ if ((parts[0] === "blocks" || parts[0] === "templates") && parts.length === 3) {
684
+ const resourceName = parts[1];
685
+ if (!resources.find((r) => r.name === resourceName)) {
686
+ console.log(chalk.green(`\n✨ New block detected: ${resourceName}`));
687
+ // Re-scan resources
688
+ try {
689
+ const newResources = await scanResources({
690
+ strict: false,
691
+ loadConfig: true,
692
+ validateSchema: true,
693
+ loadPreview: true,
694
+ requirePackageJson: true,
695
+ });
696
+ const newResource = newResources.find((r) => r.name === resourceName);
697
+ if (newResource) {
698
+ resources.push(newResource);
699
+ console.log(chalk.green(`✓ ${resourceName} added\n`));
700
+ }
701
+ }
702
+ catch (error) {
703
+ console.error(chalk.red(`Failed to scan new block ${resourceName}:`), error);
704
+ }
705
+ }
706
+ }
707
+ }
708
+ });
709
+ }
710
+ function generatePreviewHTML(resource, config, port) {
711
+ const blockPath = `/${resource.type}s/${resource.name}/src/index.tsx`;
712
+ const cssPath = `/${resource.type}s/${resource.name}/src/index.css`;
713
+ return `
714
+ <!DOCTYPE html>
715
+ <html lang="en">
716
+ <head>
717
+ <meta charset="UTF-8">
718
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
719
+ <title>${resource.displayName} - Preview</title>
720
+ <link rel="icon" type="image/svg+xml" href="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">
721
+ <script type="module" src="/@vite/client"></script>
722
+ <link rel="stylesheet" href="${cssPath}">
723
+ <style>
724
+ body { margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; }
725
+ .preview-header {
726
+ position: fixed; top: 0; left: 0; right: 0;
727
+ background: white; border-bottom: 1px solid #e0e0e0;
728
+ padding: 1rem 2rem; z-index: 1000;
729
+ display: flex; justify-content: space-between; align-items: center;
730
+ }
731
+ .preview-title { font-size: 1.25rem; font-weight: 600; margin: 0; }
732
+ .preview-back { color: #667eea; text-decoration: none; font-weight: 500; }
733
+ .preview-container { margin-top: 60px; min-height: calc(100vh - 60px); }
734
+ </style>
735
+ </head>
736
+ <body>
737
+ <div class="preview-header">
738
+ <div class="preview-title">${resource.displayName}</div>
739
+ <a href="/" class="preview-back" target="_parent">← Back to Home</a>
740
+ </div>
741
+ <div class="preview-container">
742
+ <div id="preview-root"></div>
743
+ </div>
744
+
745
+ <script type="module">
746
+ import module from '${blockPath}';
747
+ const element = document.getElementById('preview-root');
748
+ let props = ${JSON.stringify(resource.previewData || {})};
749
+ let context = module.mount(element, props);
750
+
751
+ // Listen for prop updates from parent
752
+ window.addEventListener('message', (event) => {
753
+ if (event.data.type === 'UPDATE_PROPS') {
754
+ props = event.data.props;
755
+ if (module.update && context) {
756
+ module.update(element, props, context);
757
+ } else {
758
+ if (context && module.unmount) module.unmount(element, context);
759
+ context = module.mount(element, props);
760
+ }
761
+ }
762
+ });
763
+
764
+ // Vite HMR
765
+ if (import.meta.hot) {
766
+ import.meta.hot.accept('${blockPath}', (newModule) => {
767
+ if (newModule) {
768
+ console.log('🔄 HMR update');
769
+ if (context && module.unmount) module.unmount(element, context);
770
+ context = newModule.default.mount(element, props);
771
+ }
772
+ });
773
+ }
774
+ </script>
775
+ </body>
776
+ </html>
777
+ `;
778
+ }
779
+ function generateTemplatePreviewHTML(resource, templateConfig, page, allResources, port) {
780
+ // Find all blocks used in this page
781
+ const blockImports = [];
782
+ const blockMounts = [];
783
+ // Generate imports and mounts for each block in the page
784
+ page.blocks.forEach((blockInstance, index) => {
785
+ // Block type can be "hero" or "@vendor/blocks.hero" - extract the block name
786
+ const blockName = blockInstance.type.includes('.')
787
+ ? blockInstance.type.split('.').pop()
788
+ : blockInstance.type;
789
+ // Find the block resource
790
+ const blockResource = allResources.find((r) => r.type === "block" && r.name === blockName);
791
+ if (blockResource) {
792
+ const blockPath = `/blocks/${blockName}/src/index.tsx`;
793
+ const cssPath = `/blocks/${blockName}/src/index.css`;
794
+ const varName = `block_${index}`;
795
+ const containerId = `block-${index}`;
796
+ blockImports.push(`import ${varName} from '${blockPath}';`);
797
+ blockImports.push(`import '${cssPath}';`);
798
+ const props = JSON.stringify(blockInstance.content || {});
799
+ blockMounts.push(`
800
+ {
801
+ const el = document.getElementById('${containerId}');
802
+ if (el && ${varName}.mount) {
803
+ ${varName}.mount(el, ${props});
804
+ }
805
+ }
806
+ `);
807
+ }
808
+ });
809
+ // Generate layout slot imports/mounts
810
+ const layoutSlots = templateConfig.layoutSlots || [];
811
+ const headerSlot = layoutSlots.find((s) => s.slot === "header");
812
+ const footerSlot = layoutSlots.find((s) => s.slot === "footer");
813
+ if (headerSlot) {
814
+ const blockName = headerSlot.type.includes('.')
815
+ ? headerSlot.type.split('.').pop()
816
+ : headerSlot.type;
817
+ const blockResource = allResources.find((r) => r.type === "block" && r.name === blockName);
818
+ if (blockResource) {
819
+ blockImports.push(`import headerBlock from '/blocks/${blockName}/src/index.tsx';`);
820
+ blockImports.push(`import '/blocks/${blockName}/src/index.css';`);
821
+ blockMounts.push(`
822
+ {
823
+ const el = document.getElementById('layout-header');
824
+ if (el && headerBlock.mount) {
825
+ headerBlock.mount(el, ${JSON.stringify(headerSlot.content || {})});
826
+ }
827
+ }
828
+ `);
829
+ }
830
+ }
831
+ if (footerSlot) {
832
+ const blockName = footerSlot.type.includes('.')
833
+ ? footerSlot.type.split('.').pop()
834
+ : footerSlot.type;
835
+ const blockResource = allResources.find((r) => r.type === "block" && r.name === blockName);
836
+ if (blockResource) {
837
+ blockImports.push(`import footerBlock from '/blocks/${blockName}/src/index.tsx';`);
838
+ blockImports.push(`import '/blocks/${blockName}/src/index.css';`);
839
+ blockMounts.push(`
840
+ {
841
+ const el = document.getElementById('layout-footer');
842
+ if (el && footerBlock.mount) {
843
+ footerBlock.mount(el, ${JSON.stringify(footerSlot.content || {})});
844
+ }
845
+ }
846
+ `);
847
+ }
848
+ }
849
+ // Generate page navigation tabs
850
+ const pageTabs = templateConfig.pages.map((p) => {
851
+ const isActive = p.slug === page.slug;
852
+ return `<a href="/preview/template/${resource.name}/${p.slug}" class="page-tab ${isActive ? 'active' : ''}">${p.name}</a>`;
853
+ }).join('');
854
+ // Generate block containers HTML
855
+ const blockContainers = page.blocks.map((_, index) => {
856
+ return `<div id="block-${index}" class="template-block"></div>`;
857
+ }).join('\n ');
858
+ return `
859
+ <!DOCTYPE html>
860
+ <html lang="en">
861
+ <head>
862
+ <meta charset="UTF-8">
863
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
864
+ <title>${templateConfig.name} - ${page.name}</title>
865
+ <link rel="icon" type="image/svg+xml" href="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">
866
+ <script type="module" src="/@vite/client"></script>
867
+ <style>
868
+ * { box-sizing: border-box; }
869
+ body { margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; }
870
+
871
+ .template-header {
872
+ position: fixed; top: 0; left: 0; right: 0;
873
+ background: #1a1a2e; color: white;
874
+ padding: 0.75rem 1.5rem; z-index: 1000;
875
+ display: flex; justify-content: space-between; align-items: center;
876
+ box-shadow: 0 2px 8px rgba(0,0,0,0.15);
877
+ }
878
+ .template-header-left {
879
+ display: flex; align-items: center; gap: 1.5rem;
880
+ }
881
+ .template-title {
882
+ font-size: 1rem; font-weight: 600; margin: 0;
883
+ display: flex; align-items: center; gap: 0.5rem;
884
+ }
885
+ .template-badge {
886
+ background: #667eea; color: white;
887
+ padding: 0.15rem 0.5rem; border-radius: 4px;
888
+ font-size: 0.7rem; font-weight: 500;
889
+ }
890
+ .page-tabs {
891
+ display: flex; gap: 0.25rem;
892
+ }
893
+ .page-tab {
894
+ color: rgba(255,255,255,0.7); text-decoration: none;
895
+ padding: 0.4rem 0.75rem; border-radius: 6px;
896
+ font-size: 0.85rem; font-weight: 500;
897
+ transition: all 0.2s;
898
+ }
899
+ .page-tab:hover { color: white; background: rgba(255,255,255,0.1); }
900
+ .page-tab.active { color: white; background: #667eea; }
901
+
902
+ .template-back {
903
+ color: rgba(255,255,255,0.8); text-decoration: none;
904
+ font-size: 0.85rem; font-weight: 500;
905
+ padding: 0.4rem 0.75rem; border-radius: 6px;
906
+ transition: all 0.2s;
907
+ }
908
+ .template-back:hover { color: white; background: rgba(255,255,255,0.1); }
909
+
910
+ .template-content {
911
+ margin-top: 52px;
912
+ min-height: calc(100vh - 52px);
913
+ }
914
+ .template-block {
915
+ /* Blocks render their own styles */
916
+ }
917
+ #layout-header, #layout-footer {
918
+ /* Layout slots */
919
+ }
920
+
921
+ .block-error {
922
+ padding: 2rem;
923
+ background: #fff3cd;
924
+ border: 1px solid #ffc107;
925
+ color: #856404;
926
+ text-align: center;
927
+ }
928
+ </style>
929
+ </head>
930
+ <body>
931
+ <div class="template-header">
932
+ <div class="template-header-left">
933
+ <h1 class="template-title">
934
+ <span class="template-badge">Template</span>
935
+ ${templateConfig.name}
936
+ </h1>
937
+ <div class="page-tabs">
938
+ ${pageTabs}
939
+ </div>
940
+ </div>
941
+ <a href="/" class="template-back" target="_parent">← Back to Dev</a>
942
+ </div>
943
+
944
+ <div class="template-content">
945
+ ${headerSlot ? '<div id="layout-header"></div>' : ''}
946
+ <main>
947
+ ${blockContainers || '<div class="block-error">No blocks defined for this page</div>'}
948
+ </main>
949
+ ${footerSlot ? '<div id="layout-footer"></div>' : ''}
950
+ </div>
951
+
952
+ <script type="module">
953
+ ${blockImports.join('\n ')}
954
+
955
+ // Mount all blocks
956
+ ${blockMounts.join('\n ')}
957
+ </script>
958
+ </body>
959
+ </html>
960
+ `;
961
+ }
962
+ //# sourceMappingURL=dev.js.map