desen-cli 1.0.0-draft → 1.0.0-draft.11

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.
@@ -1,5 +1,5 @@
1
1
 
2
2
  
3
- > desen-cli@1.0.0-draft build /Users/selmanay/Desktop/desen/packages/cli
3
+ > desen-cli@1.0.0-draft.10 build /Users/selmanay/Desktop/desen/packages/cli
4
4
  > tsc
5
5
 
package/dist/index.js CHANGED
@@ -172,8 +172,9 @@ program
172
172
  <div class="card">
173
173
  <h1>šŸš€ DESEN Daemon Active</h1>
174
174
  <p>The Synchronization (Sync) server is waiting for incoming <strong>UI AST</strong> data from Figma. It can safely continue running in the background.</p>
175
- <p>To preview your design live as a standalone application using the <strong>desen</strong> render engine, open a new terminal in your project directory and run the following commands:</p>
176
- <div class="code">cd desen-workspace<br/>npm install<br/>npm run dev</div>
175
+ <p>When you press <strong>Sync to Localhost</strong> in Figma, the daemon will <strong>automatically</strong>:</p>
176
+ <div class="code">1. Validate &amp; save JSON files<br/>2. Install dependencies (if needed)<br/>3. Start dev server on port 3001<br/>4. Open your browser ✨</div>
177
+ <p style="margin-top:16px;color:#888;">Zero-touch. Just press Sync and watch the magic.</p>
177
178
  </div>
178
179
  </body>
179
180
  </html>
@@ -187,19 +188,43 @@ program
187
188
  if (!payloads || payloads.length === 0 || !payloads[0].path) {
188
189
  return res.status(400).json({ error: "Missing payloads or path in request body" });
189
190
  }
190
- // 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;
191
198
  for (const item of payloads) {
192
199
  if (!item.path || !item.ast)
193
200
  continue;
194
- const result = desen_core_1.surfaceSpec.safeParse(item.ast);
201
+ let result;
202
+ if (item.path.includes('surfaces/')) {
203
+ result = desen_core_1.surfaceSpec.safeParse(item.ast);
204
+ }
205
+ else if (item.path.includes('elements/') || item.path.includes('compositions/')) {
206
+ result = desen_core_1.nodeSpec.safeParse(item.ast);
207
+ }
208
+ else if (item.path.includes('themes/')) {
209
+ result = { success: true };
210
+ }
211
+ else {
212
+ result = desen_core_1.nodeSpec.safeParse(item.ast);
213
+ }
195
214
  if (!result.success) {
196
- console.error(chalk_1.default.red(`āŒ [Fail-Closed] Payload validation failed for ${item.path}`));
197
- return res.status(400).json({
198
- error: "Validation failed against desen-core schemas",
199
- path: item.path,
200
- issues: result.error.errors
201
- });
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));
202
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.`));
203
228
  }
204
229
  const projectRoot = process.cwd();
205
230
  const workspaceDir = path_1.default.join(projectRoot, "desen-workspace");
@@ -235,8 +260,8 @@ program
235
260
  "start": "next start"
236
261
  },
237
262
  dependencies: {
238
- "desen-core": "1.0.0-draft",
239
- "desen": "1.0.0-draft",
263
+ "desen-core": "1.0.0-draft.8",
264
+ "desen": "1.0.0-draft.8",
240
265
  "next": "14.2.3",
241
266
  "react": "18.3.1",
242
267
  "react-dom": "18.3.1"
@@ -274,7 +299,7 @@ program
274
299
  const appDir = path_1.default.join(workspaceDir, "src", "app");
275
300
  fs_1.default.mkdirSync(appDir, { recursive: true });
276
301
  // Find first surface directly
277
- 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';
278
303
  // API Route to load local .desen.json dynamically
279
304
  const apiDir = path_1.default.join(appDir, "api", "surface", "route");
280
305
  fs_1.default.mkdirSync(apiDir, { recursive: true });
@@ -312,13 +337,24 @@ async function resolveRefs(obj: any, baseDir: string): Promise<any> {
312
337
  export async function GET() {
313
338
  try {
314
339
  const baseDir = process.cwd();
340
+
341
+ // Load surface
315
342
  const filePath = path.join(baseDir, '${firstSurface}');
316
343
  const content = await fs.readFile(filePath, 'utf-8');
317
- let parsed = JSON.parse(content);
344
+ let parsedSurface = JSON.parse(content);
345
+ parsedSurface = await resolveRefs(parsedSurface, baseDir);
318
346
 
319
- parsed = await resolveRefs(parsed, baseDir);
347
+ // Load tokens gracefully
348
+ let parsedTokens = {};
349
+ try {
350
+ const tokensPath = path.join(baseDir, 'desen/themes/tokens.desen.json');
351
+ const tokensContent = await fs.readFile(tokensPath, 'utf-8');
352
+ parsedTokens = await resolveRefs(JSON.parse(tokensContent), baseDir);
353
+ } catch (e) {
354
+ console.log("No custom tokens found, using defaults.");
355
+ }
320
356
 
321
- return NextResponse.json(parsed);
357
+ return NextResponse.json({ surface: parsedSurface, tokens: parsedTokens });
322
358
  } catch (e) {
323
359
  return NextResponse.json({ error: 'System is waiting for your sync!' }, { status: 404 });
324
360
  }
@@ -363,13 +399,15 @@ import { DesenProvider, DesenRenderer } from "desen";
363
399
  import { registry } from "../lib/desenRegistry";
364
400
 
365
401
  export default function Page() {
366
- const [data, setData] = useState<any>(null);
402
+ const [uiData, setUiData] = useState<any>(null);
367
403
 
368
404
  useEffect(() => {
369
405
  const fetchUI = () => {
370
406
  fetch("/api/surface/route")
371
407
  .then(res => res.json())
372
- .then(json => { if(!json.error) setData(json) })
408
+ .then(json => {
409
+ if(!json.error) setUiData(json);
410
+ })
373
411
  .catch(e => console.error(e));
374
412
  };
375
413
  fetchUI();
@@ -377,18 +415,19 @@ export default function Page() {
377
415
  return () => clearInterval(intv);
378
416
  }, []);
379
417
 
380
- if (!data) return <div style={{padding: 40, fontFamily: 'monospace'}}>Waiting for Figma Sync...</div>;
418
+ if (!uiData || !uiData.surface) return <div style={{padding: 40, fontFamily: 'monospace'}}>Waiting for Figma Sync...</div>;
381
419
 
382
420
  // SPEC.md: Surface = full page. Pass full surface object, not data.root.
383
421
  return (
384
422
  <DesenProvider
385
423
  registry={registry}
424
+ tokens={uiData.tokens?.tokens || uiData.tokens || {}}
386
425
  session_id="workspace_session"
387
426
  revision_id="rev_1"
388
427
  onTelemetry={e => console.log("[Telemetry]", e)}
389
428
  onAction={a => console.log("[Action]", a)}
390
429
  >
391
- <DesenRenderer node={data} />
430
+ <DesenRenderer node={uiData.surface} />
392
431
  </DesenProvider>
393
432
  );
394
433
  }
@@ -399,67 +438,6 @@ export default function Page() {
399
438
  fs_1.default.writeFileSync(path_1.default.join(libDir, "desenRegistry.tsx"), `
