@xenonbyte/da-vinci-workflow 0.2.1 → 0.2.2
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/CHANGELOG.md +17 -0
- package/README.md +21 -10
- package/README.zh-CN.md +21 -10
- package/bin/da-vinci-tui.js +8 -0
- package/docs/dv-command-reference.md +4 -0
- package/docs/prompt-entrypoints.md +2 -0
- package/docs/skill-usage.md +217 -0
- package/docs/workflow-overview.md +1 -0
- package/docs/zh-CN/dv-command-reference.md +4 -0
- package/docs/zh-CN/prompt-entrypoints.md +2 -0
- package/docs/zh-CN/skill-usage.md +217 -0
- package/docs/zh-CN/workflow-overview.md +1 -0
- package/lib/cli.js +23 -0
- package/package.json +4 -2
- package/tui/catalog.js +1190 -0
- package/tui/index.js +727 -0
package/tui/index.js
ADDED
|
@@ -0,0 +1,727 @@
|
|
|
1
|
+
const path = require("path");
|
|
2
|
+
const readline = require("readline");
|
|
3
|
+
const { spawnSync } = require("child_process");
|
|
4
|
+
const {
|
|
5
|
+
COMMANDS,
|
|
6
|
+
STAGES,
|
|
7
|
+
buildCommandArgs,
|
|
8
|
+
buildDisplayCommand,
|
|
9
|
+
getCommandParameterItems,
|
|
10
|
+
getCommandById,
|
|
11
|
+
getDefaultLanguage,
|
|
12
|
+
getStageById,
|
|
13
|
+
hasPlaceholders,
|
|
14
|
+
normalizeLanguage,
|
|
15
|
+
resolveLocalizedText,
|
|
16
|
+
tokenizeCommandLine
|
|
17
|
+
} = require("./catalog");
|
|
18
|
+
|
|
19
|
+
const DA_VINCI_BIN = path.resolve(__dirname, "..", "bin", "da-vinci.js");
|
|
20
|
+
|
|
21
|
+
const HELP_TEXT = {
|
|
22
|
+
en: [
|
|
23
|
+
"Da Vinci TUI",
|
|
24
|
+
"",
|
|
25
|
+
"Usage:",
|
|
26
|
+
" da-vinci tui [--project <path>] [--change <id>] [--lang en|zh] [--strict] [--json] [--continue-on-error]",
|
|
27
|
+
" da-vinci-tui [--project <path>] [--change <id>] [--lang en|zh] [--strict] [--json] [--continue-on-error]",
|
|
28
|
+
"",
|
|
29
|
+
"Keyboard:",
|
|
30
|
+
" Up/Down or j/k move selection",
|
|
31
|
+
" Enter or r run selected command",
|
|
32
|
+
" m edit the preview command before running",
|
|
33
|
+
" h show parameters for the selected command",
|
|
34
|
+
" p set project path context",
|
|
35
|
+
" c set change id context",
|
|
36
|
+
" l toggle UI language",
|
|
37
|
+
" s toggle --strict for supported commands",
|
|
38
|
+
" J toggle --json for supported commands",
|
|
39
|
+
" e toggle --continue-on-error for supported commands",
|
|
40
|
+
" ? toggle help overlay",
|
|
41
|
+
" q quit",
|
|
42
|
+
"",
|
|
43
|
+
"Notes:",
|
|
44
|
+
" - specialized commands keep placeholders such as <pen-path>; press m to edit them before execution",
|
|
45
|
+
" - when the TUI starts inside a project root, --project is omitted because CLI commands already fall back to cwd"
|
|
46
|
+
].join("\n"),
|
|
47
|
+
zh: [
|
|
48
|
+
"Da Vinci TUI",
|
|
49
|
+
"",
|
|
50
|
+
"用法:",
|
|
51
|
+
" da-vinci tui [--project <path>] [--change <id>] [--lang en|zh] [--strict] [--json] [--continue-on-error]",
|
|
52
|
+
" da-vinci-tui [--project <path>] [--change <id>] [--lang en|zh] [--strict] [--json] [--continue-on-error]",
|
|
53
|
+
"",
|
|
54
|
+
"键位:",
|
|
55
|
+
" 上/下 或 j/k 移动选中项",
|
|
56
|
+
" Enter 或 r 执行当前命令",
|
|
57
|
+
" m 先编辑预览命令,再执行",
|
|
58
|
+
" h 查看当前命令参数",
|
|
59
|
+
" p 设置项目路径上下文",
|
|
60
|
+
" c 设置 change id 上下文",
|
|
61
|
+
" l 切换中英文界面",
|
|
62
|
+
" s 为支持的命令切换 --strict",
|
|
63
|
+
" J 为支持的命令切换 --json",
|
|
64
|
+
" e 为支持的命令切换 --continue-on-error",
|
|
65
|
+
" ? 打开或关闭帮助层",
|
|
66
|
+
" q 退出",
|
|
67
|
+
"",
|
|
68
|
+
"说明:",
|
|
69
|
+
" - 特殊命令会保留 <pen-path> 这类占位符,执行前按 m 编辑即可",
|
|
70
|
+
" - 当 TUI 在项目根目录启动时,CLI 已默认使用 cwd,所以不会额外拼接 --project"
|
|
71
|
+
].join("\n")
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
function formatTuiHelp(lang) {
|
|
75
|
+
const normalized = normalizeLanguage(lang || getDefaultLanguage());
|
|
76
|
+
return HELP_TEXT[normalized];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function formatCommandParameterHelp(command, lang, width) {
|
|
80
|
+
const items = getCommandParameterItems(command);
|
|
81
|
+
const lines = [];
|
|
82
|
+
const normalized = normalizeLanguage(lang || getDefaultLanguage());
|
|
83
|
+
lines.push(normalized === "zh" ? "命令参数 (h):" : "Command Parameters (h):");
|
|
84
|
+
|
|
85
|
+
if (items.length === 0) {
|
|
86
|
+
lines.push(
|
|
87
|
+
normalized === "zh"
|
|
88
|
+
? "这条命令在 TUI 里没有额外可调参数,直接按预览命令执行即可。"
|
|
89
|
+
: "This command has no extra adjustable parameters in the TUI. Run it exactly as previewed."
|
|
90
|
+
);
|
|
91
|
+
return lines;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
for (const item of items) {
|
|
95
|
+
const signature = [item.flag, item.value].filter(Boolean).join(" ");
|
|
96
|
+
const requirementLabel = item.required
|
|
97
|
+
? normalized === "zh"
|
|
98
|
+
? "必填"
|
|
99
|
+
: "required"
|
|
100
|
+
: normalized === "zh"
|
|
101
|
+
? "可选"
|
|
102
|
+
: "optional";
|
|
103
|
+
const body = `${signature} [${requirementLabel}]: ${resolveLocalizedText(item.description, normalized)}`;
|
|
104
|
+
const wrapped = wrapText(body, Math.max(16, width - 4));
|
|
105
|
+
if (wrapped.length === 0) {
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
lines.push(`- ${wrapped[0]}`);
|
|
109
|
+
for (const line of wrapped.slice(1)) {
|
|
110
|
+
lines.push(` ${line}`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (items.some((item) => item.required)) {
|
|
115
|
+
lines.push(
|
|
116
|
+
normalized === "zh"
|
|
117
|
+
? "提示: 标记为必填的参数通常需要先按 m 编辑命令并补全占位符。"
|
|
118
|
+
: "Hint: parameters marked required usually need you to press m and replace placeholders before running."
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
return lines;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function clearScreen() {
|
|
125
|
+
process.stdout.write("\x1b[2J\x1b[H");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function wrapText(text, width) {
|
|
129
|
+
const source = String(text || "").trim();
|
|
130
|
+
if (!source) {
|
|
131
|
+
return [];
|
|
132
|
+
}
|
|
133
|
+
if (width <= 8) {
|
|
134
|
+
return [source];
|
|
135
|
+
}
|
|
136
|
+
const words = source.split(/\s+/);
|
|
137
|
+
const lines = [];
|
|
138
|
+
let current = "";
|
|
139
|
+
for (const word of words) {
|
|
140
|
+
const candidate = current ? `${current} ${word}` : word;
|
|
141
|
+
if (candidate.length <= width) {
|
|
142
|
+
current = candidate;
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (current) {
|
|
146
|
+
lines.push(current);
|
|
147
|
+
current = word;
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
lines.push(word.slice(0, width));
|
|
151
|
+
current = word.slice(width);
|
|
152
|
+
}
|
|
153
|
+
if (current) {
|
|
154
|
+
lines.push(current);
|
|
155
|
+
}
|
|
156
|
+
return lines;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function truncate(text, width) {
|
|
160
|
+
const source = String(text || "");
|
|
161
|
+
if (source.length <= width) {
|
|
162
|
+
return source;
|
|
163
|
+
}
|
|
164
|
+
if (width <= 1) {
|
|
165
|
+
return source.slice(0, width);
|
|
166
|
+
}
|
|
167
|
+
return `${source.slice(0, Math.max(0, width - 1))}…`;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function buildCatalogRows(lang) {
|
|
171
|
+
const rows = [];
|
|
172
|
+
for (const stage of STAGES) {
|
|
173
|
+
rows.push({
|
|
174
|
+
type: "stage",
|
|
175
|
+
id: stage.id,
|
|
176
|
+
label: resolveLocalizedText(stage.title, lang)
|
|
177
|
+
});
|
|
178
|
+
for (const command of COMMANDS.filter((item) => item.stageId === stage.id)) {
|
|
179
|
+
rows.push({
|
|
180
|
+
type: "command",
|
|
181
|
+
id: command.id,
|
|
182
|
+
label: resolveLocalizedText(command.title, lang),
|
|
183
|
+
command
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return rows;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function countCommandIndex(rows, rowIndex) {
|
|
191
|
+
let count = -1;
|
|
192
|
+
for (let index = 0; index <= rowIndex; index += 1) {
|
|
193
|
+
if (rows[index] && rows[index].type === "command") {
|
|
194
|
+
count += 1;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return count;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function getRowIndexForCommand(rows, commandIndex) {
|
|
201
|
+
let seen = -1;
|
|
202
|
+
for (let index = 0; index < rows.length; index += 1) {
|
|
203
|
+
if (rows[index].type === "command") {
|
|
204
|
+
seen += 1;
|
|
205
|
+
if (seen === commandIndex) {
|
|
206
|
+
return index;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return 0;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function getVisibleRows(rows, selectedRowIndex, limit) {
|
|
214
|
+
if (rows.length <= limit) {
|
|
215
|
+
return {
|
|
216
|
+
start: 0,
|
|
217
|
+
items: rows
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
const commandWindowStart = Math.max(0, selectedRowIndex - Math.floor(limit / 2));
|
|
221
|
+
const start = Math.min(commandWindowStart, Math.max(0, rows.length - limit));
|
|
222
|
+
return {
|
|
223
|
+
start,
|
|
224
|
+
items: rows.slice(start, start + limit)
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function buildPreviewCommand(state) {
|
|
229
|
+
const command = getCommandById(state.selectedCommandId);
|
|
230
|
+
return {
|
|
231
|
+
command,
|
|
232
|
+
args: buildCommandArgs(command, state),
|
|
233
|
+
display: buildDisplayCommand(command, state)
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function normalizeEditedCommand(input) {
|
|
238
|
+
const tokens = tokenizeCommandLine(input);
|
|
239
|
+
if (tokens.length === 0) {
|
|
240
|
+
return [];
|
|
241
|
+
}
|
|
242
|
+
if (tokens[0] === "da-vinci") {
|
|
243
|
+
return tokens.slice(1);
|
|
244
|
+
}
|
|
245
|
+
return tokens;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function parseEditedCommandInput(input) {
|
|
249
|
+
try {
|
|
250
|
+
return {
|
|
251
|
+
args: normalizeEditedCommand(input),
|
|
252
|
+
error: null
|
|
253
|
+
};
|
|
254
|
+
} catch (error) {
|
|
255
|
+
return {
|
|
256
|
+
args: null,
|
|
257
|
+
error
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function getExecutionCwd(projectPath) {
|
|
263
|
+
return path.resolve(projectPath || process.cwd());
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function teardownInput(input, onKeypress) {
|
|
267
|
+
if (!input) {
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
if (typeof input.off === "function" && onKeypress) {
|
|
271
|
+
input.off("keypress", onKeypress);
|
|
272
|
+
}
|
|
273
|
+
if (input.isTTY && typeof input.setRawMode === "function") {
|
|
274
|
+
input.setRawMode(false);
|
|
275
|
+
}
|
|
276
|
+
if (typeof input.pause === "function") {
|
|
277
|
+
input.pause();
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function getKeyAction(str, key = {}) {
|
|
282
|
+
if (key.ctrl && key.name === "c") {
|
|
283
|
+
return "quit";
|
|
284
|
+
}
|
|
285
|
+
if (key.name === "q") {
|
|
286
|
+
return "quit";
|
|
287
|
+
}
|
|
288
|
+
if (key.name === "up" || (key.name === "k" && key.shift !== true)) {
|
|
289
|
+
return "move_up";
|
|
290
|
+
}
|
|
291
|
+
if (key.name === "down" || (key.name === "j" && key.shift !== true)) {
|
|
292
|
+
return "move_down";
|
|
293
|
+
}
|
|
294
|
+
if (key.name === "l") {
|
|
295
|
+
return "toggle_lang";
|
|
296
|
+
}
|
|
297
|
+
if (key.name === "h") {
|
|
298
|
+
return "toggle_command_help";
|
|
299
|
+
}
|
|
300
|
+
if (key.name === "s") {
|
|
301
|
+
return "toggle_strict";
|
|
302
|
+
}
|
|
303
|
+
if ((key.name === "j" && key.shift === true) || str === "J") {
|
|
304
|
+
return "toggle_json";
|
|
305
|
+
}
|
|
306
|
+
if (key.name === "e") {
|
|
307
|
+
return "toggle_continue_on_error";
|
|
308
|
+
}
|
|
309
|
+
if (key.name === "p") {
|
|
310
|
+
return "set_project";
|
|
311
|
+
}
|
|
312
|
+
if (key.name === "c") {
|
|
313
|
+
return "set_change";
|
|
314
|
+
}
|
|
315
|
+
if (key.name === "m") {
|
|
316
|
+
return "edit_and_run";
|
|
317
|
+
}
|
|
318
|
+
if (key.name === "return" || key.name === "r") {
|
|
319
|
+
return "run";
|
|
320
|
+
}
|
|
321
|
+
if (str === "?") {
|
|
322
|
+
return "toggle_help";
|
|
323
|
+
}
|
|
324
|
+
return null;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function waitForAnyKey() {
|
|
328
|
+
return new Promise((resolve) => {
|
|
329
|
+
const handler = () => {
|
|
330
|
+
process.stdin.off("keypress", handler);
|
|
331
|
+
resolve();
|
|
332
|
+
};
|
|
333
|
+
process.stdin.on("keypress", handler);
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
async function promptInput(state, label, currentValue, options = {}) {
|
|
338
|
+
state.rawModeEnabled = false;
|
|
339
|
+
if (process.stdin.isTTY) {
|
|
340
|
+
process.stdin.setRawMode(false);
|
|
341
|
+
}
|
|
342
|
+
process.stdin.pause();
|
|
343
|
+
const rl = readline.createInterface({
|
|
344
|
+
input: process.stdin,
|
|
345
|
+
output: process.stdout
|
|
346
|
+
});
|
|
347
|
+
const promptLabel = currentValue
|
|
348
|
+
? `${label} [${currentValue}]: `
|
|
349
|
+
: `${label}: `;
|
|
350
|
+
const answer = await new Promise((resolve) => {
|
|
351
|
+
rl.question(promptLabel, (value) => resolve(value));
|
|
352
|
+
});
|
|
353
|
+
rl.close();
|
|
354
|
+
process.stdin.resume();
|
|
355
|
+
if (process.stdin.isTTY) {
|
|
356
|
+
process.stdin.setRawMode(true);
|
|
357
|
+
}
|
|
358
|
+
state.rawModeEnabled = true;
|
|
359
|
+
const normalized = String(answer || "").trim();
|
|
360
|
+
if (!normalized && options.allowBlank) {
|
|
361
|
+
return "";
|
|
362
|
+
}
|
|
363
|
+
return normalized || String(currentValue || "").trim();
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function renderState(state) {
|
|
367
|
+
clearScreen();
|
|
368
|
+
const lang = normalizeLanguage(state.lang);
|
|
369
|
+
const width = process.stdout.columns || 100;
|
|
370
|
+
const rows = buildCatalogRows(lang);
|
|
371
|
+
const selectedRowIndex = getRowIndexForCommand(
|
|
372
|
+
rows,
|
|
373
|
+
COMMANDS.findIndex((command) => command.id === state.selectedCommandId)
|
|
374
|
+
);
|
|
375
|
+
const listHeight = Math.max(10, Math.min(16, (process.stdout.rows || 40) - 20));
|
|
376
|
+
const visible = getVisibleRows(rows, selectedRowIndex, listHeight);
|
|
377
|
+
const preview = buildPreviewCommand(state);
|
|
378
|
+
const stage = getStageById(preview.command.stageId);
|
|
379
|
+
const selectedNumber = COMMANDS.findIndex((command) => command.id === preview.command.id) + 1;
|
|
380
|
+
const lines = [];
|
|
381
|
+
|
|
382
|
+
lines.push(lang === "zh" ? "Da Vinci TUI 终端导航" : "Da Vinci TUI");
|
|
383
|
+
lines.push(
|
|
384
|
+
lang === "zh"
|
|
385
|
+
? `语言: ${lang === "zh" ? "中文" : "English"} (l) | 项目: ${state.projectPath || process.cwd()} (p) | Change: ${
|
|
386
|
+
state.changeId || "auto"
|
|
387
|
+
} (c)`
|
|
388
|
+
: `Language: ${lang === "zh" ? "Chinese" : "English"} (l) | Project: ${
|
|
389
|
+
state.projectPath || process.cwd()
|
|
390
|
+
} (p) | Change: ${state.changeId || "auto"} (c)`
|
|
391
|
+
);
|
|
392
|
+
lines.push(
|
|
393
|
+
lang === "zh"
|
|
394
|
+
? `开关: strict=${state.strict ? "on" : "off"} (s) | json=${state.jsonOutput ? "on" : "off"} (J) | continue-on-error=${
|
|
395
|
+
state.continueOnError ? "on" : "off"
|
|
396
|
+
} (e)`
|
|
397
|
+
: `Flags: strict=${state.strict ? "on" : "off"} (s) | json=${state.jsonOutput ? "on" : "off"} (J) | continue-on-error=${
|
|
398
|
+
state.continueOnError ? "on" : "off"
|
|
399
|
+
} (e)`
|
|
400
|
+
);
|
|
401
|
+
lines.push(
|
|
402
|
+
lang === "zh"
|
|
403
|
+
? "操作: ↑↓/j/k 移动 | Enter/r 执行 | h 参数 | m 编辑命令 | ? 帮助 | q 退出"
|
|
404
|
+
: "Keys: Up/Down/j/k move | Enter/r run | h params | m edit command | ? help | q quit"
|
|
405
|
+
);
|
|
406
|
+
lines.push("");
|
|
407
|
+
lines.push(lang === "zh" ? "命令目录" : "Command Catalog");
|
|
408
|
+
for (let index = 0; index < visible.items.length; index += 1) {
|
|
409
|
+
const row = visible.items[index];
|
|
410
|
+
const absoluteIndex = visible.start + index;
|
|
411
|
+
if (row.type === "stage") {
|
|
412
|
+
lines.push(` [${row.label}]`);
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
const selected = absoluteIndex === selectedRowIndex;
|
|
416
|
+
const prefix = selected ? ">" : " ";
|
|
417
|
+
const title = truncate(row.label, Math.max(16, width - 8));
|
|
418
|
+
lines.push(` ${prefix} ${title}`);
|
|
419
|
+
}
|
|
420
|
+
lines.push("");
|
|
421
|
+
lines.push(
|
|
422
|
+
lang === "zh"
|
|
423
|
+
? `当前命令 (${selectedNumber}/${COMMANDS.length}): ${resolveLocalizedText(preview.command.title, lang)}`
|
|
424
|
+
: `Selected (${selectedNumber}/${COMMANDS.length}): ${resolveLocalizedText(preview.command.title, lang)}`
|
|
425
|
+
);
|
|
426
|
+
lines.push(
|
|
427
|
+
lang === "zh"
|
|
428
|
+
? `阶段: ${resolveLocalizedText(stage.title, lang)}`
|
|
429
|
+
: `Phase: ${resolveLocalizedText(stage.title, lang)}`
|
|
430
|
+
);
|
|
431
|
+
for (const line of wrapText(resolveLocalizedText(preview.command.summary, lang), width - 2)) {
|
|
432
|
+
lines.push(line);
|
|
433
|
+
}
|
|
434
|
+
lines.push("");
|
|
435
|
+
for (const line of wrapText(resolveLocalizedText(preview.command.details, lang), width - 2)) {
|
|
436
|
+
lines.push(line);
|
|
437
|
+
}
|
|
438
|
+
lines.push("");
|
|
439
|
+
lines.push(lang === "zh" ? "预览命令:" : "Preview:");
|
|
440
|
+
for (const line of wrapText(preview.display, width - 2)) {
|
|
441
|
+
lines.push(line);
|
|
442
|
+
}
|
|
443
|
+
if (hasPlaceholders(preview.args)) {
|
|
444
|
+
lines.push(
|
|
445
|
+
lang === "zh"
|
|
446
|
+
? "提示: 当前命令包含占位符,按 m 编辑后再执行。"
|
|
447
|
+
: "Hint: this command still contains placeholders; press m to edit before running."
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
if (!state.commandHelpVisible) {
|
|
451
|
+
lines.push(
|
|
452
|
+
lang === "zh"
|
|
453
|
+
? "提示: 按 h 查看当前命令支持的参数。"
|
|
454
|
+
: "Hint: press h to inspect parameters for the selected command."
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
if (state.lastStatus) {
|
|
458
|
+
lines.push("");
|
|
459
|
+
lines.push(
|
|
460
|
+
lang === "zh"
|
|
461
|
+
? `最近结果: ${state.lastStatus}`
|
|
462
|
+
: `Last result: ${state.lastStatus}`
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
if (state.helpVisible) {
|
|
466
|
+
lines.push("");
|
|
467
|
+
lines.push("-".repeat(Math.max(20, Math.min(width - 2, 48))));
|
|
468
|
+
for (const line of formatTuiHelp(lang).split("\n")) {
|
|
469
|
+
lines.push(line);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
if (state.commandHelpVisible) {
|
|
473
|
+
lines.push("");
|
|
474
|
+
lines.push("-".repeat(Math.max(20, Math.min(width - 2, 48))));
|
|
475
|
+
for (const line of formatCommandParameterHelp(preview.command, lang, width).flat()) {
|
|
476
|
+
lines.push(line);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
process.stdout.write(`${lines.join("\n")}\n`);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
async function runPreview(state, options = {}) {
|
|
484
|
+
const lang = normalizeLanguage(state.lang);
|
|
485
|
+
const preview = buildPreviewCommand(state);
|
|
486
|
+
let args = preview.args;
|
|
487
|
+
|
|
488
|
+
if (options.forceEdit || hasPlaceholders(args)) {
|
|
489
|
+
renderState(state);
|
|
490
|
+
const edited = await promptInput(
|
|
491
|
+
state,
|
|
492
|
+
lang === "zh" ? "编辑命令" : "Edit command",
|
|
493
|
+
preview.display
|
|
494
|
+
);
|
|
495
|
+
if (!edited) {
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
const parsed = parseEditedCommandInput(edited);
|
|
499
|
+
if (parsed.error) {
|
|
500
|
+
clearScreen();
|
|
501
|
+
process.stdout.write(
|
|
502
|
+
`${lang === "zh" ? "命令编辑失败" : "Command edit failed"}\n\n${parsed.error.message || String(parsed.error)}\n\n`
|
|
503
|
+
);
|
|
504
|
+
process.stdout.write(
|
|
505
|
+
`${lang === "zh" ? "按任意键返回 TUI。" : "Press any key to return to the TUI."}\n`
|
|
506
|
+
);
|
|
507
|
+
state.lastStatus = `${lang === "zh" ? "命令编辑失败" : "Command edit failed"}: ${
|
|
508
|
+
parsed.error.message || String(parsed.error)
|
|
509
|
+
}`;
|
|
510
|
+
await waitForAnyKey();
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
args = parsed.args;
|
|
514
|
+
if (args.length === 0) {
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
clearScreen();
|
|
520
|
+
process.stdout.write(
|
|
521
|
+
`${lang === "zh" ? "正在执行" : "Running"}: ${["da-vinci", ...args].join(" ")}\n\n`
|
|
522
|
+
);
|
|
523
|
+
const result = spawnSync(process.execPath, [DA_VINCI_BIN, ...args], {
|
|
524
|
+
cwd: getExecutionCwd(state.projectPath),
|
|
525
|
+
encoding: "utf8",
|
|
526
|
+
maxBuffer: 16 * 1024 * 1024
|
|
527
|
+
});
|
|
528
|
+
if (result.error) {
|
|
529
|
+
process.stdout.write(
|
|
530
|
+
`${lang === "zh" ? "执行失败" : "Execution failed"}: ${result.error.message || String(result.error)}\n\n`
|
|
531
|
+
);
|
|
532
|
+
process.stdout.write(
|
|
533
|
+
`${lang === "zh" ? "按任意键返回 TUI。" : "Press any key to return to the TUI."}\n`
|
|
534
|
+
);
|
|
535
|
+
state.lastStatus = `${["da-vinci", ...args].join(" ")} -> error`;
|
|
536
|
+
await waitForAnyKey();
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
const stdout = String(result.stdout || "");
|
|
540
|
+
const stderr = String(result.stderr || "");
|
|
541
|
+
const combined = [stdout.trim(), stderr.trim()].filter(Boolean).join("\n\n");
|
|
542
|
+
process.stdout.write(`${combined || (lang === "zh" ? "没有输出。" : "No output.")}\n\n`);
|
|
543
|
+
process.stdout.write(
|
|
544
|
+
`${lang === "zh" ? "退出码" : "Exit code"}: ${result.status == null ? "null" : result.status}\n`
|
|
545
|
+
);
|
|
546
|
+
process.stdout.write(
|
|
547
|
+
`${lang === "zh" ? "按任意键返回 TUI。" : "Press any key to return to the TUI."}\n`
|
|
548
|
+
);
|
|
549
|
+
state.lastStatus = `${["da-vinci", ...args].join(" ")} -> ${result.status == null ? "null" : result.status}`;
|
|
550
|
+
await waitForAnyKey();
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
async function launchTui(options = {}) {
|
|
554
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
555
|
+
throw new Error("`da-vinci tui` requires an interactive terminal. Use `--help` for usage.");
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
const initialProject = String(options.projectPath || process.cwd()).trim() || process.cwd();
|
|
559
|
+
const initialChange = String(options.changeId || "").trim();
|
|
560
|
+
const state = {
|
|
561
|
+
lang: normalizeLanguage(options.lang || getDefaultLanguage()),
|
|
562
|
+
projectPath: initialProject,
|
|
563
|
+
changeId: initialChange,
|
|
564
|
+
strict: options.strict === true,
|
|
565
|
+
jsonOutput: options.jsonOutput === true,
|
|
566
|
+
continueOnError: options.continueOnError === true,
|
|
567
|
+
selectedCommandId: COMMANDS[0].id,
|
|
568
|
+
helpVisible: false,
|
|
569
|
+
commandHelpVisible: false,
|
|
570
|
+
lastStatus: "",
|
|
571
|
+
rawModeEnabled: true,
|
|
572
|
+
busy: false
|
|
573
|
+
};
|
|
574
|
+
|
|
575
|
+
readline.emitKeypressEvents(process.stdin);
|
|
576
|
+
process.stdin.setRawMode(true);
|
|
577
|
+
process.stdin.resume();
|
|
578
|
+
|
|
579
|
+
let closed = false;
|
|
580
|
+
return await new Promise((resolve) => {
|
|
581
|
+
const close = () => {
|
|
582
|
+
if (closed) {
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
closed = true;
|
|
586
|
+
teardownInput(process.stdin, onKeypress);
|
|
587
|
+
clearScreen();
|
|
588
|
+
resolve();
|
|
589
|
+
};
|
|
590
|
+
|
|
591
|
+
const moveSelection = (delta) => {
|
|
592
|
+
const currentIndex = COMMANDS.findIndex((command) => command.id === state.selectedCommandId);
|
|
593
|
+
const nextIndex = (currentIndex + delta + COMMANDS.length) % COMMANDS.length;
|
|
594
|
+
state.selectedCommandId = COMMANDS[nextIndex].id;
|
|
595
|
+
};
|
|
596
|
+
|
|
597
|
+
const onKeypress = async (str, key = {}) => {
|
|
598
|
+
if (closed) {
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
if (state.busy) {
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
const action = getKeyAction(str, key);
|
|
605
|
+
if (!action) {
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
if (action === "quit") {
|
|
609
|
+
close();
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
if (action === "move_up") {
|
|
613
|
+
moveSelection(-1);
|
|
614
|
+
renderState(state);
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
if (action === "move_down") {
|
|
618
|
+
moveSelection(1);
|
|
619
|
+
renderState(state);
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
if (action === "toggle_lang") {
|
|
623
|
+
state.lang = state.lang === "zh" ? "en" : "zh";
|
|
624
|
+
renderState(state);
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
if (action === "toggle_command_help") {
|
|
628
|
+
state.commandHelpVisible = !state.commandHelpVisible;
|
|
629
|
+
renderState(state);
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
if (action === "toggle_strict") {
|
|
633
|
+
state.strict = !state.strict;
|
|
634
|
+
renderState(state);
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
if (action === "toggle_json") {
|
|
638
|
+
state.jsonOutput = !state.jsonOutput;
|
|
639
|
+
renderState(state);
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
if (action === "toggle_continue_on_error") {
|
|
643
|
+
state.continueOnError = !state.continueOnError;
|
|
644
|
+
renderState(state);
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
if (action === "set_project") {
|
|
648
|
+
state.busy = true;
|
|
649
|
+
try {
|
|
650
|
+
const value = await promptInput(
|
|
651
|
+
state,
|
|
652
|
+
state.lang === "zh" ? "项目路径" : "Project path",
|
|
653
|
+
state.projectPath
|
|
654
|
+
);
|
|
655
|
+
state.projectPath = value || process.cwd();
|
|
656
|
+
} finally {
|
|
657
|
+
state.busy = false;
|
|
658
|
+
}
|
|
659
|
+
renderState(state);
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
if (action === "set_change") {
|
|
663
|
+
state.busy = true;
|
|
664
|
+
try {
|
|
665
|
+
const value = await promptInput(
|
|
666
|
+
state,
|
|
667
|
+
state.lang === "zh" ? "Change ID(留空自动推断)" : "Change id (leave blank for auto)",
|
|
668
|
+
state.changeId,
|
|
669
|
+
{ allowBlank: true }
|
|
670
|
+
);
|
|
671
|
+
state.changeId = value;
|
|
672
|
+
} finally {
|
|
673
|
+
state.busy = false;
|
|
674
|
+
}
|
|
675
|
+
renderState(state);
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
if (action === "edit_and_run") {
|
|
679
|
+
state.busy = true;
|
|
680
|
+
try {
|
|
681
|
+
await runPreview(state, { forceEdit: true });
|
|
682
|
+
} finally {
|
|
683
|
+
state.busy = false;
|
|
684
|
+
}
|
|
685
|
+
renderState(state);
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
if (action === "run") {
|
|
689
|
+
state.busy = true;
|
|
690
|
+
try {
|
|
691
|
+
await runPreview(state, { forceEdit: false });
|
|
692
|
+
} finally {
|
|
693
|
+
state.busy = false;
|
|
694
|
+
}
|
|
695
|
+
renderState(state);
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
if (action === "toggle_help") {
|
|
699
|
+
state.helpVisible = !state.helpVisible;
|
|
700
|
+
renderState(state);
|
|
701
|
+
}
|
|
702
|
+
};
|
|
703
|
+
|
|
704
|
+
process.stdin.on("keypress", onKeypress);
|
|
705
|
+
renderState(state);
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
module.exports = {
|
|
710
|
+
COMMANDS,
|
|
711
|
+
STAGES,
|
|
712
|
+
buildCommandArgs,
|
|
713
|
+
buildDisplayCommand,
|
|
714
|
+
formatCommandParameterHelp,
|
|
715
|
+
formatTuiHelp,
|
|
716
|
+
getExecutionCwd,
|
|
717
|
+
getCommandParameterItems,
|
|
718
|
+
getCommandById,
|
|
719
|
+
getDefaultLanguage,
|
|
720
|
+
getKeyAction,
|
|
721
|
+
launchTui,
|
|
722
|
+
normalizeEditedCommand,
|
|
723
|
+
parseEditedCommandInput,
|
|
724
|
+
resolveLocalizedText,
|
|
725
|
+
teardownInput,
|
|
726
|
+
tokenizeCommandLine
|
|
727
|
+
};
|