winter-super-cli 2026.6.28 → 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 +2 -65
- package/package.json +1 -1
- package/src/agent/agent-definitions.js +2 -2
- package/src/agent/runtime.js +1 -0
- package/src/ai/provider-adapters.js +2 -0
- package/src/cli/config.js +7 -4
- package/src/cli/input-controller.js +53 -42
- package/src/cli/prompt-builder.js +2 -1
- package/src/cli/repl-commands.js +38 -0
- package/src/cli/repl.js +53 -13
- package/src/cli/slash-commands.js +3 -0
- package/src/design/commands.js +47 -7
- package/src/mcp/presets.js +49 -0
- package/src/tools/executor.js +144 -1
package/design.md
CHANGED
|
@@ -1,66 +1,3 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Nova
|
|
2
2
|
|
|
3
|
-
|
|
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
|
@@ -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
|
};
|
package/src/agent/runtime.js
CHANGED
|
@@ -64,6 +64,7 @@ export class AgentRuntime {
|
|
|
64
64
|
model: executionProfile.model,
|
|
65
65
|
enableTools: true,
|
|
66
66
|
toolPromptOnly: forceTextToolFallback,
|
|
67
|
+
toolChoiceRequired: requireToolEvidence && !usedTools && !forceTextToolFallback,
|
|
67
68
|
requireToolEvidence: requireToolEvidence && !usedTools,
|
|
68
69
|
usedMutatingTools: usedMutatingTools,
|
|
69
70
|
deferFinalContent: this.shouldVerifyBeforeFinal(messages, usedMutatingTools, autoVerificationPassed),
|
|
@@ -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',
|
|
@@ -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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
}
|
|
85
|
+
this.handleGlobalKeypress(str, key);
|
|
86
|
+
});
|
|
87
|
+
}
|
|
89
88
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
89
|
+
handleGlobalKeypress(str, key = {}) {
|
|
90
|
+
const repl = this.repl;
|
|
94
91
|
|
|
95
|
-
|
|
92
|
+
if (repl.interactiveChecklistOpen) {
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
96
95
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
96
|
+
if (key.ctrl && key.name === 'v') {
|
|
97
|
+
void this.handleDirectClipboardPaste();
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
100
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
101
|
+
if (key.name === 'return' && (key.shift || key.meta)) {
|
|
102
|
+
repl.rl.write('\\\n');
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
104
105
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
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.',
|
package/src/cli/repl-commands.js
CHANGED
|
@@ -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) {
|
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
|
|
1493
|
-
|
|
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
|
-
|
|
1498
|
-
|
|
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();
|
|
1527
|
+
|
|
1528
|
+
readline.emitKeypressEvents(process.stdin);
|
|
1529
|
+
process.stdin.on('keypress', onKeyPress);
|
|
1499
1530
|
|
|
1500
|
-
|
|
1501
|
-
|
|
1531
|
+
setImmediate(() => {
|
|
1532
|
+
ignoreInitialReturn = false;
|
|
1533
|
+
});
|
|
1502
1534
|
|
|
1503
|
-
|
|
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,10 +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 continuationAction = /\b(continue|resume|start|begin|tiep|tiếp|bat dau|bắt đầu)\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;
|
|
1967
2003
|
if (continuationAction.test(text)) return true;
|
|
1968
2004
|
|
|
1969
|
-
const strongActionAlone = /\b(fix|debug|deploy|build|test|commit|install|run|refactor|start|begin|sửa|chạy|cài|triển khai|xây dựng|bắt đầu|bat dau)\b/i;
|
|
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;
|
|
1970
2006
|
if (strongActionAlone.test(text)) return true;
|
|
1971
2007
|
|
|
1972
2008
|
return actionPattern.test(text) && targetPattern.test(text);
|
|
@@ -2039,7 +2075,7 @@ ${colors.reset}
|
|
|
2039
2075
|
if (!text.trim()) return false;
|
|
2040
2076
|
|
|
2041
2077
|
// Detect fake completion claims - model says it did something without using tools
|
|
2042
|
-
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;
|
|
2043
2079
|
if (fakeCompletionClaims.test(text)) return true;
|
|
2044
2080
|
|
|
2045
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;
|
|
@@ -2151,6 +2187,10 @@ ${colors.reset}
|
|
|
2151
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.');
|
|
2152
2188
|
}
|
|
2153
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
|
+
|
|
2154
2194
|
if (hints.length === 0) return null;
|
|
2155
2195
|
|
|
2156
2196
|
return `[Tool Router] ${hints.join(' | ')}`;
|
|
@@ -2404,7 +2444,7 @@ ${colors.reset}
|
|
|
2404
2444
|
'/provider', '/model', '/models', '/providers',
|
|
2405
2445
|
'/theme:toggle', '/tui',
|
|
2406
2446
|
'/auto', '/debug', '/doctor', '/context', '/scorecard', '/swe',
|
|
2407
|
-
'/read', '/write', '/glob', '/grep', '/bash', '/browser', '/paste',
|
|
2447
|
+
'/read', '/write', '/glob', '/grep', '/bash', '/browser', '/paste', '/figma', '/design-to-code',
|
|
2408
2448
|
'/codex', '/claude', '/karpathy', '/agents',
|
|
2409
2449
|
'/resources', '/designs', '/skills',
|
|
2410
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'] },
|
package/src/design/commands.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
}
|
package/src/mcp/presets.js
CHANGED
|
@@ -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
|
|
package/src/tools/executor.js
CHANGED
|
@@ -477,6 +477,23 @@ export class ToolExecutor {
|
|
|
477
477
|
}
|
|
478
478
|
}
|
|
479
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
|
+
},
|
|
480
497
|
{
|
|
481
498
|
type: 'function',
|
|
482
499
|
name: 'WebFetch',
|
|
@@ -625,6 +642,8 @@ export class ToolExecutor {
|
|
|
625
642
|
return await this.visibleBrowser(input, cwd);
|
|
626
643
|
case 'OpenBrowser':
|
|
627
644
|
return await this.openBrowser(input.url ?? input.uri ?? input.href, input.browser);
|
|
645
|
+
case 'DesignToCode':
|
|
646
|
+
return await this.designToCode(input, cwd);
|
|
628
647
|
case 'WebFetch':
|
|
629
648
|
return await this.webFetch(input.url ?? input.uri ?? input.href, input.prompt ?? input.query ?? input.extract);
|
|
630
649
|
case 'WebSearch':
|
|
@@ -693,7 +712,7 @@ export class ToolExecutor {
|
|
|
693
712
|
return {
|
|
694
713
|
success: false,
|
|
695
714
|
error: `Unknown tool: ${toolName}`,
|
|
696
|
-
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'],
|
|
697
716
|
recovery: 'Call one of the available tools. For file writes use Write with { "file_path": "...", "content": "..." }. For shell commands use Bash with { "command": "..." }.',
|
|
698
717
|
};
|
|
699
718
|
}
|
|
@@ -852,6 +871,44 @@ export class ToolExecutor {
|
|
|
852
871
|
return { success: true, coerced: true, args: { ...args, url, browser } };
|
|
853
872
|
}
|
|
854
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
|
+
|
|
855
912
|
if (toolName === 'Agent' || toolName === 'DelegateTask') {
|
|
856
913
|
const task = pick('task', 'goal', 'prompt', 'description');
|
|
857
914
|
if (!task) {
|
|
@@ -1171,6 +1228,13 @@ export class ToolExecutor {
|
|
|
1171
1228
|
browserdebug: 'BrowserDebug',
|
|
1172
1229
|
browser: 'BrowserDebug',
|
|
1173
1230
|
browserinspect: 'BrowserDebug',
|
|
1231
|
+
designtocode: 'DesignToCode',
|
|
1232
|
+
designcode: 'DesignToCode',
|
|
1233
|
+
figma: 'DesignToCode',
|
|
1234
|
+
figmatocode: 'DesignToCode',
|
|
1235
|
+
figmacode: 'DesignToCode',
|
|
1236
|
+
vibefigma: 'DesignToCode',
|
|
1237
|
+
figmadesign: 'DesignToCode',
|
|
1174
1238
|
parallel: 'Parallel',
|
|
1175
1239
|
parallelexecute: 'Parallel',
|
|
1176
1240
|
paralleltools: 'Parallel',
|
|
@@ -1247,6 +1311,7 @@ export class ToolExecutor {
|
|
|
1247
1311
|
case 'WebArchive':
|
|
1248
1312
|
case 'BrowserDebug':
|
|
1249
1313
|
case 'VisibleBrowser':
|
|
1314
|
+
case 'DesignToCode':
|
|
1250
1315
|
return { url: value };
|
|
1251
1316
|
case 'TaskCreate':
|
|
1252
1317
|
case 'TodoWrite':
|
|
@@ -2151,6 +2216,84 @@ export class ToolExecutor {
|
|
|
2151
2216
|
};
|
|
2152
2217
|
}
|
|
2153
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
|
+
|
|
2154
2297
|
async webFetch(url, prompt) {
|
|
2155
2298
|
if (typeof url !== 'string' || url.trim() === '') {
|
|
2156
2299
|
return { success: false, error: 'url is required', recovery: 'Example: WebFetch {"url":"https://example.com"}. Include the https:// scheme.' };
|