400
439
  import React from 'react';
401
440
 
402
- function resolveToken(tokenStr: string): string | undefined {
403
- if (!tokenStr) return undefined;
404
- const match = tokenStr.match(/t_([a-fA-F0-9]{3,8})/);
405
- return match ? \`#\${match[1]}\` : undefined;
406
- }
407
-
408
- function resolveColor(tokenStr?: string, hexStr?: string): string | undefined {
409
- if (tokenStr) return resolveToken(tokenStr);
410
- if (hexStr) return hexStr;
411
- return undefined;
412
- }
413
-
414
- function buildCssFromNode(layout: any, style: any, opts?: { isText?: boolean; isSurfaceRoot?: boolean }): React.CSSProperties {
415
- const css: any = { boxSizing: 'border-box' };
416
-
417
- if (layout) {
418
- css.display = 'flex';
419
- css.flexDirection = layout.direction === 'row' ? 'row' : 'column';
420
- if (layout.gap !== undefined && layout.gap !== 0) css.gap = layout.gap;
421
-
422
- 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' };
423
- css.alignItems = am[layout.align_items] || 'stretch';
424
- css.justifyContent = am[layout.align_content] || 'flex-start';
425
-
426
- if (layout.padding !== undefined) {
427
- css.padding = Array.isArray(layout.padding) ? layout.padding.map((p:number)=>p+'px').join(' ') : layout.padding !== 0 ? layout.padding+'px' : undefined;
428
- }
429
-
430
- if (opts?.isSurfaceRoot) {
431
- css.width = '100%'; css.minHeight = '100vh';
432
- } else {
433
- if (layout.sizing_h === 'FILL') css.width = '100%';
434
- else if (layout.sizing_h === 'FIXED' && style?.width !== undefined) css.width = style.width;
435
- if (layout.sizing_v === 'FILL') css.height = '100%';
436
- else if (layout.sizing_v === 'FIXED' && style?.height !== undefined) css.height = style.height;
437
- }
438
- }
439
-
440
- if (style) {
441
- const bgColor = resolveColor(style.bg_color_token, style.bg_color);
442
- if (bgColor) { if (opts?.isText) css.color = bgColor; else css.backgroundColor = bgColor; }
443
- const bc = resolveColor(style.border_color_token, style.border_color);
444
- if (bc) css.borderColor = bc;
445
- if (style.border_width !== undefined) { css.borderWidth = style.border_width; css.borderStyle = 'solid'; }
446
- if (style.border_radius !== undefined) {
447
- css.borderRadius = Array.isArray(style.border_radius) ? style.border_radius.map((r:number)=>r+'px').join(' ') : style.border_radius;
448
- }
449
- if (!layout && !opts?.isText) { if (style.width !== undefined) css.width = style.width; if (style.height !== undefined) css.height = style.height; }
450
- if (style.opacity !== undefined) css.opacity = style.opacity;
451
- if (style.font_size) css.fontSize = style.font_size;
452
- if (style.font_weight) css.fontWeight = style.font_weight;
453
- if (style.font_family) css.fontFamily = style.font_family;
454
- if (style.letter_spacing) css.letterSpacing = style.letter_spacing;
455
- if (style.line_height) css.lineHeight = style.line_height + 'px';
456
- if (style.text_align) css.textAlign = style.text_align;
457
- if (style.text_decoration) css.textDecoration = style.text_decoration;
458
- if (style.text_transform) css.textTransform = style.text_transform;
459
- }
460
- return css;
461
- }
462
-
463
441
  const DynamicFontLoader = ({ fonts }: { fonts?: string[] }) => {
464
442
  if (!fonts || fonts.length === 0) return null;
465
443
 
@@ -483,33 +461,44 @@ const DynamicFontLoader = ({ fonts }: { fonts?: string[] }) => {
483
461
  };
484
462
 
485
463
  const SurfaceComponent = (props: any) => (
486
- <div style={{ width: '100%', minHeight: '100vh', boxSizing: 'border-box' }}>
464
+ <div style={props.parsedStyle}>
487
465
  <DynamicFontLoader fonts={props.metadata?.fonts} />
488
466
  {props.children}
489
467
  </div>
490
468
  );
491
469
 
492
470
  const GenericContainer = (props: any) => (
493
- <div style={buildCssFromNode(props.layout, props.style, { isSurfaceRoot: props._isSurfaceRoot })}>{props.children}</div>
471
+ <div style={props.parsedStyle}>{props.children}</div>
494
472
  );
495
473
 
496
474
  const TextComponent = (props: any) => (
497
- <span style={buildCssFromNode(props.layout, props.style, { isText: true })}>{props.content || props.text || props.name}</span>
475
+ <span style={props.parsedStyle}>{props.content || props.text || props.name}</span>
498
476
  );
499
477
 
500
478
  const ButtonComponent = (props: any) => {
501
- const adjLayout = { ...props.layout };
502
- if (adjLayout && !props.children && adjLayout.align_content === 'space-between') adjLayout.align_content = 'center';
503
- const css: any = { ...buildCssFromNode(adjLayout, props.style), cursor: 'pointer', border: 'none' };
504
- if (props.text_style) {
505
- const tc = resolveColor(props.text_style.bg_color_token, props.text_style.bg_color);
506
- if (tc) css.color = tc;
507
- if (props.text_style.font_size) css.fontSize = props.text_style.font_size;
508
- if (props.text_style.font_weight) css.fontWeight = props.text_style.font_weight;
509
- if (props.text_style.font_family) css.fontFamily = props.text_style.font_family;
510
- if (props.text_style.text_align) css.textAlign = props.text_style.text_align;
479
+ const css: any = { ...props.parsedStyle, cursor: 'pointer', border: 'none' };
480
+
481
+ // If no layout, center text by default (most buttons center their text)
482
+ if (!props.layout) {
483
+ css.display = 'flex';
484
+ css.alignItems = 'center';
485
+ css.justifyContent = 'center';
511
486
  }
512
- return <button style={css}>{props.children || props.content || props.text || props.name}</button>;
487
+
488
+ const textStyle = props.style?.text_style || {};
489
+
490
+ return (
491
+ <button style={css}>
492
+ <span style={{
493
+ fontFamily: textStyle.font_family,
494
+ fontSize: textStyle.font_size,
495
+ fontWeight: textStyle.font_weight,
496
+ color: textStyle.text_color || textStyle.bg_color // Figma plugin often maps text fill to bg_color
497
+ }}>
498
+ {props.children || props.content || props.text || props.name}
499
+ </span>
500
+ </button>
501
+ );
513
502
  };
514
503
 
515
504
  export const registry: Record<string, React.FC<any>> = {
@@ -519,11 +508,18 @@ export const registry: Record<string, React.FC<any>> = {
519
508
  element: GenericContainer,
520
509
  group: GenericContainer,
521
510
  stack: GenericContainer,
511
+ Stack: GenericContainer,
512
+ Card: GenericContainer,
513
+ Grid: GenericContainer,
514
+ Header: GenericContainer,
515
+ Footer: GenericContainer,
522
516
  text: TextComponent,
523
517
  Text: TextComponent,
524
518
  button: ButtonComponent,
525
- input: (props: any) => <input style={buildCssFromNode(props.layout, props.style)} placeholder={props.content || props.name} />,
526
- icon: (props: any) => <div style={{ ...buildCssFromNode(props.layout, props.style), display:'inline-flex', alignItems:'center', justifyContent:'center' }}>āš™ļø</div>,
519
+ Button: ButtonComponent,
520
+ input: (props: any) => <input style={props.parsedStyle} placeholder={props.content || props.name} />,
521
+ icon: (props: any) => <div style={{ ...props.parsedStyle, display:'inline-flex', alignItems:'center', justifyContent:'center' }}>āš™ļø</div>,
522
+ Icon: (props: any) => <div style={{ ...props.parsedStyle, display:'inline-flex', alignItems:'center', justifyContent:'center' }}>āš™ļø</div>,
527
523
  };
528
524
  `.trim());
