desen-cli 1.0.0-draft.7 → 1.0.0-draft.8

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 (3) hide show
  1. package/dist/index.js +37 -90
  2. package/package.json +24 -21
  3. package/src/index.ts +36 -90
package/dist/index.js CHANGED
@@ -188,32 +188,43 @@ program
188
188
  if (!payloads || payloads.length === 0 || !payloads[0].path) {
189
189
  return res.status(400).json({ error: "Missing payloads or path in request body" });
190
190
  }
191
- // SECURITY BARRICADE: Validate strictly using @desen/core specs before disk IO
191
+ // ─────────────────────────────────────────────────────────
192
+ // VALIDATION (Warn-and-Proceed for Development)
193
+ // Daemon is a dev tool — strict fail-closed enforcement
194
+ // belongs in `desen validate` (CI/CD pipeline).
195
+ // Here we log warnings but NEVER block the sync.
196
+ // ─────────────────────────────────────────────────────────
197
+ let validationWarnings = 0;
192
198
  for (const item of payloads) {
193
199
  if (!item.path || !item.ast)
194
200
  continue;
195
201
  let result;
196
- if (item.path.startsWith('surface/')) {
202
+ if (item.path.includes('surfaces/')) {
197
203
  result = desen_core_1.surfaceSpec.safeParse(item.ast);
198
204
  }
199
- else if (item.path.startsWith('element/') || item.path.startsWith('composition/')) {
205
+ else if (item.path.includes('elements/') || item.path.includes('compositions/')) {
200
206
  result = desen_core_1.nodeSpec.safeParse(item.ast);
201
207
  }
202
- else if (item.path.startsWith('themes/')) {
203
- result = { success: true }; // Token bypass for now
208
+ else if (item.path.includes('themes/')) {
209
+ result = { success: true };
204
210
  }
205
211
  else {
206
212
  result = desen_core_1.nodeSpec.safeParse(item.ast);
207
213
  }
208
214
  if (!result.success) {
209
- console.error(chalk_1.default.red(`❌ [Fail-Closed] Payload validation failed for ${item.path}`));
210
- console.error(chalk_1.default.yellow(JSON.stringify(result.error.format(), null, 2)));
211
- return res.status(400).json({
212
- error: "Validation failed against desen-core schemas",
213
- path: item.path,
214
- issues: result.error.errors
215
- });
215
+ validationWarnings++;
216
+ console.warn(chalk_1.default.yellow(`⚠️ [Validation Warning] ${item.path}`));
217
+ const formatted = result.error.format();
218
+ // Log a concise summary instead of the full error tree
219
+ const issues = result.error.errors.map((e) => ` → [${e.path.join('.')}] ${e.message}`).join('\n');
220
+ console.warn(chalk_1.default.yellow(issues));
216
221
  }
222
+ else {
223
+ console.log(chalk_1.default.green(` ✓ ${item.path}`));
224
+ }
225
+ }
226
+ if (validationWarnings > 0) {
227
+ console.warn(chalk_1.default.yellow(`\n⚠️ ${validationWarnings} file(s) have validation warnings. Run 'desen validate' for strict checks.`));
217
228
  }
218
229
  const projectRoot = process.cwd();
219
230
  const workspaceDir = path_1.default.join(projectRoot, "desen-workspace");
@@ -288,7 +299,7 @@ program
288
299
  const appDir = path_1.default.join(workspaceDir, "src", "app");
289
300
  fs_1.default.mkdirSync(appDir, { recursive: true });
290
301
  // Find first surface directly
291
- const firstSurface = payloads.find((p) => p.path.startsWith('surface/'))?.path || 'surface/frame-1.desen.json';
302
+ const firstSurface = payloads.find((p) => p.path.includes('surfaces/'))?.path || 'desen/surfaces/frame-1.desen.json';
292
303
  // API Route to load local .desen.json dynamically
293
304
  const apiDir = path_1.default.join(appDir, "api", "surface", "route");
294
305
  fs_1.default.mkdirSync(apiDir, { recursive: true });
@@ -413,67 +424,6 @@ export default function Page() {
413
424
  fs_1.default.writeFileSync(path_1.default.join(libDir, "desenRegistry.tsx"), `
414
425
  import React from 'react';
415
426
 
416
- function resolveToken(tokenStr: string): string | undefined {
417
- if (!tokenStr) return undefined;
418
- const match = tokenStr.match(/t_([a-fA-F0-9]{3,8})/);
419
- return match ? \`#\${match[1]}\` : undefined;
420
- }
421
-
422
- function resolveColor(tokenStr?: string, hexStr?: string): string | undefined {
423
- if (tokenStr) return resolveToken(tokenStr);
424
- if (hexStr) return hexStr;
425
- return undefined;
426
- }
427
-
428
- function buildCssFromNode(layout: any, style: any, opts?: { isText?: boolean; isSurfaceRoot?: boolean }): React.CSSProperties {
429
- const css: any = { boxSizing: 'border-box' };
430
-
431
- if (layout) {
432
- css.display = 'flex';
433
- css.flexDirection = (layout.direction === 'row' || layout.direction === 'horizontal') ? 'row' : 'column';
434
- if (layout.gap !== undefined && layout.gap !== 0) css.gap = layout.gap;
435
-
436
- const am: Record<string, string> = { start:'flex-start', end:'flex-end', center:'center', 'space-between':'space-between', 'space-around':'space-around', 'space-evenly':'space-evenly' };
437
- css.alignItems = am[layout.align_items] || 'stretch';
438
- css.justifyContent = am[layout.align_content] || 'flex-start';
439
-
440
- if (layout.padding !== undefined) {
441
- css.padding = Array.isArray(layout.padding) ? layout.padding.map((p:number)=>p+'px').join(' ') : layout.padding !== 0 ? layout.padding+'px' : undefined;
442
- }
443
-
444
- if (opts?.isSurfaceRoot) {
445
- css.width = '100%'; css.minHeight = '100vh';
446
- } else {
447
- if (layout.sizing_h === 'FILL') css.width = '100%';
448
- else if (layout.sizing_h === 'FIXED' && style?.width !== undefined) css.width = style.width;
449
- if (layout.sizing_v === 'FILL') css.height = '100%';
450
- else if (layout.sizing_v === 'FIXED' && style?.height !== undefined) css.height = style.height;
451
- }
452
- }
453
-
454
- if (style) {
455
- const bgColor = resolveColor(style.bg_color_token, style.bg_color);
456
- if (bgColor) { if (opts?.isText) css.color = bgColor; else css.backgroundColor = bgColor; }
457
- const bc = resolveColor(style.border_color_token, style.border_color);
458
- if (bc) css.borderColor = bc;
459
- if (style.border_width !== undefined) { css.borderWidth = style.border_width; css.borderStyle = 'solid'; }
460
- if (style.border_radius !== undefined) {
461
- css.borderRadius = Array.isArray(style.border_radius) ? style.border_radius.map((r:number)=>r+'px').join(' ') : style.border_radius;
462
- }
463
- if (!layout && !opts?.isText) { if (style.width !== undefined) css.width = style.width; if (style.height !== undefined) css.height = style.height; }
464
- if (style.opacity !== undefined) css.opacity = style.opacity;
465
- if (style.font_size) css.fontSize = style.font_size;
466
- if (style.font_weight) css.fontWeight = style.font_weight;
467
- if (style.font_family) css.fontFamily = style.font_family;
468
- if (style.letter_spacing) css.letterSpacing = style.letter_spacing;
469
- if (style.line_height) css.lineHeight = style.line_height + 'px';
470
- if (style.text_align) css.textAlign = style.text_align;
471
- if (style.text_decoration) css.textDecoration = style.text_decoration;
472
- if (style.text_transform) css.textTransform = style.text_transform;
473
- }
474
- return css;
475
- }
476
-
477
427
  const DynamicFontLoader = ({ fonts }: { fonts?: string[] }) => {
478
428
  if (!fonts || fonts.length === 0) return null;
479
429
 
@@ -497,31 +447,28 @@ const DynamicFontLoader = ({ fonts }: { fonts?: string[] }) => {
497
447
  };
498
448
 
499
449
  const SurfaceComponent = (props: any) => (
500
- <div style={{ width: '100%', minHeight: '100vh', boxSizing: 'border-box' }}>
450
+ <div style={props.parsedStyle}>
501
451
  <DynamicFontLoader fonts={props.metadata?.fonts} />
502
452
  {props.children}
503
453
  </div>
504
454
  );
505
455
 
506
456
  const GenericContainer = (props: any) => (
507
- <div style={buildCssFromNode(props.layout, props.style, { isSurfaceRoot: props._isSurfaceRoot })}>{props.children}</div>
457
+ <div style={props.parsedStyle}>{props.children}</div>
508
458
  );
509
459
 
510
460
  const TextComponent = (props: any) => (
511
- <span style={buildCssFromNode(props.layout, props.style, { isText: true })}>{props.content || props.text || props.name}</span>
461
+ <span style={props.parsedStyle}>{props.content || props.text || props.name}</span>
512
462
  );
513
463
 
514
464
  const ButtonComponent = (props: any) => {
515
- const adjLayout = { ...props.layout };
516
- if (adjLayout && !props.children && adjLayout.align_content === 'space-between') adjLayout.align_content = 'center';
517
- const css: any = { ...buildCssFromNode(adjLayout, props.style), cursor: 'pointer', border: 'none' };
518
- if (props.text_style) {
519
- const tc = resolveColor(props.text_style.bg_color_token, props.text_style.bg_color);
520
- if (tc) css.color = tc;
521
- if (props.text_style.font_size) css.fontSize = props.text_style.font_size;
522
- if (props.text_style.font_weight) css.fontWeight = props.text_style.font_weight;
523
- if (props.text_style.font_family) css.fontFamily = props.text_style.font_family;
524
- if (props.text_style.text_align) css.textAlign = props.text_style.text_align;
465
+ const css: any = { ...props.parsedStyle, cursor: 'pointer', border: 'none' };
466
+
467
+ // If no layout, center text by default (most buttons center their text)
468
+ if (!props.layout) {
469
+ css.display = 'flex';
470
+ css.alignItems = 'center';
471
+ css.justifyContent = 'center';
525
472
  }
526
473
  return <button style={css}>{props.children || props.content || props.text || props.name}</button>;
527
474
  };
@@ -542,9 +489,9 @@ export const registry: Record<string, React.FC<any>> = {
542
489
  Text: TextComponent,
543
490
  button: ButtonComponent,
544
491
  Button: ButtonComponent,
545
- input: (props: any) => <input style={buildCssFromNode(props.layout, props.style)} placeholder={props.content || props.name} />,
546
- icon: (props: any) => <div style={{ ...buildCssFromNode(props.layout, props.style), display:'inline-flex', alignItems:'center', justifyContent:'center' }}>⚙️</div>,
547
- Icon: (props: any) => <div style={{ ...buildCssFromNode(props.layout, props.style), display:'inline-flex', alignItems:'center', justifyContent:'center' }}>⚙️</div>,
492
+ input: (props: any) => <input style={props.parsedStyle} placeholder={props.content || props.name} />,
493
+ icon: (props: any) => <div style={{ ...props.parsedStyle, display:'inline-flex', alignItems:'center', justifyContent:'center' }}>⚙️</div>,
494
+ Icon: (props: any) => <div style={{ ...props.parsedStyle, display:'inline-flex', alignItems:'center', justifyContent:'center' }}>⚙️</div>,
548
495
  };
549
496
  `.trim());
550
497
  console.log(chalk_1.default.green(`✅ Done! Standalone App Scaffolded.`));
package/package.json CHANGED
@@ -1,23 +1,26 @@
1
1
  {
2
- "name": "desen-cli",
3
- "version": "1.0.0-draft.7",
4
- "main": "dist/index.js",
5
- "dependencies": {
6
- "chalk": "^5.6.2",
7
- "commander": "^14.0.3",
8
- "express": "^4.19.2",
9
- "cors": "^2.8.5",
10
- "desen-core": "1.0.0-draft.7"
11
- },
12
- "devDependencies": {
13
- "@types/cors": "^2.8.19",
14
- "@types/express": "^5.0.6",
15
- "@types/node": "^20.19.33",
16
- "typescript": "^5.0.0"
17
- },
18
- "scripts": {
19
- "build": "tsc",
20
- "dev": "tsc -w",
21
- "clean": "rm -rf dist"
22
- }
2
+ "name": "desen-cli",
3
+ "version": "1.0.0-draft.8",
4
+ "main": "dist/index.js",
5
+ "bin": {
6
+ "desen": "dist/index.js"
7
+ },
8
+ "scripts": {
9
+ "build": "tsc",
10
+ "dev": "tsc -w",
11
+ "clean": "rm -rf dist"
12
+ },
13
+ "dependencies": {
14
+ "desen-core": "1.0.0-draft.8",
15
+ "chalk": "^5.6.2",
16
+ "commander": "^14.0.3",
17
+ "express": "^4.19.2",
18
+ "cors": "^2.8.5"
19
+ },
20
+ "devDependencies": {
21
+ "@types/cors": "^2.8.19",
22
+ "@types/express": "^5.0.6",
23
+ "@types/node": "^20.19.33",
24
+ "typescript": "^5.0.0"
25
+ }
23
26
  }
package/src/index.ts CHANGED
@@ -209,31 +209,41 @@ program
209
209
  return res.status(400).json({ error: "Missing payloads or path in request body" });
210
210
  }
211
211
 
212
- // SECURITY BARRICADE: Validate strictly using @desen/core specs before disk IO
212
+ // ─────────────────────────────────────────────────────────
213
+ // VALIDATION (Warn-and-Proceed for Development)
214
+ // Daemon is a dev tool — strict fail-closed enforcement
215
+ // belongs in `desen validate` (CI/CD pipeline).
216
+ // Here we log warnings but NEVER block the sync.
217
+ // ─────────────────────────────────────────────────────────
218
+ let validationWarnings = 0;
213
219
  for (const item of payloads) {
214
220
  if (!item.path || !item.ast) continue;
215
221
 
216
222
  let result: any;
217
- if (item.path.startsWith('surface/')) {
223
+ if (item.path.includes('surfaces/')) {
218
224
  result = surfaceSpec.safeParse(item.ast);
219
- } else if (item.path.startsWith('element/') || item.path.startsWith('composition/')) {
225
+ } else if (item.path.includes('elements/') || item.path.includes('compositions/')) {
220
226
  result = nodeSpec.safeParse(item.ast);
221
- } else if (item.path.startsWith('themes/')) {
222
- result = { success: true }; // Token bypass for now
227
+ } else if (item.path.includes('themes/')) {
228
+ result = { success: true };
223
229
  } else {
224
230
  result = nodeSpec.safeParse(item.ast);
225
231
  }
226
232
 
227
233
  if (!result.success) {
228
- console.error(chalk.red(`❌ [Fail-Closed] Payload validation failed for ${item.path}`));
229
- console.error(chalk.yellow(JSON.stringify(result.error.format(), null, 2)));
230
- return res.status(400).json({
231
- error: "Validation failed against desen-core schemas",
232
- path: item.path,
233
- issues: result.error.errors
234
- });
234
+ validationWarnings++;
235
+ console.warn(chalk.yellow(`⚠️ [Validation Warning] ${item.path}`));
236
+ const formatted = result.error.format();
237
+ // Log a concise summary instead of the full error tree
238
+ const issues = result.error.errors.map((e: any) => ` → [${e.path.join('.')}] ${e.message}`).join('\n');
239
+ console.warn(chalk.yellow(issues));
240
+ } else {
241
+ console.log(chalk.green(` ✓ ${item.path}`));
235
242
  }
236
243
  }
244
+ if (validationWarnings > 0) {
245
+ console.warn(chalk.yellow(`\n⚠️ ${validationWarnings} file(s) have validation warnings. Run 'desen validate' for strict checks.`));
246
+ }
237
247
 
238
248
  const projectRoot = process.cwd();
239
249
  const workspaceDir = path.join(projectRoot, "desen-workspace");
@@ -317,7 +327,7 @@ program
317
327
  fs.mkdirSync(appDir, { recursive: true });
318
328
 
319
329
  // Find first surface directly
320
- const firstSurface = payloads.find((p: any) => p.path.startsWith('surface/'))?.path || 'surface/frame-1.desen.json';
330
+ const firstSurface = payloads.find((p: any) => p.path.includes('surfaces/'))?.path || 'desen/surfaces/frame-1.desen.json';
321
331
 
322
332
  // API Route to load local .desen.json dynamically
323
333
  const apiDir = path.join(appDir, "api", "surface", "route");
@@ -448,67 +458,6 @@ export default function Page() {
448
458
  fs.writeFileSync(path.join(libDir, "desenRegistry.tsx"), `
449
459
  import React from 'react';
450
460
 
451
- function resolveToken(tokenStr: string): string | undefined {
452
- if (!tokenStr) return undefined;
453
- const match = tokenStr.match(/t_([a-fA-F0-9]{3,8})/);
454
- return match ? \`#\${match[1]}\` : undefined;
455
- }
456
-
457
- function resolveColor(tokenStr?: string, hexStr?: string): string | undefined {
458
- if (tokenStr) return resolveToken(tokenStr);
459
- if (hexStr) return hexStr;
460
- return undefined;
461
- }
462
-
463
- function buildCssFromNode(layout: any, style: any, opts?: { isText?: boolean; isSurfaceRoot?: boolean }): React.CSSProperties {
464
- const css: any = { boxSizing: 'border-box' };
465
-
466
- if (layout) {
467
- css.display = 'flex';
468
- css.flexDirection = (layout.direction === 'row' || layout.direction === 'horizontal') ? 'row' : 'column';
469
- if (layout.gap !== undefined && layout.gap !== 0) css.gap = layout.gap;
470
-
471
- const am: Record<string, string> = { start:'flex-start', end:'flex-end', center:'center', 'space-between':'space-between', 'space-around':'space-around', 'space-evenly':'space-evenly' };
472
- css.alignItems = am[layout.align_items] || 'stretch';
473
- css.justifyContent = am[layout.align_content] || 'flex-start';
474
-
475
- if (layout.padding !== undefined) {
476
- css.padding = Array.isArray(layout.padding) ? layout.padding.map((p:number)=>p+'px').join(' ') : layout.padding !== 0 ? layout.padding+'px' : undefined;
477
- }
478
-
479
- if (opts?.isSurfaceRoot) {
480
- css.width = '100%'; css.minHeight = '100vh';
481
- } else {
482
- if (layout.sizing_h === 'FILL') css.width = '100%';
483
- else if (layout.sizing_h === 'FIXED' && style?.width !== undefined) css.width = style.width;
484
- if (layout.sizing_v === 'FILL') css.height = '100%';
485
- else if (layout.sizing_v === 'FIXED' && style?.height !== undefined) css.height = style.height;
486
- }
487
- }
488
-
489
- if (style) {
490
- const bgColor = resolveColor(style.bg_color_token, style.bg_color);
491
- if (bgColor) { if (opts?.isText) css.color = bgColor; else css.backgroundColor = bgColor; }
492
- const bc = resolveColor(style.border_color_token, style.border_color);
493
- if (bc) css.borderColor = bc;
494
- if (style.border_width !== undefined) { css.borderWidth = style.border_width; css.borderStyle = 'solid'; }
495
- if (style.border_radius !== undefined) {
496
- css.borderRadius = Array.isArray(style.border_radius) ? style.border_radius.map((r:number)=>r+'px').join(' ') : style.border_radius;
497
- }
498
- if (!layout && !opts?.isText) { if (style.width !== undefined) css.width = style.width; if (style.height !== undefined) css.height = style.height; }
499
- if (style.opacity !== undefined) css.opacity = style.opacity;
500
- if (style.font_size) css.fontSize = style.font_size;
501
- if (style.font_weight) css.fontWeight = style.font_weight;
502
- if (style.font_family) css.fontFamily = style.font_family;
503
- if (style.letter_spacing) css.letterSpacing = style.letter_spacing;
504
- if (style.line_height) css.lineHeight = style.line_height + 'px';
505
- if (style.text_align) css.textAlign = style.text_align;
506
- if (style.text_decoration) css.textDecoration = style.text_decoration;
507
- if (style.text_transform) css.textTransform = style.text_transform;
508
- }
509
- return css;
510
- }
511
-
512
461
  const DynamicFontLoader = ({ fonts }: { fonts?: string[] }) => {
513
462
  if (!fonts || fonts.length === 0) return null;
514
463
 
@@ -532,31 +481,28 @@ const DynamicFontLoader = ({ fonts }: { fonts?: string[] }) => {
532
481
  };
533
482
 
534
483
  const SurfaceComponent = (props: any) => (
535
- <div style={{ width: '100%', minHeight: '100vh', boxSizing: 'border-box' }}>
484
+ <div style={props.parsedStyle}>
536
485
  <DynamicFontLoader fonts={props.metadata?.fonts} />
537
486
  {props.children}
538
487
  </div>
539
488
  );
540
489
 
541
490
  const GenericContainer = (props: any) => (
542
- <div style={buildCssFromNode(props.layout, props.style, { isSurfaceRoot: props._isSurfaceRoot })}>{props.children}</div>
491
+ <div style={props.parsedStyle}>{props.children}</div>
543
492
  );
544
493
 
545
494
  const TextComponent = (props: any) => (
546
- <span style={buildCssFromNode(props.layout, props.style, { isText: true })}>{props.content || props.text || props.name}</span>
495
+ <span style={props.parsedStyle}>{props.content || props.text || props.name}</span>
547
496
  );
548
497
 
549
498
  const ButtonComponent = (props: any) => {
550
- const adjLayout = { ...props.layout };
551
- if (adjLayout && !props.children && adjLayout.align_content === 'space-between') adjLayout.align_content = 'center';
552
- const css: any = { ...buildCssFromNode(adjLayout, props.style), cursor: 'pointer', border: 'none' };
553
- if (props.text_style) {
554
- const tc = resolveColor(props.text_style.bg_color_token, props.text_style.bg_color);
555
- if (tc) css.color = tc;
556
- if (props.text_style.font_size) css.fontSize = props.text_style.font_size;
557
- if (props.text_style.font_weight) css.fontWeight = props.text_style.font_weight;
558
- if (props.text_style.font_family) css.fontFamily = props.text_style.font_family;
559
- if (props.text_style.text_align) css.textAlign = props.text_style.text_align;
499
+ const css: any = { ...props.parsedStyle, cursor: 'pointer', border: 'none' };
500
+
501
+ // If no layout, center text by default (most buttons center their text)
502
+ if (!props.layout) {
503
+ css.display = 'flex';
504
+ css.alignItems = 'center';
505
+ css.justifyContent = 'center';
560
506
  }
561
507
  return <button style={css}>{props.children || props.content || props.text || props.name}</button>;
562
508
  };
@@ -577,9 +523,9 @@ export const registry: Record<string, React.FC<any>> = {
577
523
  Text: TextComponent,
578
524
  button: ButtonComponent,
579
525
  Button: ButtonComponent,
580
- input: (props: any) => <input style={buildCssFromNode(props.layout, props.style)} placeholder={props.content || props.name} />,
581
- icon: (props: any) => <div style={{ ...buildCssFromNode(props.layout, props.style), display:'inline-flex', alignItems:'center', justifyContent:'center' }}>⚙️</div>,
582
- Icon: (props: any) => <div style={{ ...buildCssFromNode(props.layout, props.style), display:'inline-flex', alignItems:'center', justifyContent:'center' }}>⚙️</div>,
526
+ input: (props: any) => <input style={props.parsedStyle} placeholder={props.content || props.name} />,
527
+ icon: (props: any) => <div style={{ ...props.parsedStyle, display:'inline-flex', alignItems:'center', justifyContent:'center' }}>⚙️</div>,
528
+ Icon: (props: any) => <div style={{ ...props.parsedStyle, display:'inline-flex', alignItems:'center', justifyContent:'center' }}>⚙️</div>,
583
529
  };
584
530
  `.trim());
585
531