winter-super-cli 2026.6.27 → 2026.6.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/design.md CHANGED
@@ -1,66 +1,3 @@
1
- # Design Guidance
1
+ # Nova
2
2
 
3
- File này không phải danh bạ trang trí. Đây là checklist bắt buộc khi Winter xử lý UI, brand, landing page, dashboard, hoặc component.
4
-
5
- ## Resource Status
6
- - Source: `resources\local\awesome-design-md\README.md`
7
- - Corpus: `resources\local\awesome-design-md\design-md`
8
- - Manifest: 142 files, 1.8 MB
9
- - Status: Corpus local sẵn sàng tại `resources\local\awesome-design-md\design-md`.
10
-
11
- ## How Winter Must Use This
12
- 1. Khi task liên quan UI/design/brand, tìm brand hoặc style gần nhất trong `awesome-design-md/design-md` trước khi viết code.
13
- 2. Nếu user nêu brand cụ thể, ưu tiên đúng brand đó; nếu không có, chọn brand gần nhất theo domain và nói rõ giả định.
14
- 3. Không tự bịa palette, spacing, typography, tone khi local design guide có dữ liệu phù hợp.
15
- 4. Với app/work tool, ưu tiên layout rõ, dense, dễ scan; không dùng hero marketing nếu user đang cần tool thật.
16
- 5. Sau khi sửa UI, kiểm tra responsive và text overflow nếu có thể chạy app.
17
-
18
- ## Available Brand Samples (40+)
19
- - airbnb
20
- - airtable
21
- - apple
22
- - binance
23
- - bmw
24
- - bmw-m
25
- - bugatti
26
- - cal
27
- - claude
28
- - clay
29
- - clickhouse
30
- - cohere
31
- - coinbase
32
- - composio
33
- - cursor
34
- - elevenlabs
35
- - expo
36
- - ferrari
37
- - figma
38
- - framer
39
- - hashicorp
40
- - ibm
41
- - intercom
42
- - kraken
43
- - lamborghini
44
- - linear.app
45
- - lovable
46
- - mastercard
47
- - meta
48
- - minimax
49
- - mintlify
50
- - miro
51
- - mistral.ai
52
- - mongodb
53
- - nike
54
- - notion
55
- - nvidia
56
- - ollama
57
- - opencode.ai
58
- - pinterest
59
-
60
- ## Useful Commands
61
- - `/designs` để liệt kê hoặc tìm design systems.
62
- - `/read resources/local/awesome-design-md/README.md` để đọc overview.
63
- - `/grep <brand> resources/local/awesome-design-md/design-md` để tìm brand/style guide.
64
-
65
- ---
66
- *File này được tự động tạo bởi Winter CLI.*
3
+ Design rules.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "winter-super-cli",
3
- "version": "2026.6.27",
3
+ "version": "2026.6.29",
4
4
  "description": "❄️ AI-Powered Development CLI with Interactive REPL",
5
5
  "type": "module",
6
6
  "main": "bin/winter.js",
@@ -3,11 +3,11 @@ import { pathToFileURL } from 'url';
3
3
  import { promises as fs } from 'fs';
4
4
 
5
5
  const DEFAULT_TOOL_SETS = {
6
- general: ['Read', 'Write', 'Edit', 'Bash', 'Glob', 'Grep', 'OpenBrowser', 'VisibleBrowser', 'BrowserDebug', 'WebFetch', 'WebSearch', 'Parallel', 'Agent', 'DelegateTask'],
6
+ general: ['Read', 'Write', 'Edit', 'Bash', 'Glob', 'Grep', 'OpenBrowser', 'VisibleBrowser', 'BrowserDebug', 'DesignToCode', 'WebFetch', 'WebSearch', 'Parallel', 'Agent', 'DelegateTask'],
7
7
  plan: ['Read', 'Grep', 'Glob', 'WebFetch', 'WebSearch', 'Parallel'],
8
8
  review: ['Read', 'Grep', 'Glob', 'Bash', 'WebFetch'],
9
9
  debug: ['Read', 'Write', 'Edit', 'Bash', 'Grep', 'Glob', 'OpenBrowser', 'VisibleBrowser', 'BrowserDebug', 'WebFetch', 'Parallel'],
10
- design: ['Read', 'Write', 'Edit', 'Bash', 'Grep', 'Glob', 'OpenBrowser', 'VisibleBrowser', 'BrowserDebug', 'WebFetch'],
10
+ design: ['Read', 'Write', 'Edit', 'Bash', 'Grep', 'Glob', 'OpenBrowser', 'VisibleBrowser', 'BrowserDebug', 'DesignToCode', 'WebFetch'],
11
11
  research: ['Read', 'Grep', 'Glob', 'WebFetch', 'WebSearch', 'Parallel'],
12
12
  swe: ['Read', 'Write', 'Edit', 'Bash', 'Grep', 'Glob', 'OpenBrowser', 'VisibleBrowser', 'BrowserDebug', 'Parallel'],
13
13
  };