529
525
  console.log(chalk_1.default.green(`āœ… Done! Standalone App Scaffolded.`));
@@ -538,6 +534,74 @@ export const registry: Record<string, React.FC<any>> = {
538
534
  }
539
535
  }
540
536
  } // End if(!package.json)
537
+ // ─────────────────────────────────────────────────────────
538
+ // AUTO-PILOT: npm install + npm run dev + browser open
539
+ // Zero-touch DX — developer never leaves Figma
540
+ // ─────────────────────────────────────────────────────────
541
+ const nodeModulesPath = path_1.default.join(workspaceDir, "node_modules");
542
+ const devServerPort = 3001;
543
+ const isPortInUse = (port) => {
544
+ return new Promise((resolve) => {
545
+ const net = require("net");
546
+ const tester = net.createServer()
547
+ .once("error", (err) => {
548
+ if (err.code === "EADDRINUSE")
549
+ resolve(true);
550
+ else
551
+ resolve(false);
552
+ })
553
+ .once("listening", () => {
554
+ tester.close(() => resolve(false));
555
+ })
556
+ .listen(port);
557
+ });
558
+ };
559
+ const runNpmInstallIfNeeded = async () => {
560
+ if (fs_1.default.existsSync(nodeModulesPath))
561
+ return;
562
+ console.log(chalk_1.default.blue(`\nšŸ“¦ Installing dependencies...`));
563
+ const { execSync } = require("child_process");
564
+ try {
565
+ execSync("npm install", { cwd: workspaceDir, stdio: "inherit" });
566
+ console.log(chalk_1.default.green(`āœ… Dependencies installed.`));
567
+ }
568
+ catch (e) {
569
+ console.error(chalk_1.default.red(`āŒ npm install failed: ${e.message}`));
570
+ }
571
+ };
572
+ const startDevServerIfNeeded = async () => {
573
+ const portBusy = await isPortInUse(devServerPort);
574
+ if (portBusy) {
575
+ console.log(chalk_1.default.gray(` Dev server already running on port ${devServerPort}.`));
576
+ return;
577
+ }
578
+ console.log(chalk_1.default.blue(`\nšŸš€ Starting dev server on port ${devServerPort}...`));
579
+ const { spawn } = require("child_process");
580
+ const child = spawn("npm", ["run", "dev"], {
581
+ cwd: workspaceDir,
582
+ detached: true,
583
+ stdio: "ignore"
584
+ });
585
+ child.unref();
586
+ console.log(chalk_1.default.green(`āœ… Dev server launched (PID: ${child.pid}).`));
587
+ // Wait a bit then open browser
588
+ setTimeout(() => {
589
+ const openCmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
590
+ const { exec } = require("child_process");
591
+ exec(`${openCmd} http://localhost:${devServerPort}`);
592
+ console.log(chalk_1.default.cyan(`🌐 Browser opened: http://localhost:${devServerPort}`));
593
+ }, 3000);
594
+ };
595
+ // Fire and forget — don't block the HTTP response
596
+ (async () => {
597
+ try {
598
+ await runNpmInstallIfNeeded();
599
+ await startDevServerIfNeeded();
600
+ }
601
+ catch (e) {
602
+ console.error(chalk_1.default.yellow(`āš ļø Auto-pilot warning: ${e.message}`));
603
+ }
604
+ })();
541
605
  res.status(200).json({ success: true, message: `Successfully saved ${savedCount} files.` });
542
606
  }
543
607
  catch (error) {
package/package.json CHANGED
@@ -1,26 +1,26 @@
1
1
  {
2
- "name": "desen-cli",
3
- "version": "1.0.0-draft",
4
- "main": "dist/index.js",
5
- "bin": {
6
- "desen": "./dist/index.js"
7
- },
8
- "dependencies": {
9
- "chalk": "^5.6.2",
10
- "commander": "^14.0.3",
11
- "express": "^4.19.2",
12
- "cors": "^2.8.5",
13
- "desen-core": "1.0.0-draft"
14
- },
15
- "devDependencies": {
16
- "@types/cors": "^2.8.19",
17
- "@types/express": "^5.0.6",
18
- "@types/node": "^20.19.33",
19
- "typescript": "^5.0.0"
20
- },
21
- "scripts": {
22
- "build": "tsc",
23
- "dev": "tsc -w",
24
- "clean": "rm -rf dist"
25
- }
26
- }
2
+ "name": "desen-cli",
3
+ "version": "1.0.0-draft.11",
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": "workspace:*",
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
+ }
26
+ }
package/src/index.ts CHANGED
@@ -4,7 +4,7 @@ import { Command } from "commander";
4
4
  import chalk from "chalk";
5
5
  import fs from "fs";
6
6
  import path from "path";