@@ -37,6 +37,7 @@ export class AgentRuntime {
37
37
  const executionProfile = repl.selectExecutionProfile(messages, { enableTools: true });
38
38
  const requireToolEvidence = repl.actionRequiresTools(messages);
39
39
  let noToolActionRetries = 0;
40
+ let unfinishedActionRetries = 0;
40
41
  const sessionContext = repl.session?.getContext?.() || {};
41
42
  const profile = sessionContext.workflowProfile || 'general';
42
43
  const depth = /deep/i.test(profile) ? 'deep' : 'standard';
@@ -63,6 +64,7 @@ export class AgentRuntime {
63
64
  model: executionProfile.model,
64
65
  enableTools: true,
65
66
  toolPromptOnly: forceTextToolFallback,
67
+ toolChoiceRequired: requireToolEvidence && !usedTools && !forceTextToolFallback,
66
68
  requireToolEvidence: requireToolEvidence && !usedTools,
67
69
  usedMutatingTools: usedMutatingTools,
68
70
  deferFinalContent: this.shouldVerifyBeforeFinal(messages, usedMutatingTools, autoVerificationPassed),
@@ -140,6 +142,32 @@ export class AgentRuntime {
140
142
  reachedToolLimit = false;
141
143
  break;
142
144
  }
145
+ if (
146
+ turn.finalContent &&
147
+ requireToolEvidence &&
148
+ usedTools &&
149
+ !usedMutatingTools &&
150
+ repl.responseIndicatesUnfinishedAction?.(turn.finalContent)
151
+ ) {
152
+ unfinishedActionRetries++;
153
+ if (unfinishedActionRetries > 3) {
154
+ finalContent = 'Chưa hoàn thành: model chỉ trả lời trạng thái sau khi inspect, chưa thực hiện thay đổi. Winter đã dừng để tránh báo tiến độ giả.';
155
+ console.log(`\n${colors.yellow}${finalContent}${colors.reset}\n`);
156
+ reachedToolLimit = false;
157
+ break;
158
+ }
159
+ messages.push({
160
+ role: 'assistant',
161
+ content: assistantMsg.content || '',
162
+ });
163
+ messages.push({
164
+ role: 'user',
165
+ content: repl.buildUnfinishedActionCorrection(messages, turn.finalContent),
166
+ });
167
+ forceTextToolFallback = true;
168
+ finalContent = '';
169
+ continue;
170
+ }
143
171
  if (turn.finalContent) {
144
172
  finalContent = turn.finalContent;
145
173
  }
@@ -124,6 +124,7 @@ function buildOpenAIRequest(provider, messages, options) {
124
124
  if (options.reasoning?.reasoning_effort) body.reasoning_effort = options.reasoning.reasoning_effort;
125
125
  if (options.reasoning?.thinking) body.thinking = options.reasoning.thinking;
126
126
  if (options.tools?.length) body.tools = options.tools;
127
+ if (options.toolChoiceRequired && options.tools?.length) body.tool_choice = 'required';
127
128
 
128
129
  return {
129
130
  url: `${trimTrailingSlash(provider.baseURL)}/chat/completions`,
@@ -147,6 +148,7 @@ function buildAnthropicRequest(provider, messages, options) {
147
148
  if (options.stream) body.stream = true;
148
149
  if (options.reasoning?.thinking) body.thinking = options.reasoning.thinking;
149
150
  if (options.tools?.length) body.tools = options.tools.map(toAnthropicTool).filter(Boolean);
151
+ if (options.toolChoiceRequired && body.tools?.length) body.tool_choice = { type: 'any' };
150
152
 
151
153
  const headers = {
152
154
  'Content-Type': 'application/json',
package/src/cli/config.js CHANGED
@@ -7,7 +7,7 @@ import { promises as fs } from 'fs';
7
7
  import path from 'path';
8
8
  import { homedir } from 'os';
9
9
  import { loadEnvFile, stripInlineSecrets } from './secret-env.js';
10
- import { CHROME_DEVTOOLS_MCP_NAME, createChromeDevtoolsMcpServer, ensureMcpConfigShape } from '../mcp/presets.js';
10
+ import { CHROME_DEVTOOLS_MCP_NAME, FIGMA_MCP_NAME, createChromeDevtoolsMcpServer, createFigmaMcpServer, ensureMcpConfigShape } from '../mcp/presets.js';
11
11
 
12
12
  export class ConfigLoader {
13
13
  constructor() {
@@ -36,11 +36,14 @@ export class ConfigLoader {
36
36
  if (!next.mcp.servers.some(server => server?.name === CHROME_DEVTOOLS_MCP_NAME)) {
37
37
  next.mcp.servers.push(createChromeDevtoolsMcpServer(['--isolated']));
38
38
  }
39
+ if (!next.mcp.servers.some(server => server?.name === FIGMA_MCP_NAME)) {
40
+ next.mcp.servers.push(createFigmaMcpServer());
41
+ }
39
42
  next.permissions.allowlist.tools = [
40
43
  ...new Set([...(next.permissions.allowlist.tools || []), 'MCP']),
41
44
  ];
42
45
  next.permissions.allowlist.mcpServers = [
43
- ...new Set([...(next.permissions.allowlist.mcpServers || []), CHROME_DEVTOOLS_MCP_NAME]),
46
+ ...new Set([...(next.permissions.allowlist.mcpServers || []), CHROME_DEVTOOLS_MCP_NAME, FIGMA_MCP_NAME]),
44
47
  ];
45
48
  return next;
46
49
  }
@@ -79,11 +82,11 @@ export class ConfigLoader {
79
82
  allowlist: {
80
83
  tools: ['Read', 'Glob', 'Grep', 'LSP', 'TaskCreate', 'TaskUpdate', 'TaskList', 'WebFetch', 'WebSearch', 'Parallel', 'MCP'],
81
84
  commands: [],
82
- mcpServers: [CHROME_DEVTOOLS_MCP_NAME],
85
+ mcpServers: [CHROME_DEVTOOLS_MCP_NAME, FIGMA_MCP_NAME],
83
86
  },
84
87
  },
85
88
  mcp: {
86
- servers: [createChromeDevtoolsMcpServer(['--isolated'])],
89
+ servers: [createChromeDevtoolsMcpServer(['--isolated']), createFigmaMcpServer()],
87
90
  },
88
91
  routing: {
89
92
  strategy: 'heuristic',
@@ -121,11 +124,11 @@ export class ConfigLoader {
121
124
  enabled: true,
122
125
  restrictToWorkspace: true,
123
126
  allowedCommands: [
124
- 'git', 'npm', 'npx', 'node', 'python',
127
+ 'git', 'npm', 'npx', 'node', 'python', 'powershell', 'pwsh', 'cmd',
125
128
  'ping', 'test-connection', 'curl', 'wget', 'iwr', 'irm',
126
129
  'invoke-webrequest', 'invoke-restmethod', 'nslookup', 'resolve-dnsname',
127
130
  'tracert', 'traceroute', 'pathping', 'dig', 'ipconfig', 'ifconfig',
128
- 'ip', 'netstat', 'speedtest', 'speedtest-cli', 'measure-command',
131
+ 'ip', 'netstat', 'speedtest', 'speedtest-cli', 'measure-command', 'where',
129
132
  ],
130
133
  },
131
134
  session: {
@@ -82,58 +82,69 @@ export class WinterInputController {
82
82
  readline.emitKeypressEvents(process.stdin, repl.rl);
83
83
 
84
84
  process.stdin.on('keypress', (str, key = {}) => {
85
- if (key.ctrl && key.name === 'v') {
86
- void this.handleDirectClipboardPaste();
87
- return;
88
- }
85
+ this.handleGlobalKeypress(str, key);
86
+ });
87
+ }
89
88
 
90
- if (key.name === 'return' && (key.shift || key.meta)) {
91
- repl.rl.write('\\\n');
92
- return;
93
- }
89
+ handleGlobalKeypress(str, key = {}) {
90
+ const repl = this.repl;
94
91
 
95
- if (key.ctrl || key.meta) return;
92
+ if (repl.interactiveChecklistOpen) {
93
+ return true;
94
+ }
96
95
 
97
- if (typeof str === 'string' && str.length > 1) {
98
- return;
99
- }
96
+ if (key.ctrl && key.name === 'v') {
97
+ void this.handleDirectClipboardPaste();
98
+ return true;
99
+ }
100
100
 
101
- if (repl.slashMenu.open && this.handleSlashMenuKey(key)) {
102
- return;
103
- }
101
+ if (key.name === 'return' && (key.shift || key.meta)) {
102
+ repl.rl.write('\\\n');
103
+ return true;
104
+ }
104
105
 
105
- if (key.name === 'escape') {
106
- if (repl.isProcessing) {
107
- // Cancel current AI turn
108
- repl.isCancelled = true;
109
- if (repl.spinner) repl.spinner.stop();
110
- console.log(`\n${colors.red}[ Đã nhận lệnh HỦY... AI sẽ kết thúc ở thao tác tiếp theo ]${colors.reset}`);
111
- } else {
112
- // Double-ESC to end session
113
- const now = Date.now();
114
- if (this._lastEscTime && (now - this._lastEscTime) < 500) {
115
- console.log(`\n\n${colors.cyan}Cảm ơn đã sử dụng Winter!${colors.reset}`);
116
- console.log(`${colors.yellow}Tiếp tục phiên làm việc:${colors.reset}`);
117
- console.log(`${colors.bright}${colors.green}winter --session ${repl.session?.getSessionId?.() || ''}${colors.reset}\n`);
118
- process.exit(0);
119
- }
120
- this._lastEscTime = now;
121
- console.log(`${colors.dim}Press ESC again to end session${colors.reset}`);
106
+ if (key.ctrl || key.meta) return false;
107
+
108
+ if (typeof str === 'string' && str.length > 1) {
109
+ return false;
110
+ }
111
+
112
+ if (repl.slashMenu.open && this.handleSlashMenuKey(key)) {
113
+ return true;
114
+ }
115
+
116
+ if (key.name === 'escape') {
117
+ if (repl.isProcessing) {
118
+ // Cancel current AI turn
119
+ repl.isCancelled = true;
120
+ if (repl.spinner) repl.spinner.stop();
121
+ console.log(`\n${colors.red}[ Đã nhận lệnh HỦY... AI sẽ kết thúc ở thao tác tiếp theo ]${colors.reset}`);
122
+ } else {
123
+ // Double-ESC to end session
124
+ const now = Date.now();
125
+ if (this._lastEscTime && (now - this._lastEscTime) < 500) {
126
+ console.log(`\n\n${colors.cyan}Cảm ơn đã sử dụng Winter!${colors.reset}`);
127
+ console.log(`${colors.yellow}Tiếp tục phiên làm việc:${colors.reset}`);
128
+ console.log(`${colors.bright}${colors.green}winter --session ${repl.session?.getSessionId?.() || ''}${colors.reset}\n`);
129
+ process.exit(0);
122
130
  }
123
- return;
131
+ this._lastEscTime = now;
132
+ console.log(`${colors.dim}Press ESC again to end session${colors.reset}`);
124
133
  }
134
+ return true;
135
+ }
125
136
 
126
- queueMicrotask(() => {
127
- const line = repl.rl?.line || '';
128
- if (!line.startsWith('/')) {
129
- this.closeSlashMenu();
130
- repl.rl?.prompt?.(true);
131
- return;
132
- }
137
+ queueMicrotask(() => {
138
+ const line = repl.rl?.line || '';
139
+ if (!line.startsWith('/')) {
140
+ this.closeSlashMenu();
141
+ repl.rl?.prompt?.(true);
142
+ return;
143
+ }
133
144
 
134
- this.openSlashMenu(line);
135
- });
145
+ this.openSlashMenu(line);
136
146
  });
147
+ return true;
137
148
  }
138
149
 
139
150
  async handleDirectClipboardPaste() {
@@ -138,6 +138,7 @@ export class PromptBuilder {
138
138
  `Open browser requests: if the user asks to "mở chrome", "open Chrome", or open a URL in a visible browser, use OpenBrowser. Do NOT use Bash, Get-Command, Start-Process, cmd start, or shell app launch commands for this.`,
139
139
  `Browser capability: You CAN browse URLs! Use WebFetch only for static page text extraction. For live Chrome debugging and visible browser control, prefer MCP server "chrome-devtools" when configured: use MCP tool "new_page" or "navigate_page", then "take_snapshot", "click", "fill"/"fill_form", "take_screenshot", "evaluate_script", "list_console_messages", "list_network_requests", or performance trace tools. If MCP is unavailable, use VisibleBrowser for real visible Puppeteer control. BrowserDebug is headless fallback only when visible control is unnecessary.`,
140
140
  `Browser interaction rule: if the user asks to click, press, fill, select, submit, open a web app path, or inspect page-by-page data, WebFetch is not enough and BrowserDebug is not user-visible. Use chrome-devtools MCP or VisibleBrowser so the user can watch a normal browser. Never claim "đã bấm/đã điền/đã mở/đã kiểm tra" from prose alone.`,
141
+ `Figma/design-to-code: if the user gives a Figma URL or asks to implement a Figma frame, call DesignToCode with the Figma frame URL and an output_path. Do not only describe the design. If the user asks to modify the Figma canvas itself, use MCP server "figma" when configured and say clearly when it is not connected.`,
141
142
  `When a task touches coding, agents, UI, brand, or design, inspect the relevant required local resource in depth before deciding.`,
142
143
  `If the user asks you to modify, run, inspect, check, publish, commit, or otherwise act on the project, you MUST use tools. Do not claim completion without a tool result from this turn.`,
143
144
  ``,
@@ -248,7 +249,7 @@ export class PromptBuilder {
248
249
  rolePrompt,
249
250
  '',
250
251
  '## Tool Rules',
251
- '- Canonical tools: Read, Write, Edit, Bash, Glob, Grep, TaskCreate, TaskUpdate, TaskList, OpenBrowser, VisibleBrowser, BrowserDebug, WebFetch, WebSearch, MCP, Agent, DelegateTask, ParallelAgent.',
252
+ '- Canonical tools: Read, Write, Edit, Bash, Glob, Grep, TaskCreate, TaskUpdate, TaskList, OpenBrowser, VisibleBrowser, BrowserDebug, DesignToCode, WebFetch, WebSearch, MCP, Agent, DelegateTask, ParallelAgent.',
252
253
  '- For multi-agent work, use DelegateTask for one isolated subagent or ParallelAgent for independent concurrent subagents; do not pretend delegation happened without tool evidence.',
253
254
  '- If native tool calls are unavailable, output exactly one fallback tool call and no prose: <invoke name="Read"><parameter name="path">README.md</parameter></invoke> OR {"tool":"Read","arguments":{"path":"README.md"}} OR CALL_TOOL Read {"path":"README.md"}.',
254
255
  '- Treat skills, memories, bundled resources, local project rules, and the tool list as operational context. Use them proactively when relevant.',
@@ -219,6 +219,38 @@ async function handleBrowserCommand(repl, args = []) {
219
219
  if (result.recovery) console.log(`${colors.dim}${result.recovery}${colors.reset}`);
220
220
  }
221
221
 
222
+ async function handleFigmaCommand(repl, args = []) {
223
+ const parsed = parseFlagArgs(args);
224
+ const [urlArg, outputArg] = parsed.positional;
225
+ const url = parsed.url || parsed.figmaUrl || urlArg;
226
+ if (!url || url === 'help' || url === '--help') {
227
+ console.log(`${colors.cyan}/figma usage:${colors.reset}`);
228
+ console.log(' /figma <figma-frame-url> [output.tsx] --force');
229
+ console.log(' /design-to-code <figma-frame-url> --output src/components/FigmaDesign.tsx');
230
+ console.log(`${colors.dim}Requires FIGMA_TOKEN in project .env, environment, or --token.${colors.reset}`);
231
+ return;
232
+ }
233
+
234
+ const payload = {
235
+ url: String(url).replace(/^['"]|['"]$/g, ''),
236
+ output_path: parsed.output || parsed.out || parsed.component || outputArg,
237
+ assets_dir: parsed.assets || parsed.assetsDir,
238
+ force: parsed.force === true,
239
+ no_tailwind: parsed.noTailwind === true || parsed.noTailwind === 'true',
240
+ token: parsed.token,
241
+ };
242
+ console.log(`${colors.cyan}DesignToCode:${colors.reset} ${payload.url}`);
243
+ const result = await repl.tools.execute('DesignToCode', payload, { cwd: repl.projectPath });
244
+ if (result.success) {
245
+ console.log(`${colors.green}OK DesignToCode${result.outputPath ? ` -> ${result.outputPath}` : ''}${colors.reset}`);
246
+ const output = [result.stdout, result.stderr].filter(Boolean).join('\n').trim();
247
+ if (output) console.log(output.length > 4000 ? `${output.slice(0, 4000)}...` : output);
248
+ return;
249
+ }
250
+ console.log(`${colors.red}FAIL DesignToCode: ${result.error || 'unknown error'}${colors.reset}`);
251
+ if (result.recovery) console.log(`${colors.dim}${result.recovery}${colors.reset}`);
252
+ }
253
+
222
254
  /**
223
255
  * Handle slash commands in the Winter REPL.
224
256
  * Delegated from WinterREPL.handleSlashCommand to reduce file size.
@@ -467,6 +499,12 @@ export async function handleSlashCommand(repl, input) {
467
499
  await handleBrowserCommand(repl, args);
468
500
  return;
469
501
 
502
+ case '/figma':
503
+ case '/design-to-code':
504
+ case '/vibefigma':
505
+ await handleFigmaCommand(repl, args);
506
+ return;
507
+
470
508
  // Inline Completion
471
509
  case '/complete':
472
510
  if (args.length === 0) {
@@ -673,7 +711,6 @@ export async function handleSlashCommand(repl, input) {
673
711
  break;
674
712
 
675
713
  // Planning
676
- case '/plan':
677
714
  case '/plans':
678
715
  const plans = repl.session.getPlans();
679
716
  if (plans.length === 0) {
@@ -683,6 +720,14 @@ export async function handleSlashCommand(repl, input) {
683
720
  plans.forEach(p => console.log(` [${p.status}] ${p.title}`));
684
721
  }
685
722
  break;
723
+ case '/plan':
724
+ if (args.length === 0) {
725
+ console.log(`${colors.yellow}Usage: /plan <task>${colors.reset}`);
726
+ console.log(`${colors.dim}Use /plans to list active plans.${colors.reset}`);
727
+ return;
728
+ }
729
+ await repl.generateInteractivePlan(args.join(' '));
730
+ return;
686
731
  case '/task':
687
732
  case '/tasks':
688
733
  console.log(`${colors.cyan}Tasks:${colors.reset}`);
@@ -848,7 +893,6 @@ export async function handleSlashCommand(repl, input) {
848
893
  }
849
894
  console.log(`${colors.yellow}Usage: /doctor [full|tools|context|scorecard]${colors.reset}`);
850
895
  return;
851
- case '/plan:':
852
896
  case '/plan-gen':
853
897
  if (args.length === 0) {
854
898
  console.log(`${colors.yellow}Usage: /plan <task>${colors.reset}`);
package/src/cli/repl.js CHANGED
@@ -93,6 +93,7 @@ export class WinterREPL {
93
93
  this.history = [];
94
94
  this.maxHistory = 500;
95
95
  this.slashMenu = { open: false, line: '', items: [], selected: 0 };
96
+ this.interactiveChecklistOpen = false;
96
97
  this.inputQueue = Promise.resolve();
97
98
  this.readlineClosed = false;
98
99
  this.taskQueue = [];
@@ -1443,11 +1444,22 @@ export class WinterREPL {
1443
1444
  async showInteractiveChecklist(title, items) {
1444
1445
  if (!items || items.length === 0) return [];
1445
1446
 
1447
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
1448
+ console.log(`\n\x1b[36m${title}\x1b[0m`);
1449
+ console.log(`\x1b[2mNon-interactive terminal detected; selecting all steps.\x1b[0m`);
1450
+ items.forEach(item => console.log(` \x1b[32m[x]\x1b[0m ${item}`));
1451
+ this.interactiveChecklistOpen = false;
1452
+ return [...items];
1453
+ }
1454
+
1446
1455
  return new Promise((resolve) => {
1447
1456
  let cursor = 0;
1448
1457
  const selected = new Set(items.map((_, i) => i)); // default select all
1458
+ let ignoreInitialReturn = true;
1449
1459
 
1450
1460
  let printedLines = 0;
1461
+ const wasRaw = Boolean(process.stdin.isRaw);
1462
+ const wasPaused = process.stdin.isPaused?.() ?? false;
1451
1463
  const render = () => {
1452
1464
  // Xóa những dòng đã in trước đó.
1453
1465
  if (printedLines > 0) {
@@ -1475,6 +1487,11 @@ export class WinterREPL {
1475
1487
  if (selected.has(cursor)) selected.delete(cursor);
1476
1488
  else selected.add(cursor);
1477
1489
  } else if (key.name === 'return') {
1490
+ if (ignoreInitialReturn) {
1491
+ ignoreInitialReturn = false;
1492
+ render();
1493
+ return;
1494
+ }
1478
1495
  cleanup();
1479
1496
  const result = items.filter((_, i) => selected.has(i));
1480
1497
  resolve(result);
@@ -1489,18 +1506,37 @@ export class WinterREPL {
1489
1506
 
1490
1507
  const cleanup = () => {
1491
1508
  process.stdin.removeListener('keypress', onKeyPress);
1492
- if (process.stdin.isTTY) process.stdin.setRawMode(false);
1493
- this.rl.resume();
1509
+ if (process.stdin.isTTY && typeof process.stdin.setRawMode === 'function') {
1510
+ process.stdin.setRawMode(wasRaw);
1511
+ }
1512
+ if (wasPaused && typeof process.stdin.pause === 'function') {
1513
+ process.stdin.pause();
1514
+ }
1515
+ this.interactiveChecklistOpen = false;
1516
+ this.rl?.resume?.();
1494
1517
  process.stdout.write('\n');
1495
1518
  };
1496
1519
 
1497
- this.rl.pause();
1498
- if (process.stdin.isTTY) process.stdin.setRawMode(true);
1520
+ try {
1521
+ this.interactiveChecklistOpen = true;
1522
+ this.rl?.pause?.();
1523
+ if (process.stdin.isTTY && typeof process.stdin.setRawMode === 'function') {
1524
+ process.stdin.setRawMode(true);
1525
+ }
1526
+ process.stdin.resume();
1499
1527
 
1500
- readline.emitKeypressEvents(process.stdin);
1501
- process.stdin.on('keypress', onKeyPress);
1528
+ readline.emitKeypressEvents(process.stdin);
1529
+ process.stdin.on('keypress', onKeyPress);
1502
1530
 
1503
- render();
1531
+ setImmediate(() => {
1532
+ ignoreInitialReturn = false;
1533
+ });
1534
+
1535
+ render();
1536
+ } catch (error) {
1537
+ cleanup();
1538
+ throw error;
1539
+ }
1504
1540
  });
1505
1541
  }
1506
1542
 
@@ -1832,9 +1868,9 @@ ${colors.reset}
1832
1868
  return byName(['Read', 'Grep', 'Glob', 'WebFetch', 'WebSearch', 'Parallel']);
1833
1869
  case 'design':
1834
1870
  case 'ui':
1835
- return byName(['Read', 'Write', 'Edit', 'Bash', 'Grep', 'Glob', 'OpenBrowser', 'VisibleBrowser', 'BrowserDebug', 'WebFetch', 'MCP']);
1871
+ return byName(['Read', 'Write', 'Edit', 'Bash', 'Grep', 'Glob', 'OpenBrowser', 'VisibleBrowser', 'BrowserDebug', 'DesignToCode', 'WebFetch', 'MCP']);
1836
1872
  default:
1837
- return byName(['Read', 'Write', 'Edit', 'Bash', 'Glob', 'Grep', 'OpenBrowser', 'VisibleBrowser', 'BrowserDebug', 'WebFetch', 'WebSearch', 'MCP', 'Parallel', 'Agent', 'DelegateTask', 'ParallelAgent']);
1873
+ return byName(['Read', 'Write', 'Edit', 'Bash', 'Glob', 'Grep', 'OpenBrowser', 'VisibleBrowser', 'BrowserDebug', 'DesignToCode', 'WebFetch', 'WebSearch', 'MCP', 'Parallel', 'Agent', 'DelegateTask', 'ParallelAgent']);
1838
1874
  }
1839
1875
  }
1840
1876
 
@@ -1963,7 +1999,10 @@ ${colors.reset}
1963
1999
  if (this.isBrowserInteractionRequest(rawText)) return true;
1964
2000
 
1965
2001
  // Even without explicit target, some verbs are strong enough on their own
1966
- const strongActionAlone = /\b(fix|debug|deploy|build|test|commit|install|run|refactor|sửa|chạy|cài|triển khai|xây dựng)\b/i;
2002
+ const continuationAction = /\b(continue|resume|start|begin|do it|go ahead|tiep|tiếp|bat dau|bắt đầu|lam di|làm đi)\b/i;
2003
+ if (continuationAction.test(text)) return true;
2004
+
2005
+ const strongActionAlone = /\b(fix|debug|deploy|build|test|commit|install|run|refactor|start|begin|do|sửa|chạy|cài|triển khai|xây dựng|bắt đầu|bat dau|làm|lam)\b/i;
1967
2006
  if (strongActionAlone.test(text)) return true;
1968
2007
 
1969
2008
  return actionPattern.test(text) && targetPattern.test(text);
@@ -1990,6 +2029,38 @@ ${colors.reset}
1990
2029
  return true;
1991
2030
  }
1992
2031
 
2032
+ responseIndicatesUnfinishedAction(content = '') {
2033
+ const raw = String(content || '').trim();
2034
+ if (!raw) return false;
2035
+ const text = `${raw}\n${this.normalizeIntentText(raw)}`.toLowerCase();
2036
+
2037
+ const inProgress = /\b(đang|dang|sẽ|se|tiếp theo|tiep theo|next(?:,|\s+i|\s+step)?|i(?:'ll| will| am going to)|going to|để tôi|de toi|let me|cần (?:thêm|sửa|tạo|viết)|can (?:add|create|implement)|need to (?:add|create|implement|edit|write|fix)|thiếu .*?(?:endpoint|api|view|ui|frontend|backend|route|function)|missing .*?(?:endpoint|api|view|ui|frontend|backend|route|function))\b/i;
2038
+ const workVerb = /\b(làm|lam|sửa|sua|thêm|them|tạo|tao|viết|viet|cập nhật|cap nhat|implement|add|create|write|edit|update|patch|fix|build|wire|connect|endpoint|api|view|ui|frontend|backend)\b/i;
2039
+ const onlyInspection = /\b(đọc|doc|read|grep|search|inspect|kiểm tra|kiem tra|found|thấy|thay|xác định|xac dinh)\b/i;
2040
+
2041
+ if (inProgress.test(text) && (workVerb.test(text) || onlyInspection.test(text))) return true;
2042
+ if (/đang làm tiếp|dang lam tiep|doing next|working on it|đang sửa|dang sua|đang thêm|dang them/i.test(text)) return true;
2043
+ return false;
2044
+ }
2045
+
2046
+ buildUnfinishedActionCorrection(messages = [], content = '') {
2047
+ const request = this.getLatestUserText(messages);
2048
+ return [
2049
+ 'RUNTIME ENFORCEMENT: Your previous response was only progress/status, not completion.',
2050
+ '',
2051
+ 'You already inspected, but the user asked you to continue/do the work. Keep going with tool calls now:',
2052
+ '1. If you identified missing backend/frontend pieces, call Read/Grep for the exact files still needed.',
2053
+ '2. Call Edit/Write/InsertText/StrReplaceAll to make the code changes.',
2054
+ '3. Call Bash to run the closest test/build/smoke check.',
2055
+ '4. Only then give the final answer.',
2056
+ '',
2057
+ 'Do not answer with "đang làm", "sẽ thêm", "cần thêm", or a plan. Use tools.',
2058
+ '',
2059
+ `Original user request: ${request}`,
2060
+ content ? `Blocked progress response: ${String(content).slice(0, 1000)}` : '',
2061
+ ].filter(Boolean).join('\n');
2062
+ }
2063
+
1993
2064
  isBrowserInteractionRequest(text = '') {
1994
2065
  const raw = String(text || '');
1995
2066
  const normalized = this.normalizeIntentText(raw);
@@ -2004,7 +2075,7 @@ ${colors.reset}
2004
2075
  if (!text.trim()) return false;
2005
2076
 
2006
2077
  // Detect fake completion claims - model says it did something without using tools
2007
- const fakeCompletionClaims = /(?:đã (?:sửa|tạo|viết|xóa|cập nhật|thêm|chỉnh|xong|hoàn thành|fix|update|edit|write|create|delete|remove|modify|change|apply|deploy|push)|i(?:'ve| have) (?:fixed|created|written|updated|added|modified|changed|edited|applied|deployed|deleted|removed|patched|implemented|refactored)|done!|xong rồi|hoàn thành|đã hoàn tất|hoàn tất|the (?:fix|change|update|edit|modification) (?:has been|is) (?:applied|done|completed|made)|here(?:'s| is) the (?:fix|update|change|solution|implementation|code)|file (?:has been|was) (?:updated|created|modified|written|changed)|changes? (?:have been|has been|were) (?:made|applied|saved)|successfully (?:updated|created|modified|fixed|applied|changed|written))/i;
2078
+ const fakeCompletionClaims = /(?:đã (?:làm|sửa|tạo|viết|xóa|cập nhật|thêm|chỉnh|xong|hoàn thành|fix|update|edit|write|create|delete|remove|modify|change|apply|deploy|push)|i(?:'ve| have) (?:done|fixed|created|written|updated|added|modified|changed|edited|applied|deployed|deleted|removed|patched|implemented|refactored)|done!|xong rồi|hoàn thành|đã hoàn tất|hoàn tất|phase \d+ done|build check:\s*pass|files? (?:tạo|sửa|created|modified|changed)|the (?:fix|change|update|edit|modification) (?:has been|is) (?:applied|done|completed|made)|here(?:'s| is) the (?:fix|update|change|solution|implementation|code)|file (?:has been|was) (?:updated|created|modified|written|changed)|changes? (?:have been|has been|were) (?:made|applied|saved)|successfully (?:updated|created|modified|fixed|applied|changed|written))/i;
2008
2079
  if (fakeCompletionClaims.test(text)) return true;
2009
2080
 
2010
2081
  const fakeBrowserClaims = /(?:đã|da|i(?:'ve| have))\s+(?:bấm|bam|click(?:ed)?|mở|mo|open(?:ed)?|điền|dien|fill(?:ed)?|chọn|chon|select(?:ed)?|submit(?:ted)?|vào|vao|navigate(?:d)?)/i;
@@ -2116,6 +2187,10 @@ ${colors.reset}
2116
2187
  hints.push('TOOL HINT: For "open Chrome" / "mở chrome", call OpenBrowser {"browser":"chrome","url":"about:blank"}. Do NOT call Bash/Get-Command/Start-Process. If configured, use MCP {"server":"chrome-devtools","tool":"list"} for page state, console, network, screenshots, and performance.');
2117
2188
  }
2118
2189
 
2190
+ if (/\b(figma|vibefigma|design[- ]?to[- ]?code|design to code|frame|node-id)\b/i.test(text)) {
2191
+ hints.push('TOOL HINT: For Figma design-to-code, call DesignToCode with {"url":"<Figma frame URL>","output_path":"src/components/Name.tsx"}. Do NOT only describe the design. If the user asks direct Figma canvas manipulation, use MCP server "figma" when configured; otherwise explain that Winter needs the Figma MCP server connected.');
2192
+ }
2193
+
2119
2194
  if (hints.length === 0) return null;
2120
2195
 
2121
2196
  return `[Tool Router] ${hints.join(' | ')}`;
@@ -2369,7 +2444,7 @@ ${colors.reset}
2369
2444
  '/provider', '/model', '/models', '/providers',
2370
2445
  '/theme:toggle', '/tui',
2371
2446
  '/auto', '/debug', '/doctor', '/context', '/scorecard', '/swe',
2372
- '/read', '/write', '/glob', '/grep', '/bash', '/browser', '/paste',
2447
+ '/read', '/write', '/glob', '/grep', '/bash', '/browser', '/paste', '/figma', '/design-to-code',
2373
2448
  '/codex', '/claude', '/karpathy', '/agents',
2374
2449
  '/resources', '/designs', '/skills',
2375
2450
  '/ecc',
@@ -37,6 +37,9 @@ export const SLASH_COMMANDS = [
37
37
  { cmd: '/bash', desc: 'Run command', usage: '/bash <command>' },
38
38
  { cmd: '/image', desc: 'Analyze image/screenshot or clipboard image', usage: '/image [file] [question]' },
39
39
  { cmd: '/paste', desc: 'Read clipboard text or image', usage: '/paste [prompt]' },
40
+ { cmd: '/figma', desc: 'Convert selected Figma frame to code', usage: '/figma <figma-url> [output.tsx] [--force]' },
41
+ { cmd: '/design-to-code', desc: 'Alias for Figma design-to-code', usage: '/design-to-code <figma-url> --output src/components/FigmaDesign.tsx' },
42
+ { cmd: '/vibefigma', desc: 'Run bundled vibefigma design-to-code workflow', usage: '/vibefigma <figma-url> [output.tsx]' },
40
43
  { cmd: '/design', desc: 'Design commands', sub: ['search', 'add', 'apply', 'list', 'preview'] },
41
44
  { cmd: '/designs', desc: 'List/search awesome-design-md systems', usage: '/designs [query]' },
42
45
  { cmd: '/skill', desc: 'Skills management', sub: ['list', 'enable', 'create'] },
@@ -15,35 +15,75 @@ export class DesignCommands {
15
15
  this.repl = repl;
16
16
  this.session = repl.session;
17
17
  this.config = repl.config;
18
+ this.projectPath = repl.projectPath || null;
18
19
  this.brandsDir = path.join(packageRoot, 'resources', 'local', 'awesome-design-md', 'design-md');
19
20
  }
20
21
 
21
22
  async execute(action, args) {
22
- switch (action) {
23
+ const requestedAction = String(action || '').trim();
24
+ if (!requestedAction) {
25
+ await this.printHelp();
26
+ await this.listBrands();
27
+ return;
28
+ }
29
+
30
+ switch (requestedAction.toLowerCase()) {
23
31
  case 'search':
24
32
  case 'find':
33
+ case 'tim':
25
34
  await this.search(args[0] || '');
26
35
  break;
27
36
  case 'add':
37
+ case 'install':
28
38
  await this.addBrand(args[0]);
29
39
  break;
30
40
  case 'apply':
41
+ case 'use':
42
+ case 'set':
31
43
  await this.applyBrand(args[0]);
32
44
  break;
33
45
  case 'list':
46
+ case 'ls':
34
47
  await this.listBrands();
35
48
  break;
36
49
  case 'preview':
50
+ case 'show':
51
+ case 'view':
37
52
  await this.previewBrand(args[0]);
38
53
  break;
39
54
  case 'init':
40
55
  await this.initBrandsDir();
41
56
  break;
57
+ case 'help':
58
+ case '--help':
59
+ case '-h':
60
+ await this.printHelp();
61
+ break;
42
62
  default:
43
- await this.listBrands();
63
+ if (await this.brandExists(requestedAction)) {
64
+ await this.applyBrand(requestedAction);
65
+ } else {
66
+ await this.search(requestedAction);
67
+ }
44
68
  }
45
69
  }
46
70
 
71
+ async printHelp() {
72
+ console.log(`${colors.cyan}/design usage:${colors.reset}`);
73
+ console.log(' /design <brand> Apply a design system to the current project');
74
+ console.log(' /design search <query> Search bundled design systems');
75
+ console.log(' /design preview <brand> Preview DESIGN.md');
76
+ console.log(' /design add <brand> Copy DESIGN.md into the current project');
77
+ console.log(' /design list List all bundled brands');
78
+ console.log('');
79
+ }
80
+
81
+ async brandExists(brand) {
82
+ if (!brand) return false;
83
+ const brandDir = path.join(this.brandsDir, brand);
84
+ return await fs.access(brandDir).then(() => true).catch(() => false);
85
+ }
86
+
47
87
  async search(query) {
48
88
  if (!query) {
49
89
  console.log(`${colors.yellow}Usage: winter design search <query>${colors.reset}`);
@@ -67,7 +107,7 @@ export class DesignCommands {
67
107
  });
68
108
  console.log('');
69
109
 
70
- await this.session.addToMemory(`Searched design brands for: ${query}`, 'search');
110
+ await this.session?.addToMemory?.(`Searched design brands for: ${query}`, 'search');
71
111
  } catch (error) {
72
112
  console.log(`${colors.red}${statusIcons.error} Error: ${error.message}${colors.reset}`);
73
113
  console.log(`${colors.dim}Make sure you have awesome-design-md installed${colors.reset}`);
@@ -103,11 +143,11 @@ export class DesignCommands {
103
143
  }
104
144
 
105
145
  // Write to current project
106
- const targetPath = path.join(process.cwd(), fileName);
146
+ const targetPath = path.join(this.repl?.projectPath || this.projectPath || process.cwd(), fileName);
107
147
  await fs.writeFile(targetPath, fileContent);
108
148
 
109
149
  console.log(`${statusIcons.success} Added ${fileName} for ${brand}`);
110
- await this.session.addToMemory(`Added design file: ${brand}`, 'design');
150
+ await this.session?.addToMemory?.(`Added design file: ${brand}`, 'design');
111
151
  } catch (error) {
112
152
  console.log(`${colors.red}${statusIcons.error} Error: ${error.message}${colors.reset}`);
113
153
  }
@@ -142,7 +182,7 @@ export class DesignCommands {
142
182
 
143
183
  console.log(`${colors.cyan}${statusIcons.info} Analyzing and applying ${brand} design system...${colors.reset}`);
144
184
 
145
- const prompt = `Please act as a Senior UI/UX Engineer. Analyze the following design system (${brand}) and completely refactor the UI and styles in this project to match its specifications. Focus on colors, typography, border radiuses, interactive states, and overall visual aesthetics as defined in the document.
185
+ const prompt = `Please act as a Senior UI/UX Engineer. Analyze the following design system (${brand}) and apply it to this project with real tool calls. Inspect the existing UI files/styles first, then make focused code changes and verify with the closest build/typecheck/smoke command. Do not only describe the design. Focus on colors, typography, border radiuses, interactive states, and overall visual aesthetics as defined in the document.
146
186
 
147
187
  <design_system>
148
188
  ${fileContent}
@@ -229,7 +269,7 @@ Start by reviewing the codebase, especially tailwind configs or global css, then
229
269
  }
230
270
 
231
271
  async initBrandsDir() {
232
- const localDir = path.join(process.cwd(), '.design-systems');
272
+ const localDir = path.join(this.repl?.projectPath || this.projectPath || process.cwd(), '.design-systems');
233
273
  await fs.mkdir(localDir, { recursive: true });
234
274
  console.log(`${statusIcons.success} Created ${localDir}/`);
235
275
  }
@@ -1,7 +1,11 @@
1
1
  export const CHROME_DEVTOOLS_MCP_NAME = 'chrome-devtools';
2
+ export const FIGMA_MCP_NAME = 'figma';
2
3
 
3
4
  const CHROME_DEVTOOLS_PACKAGE = 'chrome-devtools-mcp@latest';
4
5
  const CHROME_DEVTOOLS_SOURCE = 'https://github.com/ChromeDevTools/chrome-devtools-mcp';
6
+ const MCP_REMOTE_PACKAGE = 'mcp-remote@latest';
7
+ const FIGMA_DEV_MODE_MCP_URL = 'http://127.0.0.1:3845/mcp';
8
+ const FIGMA_DEV_MODE_MCP_SOURCE = 'https://developers.figma.com/docs/figma-mcp-server/local-server-installation/';
5
9
 
6
10
  const CHROME_DEVTOOLS_FLAGS_WITH_VALUES = new Set([
7
11
  '--browser-url',
@@ -27,6 +31,10 @@ export function isChromeDevtoolsPreset(name = '') {
27
31
  return ['chrome-devtools', 'chromedevtools', 'chrome', 'devtools', 'cdp'].includes(normalizeMcpPresetName(name));
28
32
  }
29
33
 
34
+ export function isFigmaPreset(name = '') {
35
+ return ['figma', 'figma-dev-mode', 'figmadevmode', 'figma-desktop', 'figmadesktop'].includes(normalizeMcpPresetName(name));
36
+ }
37
+
30
38
  export function buildChromeDevtoolsArgs(options = []) {
31
39
  const input = Array.isArray(options) ? [...options] : [];
32
40
  const args = ['-y', CHROME_DEVTOOLS_PACKAGE];
@@ -85,10 +93,51 @@ export function createChromeDevtoolsMcpServer(options = [], platform = process.p
85
93
  };
86
94
  }
87
95
 
96
+ export function createFigmaMcpServer(options = [], platform = process.platform, env = process.env) {
97
+ const input = Array.isArray(options) ? [...options] : [];
98
+ const customUrlIndex = input.findIndex(value => value === '--url' || value === '--endpoint');
99
+ const endpoint = customUrlIndex >= 0 && input[customUrlIndex + 1]
100
+ ? String(input[customUrlIndex + 1])
101
+ : FIGMA_DEV_MODE_MCP_URL;
102
+ const npxArgs = ['-y', MCP_REMOTE_PACKAGE, endpoint];
103
+ const common = {
104
+ name: FIGMA_MCP_NAME,
105
+ enabled: true,
106
+ requestTimeoutMs: 60000,
107
+ metadata: {
108
+ preset: FIGMA_MCP_NAME,
109
+ source: FIGMA_DEV_MODE_MCP_SOURCE,
110
+ endpoint,
111
+ purpose: 'Figma Dev Mode MCP for direct design context, selected frame inspection, assets, variables, and design-to-code workflows.',
112
+ },
113
+ };
114
+
115
+ if (platform === 'win32') {
116
+ return {
117
+ ...common,
118
+ command: 'cmd',
119
+ args: ['/c', 'npx', ...npxArgs],
120
+ env: {
121
+ SystemRoot: env.SystemRoot || 'C:\\Windows',
122
+ PROGRAMFILES: env.PROGRAMFILES || 'C:\\Program Files',
123
+ },
124
+ };
125
+ }
126
+
127
+ return {
128
+ ...common,
129
+ command: 'npx',
130
+ args: npxArgs,
131
+ };
132
+ }
133
+
88
134
  export function getMcpPreset(name, options = []) {
89
135
  if (isChromeDevtoolsPreset(name)) {
90
136
  return createChromeDevtoolsMcpServer(options);
91
137
  }
138
+ if (isFigmaPreset(name)) {
139
+ return createFigmaMcpServer(options);
140
+ }
92
141
  throw new Error(`Unknown MCP preset: ${name}`);
93
142
  }
94
143
 
@@ -32,8 +32,9 @@ export class ToolExecutor {
32
32
  this.projectPath = repl?.projectPath || process.cwd();
33
33
  this.allowedCommands = [
34
34
  'git', 'npm', 'npx', 'node', 'python', 'code', 'pnpm', 'yarn', 'bun', 'pip', 'cargo', 'rustc',
35
+ 'powershell', 'pwsh', 'cmd',
35
36
  'echo', 'printf', 'cat', 'ls', 'dir', 'type', 'copy', 'mkdir', 'get-childitem', 'set-content',
36
- 'get-content', 'test-path', 'get-date',
37
+ 'get-content', 'test-path', 'get-date', 'where',
37
38
  'ping', 'test-connection', 'curl', 'wget', 'iwr', 'irm', 'invoke-webrequest', 'invoke-restmethod',
38
39
  'nslookup', 'resolve-dnsname', 'tracert', 'traceroute', 'pathping', 'dig', 'ipconfig', 'ifconfig',
39
40
  'ip', 'netstat', 'speedtest', 'speedtest-cli', 'measure-command',
@@ -476,6 +477,23 @@ export class ToolExecutor {
476
477
  }
477
478
  }
478
479
  },
480
+ {
481
+ type: 'function',
482
+ name: 'DesignToCode',
483
+ description: 'Convert a selected Figma design/frame URL into project code using the bundled vibefigma workflow. Use when the user gives a Figma URL, asks for Figma design-to-code, or asks to implement a selected Figma frame. Requires FIGMA_TOKEN in env/.env or a token argument.',
484
+ parameters: {
485
+ type: 'object',
486
+ properties: {
487
+ url: { type: 'string', description: 'Figma design/file URL. Best results require a selected node-id in the URL.' },
488
+ output_path: { type: 'string', description: 'Optional React component output path, e.g. src/components/FigmaDesign.tsx' },
489
+ assets_dir: { type: 'string', description: 'Optional asset output directory, default handled by vibefigma.' },
490
+ force: { type: 'boolean', description: 'Overwrite existing generated files without prompting.' },
491
+ no_tailwind: { type: 'boolean', description: 'Generate regular CSS instead of Tailwind.' },
492
+ token: { type: 'string', description: 'Optional Figma access token. Prefer FIGMA_TOKEN env/.env instead.' },
493
+ },
494
+ required: ['url']
495
+ }
496
+ },
479
497
  {
480
498
  type: 'function',
481
499
  name: 'WebFetch',
@@ -624,6 +642,8 @@ export class ToolExecutor {
624
642
  return await this.visibleBrowser(input, cwd);
625
643
  case 'OpenBrowser':
626
644
  return await this.openBrowser(input.url ?? input.uri ?? input.href, input.browser);
645
+ case 'DesignToCode':
646
+ return await this.designToCode(input, cwd);
627
647
  case 'WebFetch':
628
648
  return await this.webFetch(input.url ?? input.uri ?? input.href, input.prompt ?? input.query ?? input.extract);
629
649
  case 'WebSearch':
@@ -692,7 +712,7 @@ export class ToolExecutor {
692
712
  return {
693
713
  success: false,
694
714
  error: `Unknown tool: ${toolName}`,
695
- availableTools: ['Read', 'Write', 'Edit', 'Bash', 'Glob', 'Grep', 'TaskCreate', 'TaskUpdate', 'TaskList', 'MCP', 'Parallel', 'OpenBrowser', 'VisibleBrowser', 'BrowserDebug', 'WebFetch', 'WebSearch', 'WebArchive', 'HtmlEffectiveness', 'NotebookRead', 'NotebookEdit', 'TodoWrite', 'TodoList', 'ScheduleWakeup', 'AskUserQuestion', 'Agent', 'DelegateTask', 'ParallelAgent', 'InsertText', 'StrReplaceAll'],
715
+ availableTools: ['Read', 'Write', 'Edit', 'Bash', 'Glob', 'Grep', 'TaskCreate', 'TaskUpdate', 'TaskList', 'MCP', 'Parallel', 'OpenBrowser', 'VisibleBrowser', 'BrowserDebug', 'DesignToCode', 'WebFetch', 'WebSearch', 'WebArchive', 'HtmlEffectiveness', 'NotebookRead', 'NotebookEdit', 'TodoWrite', 'TodoList', 'ScheduleWakeup', 'AskUserQuestion', 'Agent', 'DelegateTask', 'ParallelAgent', 'InsertText', 'StrReplaceAll'],
696
716
  recovery: 'Call one of the available tools. For file writes use Write with { "file_path": "...", "content": "..." }. For shell commands use Bash with { "command": "..." }.',
697
717
  };
698
718
  }
@@ -851,6 +871,44 @@ export class ToolExecutor {
851
871
  return { success: true, coerced: true, args: { ...args, url, browser } };
852
872
  }
853
873
 
874
+ if (toolName === 'DesignToCode') {
875
+ const url = pick('url', 'figmaUrl', 'figma_url', 'href', 'uri', 'input');
876
+ if (!url) {
877
+ return {
878
+ success: false,
879
+ error: 'Figma url is required',
880
+ recovery: 'Example: DesignToCode {"url":"https://www.figma.com/design/FILE/Name?node-id=1-2","output_path":"src/components/FigmaDesign.tsx"}',
881
+ };
882
+ }
883
+ if (!/^https:\/\/(?:www\.)?figma\.com\/(?:design|file)\//i.test(url)) {
884
+ return {
885
+ success: false,
886
+ error: 'DesignToCode requires a Figma design/file URL',
887
+ recovery: 'Copy a Figma frame/component link such as https://www.figma.com/design/FILE/Name?node-id=1-2.',
888
+ };
889
+ }
890
+ const outputPath = pick('output_path', 'outputPath', 'component', 'path', 'file');
891
+ const assetsDir = pick('assets_dir', 'assetsDir', 'assets');
892
+ if (outputPath) {
893
+ const outputSafety = await this.validateWorkspacePath(this.resolveInputPath(outputPath, cwd), 'DesignToCode output path');
894
+ if (outputSafety?.success === false) return outputSafety;
895
+ }
896
+ if (assetsDir) {
897
+ const assetsSafety = await this.validateWorkspacePath(this.resolveInputPath(assetsDir, cwd), 'DesignToCode assets directory');
898
+ if (assetsSafety?.success === false) return assetsSafety;
899
+ }
900
+ return {
901
+ success: true,
902
+ coerced: true,
903
+ args: {
904
+ ...args,
905
+ url,
906
+ output_path: outputPath,
907
+ assets_dir: assetsDir,
908
+ },
909
+ };
910
+ }
911
+
854
912
  if (toolName === 'Agent' || toolName === 'DelegateTask') {
855
913
  const task = pick('task', 'goal', 'prompt', 'description');
856
914
  if (!task) {
@@ -1170,6 +1228,13 @@ export class ToolExecutor {
1170
1228
  browserdebug: 'BrowserDebug',
1171
1229
  browser: 'BrowserDebug',
1172
1230
  browserinspect: 'BrowserDebug',
1231
+ designtocode: 'DesignToCode',
1232
+ designcode: 'DesignToCode',
1233
+ figma: 'DesignToCode',
1234
+ figmatocode: 'DesignToCode',
1235
+ figmacode: 'DesignToCode',
1236
+ vibefigma: 'DesignToCode',
1237
+ figmadesign: 'DesignToCode',
1173
1238
  parallel: 'Parallel',
1174
1239
  parallelexecute: 'Parallel',
1175
1240
  paralleltools: 'Parallel',
@@ -1246,6 +1311,7 @@ export class ToolExecutor {
1246
1311
  case 'WebArchive':
1247
1312
  case 'BrowserDebug':
1248
1313
  case 'VisibleBrowser':
1314
+ case 'DesignToCode':
1249
1315
  return { url: value };
1250
1316
  case 'TaskCreate':
1251
1317
  case 'TodoWrite':
@@ -1620,10 +1686,11 @@ export class ToolExecutor {
1620
1686
  async execWindowsCommand(command, cwd, timeout, requestedShell = 'auto') {
1621
1687
  const shell = requestedShell === 'auto' ? this.detectWindowsShell(command) : requestedShell;
1622
1688
  if (shell === 'cmd') {
1623
- return await execFileAsync('cmd.exe', ['/d', '/s', '/c', command], {
1689
+ return await execFileAsync('cmd.exe', ['/d', '/c', command], {
1624
1690
  cwd,
1625
1691
  timeout,
1626
1692
  windowsHide: true,
1693
+ windowsVerbatimArguments: true,
1627
1694
  maxBuffer: 10 * 1024 * 1024,
1628
1695
  });
1629
1696
  }
@@ -1649,7 +1716,7 @@ export class ToolExecutor {
1649
1716
 
1650
1717
  looksLikeCmd(command) {
1651
1718
  return /\s(&&|\|\|)\s/.test(command)
1652
- || /(^|[&]\s*)(dir|copy|xcopy|del|erase|move|ren|type|echo|set|if|for|mkdir|rmdir)\b/i.test(command)
1719
+ || /(^|[&]\s*)(dir|copy|xcopy|del|erase|move|ren|type|echo|set|if|for|mkdir|rmdir|where)\b/i.test(command)
1653
1720
  || /^\s*@?echo\s+/i.test(command)
1654
1721
  || /(^|\s)(\/b|\/s|\/q|\/y)\b/i.test(command);
1655
1722
  }
@@ -2149,6 +2216,84 @@ export class ToolExecutor {
2149
2216
  };
2150
2217
  }
2151
2218
 
2219
+ async readProjectEnvValue(name, cwd = this.projectPath) {
2220
+ if (process.env[name]) return process.env[name];
2221
+ try {
2222
+ const envPath = path.join(cwd || this.projectPath, '.env');
2223
+ const raw = await fs.readFile(envPath, 'utf8');
2224
+ const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
2225
+ const pattern = new RegExp(`^\\s*${escaped}\\s*=\\s*(.*)\\s*$`, 'm');
2226
+ const match = raw.match(pattern);
2227
+ if (!match) return '';
2228
+ return String(match[1] || '').trim().replace(/^['"]|['"]$/g, '');
2229
+ } catch {
2230
+ return '';
2231
+ }
2232
+ }
2233
+
2234
+ async designToCode(input = {}, cwd = this.projectPath) {
2235
+ const url = String(input.url || input.figmaUrl || input.figma_url || input.href || input.uri || '').trim();
2236
+ if (!url) {
2237
+ return {
2238
+ success: false,
2239
+ error: 'Figma url is required',
2240
+ recovery: 'Use DesignToCode {"url":"https://www.figma.com/design/FILE/Name?node-id=1-2","output_path":"src/components/FigmaDesign.tsx"}.',
2241
+ };
2242
+ }
2243
+
2244
+ const token = String(input.token || input.access_token || input.accessToken || await this.readProjectEnvValue('FIGMA_TOKEN', cwd) || await this.readProjectEnvValue('FIGMA_ACCESS_TOKEN', cwd) || '').trim();
2245
+ if (!token) {
2246
+ return {
2247
+ success: false,
2248
+ error: 'FIGMA_TOKEN is required for Figma design-to-code',
2249
+ url,
2250
+ recovery: 'Add FIGMA_TOKEN to your project .env or pass token to DesignToCode. Then retry with a selected frame URL that includes node-id.',
2251
+ };
2252
+ }
2253
+
2254
+ const args = ['-y', 'vibefigma', url];
2255
+ const outputPath = input.output_path || input.outputPath || input.component || input.path || input.file;
2256
+ const assetsDir = input.assets_dir || input.assetsDir || input.assets;
2257
+ if (outputPath) args.push('-c', String(outputPath));
2258
+ if (assetsDir) args.push('-a', String(assetsDir));
2259
+ if (input.force) args.push('--force');
2260
+ if (input.no_tailwind || input.noTailwind) args.push('--no-tailwind');
2261
+
2262
+ const command = process.platform === 'win32' ? 'npx.cmd' : 'npx';
2263
+ try {
2264
+ const { stdout, stderr } = await execFileAsync(command, args, {
2265
+ cwd,
2266
+ env: { ...process.env, FIGMA_TOKEN: token },
2267
+ timeout: Number(input.timeout || input.timeout_ms || input.timeoutMs || 180000),
2268
+ maxBuffer: 20 * 1024 * 1024,
2269
+ windowsHide: true,
2270
+ });
2271
+ return {
2272
+ success: true,
2273
+ tool: 'DesignToCode',
2274
+ url,
2275
+ outputPath: outputPath || null,
2276
+ assetsDir: assetsDir || null,
2277
+ command: `npx -y vibefigma "${url}"${outputPath ? ` -c ${outputPath}` : ''}${assetsDir ? ` -a ${assetsDir}` : ''}${input.force ? ' --force' : ''}${input.no_tailwind || input.noTailwind ? ' --no-tailwind' : ''}`,
2278
+ stdout,
2279
+ stderr,
2280
+ recovery: 'Review the generated component, then run the project typecheck/build and clean up responsiveness if needed.',
2281
+ };
2282
+ } catch (error) {
2283
+ return {
2284
+ success: false,
2285
+ tool: 'DesignToCode',
2286
+ url,
2287
+ outputPath: outputPath || null,
2288
+ error: error.message,
2289
+ exitCode: error.code,
2290
+ stdout: error.stdout || '',
2291
+ stderr: error.stderr || '',
2292
+ recovery: 'Check that the Figma URL is a selected frame/component, FIGMA_TOKEN is valid, and network access can reach Figma. Then retry DesignToCode.',
2293
+ };
2294
+ }
2295
+ }
2296
+
2152
2297
  async webFetch(url, prompt) {
2153
2298
  if (typeof url !== 'string' || url.trim() === '') {
2154
2299
  return { success: false, error: 'url is required', recovery: 'Example: WebFetch {"url":"https://example.com"}. Include the https:// scheme.' };