7
- import { surfaceSpec } from "desen-core";
7
+ import { surfaceSpec, nodeSpec } from "desen-core";
8
8
  import express from "express";
9
9
  import cors from "cors";
10
10
 
@@ -190,8 +190,9 @@ program
190
190
  <div class="card">
191
191
  <h1>šŸš€ DESEN Daemon Active</h1>
192
192
  <p>The Synchronization (Sync) server is waiting for incoming <strong>UI AST</strong> data from Figma. It can safely continue running in the background.</p>
193
- <p>To preview your design live as a standalone application using the <strong>desen</strong> render engine, open a new terminal in your project directory and run the following commands:</p>
194
- <div class="code">cd desen-workspace<br/>npm install<br/>npm run dev</div>
193
+ <p>When you press <strong>Sync to Localhost</strong> in Figma, the daemon will <strong>automatically</strong>:</p>
194
+ <div class="code">1. Validate &amp; save JSON files<br/>2. Install dependencies (if needed)<br/>3. Start dev server on port 3001<br/>4. Open your browser ✨</div>
195
+ <p style="margin-top:16px;color:#888;">Zero-touch. Just press Sync and watch the magic.</p>
195
196
  </div>
196
197
  </body>
197
198
  </html>
@@ -208,20 +209,41 @@ program
208
209
  return res.status(400).json({ error: "Missing payloads or path in request body" });
209
210
  }
210
211
 
211
- // 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;
212
219
  for (const item of payloads) {
213
220
  if (!item.path || !item.ast) continue;
214
221
 
215
- const result = surfaceSpec.safeParse(item.ast);
222
+ let result: any;
223
+ if (item.path.includes('surfaces/')) {
224
+ result = surfaceSpec.safeParse(item.ast);
225
+ } else if (item.path.includes('elements/') || item.path.includes('compositions/')) {
226
+ result = nodeSpec.safeParse(item.ast);
227
+ } else if (item.path.includes('themes/')) {
228
+ result = { success: true };
229
+ } else {
230
+ result = nodeSpec.safeParse(item.ast);
231
+ }
232
+
216
233
  if (!result.success) {
217
- console.error(chalk.red(`āŒ [Fail-Closed] Payload validation failed for ${item.path}`));
218
- return res.status(400).json({
219
- error: "Validation failed against desen-core schemas",
220
- path: item.path,
221
- issues: result.error.errors
222
- });
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}`));
223
242
  }
224
243
  }
244
+ if (validationWarnings > 0) {
245
+ console.warn(chalk.yellow(`\nāš ļø ${validationWarnings} file(s) have validation warnings. Run 'desen validate' for strict checks.`));
246
+ }
225
247
 
226
248
  const projectRoot = process.cwd();
227
249
  const workspaceDir = path.join(projectRoot, "desen-workspace");
@@ -263,8 +285,8 @@ program
263
285
  "start": "next start"
264
286
  },
265
287
  dependencies: {
266
- "desen-core": "1.0.0-draft",
267
- "desen": "1.0.0-draft",
288
+ "desen-core": "1.0.0-draft.8",
289
+ "desen": "1.0.0-draft.8",
268
290
  "next": "14.2.3",
269
291
  "react": "18.3.1",
270
292
  "react-dom": "18.3.1"
@@ -305,7 +327,7 @@ program
305
327
  fs.mkdirSync(appDir, { recursive: true });
306
328
 
307
329
  // Find first surface directly
308
- 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';
309
331
 
310
332
  // API Route to load local .desen.json dynamically
311
333
  const apiDir = path.join(appDir, "api", "surface", "route");
@@ -344,13 +366,24 @@ async function resolveRefs(obj: any, baseDir: string): Promise<any> {
344
366
  export async function GET() {
345
367
  try {
346
368
  const baseDir = process.cwd();
369
+
370
+ // Load surface
347
371
  const filePath = path.join(baseDir, '${firstSurface}');
348
372
  const content = await fs.readFile(filePath, 'utf-8');
349
- let parsed = JSON.parse(content);
373
+ let parsedSurface = JSON.parse(content);
374
+ parsedSurface = await resolveRefs(parsedSurface, baseDir);
350
375
 
351
- parsed = await resolveRefs(parsed, baseDir);
376
+ // Load tokens gracefully
377
+ let parsedTokens = {};
378
+ try {
379
+ const tokensPath = path.join(baseDir, 'desen/themes/tokens.desen.json');
380
+ const tokensContent = await fs.readFile(tokensPath, 'utf-8');
381
+ parsedTokens = await resolveRefs(JSON.parse(tokensContent), baseDir);
382
+ } catch (e) {
383
+ console.log("No custom tokens found, using defaults.");
384
+ }
352
385
 
353
- return NextResponse.json(parsed);
386
+ return NextResponse.json({ surface: parsedSurface, tokens: parsedTokens });
354
387
  } catch (e) {
355
388
  return NextResponse.json({ error: 'System is waiting for your sync!' }, { status: 404 });
356
389
  }
@@ -398,13 +431,15 @@ import { DesenProvider, DesenRenderer } from "desen";
398
431
  import { registry } from "../lib/desenRegistry";
399
432
 
400
433
  export default function Page() {
401
- const [data, setData] = useState<any>(null);
434
+ const [uiData, setUiData] = useState<any>(null);
402
435
 
403
436
  useEffect(() => {
404
437
  const fetchUI = () => {
405
438
  fetch("/api/surface/route")
406
439
  .then(res => res.json())
407
- .then(json => { if(!json.error) setData(json) })
440
+ .then(json => {
441
+ if(!json.error) setUiData(json);
442
+ })
408
443
  .catch(e => console.error(e));
409
444
  };
410
445
  fetchUI();
@@ -412,18 +447,19 @@ export default function Page() {
412
447
  return () => clearInterval(intv);
413
448
  }, []);
414
449
 
415
- if (!data) return <div style={{padding: 40, fontFamily: 'monospace'}}>Waiting for Figma Sync...</div>;
450
+ if (!uiData || !uiData.surface) return <div style={{padding: 40, fontFamily: 'monospace'}}>Waiting for Figma Sync...</div>;
416
451
 
417
452
  // SPEC.md: Surface = full page. Pass full surface object, not data.root.
418
453
  return (
419
454
  <DesenProvider
420
455
  registry={registry}
456
+ tokens={uiData.tokens?.tokens || uiData.tokens || {}}
421
457
  session_id="workspace_session"
422
458
  revision_id="rev_1"
423
459
  onTelemetry={e => console.log("[Telemetry]", e)}
424
460
  onAction={a => console.log("[Action]", a)}
425
461
  >
426
- <DesenRenderer node={data} />
462
+ <DesenRenderer node={uiData.surface} />
427
463
  </DesenProvider>
428
464
  );
429
465
  }
@@ -436,67 +472,6 @@ export default function Page() {
436
472
  fs.writeFileSync(path.join(libDir, "desenRegistry.tsx"), `
437
473
  import React from 'react';
438
474
 
439
- function resolveToken(tokenStr: string): string | undefined {
440
- if (!tokenStr) return undefined;
441
- const match = tokenStr.match(/t_([a-fA-F0-9]{3,8})/);
442
- return match ? \`#\${match[1]}\` : undefined;
443
- }
444
-
445
- function resolveColor(tokenStr?: string, hexStr?: string): string | undefined {
446
- if (tokenStr) return resolveToken(tokenStr);
447
- if (hexStr) return hexStr;
448
- return undefined;
449
- }
450
-
451
- function buildCssFromNode(layout: any, style: any, opts?: { isText?: boolean; isSurfaceRoot?: boolean }): React.CSSProperties {
452
- const css: any = { boxSizing: 'border-box' };
453
-
454
- if (layout) {
455
- css.display = 'flex';
456
- css.flexDirection = layout.direction === 'row' ? 'row' : 'column';
457
- if (layout.gap !== undefined && layout.gap !== 0) css.gap = layout.gap;
458
-
459
- 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' };
460
- css.alignItems = am[layout.align_items] || 'stretch';
461
- css.justifyContent = am[layout.align_content] || 'flex-start';
462
-
463
- if (layout.padding !== undefined) {
464
- css.padding = Array.isArray(layout.padding) ? layout.padding.map((p:number)=>p+'px').join(' ') : layout.padding !== 0 ? layout.padding+'px' : undefined;
465
- }
466
-
467
- if (opts?.isSurfaceRoot) {
468
- css.width = '100%'; css.minHeight = '100vh';
469
- } else {
470
- if (layout.sizing_h === 'FILL') css.width = '100%';
471
- else if (layout.sizing_h === 'FIXED' && style?.width !== undefined) css.width = style.width;
472
- if (layout.sizing_v === 'FILL') css.height = '100%';
473
- else if (layout.sizing_v === 'FIXED' && style?.height !== undefined) css.height = style.height;
474
- }
475
- }
476
-
477
- if (style) {
478
- const bgColor = resolveColor(style.bg_color_token, style.bg_color);
479
- if (bgColor) { if (opts?.isText) css.color = bgColor; else css.backgroundColor = bgColor; }
480
- const bc = resolveColor(style.border_color_token, style.border_color);
481
- if (bc) css.borderColor = bc;
482
- if (style.border_width !== undefined) { css.borderWidth = style.border_width; css.borderStyle = 'solid'; }
483
- if (style.border_radius !== undefined) {
484
- css.borderRadius = Array.isArray(style.border_radius) ? style.border_radius.map((r:number)=>r+'px').join(' ') : style.border_radius;
485
- }
486
- if (!layout && !opts?.isText) { if (style.width !== undefined) css.width = style.width; if (style.height !== undefined) css.height = style.height; }
487
- if (style.opacity !== undefined) css.opacity = style.opacity;
488
- if (style.font_size) css.fontSize = style.font_size;
489
- if (style.font_weight) css.fontWeight = style.font_weight;
490
- if (style.font_family) css.fontFamily = style.font_family;
491
- if (style.letter_spacing) css.letterSpacing = style.letter_spacing;
492
- if (style.line_height) css.lineHeight = style.line_height + 'px';
493
- if (style.text_align) css.textAlign = style.text_align;
494
- if (style.text_decoration) css.textDecoration = style.text_decoration;
495
- if (style.text_transform) css.textTransform = style.text_transform;
496
- }
497
- return css;
498
- }
499
-
500
475
  const DynamicFontLoader = ({ fonts }: { fonts?: string[] }) => {
501
476
  if (!fonts || fonts.length === 0) return null;
502
477
 
@@ -520,33 +495,44 @@ const DynamicFontLoader = ({ fonts }: { fonts?: string[] }) => {
520
495
  };
521
496
 
522
497
  const SurfaceComponent = (props: any) => (
523
- <div style={{ width: '100%', minHeight: '100vh', boxSizing: 'border-box' }}>
498
+ <div style={props.parsedStyle}>
524
499
  <DynamicFontLoader fonts={props.metadata?.fonts} />
525
500
  {props.children}
526
501
  </div>
527
502
  );
528
503
 
529
504
  const GenericContainer = (props: any) => (
530
- <div style={buildCssFromNode(props.layout, props.style, { isSurfaceRoot: props._isSurfaceRoot })}>{props.children}</div>
505
+ <div style={props.parsedStyle}>{props.children}</div>
531
506
  );
532
507
 
533
508
  const TextComponent = (props: any) => (
534
- <span style={buildCssFromNode(props.layout, props.style, { isText: true })}>{props.content || props.text || props.name}</span>
509
+ <span style={props.parsedStyle}>{props.content || props.text || props.name}</span>
535
510
  );
536
511
 
537
512
  const ButtonComponent = (props: any) => {
538
- const adjLayout = { ...props.layout };
539
- if (adjLayout && !props.children && adjLayout.align_content === 'space-between') adjLayout.align_content = 'center';
540
- const css: any = { ...buildCssFromNode(adjLayout, props.style), cursor: 'pointer', border: 'none' };
541
- if (props.text_style) {
542
- const tc = resolveColor(props.text_style.bg_color_token, props.text_style.bg_color);
543
- if (tc) css.color = tc;
544
- if (props.text_style.font_size) css.fontSize = props.text_style.font_size;
545
- if (props.text_style.font_weight) css.fontWeight = props.text_style.font_weight;
546
- if (props.text_style.font_family) css.fontFamily = props.text_style.font_family;
547
- if (props.text_style.text_align) css.textAlign = props.text_style.text_align;
513
+ const css: any = { ...props.parsedStyle, cursor: 'pointer', border: 'none' };
514
+
515
+ // If no layout, center text by default (most buttons center their text)
516
+ if (!props.layout) {
517
+ css.display = 'flex';
518
+ css.alignItems = 'center';
519
+ css.justifyContent = 'center';
548
520
  }
549
- return <button style={css}>{props.children || props.content || props.text || props.name}</button>;
521
+
522
+ const textStyle = props.style?.text_style || {};
523
+
524
+ return (
525
+ <button style={css}>
526
+ <span style={{
527
+ fontFamily: textStyle.font_family,
528
+ fontSize: textStyle.font_size,
529
+ fontWeight: textStyle.font_weight,
530
+ color: textStyle.text_color || textStyle.bg_color // Figma plugin often maps text fill to bg_color
531
+ }}>
532
+ {props.children || props.content || props.text || props.name}
533
+ </span>
534
+ </button>
535
+ );
550
536
  };
551
537
 
552
538
  export const registry: Record<string, React.FC<any>> = {
@@ -556,11 +542,18 @@ export const registry: Record<string, React.FC<any>> = {
556
542
  element: GenericContainer,
557
543
  group: GenericContainer,
558
544
  stack: GenericContainer,
545
+ Stack: GenericContainer,
546
+ Card: GenericContainer,
547
+ Grid: GenericContainer,
548
+ Header: GenericContainer,
549
+ Footer: GenericContainer,
559
550
  text: TextComponent,
560
551
  Text: TextComponent,
561
552
  button: ButtonComponent,
562
- input: (props: any) => <input style={buildCssFromNode(props.layout, props.style)} placeholder={props.content || props.name} />,
563
- icon: (props: any) => <div style={{ ...buildCssFromNode(props.layout, props.style), display:'inline-flex', alignItems:'center', justifyContent:'center' }}>āš™ļø</div>,
553
+ Button: ButtonComponent,
554
+ input: (props: any) => <input style={props.parsedStyle} placeholder={props.content || props.name} />,
555
+ icon: (props: any) => <div style={{ ...props.parsedStyle, display:'inline-flex', alignItems:'center', justifyContent:'center' }}>āš™ļø</div>,
556
+ Icon: (props: any) => <div style={{ ...props.parsedStyle, display:'inline-flex', alignItems:'center', justifyContent:'center' }}>āš™ļø</div>,
564
557
  };
565
558
  `.trim());
566
559
 
@@ -578,6 +571,77 @@ export const registry: Record<string, React.FC<any>> = {
578
571
  }
579
572
  } // End if(!package.json)
580
573
 
574
+ // ─────────────────────────────────────────────────────────
575
+ // AUTO-PILOT: npm install + npm run dev + browser open
576
+ // Zero-touch DX — developer never leaves Figma
577
+ // ─────────────────────────────────────────────────────────
578
+ const nodeModulesPath = path.join(workspaceDir, "node_modules");
579
+ const devServerPort = 3001;
580
+
581
+ const isPortInUse = (port: number): Promise<boolean> => {
582
+ return new Promise((resolve) => {
583
+ const net = require("net");
584
+ const tester = net.createServer()
585
+ .once("error", (err: any) => {
586
+ if (err.code === "EADDRINUSE") resolve(true);
587
+ else resolve(false);
588
+ })
589
+ .once("listening", () => {
590
+ tester.close(() => resolve(false));
591
+ })
592
+ .listen(port);
593
+ });
594
+ };
595
+
596
+ const runNpmInstallIfNeeded = async () => {
597
+ if (fs.existsSync(nodeModulesPath)) return;
598
+
599
+ console.log(chalk.blue(`\nšŸ“¦ Installing dependencies...`));
600
+ const { execSync } = require("child_process");
601
+ try {
602
+ execSync("npm install", { cwd: workspaceDir, stdio: "inherit" });
603
+ console.log(chalk.green(`āœ… Dependencies installed.`));
604
+ } catch (e: any) {
605
+ console.error(chalk.red(`āŒ npm install failed: ${e.message}`));
606
+ }
607
+ };
608
+
609
+ const startDevServerIfNeeded = async () => {
610
+ const portBusy = await isPortInUse(devServerPort);
611
+ if (portBusy) {
612
+ console.log(chalk.gray(` Dev server already running on port ${devServerPort}.`));
613
+ return;
614
+ }
615
+
616
+ console.log(chalk.blue(`\nšŸš€ Starting dev server on port ${devServerPort}...`));
617
+ const { spawn } = require("child_process");
618
+ const child = spawn("npm", ["run", "dev"], {
619
+ cwd: workspaceDir,
620
+ detached: true,
621
+ stdio: "ignore"
622
+ });
623
+ child.unref();
624
+ console.log(chalk.green(`āœ… Dev server launched (PID: ${child.pid}).`));
625
+
626
+ // Wait a bit then open browser
627
+ setTimeout(() => {
628
+ const openCmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
629
+ const { exec } = require("child_process");
630
+ exec(`${openCmd} http://localhost:${devServerPort}`);
631
+ console.log(chalk.cyan(`🌐 Browser opened: http://localhost:${devServerPort}`));
632
+ }, 3000);
633
+ };
634
+
635
+ // Fire and forget — don't block the HTTP response
636
+ (async () => {
637
+ try {
638
+ await runNpmInstallIfNeeded();
639
+ await startDevServerIfNeeded();
640
+ } catch (e: any) {
641
+ console.error(chalk.yellow(`āš ļø Auto-pilot warning: ${e.message}`));
642
+ }
643
+ })();
644
+
581
645
  res.status(200).json({ success: true, message: `Successfully saved ${savedCount} files.` });
582
646
  } catch (error: any) {
583
647
  console.error(chalk.red("āŒ Sync Error:"), error);