@xenonbyte/da-vinci-workflow 0.2.1 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +33 -12
  3. package/README.zh-CN.md +34 -12
  4. package/bin/da-vinci-tui.js +8 -0
  5. package/commands/claude/dv/breakdown.md +8 -0
  6. package/commands/claude/dv/build.md +11 -0
  7. package/commands/claude/dv/design.md +5 -2
  8. package/commands/claude/dv/tasks.md +8 -0
  9. package/commands/claude/dv/verify.md +9 -0
  10. package/commands/codex/prompts/dv-breakdown.md +8 -0
  11. package/commands/codex/prompts/dv-build.md +11 -0
  12. package/commands/codex/prompts/dv-design.md +5 -2
  13. package/commands/codex/prompts/dv-tasks.md +8 -0
  14. package/commands/codex/prompts/dv-verify.md +8 -0
  15. package/commands/gemini/dv/breakdown.toml +8 -0
  16. package/commands/gemini/dv/build.toml +11 -0
  17. package/commands/gemini/dv/design.toml +5 -2
  18. package/commands/gemini/dv/tasks.toml +8 -0
  19. package/commands/gemini/dv/verify.toml +8 -0
  20. package/docs/dv-command-reference.md +47 -0
  21. package/docs/execution-chain-plan.md +10 -3
  22. package/docs/mode-use-cases.md +2 -1
  23. package/docs/pencil-rendering-workflow.md +15 -12
  24. package/docs/prompt-entrypoints.md +2 -0
  25. package/docs/prompt-presets/README.md +1 -1
  26. package/docs/prompt-presets/desktop-app.md +3 -3
  27. package/docs/prompt-presets/mobile-app.md +3 -3
  28. package/docs/prompt-presets/tablet-app.md +3 -3
  29. package/docs/prompt-presets/web-app.md +3 -3
  30. package/docs/skill-usage.md +224 -0
  31. package/docs/workflow-examples.md +16 -13
  32. package/docs/workflow-overview.md +3 -0
  33. package/docs/zh-CN/dv-command-reference.md +47 -0
  34. package/docs/zh-CN/mode-use-cases.md +2 -1
  35. package/docs/zh-CN/pencil-rendering-workflow.md +15 -12
  36. package/docs/zh-CN/prompt-entrypoints.md +2 -0
  37. package/docs/zh-CN/prompt-presets/README.md +1 -1
  38. package/docs/zh-CN/prompt-presets/desktop-app.md +3 -3
  39. package/docs/zh-CN/prompt-presets/mobile-app.md +3 -3
  40. package/docs/zh-CN/prompt-presets/tablet-app.md +3 -3
  41. package/docs/zh-CN/prompt-presets/web-app.md +3 -3
  42. package/docs/zh-CN/skill-usage.md +224 -0
  43. package/docs/zh-CN/workflow-examples.md +15 -13
  44. package/docs/zh-CN/workflow-overview.md +3 -0
  45. package/examples/greenfield-spec-markupflow/.da-vinci/state/execution-signals/demo__lint-tasks.json +16 -0
  46. package/lib/audit-parsers.js +18 -9
  47. package/lib/audit.js +3 -26
  48. package/lib/cli.js +72 -0
  49. package/lib/design-source-registry.js +146 -0
  50. package/lib/save-current-design.js +790 -0
  51. package/lib/supervisor-review.js +1 -1
  52. package/lib/workflow-bootstrap.js +2 -13
  53. package/lib/workflow-persisted-state.js +3 -1
  54. package/lib/workflow-state.js +51 -3
  55. package/package.json +4 -2
  56. package/tui/catalog.js +1293 -0
  57. package/tui/index.js +2583 -0
package/tui/index.js ADDED
@@ -0,0 +1,2583 @@
1
+ const fs = require("fs");
2
+ const os = require("os");
3
+ const path = require("path");
4
+ const readline = require("readline");
5
+ const { spawnSync } = require("child_process");
6
+ const { version: PACKAGE_VERSION } = require("../package.json");
7
+ const {
8
+ SCENES,
9
+ COMMANDS,
10
+ buildCommandArgs,
11
+ buildDisplayCommand,
12
+ getCommandParameterItems,
13
+ getCommandById,
14
+ getDefaultLanguage,
15
+ hasPlaceholders,
16
+ normalizeLanguage,
17
+ resolveLocalizedText,
18
+ tokenizeCommandLine
19
+ } = require("./catalog");
20
+
21
+ const DA_VINCI_BIN = path.resolve(__dirname, "..", "bin", "da-vinci.js");
22
+ const PREFERENCES_PATH = path.join(os.homedir(), ".da-vinci", "tui-preferences.json");
23
+ const LOGS_DIR_RELATIVE = path.join(".da-vinci", "logs");
24
+ const BRAND_LOGO_MIN_INNER_WIDTH = 51;
25
+ const MIN_TUI_WIDTH = 36;
26
+ const DEFAULT_TUI_WIDTH = 100;
27
+ const MIN_TUI_HEIGHT = 16;
28
+ const DEFAULT_TUI_HEIGHT = 40;
29
+
30
+ const PROMPT_PRESET_FILES = {
31
+ mobile: "mobile-app.md",
32
+ desktop: "desktop-app.md",
33
+ tablet: "tablet-app.md",
34
+ web: "web-app.md"
35
+ };
36
+
37
+ const PROMPT_MODE_TO_HEADING = {
38
+ mode1: "Simple Redesign",
39
+ mode2: "Complex Redesign",
40
+ mode3: "Redesign With Reference Directory",
41
+ mode4: "Design-Only",
42
+ mode5: "Continue"
43
+ };
44
+
45
+ const QUALITY_TO_VARIANT = {
46
+ normal: 1,
47
+ "high quality": 2,
48
+ masterpiece: 3
49
+ };
50
+
51
+ const DESIGN_PLATFORM_OPTIONS = [
52
+ {
53
+ value: "mobile",
54
+ label: { en: "Mobile", zh: "移动端" }
55
+ },
56
+ {
57
+ value: "desktop",
58
+ label: { en: "Desktop", zh: "桌面端" }
59
+ },
60
+ {
61
+ value: "tablet",
62
+ label: { en: "Tablet", zh: "平板端" }
63
+ },
64
+ {
65
+ value: "web",
66
+ label: { en: "Web", zh: "Web 端" }
67
+ }
68
+ ];
69
+
70
+ const DESIGN_QUALITY_OPTIONS = [
71
+ {
72
+ value: "normal",
73
+ variant: 1,
74
+ label: { en: "Normal", zh: "普通" }
75
+ },
76
+ {
77
+ value: "high quality",
78
+ variant: 2,
79
+ label: { en: "High Quality", zh: "高质量" }
80
+ },
81
+ {
82
+ value: "masterpiece",
83
+ variant: 3,
84
+ label: { en: "Masterpiece", zh: "杰作级" }
85
+ }
86
+ ];
87
+
88
+ const PLATFORM_INSTALL_OPTIONS = [
89
+ {
90
+ value: "codex",
91
+ label: { en: "Codex", zh: "Codex" }
92
+ },
93
+ {
94
+ value: "claude",
95
+ label: { en: "Claude Code", zh: "Claude Code" }
96
+ },
97
+ {
98
+ value: "gemini",
99
+ label: { en: "Gemini", zh: "Gemini" }
100
+ }
101
+ ];
102
+
103
+ const HELP_TEXT = {
104
+ en: [
105
+ "Da Vinci TUI",
106
+ "",
107
+ "Usage:",
108
+ " da-vinci tui [--project <path>] [--change <id>] [--lang en|zh] [--strict] [--json] [--continue-on-error] [--tui-width <cols>] [--alt-screen|--no-alt-screen]",
109
+ " da-vinci-tui [--project <path>] [--change <id>] [--lang en|zh] [--strict] [--json] [--continue-on-error] [--tui-width <cols>] [--alt-screen|--no-alt-screen]",
110
+ "",
111
+ "Model:",
112
+ " - root menu is scene-first, not command-first",
113
+ " - scene pages focus on operator workflows, not raw command browsing",
114
+ " - width defaults to current TTY columns; pin via --tui-width",
115
+ "",
116
+ "Keyboard:",
117
+ " Up/Down or j/k move selection",
118
+ " Enter or r confirm selected menu item",
119
+ " Back/exit use menu items (Back / Exit)",
120
+ " t cycle theme mode",
121
+ " l toggle language",
122
+ " p set project path",
123
+ " c set change id",
124
+ " s toggle --strict",
125
+ " J toggle --json",
126
+ " e toggle --continue-on-error",
127
+ " ? toggle help",
128
+ " Ctrl-C emergency exit"
129
+ ].join("\n"),
130
+ zh: [
131
+ "Da Vinci TUI",
132
+ "",
133
+ "用法:",
134
+ " da-vinci tui [--project <path>] [--change <id>] [--lang en|zh] [--strict] [--json] [--continue-on-error] [--tui-width <cols>] [--alt-screen|--no-alt-screen]",
135
+ " da-vinci-tui [--project <path>] [--change <id>] [--lang en|zh] [--strict] [--json] [--continue-on-error] [--tui-width <cols>] [--alt-screen|--no-alt-screen]",
136
+ "",
137
+ "模型:",
138
+ " - 首屏按场景组织,不再先暴露命令目录",
139
+ " - 场景页聚焦操作流程,不暴露原子命令浏览",
140
+ " - 宽度默认跟随当前 TTY 列数;可用 --tui-width 固定",
141
+ "",
142
+ "键位:",
143
+ " 上/下 或 j/k 移动选中项",
144
+ " Enter 或 r 确认当前菜单项",
145
+ " 返回/退出 通过菜单项(上一步/退出)",
146
+ " t 切换主题",
147
+ " l 切换语言",
148
+ " p 设置项目路径",
149
+ " c 设置 change id",
150
+ " s 切换 --strict",
151
+ " J 切换 --json",
152
+ " e 切换 --continue-on-error",
153
+ " ? 打开或关闭帮助",
154
+ " Ctrl-C 紧急退出"
155
+ ].join("\n")
156
+ };
157
+
158
+ const ANSI_PATTERN = /\x1b\[[0-?]*[ -/]*[@-~]/g;
159
+
160
+ function detectTerminalThemeMode() {
161
+ const forced = String(process.env.DA_VINCI_TUI_THEME || "").trim().toLowerCase();
162
+ if (forced === "light" || forced === "dark" || forced === "mono") {
163
+ return forced;
164
+ }
165
+ const colorfgbg = String(process.env.COLORFGBG || "").trim();
166
+ if (colorfgbg) {
167
+ const tokens = colorfgbg
168
+ .split(";")
169
+ .map((token) => Number.parseInt(token, 10))
170
+ .filter((token) => Number.isFinite(token));
171
+ if (tokens.length > 0) {
172
+ const background = tokens[tokens.length - 1];
173
+ if (background >= 7) {
174
+ return "light";
175
+ }
176
+ return "dark";
177
+ }
178
+ }
179
+ return "dark";
180
+ }
181
+
182
+ function createTheme(mode) {
183
+ if (mode === "light") {
184
+ return {
185
+ reset: "\x1b[0m",
186
+ bold: "\x1b[1m",
187
+ dim: "\x1b[2m",
188
+ textStrong: "\x1b[38;5;234m",
189
+ soft: "\x1b[38;5;238m",
190
+ muted: "\x1b[38;5;240m",
191
+ border: "\x1b[38;5;245m",
192
+ accent: "\x1b[38;5;18m",
193
+ green: "\x1b[38;5;22m",
194
+ amber: "\x1b[38;5;94m",
195
+ red: "\x1b[38;5;88m",
196
+ info: "\x1b[38;5;24m"
197
+ };
198
+ }
199
+ if (mode === "mono") {
200
+ return {
201
+ reset: "\x1b[0m",
202
+ bold: "",
203
+ dim: "",
204
+ textStrong: "",
205
+ soft: "",
206
+ muted: "",
207
+ border: "",
208
+ accent: "",
209
+ green: "",
210
+ amber: "",
211
+ red: "",
212
+ info: ""
213
+ };
214
+ }
215
+ return {
216
+ reset: "\x1b[0m",
217
+ bold: "\x1b[1m",
218
+ dim: "\x1b[2m",
219
+ textStrong: "\x1b[38;5;255m",
220
+ soft: "\x1b[38;5;252m",
221
+ muted: "\x1b[38;5;244m",
222
+ border: "\x1b[38;5;240m",
223
+ accent: "\x1b[38;5;45m",
224
+ green: "\x1b[38;5;78m",
225
+ amber: "\x1b[38;5;221m",
226
+ red: "\x1b[38;5;203m",
227
+ info: "\x1b[38;5;117m"
228
+ };
229
+ }
230
+
231
+ let THEME_MODE = detectTerminalThemeMode();
232
+ let THEME = createTheme(THEME_MODE);
233
+
234
+ function applyThemeMode(mode) {
235
+ THEME_MODE = mode;
236
+ THEME = createTheme(mode);
237
+ }
238
+
239
+ function cycleThemeMode(mode) {
240
+ const ordered = ["dark", "light", "mono"];
241
+ const currentIndex = ordered.indexOf(mode);
242
+ return ordered[(currentIndex + 1 + ordered.length) % ordered.length];
243
+ }
244
+
245
+ function supportsAnsiStyling(stream = process.stdout) {
246
+ return Boolean(stream && stream.isTTY && process.env.NO_COLOR == null);
247
+ }
248
+
249
+ function paint(text, ...styles) {
250
+ if (!supportsAnsiStyling()) {
251
+ return String(text || "");
252
+ }
253
+ return `${styles.join("")}${text}${THEME.reset}`;
254
+ }
255
+
256
+ function stripAnsi(text) {
257
+ return String(text || "").replace(ANSI_PATTERN, "");
258
+ }
259
+
260
+ function visibleLength(text) {
261
+ return stripAnsi(text).length;
262
+ }
263
+
264
+ function truncatePlain(text, width) {
265
+ const source = String(text || "");
266
+ if (source.length <= width) {
267
+ return source;
268
+ }
269
+ if (width <= 1) {
270
+ return source.slice(0, width);
271
+ }
272
+ return `${source.slice(0, Math.max(0, width - 1))}…`;
273
+ }
274
+
275
+ function truncateMiddle(text, width) {
276
+ const source = String(text || "");
277
+ if (source.length <= width) {
278
+ return source;
279
+ }
280
+ if (width <= 5) {
281
+ return truncatePlain(source, width);
282
+ }
283
+ const left = Math.ceil((width - 1) / 2);
284
+ const right = Math.max(1, width - left - 1);
285
+ return `${source.slice(0, left)}…${source.slice(source.length - right)}`;
286
+ }
287
+
288
+ function escapeRegExp(value) {
289
+ return String(value || "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
290
+ }
291
+
292
+ function padVisible(text, width) {
293
+ const source = String(text || "");
294
+ const plain = stripAnsi(source);
295
+ const fitted = plain.length > width ? truncatePlain(plain, width) : source;
296
+ const missing = Math.max(0, width - visibleLength(fitted));
297
+ return `${fitted}${" ".repeat(missing)}`;
298
+ }
299
+
300
+ function wrapText(text, width) {
301
+ const source = String(text || "").trim();
302
+ if (!source) {
303
+ return [];
304
+ }
305
+ if (width <= 8) {
306
+ return [source];
307
+ }
308
+ const words = source.split(/\s+/);
309
+ const lines = [];
310
+ let current = "";
311
+ for (const word of words) {
312
+ const candidate = current ? `${current} ${word}` : word;
313
+ if (candidate.length <= width) {
314
+ current = candidate;
315
+ continue;
316
+ }
317
+ if (current) {
318
+ lines.push(current);
319
+ current = word;
320
+ continue;
321
+ }
322
+ lines.push(word.slice(0, width));
323
+ current = word.slice(width);
324
+ }
325
+ if (current) {
326
+ lines.push(current);
327
+ }
328
+ return lines;
329
+ }
330
+
331
+ function centerPlain(text, width) {
332
+ const source = truncatePlain(String(text || ""), width);
333
+ const missing = Math.max(0, width - source.length);
334
+ const left = Math.floor(missing / 2);
335
+ const right = Math.max(0, missing - left);
336
+ return `${" ".repeat(left)}${source}${" ".repeat(right)}`;
337
+ }
338
+
339
+ function parsePositiveInteger(value) {
340
+ const parsed = Number.parseInt(String(value || "").trim(), 10);
341
+ if (!Number.isFinite(parsed) || parsed <= 0) {
342
+ return null;
343
+ }
344
+ return Math.floor(parsed);
345
+ }
346
+
347
+ function normalizeLayoutWidth(value) {
348
+ const parsed = parsePositiveInteger(value);
349
+ if (!parsed) {
350
+ return null;
351
+ }
352
+ return Math.max(MIN_TUI_WIDTH, parsed);
353
+ }
354
+
355
+ function resolveRenderWidth(state, viewport = {}) {
356
+ const ttyWidth = parsePositiveInteger(viewport.columns) || parsePositiveInteger(process.stdout.columns) || DEFAULT_TUI_WIDTH;
357
+ const explicit =
358
+ normalizeLayoutWidth(viewport.layoutWidth) ||
359
+ normalizeLayoutWidth(state && state.layoutWidth) ||
360
+ normalizeLayoutWidth(process.env.DA_VINCI_TUI_WIDTH) ||
361
+ normalizeLayoutWidth(process.env.DA_VINCI_TUI_LAYOUT_WIDTH);
362
+ const bounded = explicit ? Math.min(ttyWidth, explicit) : ttyWidth;
363
+ return Math.max(MIN_TUI_WIDTH, bounded);
364
+ }
365
+
366
+ function resolveRenderHeight(viewport = {}) {
367
+ const ttyHeight = parsePositiveInteger(viewport.rows) || parsePositiveInteger(process.stdout.rows) || DEFAULT_TUI_HEIGHT;
368
+ return Math.max(MIN_TUI_HEIGHT, ttyHeight);
369
+ }
370
+
371
+ function parseBooleanPreference(value) {
372
+ if (value === true || value === false) {
373
+ return value;
374
+ }
375
+ const normalized = String(value || "").trim().toLowerCase();
376
+ if (["1", "true", "yes", "on"].includes(normalized)) {
377
+ return true;
378
+ }
379
+ if (["0", "false", "no", "off"].includes(normalized)) {
380
+ return false;
381
+ }
382
+ return null;
383
+ }
384
+
385
+ function resolveAltScreenMode(options = {}, preferences = {}) {
386
+ const explicit = parseBooleanPreference(options.altScreen);
387
+ if (explicit !== null) {
388
+ return explicit;
389
+ }
390
+ const persisted = parseBooleanPreference(preferences.altScreen);
391
+ if (persisted !== null) {
392
+ return persisted;
393
+ }
394
+ const fromEnv = parseBooleanPreference(process.env.DA_VINCI_TUI_ALT_SCREEN);
395
+ if (fromEnv !== null) {
396
+ return fromEnv;
397
+ }
398
+ return false;
399
+ }
400
+
401
+ function createPanel(title, lines, width) {
402
+ const panelWidth = Math.max(24, width);
403
+ const innerWidth = panelWidth - 2;
404
+ const top = `${paint("╔", THEME.border)}${paint("═".repeat(innerWidth), THEME.border)}${paint("╗", THEME.border)}`;
405
+ const titleLine = title
406
+ ? `${paint("║", THEME.border)}${paint(centerPlain(title, innerWidth), THEME.accent, THEME.bold)}${paint("║", THEME.border)}`
407
+ : null;
408
+ const divider = title
409
+ ? `${paint("╠", THEME.border)}${paint("═".repeat(innerWidth), THEME.border)}${paint("╣", THEME.border)}`
410
+ : null;
411
+ const body = (lines || []).map((line) => `${paint("║", THEME.border)}${padVisible(line, innerWidth)}${paint("║", THEME.border)}`);
412
+ const bottom = `${paint("╚", THEME.border)}${paint("═".repeat(innerWidth), THEME.border)}${paint("╝", THEME.border)}`;
413
+ return [top, ...(titleLine ? [titleLine, divider] : []), ...body, bottom];
414
+ }
415
+
416
+ function enterAlternateScreen() {
417
+ process.stdout.write("\x1b[?1049h");
418
+ }
419
+
420
+ function exitAlternateScreen() {
421
+ process.stdout.write("\x1b[?1049l");
422
+ }
423
+
424
+ function hideCursor() {
425
+ process.stdout.write("\x1b[?25l");
426
+ }
427
+
428
+ function showCursor() {
429
+ process.stdout.write("\x1b[?25h");
430
+ }
431
+
432
+ function insetBlock(lines, screenWidth, blockWidth, options = {}) {
433
+ const align = options.align || "left";
434
+ const gutter = Number.isFinite(options.gutter) ? Math.max(0, options.gutter) : 0;
435
+ const left = align === "center" ? Math.max(0, Math.floor((screenWidth - blockWidth) / 2)) : Math.max(0, gutter);
436
+ const prefix = " ".repeat(left);
437
+ return (lines || []).map((line) => `${prefix}${line}`);
438
+ }
439
+
440
+ function createMenuLine(label, selected) {
441
+ if (selected) {
442
+ return `${paint("❯", THEME.green, THEME.bold)} ${paint(label, THEME.accent, THEME.bold)}`;
443
+ }
444
+ return `${paint("›", THEME.soft)} ${paint(label, THEME.soft)}`;
445
+ }
446
+
447
+ function formatThemeLabel(mode, lang) {
448
+ if (lang === "zh") {
449
+ if (mode === "light") {
450
+ return "浅色";
451
+ }
452
+ if (mode === "mono") {
453
+ return "单色";
454
+ }
455
+ return "深色";
456
+ }
457
+ return mode;
458
+ }
459
+
460
+ function formatTuiHelp(lang) {
461
+ const normalized = normalizeLanguage(lang || getDefaultLanguage());
462
+ return HELP_TEXT[normalized];
463
+ }
464
+
465
+ function formatCommandParameterHelp(command, lang, width) {
466
+ const normalized = normalizeLanguage(lang || getDefaultLanguage());
467
+ const items = getCommandParameterItems(command);
468
+ const lines = [];
469
+ lines.push(normalized === "zh" ? "命令参数 (h):" : "Command Parameters (h):");
470
+ if (items.length === 0) {
471
+ lines.push(
472
+ normalized === "zh"
473
+ ? "这条命令在 TUI 里没有额外可调参数,直接按预览执行即可。"
474
+ : "This command has no extra adjustable parameters in the TUI. Run it exactly as previewed."
475
+ );
476
+ return lines;
477
+ }
478
+ for (const item of items) {
479
+ const signature = [item.flag, item.value].filter(Boolean).join(" ");
480
+ const requirementLabel = item.required
481
+ ? normalized === "zh"
482
+ ? "必填"
483
+ : "required"
484
+ : normalized === "zh"
485
+ ? "可选"
486
+ : "optional";
487
+ const content = `${signature} [${requirementLabel}]: ${resolveLocalizedText(item.description, normalized)}`;
488
+ const wrapped = wrapText(content, Math.max(16, width - 4));
489
+ if (wrapped.length === 0) {
490
+ continue;
491
+ }
492
+ lines.push(`- ${wrapped[0]}`);
493
+ for (const line of wrapped.slice(1)) {
494
+ lines.push(` ${line}`);
495
+ }
496
+ }
497
+ return lines;
498
+ }
499
+
500
+ function normalizeEditedCommand(input) {
501
+ const tokens = tokenizeCommandLine(input);
502
+ if (tokens.length === 0) {
503
+ return [];
504
+ }
505
+ if (tokens[0] === "da-vinci") {
506
+ return tokens.slice(1);
507
+ }
508
+ return tokens;
509
+ }
510
+
511
+ function parseEditedCommandInput(input) {
512
+ try {
513
+ return { args: normalizeEditedCommand(input), error: null };
514
+ } catch (error) {
515
+ return { args: null, error };
516
+ }
517
+ }
518
+
519
+ function getExecutionCwd(projectPath) {
520
+ return path.resolve(projectPath || process.cwd());
521
+ }
522
+
523
+ function ensureDir(dirPath) {
524
+ fs.mkdirSync(dirPath, { recursive: true });
525
+ }
526
+
527
+ function loadJsonFile(filePath, fallback = null) {
528
+ try {
529
+ return JSON.parse(fs.readFileSync(filePath, "utf8"));
530
+ } catch (_error) {
531
+ return fallback;
532
+ }
533
+ }
534
+
535
+ function saveJsonFile(filePath, value) {
536
+ ensureDir(path.dirname(filePath));
537
+ fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8");
538
+ }
539
+
540
+ function loadPreferences() {
541
+ return loadJsonFile(PREFERENCES_PATH, {});
542
+ }
543
+
544
+ function savePreferences(updates) {
545
+ const current = loadPreferences() || {};
546
+ saveJsonFile(PREFERENCES_PATH, { ...current, ...updates });
547
+ }
548
+
549
+ function formatDateToken(date = new Date()) {
550
+ const y = date.getFullYear();
551
+ const m = `${date.getMonth() + 1}`.padStart(2, "0");
552
+ const d = `${date.getDate()}`.padStart(2, "0");
553
+ return `${y}-${m}-${d}`;
554
+ }
555
+
556
+ function resolveDailyLogPath(projectPath, date = new Date()) {
557
+ return path.join(getExecutionCwd(projectPath), LOGS_DIR_RELATIVE, `${formatDateToken(date)}.ndjson`);
558
+ }
559
+
560
+ function appendLogEvent(state, scene, event, status = "ok", details = "") {
561
+ try {
562
+ const logPath = resolveDailyLogPath(state.projectPath);
563
+ ensureDir(path.dirname(logPath));
564
+ const payload = {
565
+ time: new Date().toISOString(),
566
+ scene,
567
+ event,
568
+ status,
569
+ project: getExecutionCwd(state.projectPath),
570
+ change: String(state.changeId || "auto"),
571
+ details: String(details || "")
572
+ };
573
+ fs.appendFileSync(logPath, `${JSON.stringify(payload)}\n`, "utf8");
574
+ } catch (_error) {
575
+ // Logging is diagnostic-only. Never block interactive flows.
576
+ }
577
+ }
578
+
579
+ function parseJsonEnvelope(output) {
580
+ const text = String(output || "").trim();
581
+ if (!text) {
582
+ return null;
583
+ }
584
+ const start = text.indexOf("{");
585
+ const end = text.lastIndexOf("}");
586
+ if (start < 0 || end < 0 || end <= start) {
587
+ return null;
588
+ }
589
+ try {
590
+ return JSON.parse(text.slice(start, end + 1));
591
+ } catch (_error) {
592
+ return null;
593
+ }
594
+ }
595
+
596
+ function resolveActiveChangeId(projectPath, requestedChangeId) {
597
+ const requested = String(requestedChangeId || "").trim();
598
+ if (requested) {
599
+ return requested;
600
+ }
601
+ const changesRoot = path.join(getExecutionCwd(projectPath), ".da-vinci", "changes");
602
+ if (!fs.existsSync(changesRoot)) {
603
+ return "";
604
+ }
605
+ let dirs = [];
606
+ try {
607
+ dirs = fs
608
+ .readdirSync(changesRoot, { withFileTypes: true })
609
+ .filter((entry) => entry.isDirectory())
610
+ .map((entry) => entry.name)
611
+ .sort();
612
+ } catch (_error) {
613
+ return "";
614
+ }
615
+ const nonEmpty = dirs.filter((name) => {
616
+ const full = path.join(changesRoot, name);
617
+ try {
618
+ return fs.readdirSync(full).length > 0;
619
+ } catch (_error) {
620
+ return false;
621
+ }
622
+ });
623
+ if (nonEmpty.length === 1) {
624
+ return nonEmpty[0];
625
+ }
626
+ return "";
627
+ }
628
+
629
+ function getSceneById(sceneId) {
630
+ return SCENES.find((scene) => scene.id === sceneId) || null;
631
+ }
632
+
633
+ function createHeaderLines(state, lang, width) {
634
+ const innerWidth = Math.max(12, width - 4);
635
+ const lines = [];
636
+
637
+ if (innerWidth >= BRAND_LOGO_MIN_INNER_WIDTH) {
638
+ lines.push(paint(centerPlain("██████╗ █████╗ ██╗ ██╗██╗███╗ ██╗ ██████╗██╗", innerWidth), THEME.textStrong, THEME.bold));
639
+ lines.push(paint(centerPlain("██╔══██╗██╔══██╗ ██║ ██║██║████╗ ██║██╔════╝██║", innerWidth), THEME.textStrong, THEME.bold));
640
+ lines.push(paint(centerPlain("██║ ██║███████║ ██║ ██║██║██╔██╗ ██║██║ ██║", innerWidth), THEME.textStrong, THEME.bold));
641
+ lines.push(paint(centerPlain("██████╔╝██║ ██║ ╚██████╔╝██║██║ ╚████║╚██████╗██║", innerWidth), THEME.textStrong, THEME.bold));
642
+ } else {
643
+ lines.push(paint(centerPlain("DA VINCI", innerWidth), THEME.textStrong, THEME.bold));
644
+ }
645
+ lines.push("");
646
+ lines.push(paint(centerPlain(`Da Vinci Workflow v${PACKAGE_VERSION}`, innerWidth), THEME.accent, THEME.bold));
647
+
648
+ const projectText = truncateMiddle(state.projectPath || process.cwd(), Math.max(16, innerWidth - 12));
649
+ const changeText = resolveHeaderChangeText(state);
650
+ lines.push(paint(`[Project ${projectText}]`, THEME.soft, THEME.bold));
651
+ lines.push(paint(`[Change ${changeText}]`, THEME.soft, THEME.bold));
652
+ return lines;
653
+ }
654
+
655
+ function resolveHeaderChangeText(state) {
656
+ const explicit = String(state && state.changeId ? state.changeId : "").trim();
657
+ if (explicit) {
658
+ return explicit;
659
+ }
660
+ const hasChanges = listExistingChangeIds(state && state.projectPath).length > 0;
661
+ if (!hasChanges) {
662
+ return "none";
663
+ }
664
+ return "unselected";
665
+ }
666
+
667
+ function getSceneActions(state, sceneId) {
668
+ const lang = normalizeLanguage(state.lang);
669
+ const zh = lang === "zh";
670
+
671
+ const sceneActions = {
672
+ "prompt-templates": [
673
+ {
674
+ id: "prompt-mobile",
675
+ label: zh ? "移动端模板" : "Mobile Templates",
676
+ summary: zh ? "选择 mode1-5 并预览/复制英文模板" : "Select mode1-5 and preview/copy canonical English template",
677
+ run: async () => {
678
+ await runPromptTemplateFlow(state, "mobile");
679
+ }
680
+ },
681
+ {
682
+ id: "prompt-desktop",
683
+ label: zh ? "桌面端模板" : "Desktop Templates",
684
+ summary: zh ? "选择 mode1-5 并预览/复制英文模板" : "Select mode1-5 and preview/copy canonical English template",
685
+ run: async () => {
686
+ await runPromptTemplateFlow(state, "desktop");
687
+ }
688
+ },
689
+ {
690
+ id: "prompt-tablet",
691
+ label: zh ? "平板模板" : "Tablet Templates",
692
+ summary: zh ? "选择 mode1-5 并预览/复制英文模板" : "Select mode1-5 and preview/copy canonical English template",
693
+ run: async () => {
694
+ await runPromptTemplateFlow(state, "tablet");
695
+ }
696
+ },
697
+ {
698
+ id: "prompt-web",
699
+ label: zh ? "Web 模板" : "Web Templates",
700
+ summary: zh ? "选择 mode1-5 并预览/复制英文模板" : "Select mode1-5 and preview/copy canonical English template",
701
+ run: async () => {
702
+ await runPromptTemplateFlow(state, "web");
703
+ }
704
+ }
705
+ ],
706
+ "design-work": [
707
+ {
708
+ id: "design-icon-sync",
709
+ label: zh ? "同步图标库" : "Sync Icon Library",
710
+ summary: zh ? "运行 icon-sync" : "Run icon-sync",
711
+ run: async () => {
712
+ await runCommandById(state, "icon-sync", {
713
+ sceneId: "design-work",
714
+ eventName: "icon_sync",
715
+ loadingView: {
716
+ titleZh: "正在同步图标库",
717
+ titleEn: "Syncing Icon Library",
718
+ lines: [
719
+ zh
720
+ ? "该操作需要联网拉取多个图标源,通常比普通命令更慢。"
721
+ : "This operation fetches multiple icon sources over the network and can take longer than usual commands.",
722
+ zh
723
+ ? "完成后会自动进入结果页。"
724
+ : "The result page will appear automatically when sync finishes."
725
+ ]
726
+ }
727
+ });
728
+ }
729
+ },
730
+ {
731
+ id: "design-save-current",
732
+ label: zh ? "保存当前设计稿" : "Save Current Design",
733
+ summary: zh ? "把当前 live 设计回写到项目 .pen" : "Persist the current live design back to project .pen",
734
+ run: async () => {
735
+ await runCommandById(state, "save-current-design", {
736
+ sceneId: "design-work",
737
+ eventName: "save_current_design",
738
+ loadingView: {
739
+ titleZh: "正在保存当前设计稿",
740
+ titleEn: "Saving Current Design",
741
+ lines: [
742
+ zh
743
+ ? "正在从 live 设计会话抓取最新快照并写回项目 .pen 文件。"
744
+ : "Capturing the latest snapshot from the live design session and writing it back to the project .pen file.",
745
+ zh
746
+ ? "完成后会自动显示保存结果(成功或失败)。"
747
+ : "A final result page (success or failure) will be shown automatically."
748
+ ]
749
+ }
750
+ });
751
+ }
752
+ },
753
+ {
754
+ id: "design-quality",
755
+ label: zh ? "Visual Assist 预设" : "Visual Assist Preset",
756
+ summary: zh ? "先选平台再选3档质量,自动写入英文 Visual Assist" : "Pick platform then one of 3 quality levels and auto-write canonical English Visual Assist",
757
+ run: async () => {
758
+ await runDesignQualityFlow(state);
759
+ }
760
+ }
761
+ ],
762
+ "install-uninstall": [
763
+ {
764
+ id: "platform-status",
765
+ label: zh ? "状态" : "Status",
766
+ summary: zh ? "查看 da-vinci 当前安装状态" : "Show current da-vinci installation status",
767
+ run: async () => {
768
+ await runCommandById(state, "status", {
769
+ sceneId: "install-uninstall",
770
+ eventName: "install_uninstall_status"
771
+ });
772
+ }
773
+ },
774
+ {
775
+ id: "platform-install",
776
+ label: zh ? "安装" : "Install",
777
+ summary: zh ? "一键安装所有平台(Codex/Claude Code/Gemini)" : "Install all platforms in one step (Codex/Claude Code/Gemini)",
778
+ run: async () => {
779
+ await runPlatformLifecycleFlow(state, "install");
780
+ }
781
+ },
782
+ {
783
+ id: "platform-uninstall",
784
+ label: zh ? "卸载" : "Uninstall",
785
+ summary: zh ? "一键卸载所有平台(Codex/Claude Code/Gemini)" : "Uninstall all platforms in one step (Codex/Claude Code/Gemini)",
786
+ run: async () => {
787
+ await runPlatformLifecycleFlow(state, "uninstall");
788
+ }
789
+ }
790
+ ],
791
+ more: [
792
+ {
793
+ id: "more-language",
794
+ label: zh ? "语言" : "Language",
795
+ summary: zh ? "设置并持久化 TUI 语言" : "Set and persist TUI language",
796
+ run: async () => {
797
+ await chooseLanguage(state);
798
+ }
799
+ },
800
+ {
801
+ id: "more-logs",
802
+ label: zh ? "日志" : "Logs",
803
+ summary: zh ? "按天查看并复制项目日志" : "View and copy daily project logs",
804
+ run: async () => {
805
+ await showLogsFlow(state);
806
+ }
807
+ }
808
+ ]
809
+ };
810
+
811
+ return sceneActions[sceneId] || [];
812
+ }
813
+
814
+ function getSceneActionIndex(state) {
815
+ const key = state.activeSceneId;
816
+ const value = state.sceneSelectionById[key];
817
+ const actions = getSceneActions(state, state.activeSceneId);
818
+ const menuSize = actions.length + 1;
819
+ if (typeof value === "number" && Number.isFinite(value)) {
820
+ if (menuSize <= 0) {
821
+ return 0;
822
+ }
823
+ return ((value % menuSize) + menuSize) % menuSize;
824
+ }
825
+ return 0;
826
+ }
827
+
828
+ function setSceneActionIndex(state, value) {
829
+ const actions = getSceneActions(state, state.activeSceneId);
830
+ const menuSize = actions.length + 1;
831
+ if (menuSize <= 0) {
832
+ state.sceneSelectionById[state.activeSceneId] = 0;
833
+ return;
834
+ }
835
+ const next = (value + menuSize) % menuSize;
836
+ state.sceneSelectionById[state.activeSceneId] = next;
837
+ }
838
+
839
+ function buildSceneMenuLines(state, lang, width) {
840
+ const lines = [];
841
+ const menuSize = SCENES.length + 1;
842
+ const selectedIndex =
843
+ Number.isFinite(state.rootSelectionIndex) && menuSize > 0
844
+ ? ((state.rootSelectionIndex % menuSize) + menuSize) % menuSize
845
+ : 0;
846
+ for (let index = 0; index < SCENES.length; index += 1) {
847
+ const scene = SCENES[index];
848
+ const selected = selectedIndex === index;
849
+ lines.push(createMenuLine(truncatePlain(resolveLocalizedText(scene.title, lang), Math.max(12, width - 4)), selected));
850
+ }
851
+ lines.push("");
852
+ lines.push(createMenuLine(lang === "zh" ? "退出" : "Exit", selectedIndex === SCENES.length));
853
+ return lines;
854
+ }
855
+
856
+ function buildSceneSummaryLines(state, lang, width) {
857
+ if (state.rootSelectionIndex === SCENES.length) {
858
+ const exitTitle = lang === "zh" ? "退出" : "Exit";
859
+ const exitSummary =
860
+ lang === "zh"
861
+ ? "结束当前 TUI 会话并返回终端。"
862
+ : "Close the current TUI session and return to the terminal.";
863
+ return [paint(exitTitle, THEME.accent, THEME.bold), paint(exitSummary, THEME.soft)];
864
+ }
865
+ const scene = getSceneById(state.activeSceneId) || SCENES[0];
866
+ const lines = [paint(resolveLocalizedText(scene.title, lang), THEME.accent, THEME.bold)];
867
+ for (const line of wrapText(resolveLocalizedText(scene.summary, lang), width)) {
868
+ lines.push(paint(line, THEME.soft));
869
+ }
870
+ return lines;
871
+ }
872
+
873
+ function buildSceneActionsLines(state, lang, width) {
874
+ const actions = getSceneActions(state, state.activeSceneId);
875
+ const selectedIndex = getSceneActionIndex(state);
876
+ const lines = [];
877
+ for (let index = 0; index < actions.length; index += 1) {
878
+ const action = actions[index];
879
+ lines.push(createMenuLine(truncatePlain(action.label, Math.max(12, width - 4)), index === selectedIndex));
880
+ }
881
+ lines.push("");
882
+ lines.push(createMenuLine(lang === "zh" ? "上一步" : "Back", selectedIndex === actions.length));
883
+ return lines;
884
+ }
885
+
886
+ function buildSceneActionSummaryLines(state, lang, width) {
887
+ const actions = getSceneActions(state, state.activeSceneId);
888
+ const selectedIndex = getSceneActionIndex(state);
889
+ if (selectedIndex >= actions.length) {
890
+ return [
891
+ paint(lang === "zh" ? "上一步" : "Back", THEME.accent, THEME.bold),
892
+ paint(
893
+ lang === "zh"
894
+ ? "返回场景选择列表。"
895
+ : "Return to the workflow scene list.",
896
+ THEME.soft
897
+ )
898
+ ];
899
+ }
900
+ const selected = actions[selectedIndex] || null;
901
+ const lines = [];
902
+ if (!selected) {
903
+ lines.push(paint(lang === "zh" ? "当前场景没有可执行动作。" : "No actions configured for this scene.", THEME.soft));
904
+ return lines;
905
+ }
906
+ lines.push(paint(selected.label, THEME.textStrong, THEME.bold));
907
+ for (const line of wrapText(selected.summary, width)) {
908
+ lines.push(paint(line, THEME.soft));
909
+ }
910
+ return lines;
911
+ }
912
+
913
+ function getWorkItemSelectionIndex(state) {
914
+ const choices = Array.isArray(state.workItemChoices) ? state.workItemChoices : [];
915
+ const menuSize = choices.length + 1;
916
+ const value = Number.isFinite(state.workItemSelectionIndex) ? state.workItemSelectionIndex : 0;
917
+ if (menuSize <= 0) {
918
+ return 0;
919
+ }
920
+ return ((value % menuSize) + menuSize) % menuSize;
921
+ }
922
+
923
+ function setWorkItemSelectionIndex(state, value) {
924
+ const choices = Array.isArray(state.workItemChoices) ? state.workItemChoices : [];
925
+ const menuSize = choices.length + 1;
926
+ if (menuSize <= 0) {
927
+ state.workItemSelectionIndex = 0;
928
+ return;
929
+ }
930
+ state.workItemSelectionIndex = ((value % menuSize) + menuSize) % menuSize;
931
+ }
932
+
933
+ function buildWorkItemSwitchLines(state, lang, width) {
934
+ const choices = Array.isArray(state.workItemChoices) ? state.workItemChoices : [];
935
+ const selectedIndex = getWorkItemSelectionIndex(state);
936
+ const lines = [];
937
+ for (let index = 0; index < choices.length; index += 1) {
938
+ const label = choices[index] === state.changeId
939
+ ? `${choices[index]} ${lang === "zh" ? "(当前)" : "(current)"}`
940
+ : choices[index];
941
+ lines.push(createMenuLine(truncatePlain(label, Math.max(12, width - 4)), index === selectedIndex));
942
+ }
943
+ lines.push("");
944
+ lines.push(createMenuLine(lang === "zh" ? "上一步" : "Back", selectedIndex === choices.length));
945
+ return lines;
946
+ }
947
+
948
+ function buildWorkItemSwitchSummaryLines(state, lang, width) {
949
+ const choices = Array.isArray(state.workItemChoices) ? state.workItemChoices : [];
950
+ const selectedIndex = getWorkItemSelectionIndex(state);
951
+ if (selectedIndex >= choices.length) {
952
+ return [
953
+ paint(lang === "zh" ? "上一步" : "Back", THEME.accent, THEME.bold),
954
+ paint(
955
+ lang === "zh"
956
+ ? "返回场景列表。"
957
+ : "Return to the scene list.",
958
+ THEME.soft
959
+ )
960
+ ];
961
+ }
962
+ const selected = choices[selectedIndex] || "";
963
+ const lines = [
964
+ paint(lang === "zh" ? "切换到工作项" : "Switch To Work Item", THEME.accent, THEME.bold),
965
+ paint(selected, THEME.textStrong, THEME.bold)
966
+ ];
967
+ for (const line of wrapText(
968
+ lang === "zh"
969
+ ? "按 Enter 将当前上下文切换到这个 change。"
970
+ : "Press Enter to switch the active context to this change.",
971
+ width
972
+ )) {
973
+ lines.push(paint(line, THEME.soft));
974
+ }
975
+ return lines;
976
+ }
977
+
978
+ function buildScreenLines(state, viewport = {}) {
979
+ const lang = normalizeLanguage(state.lang);
980
+ const width = resolveRenderWidth(state, viewport);
981
+ const height = resolveRenderHeight(viewport);
982
+ const horizontalGutter = width >= 120 ? 3 : width >= 84 ? 2 : 1;
983
+ const contentWidth = Math.max(24, width - horizontalGutter * 2);
984
+ const headerWidth = contentWidth;
985
+ const compactHeight = height < 32;
986
+ const lines = [];
987
+
988
+ lines.push(
989
+ ...insetBlock(
990
+ createPanel("", createHeaderLines(state, lang, headerWidth), headerWidth),
991
+ width,
992
+ headerWidth,
993
+ { align: "left", gutter: horizontalGutter }
994
+ )
995
+ );
996
+ lines.push("");
997
+
998
+ if (state.viewMode === "scene_list") {
999
+ lines.push(
1000
+ ...insetBlock(
1001
+ buildSceneMenuLines(state, lang, contentWidth - 4),
1002
+ width,
1003
+ contentWidth,
1004
+ { align: "left", gutter: horizontalGutter }
1005
+ )
1006
+ );
1007
+ if (!compactHeight) {
1008
+ lines.push("");
1009
+ lines.push(
1010
+ ...insetBlock(
1011
+ createPanel(lang === "zh" ? "场景说明" : "Scene Summary", buildSceneSummaryLines(state, lang, contentWidth - 4), contentWidth),
1012
+ width,
1013
+ contentWidth,
1014
+ { align: "left", gutter: horizontalGutter }
1015
+ )
1016
+ );
1017
+ }
1018
+ } else if (state.viewMode === "scene_actions") {
1019
+ lines.push(
1020
+ ...insetBlock(
1021
+ buildSceneActionsLines(state, lang, contentWidth - 4),
1022
+ width,
1023
+ contentWidth,
1024
+ { align: "left", gutter: horizontalGutter }
1025
+ )
1026
+ );
1027
+ if (!compactHeight) {
1028
+ lines.push("");
1029
+ lines.push(
1030
+ ...insetBlock(
1031
+ createPanel(lang === "zh" ? "动作说明" : "Action Summary", buildSceneActionSummaryLines(state, lang, contentWidth - 4), contentWidth),
1032
+ width,
1033
+ contentWidth,
1034
+ { align: "left", gutter: horizontalGutter }
1035
+ )
1036
+ );
1037
+ }
1038
+ } else if (state.viewMode === "work_item_switch") {
1039
+ lines.push(
1040
+ ...insetBlock(
1041
+ buildWorkItemSwitchLines(state, lang, contentWidth - 4),
1042
+ width,
1043
+ contentWidth,
1044
+ { align: "left", gutter: horizontalGutter }
1045
+ )
1046
+ );
1047
+ if (!compactHeight) {
1048
+ lines.push("");
1049
+ lines.push(
1050
+ ...insetBlock(
1051
+ createPanel(
1052
+ lang === "zh" ? "工作项说明" : "Work Item Summary",
1053
+ buildWorkItemSwitchSummaryLines(state, lang, contentWidth - 4),
1054
+ contentWidth
1055
+ ),
1056
+ width,
1057
+ contentWidth,
1058
+ { align: "left", gutter: horizontalGutter }
1059
+ )
1060
+ );
1061
+ }
1062
+ } else {
1063
+ lines.push(
1064
+ ...insetBlock(
1065
+ buildSceneMenuLines(state, lang, contentWidth - 4),
1066
+ width,
1067
+ contentWidth,
1068
+ { align: "left", gutter: horizontalGutter }
1069
+ )
1070
+ );
1071
+ if (!compactHeight) {
1072
+ lines.push("");
1073
+ lines.push(
1074
+ ...insetBlock(
1075
+ createPanel(lang === "zh" ? "场景说明" : "Scene Summary", buildSceneSummaryLines(state, lang, contentWidth - 4), contentWidth),
1076
+ width,
1077
+ contentWidth,
1078
+ { align: "left", gutter: horizontalGutter }
1079
+ )
1080
+ );
1081
+ }
1082
+ }
1083
+
1084
+ if (state.helpVisible) {
1085
+ lines.push("");
1086
+ lines.push(
1087
+ ...insetBlock(
1088
+ createPanel(lang === "zh" ? "帮助" : "Help", formatTuiHelp(lang).split("\n").slice(1), contentWidth),
1089
+ width,
1090
+ contentWidth,
1091
+ { align: "left", gutter: horizontalGutter }
1092
+ )
1093
+ );
1094
+ }
1095
+
1096
+ if (lines.length <= height) {
1097
+ return lines;
1098
+ }
1099
+ const clipped = lines.slice(0, Math.max(1, height - 1));
1100
+ clipped.push(paint("[output clipped to viewport height]", THEME.muted));
1101
+ return clipped;
1102
+ }
1103
+
1104
+ function clearScreen() {
1105
+ process.stdout.write("\x1b[2J\x1b[3J\x1b[H");
1106
+ }
1107
+
1108
+ function renderState(state) {
1109
+ clearScreen();
1110
+ process.stdout.write(buildScreenLines(state).join("\n"));
1111
+ }
1112
+
1113
+ function teardownInput(input, onKeypress) {
1114
+ if (!input) {
1115
+ return;
1116
+ }
1117
+ if (typeof input.off === "function" && onKeypress) {
1118
+ input.off("keypress", onKeypress);
1119
+ }
1120
+ if (input.isTTY && typeof input.setRawMode === "function") {
1121
+ input.setRawMode(false);
1122
+ }
1123
+ if (typeof input.pause === "function") {
1124
+ input.pause();
1125
+ }
1126
+ }
1127
+
1128
+ function getKeyAction(str, key = {}) {
1129
+ if (key.ctrl && key.name === "c") {
1130
+ return "quit";
1131
+ }
1132
+ if (key.name === "up" || (key.name === "k" && key.shift !== true)) {
1133
+ return "move_up";
1134
+ }
1135
+ if (key.name === "down" || (key.name === "j" && key.shift !== true)) {
1136
+ return "move_down";
1137
+ }
1138
+ if (key.name === "l") {
1139
+ return "toggle_lang";
1140
+ }
1141
+ if (key.name === "t") {
1142
+ return "toggle_theme";
1143
+ }
1144
+ if (key.name === "s") {
1145
+ return "toggle_strict";
1146
+ }
1147
+ if ((key.name === "j" && key.shift === true) || str === "J") {
1148
+ return "toggle_json";
1149
+ }
1150
+ if (key.name === "e") {
1151
+ return "toggle_continue_on_error";
1152
+ }
1153
+ if (key.name === "p") {
1154
+ return "set_project";
1155
+ }
1156
+ if (key.name === "c") {
1157
+ return "set_change";
1158
+ }
1159
+ if (key.name === "return" || key.name === "r") {
1160
+ return "run";
1161
+ }
1162
+ if (str === "?") {
1163
+ return "toggle_help";
1164
+ }
1165
+ return null;
1166
+ }
1167
+
1168
+ function waitForAnyKey() {
1169
+ return new Promise((resolve) => {
1170
+ const handler = () => {
1171
+ process.stdin.off("keypress", handler);
1172
+ resolve();
1173
+ };
1174
+ process.stdin.on("keypress", handler);
1175
+ });
1176
+ }
1177
+
1178
+ async function promptInput(state, label, currentValue, options = {}) {
1179
+ state.rawModeEnabled = false;
1180
+ if (process.stdin.isTTY) {
1181
+ process.stdin.setRawMode(false);
1182
+ }
1183
+ process.stdin.pause();
1184
+ const rl = readline.createInterface({
1185
+ input: process.stdin,
1186
+ output: process.stdout
1187
+ });
1188
+ const promptLabel = currentValue ? `${label} [${currentValue}]: ` : `${label}: `;
1189
+ const answer = await new Promise((resolve) => {
1190
+ rl.question(promptLabel, (value) => resolve(value));
1191
+ });
1192
+ rl.close();
1193
+ process.stdin.resume();
1194
+ if (process.stdin.isTTY) {
1195
+ process.stdin.setRawMode(true);
1196
+ }
1197
+ state.rawModeEnabled = true;
1198
+ const normalized = String(answer || "").trim();
1199
+ if (!normalized && options.allowBlank) {
1200
+ return "";
1201
+ }
1202
+ return normalized || String(currentValue || "").trim();
1203
+ }
1204
+
1205
+ function resolveOptionLabel(option, lang) {
1206
+ const label = option && option.label != null ? option.label : option;
1207
+ if (typeof label === "string") {
1208
+ return label;
1209
+ }
1210
+ return resolveLocalizedText(label, lang);
1211
+ }
1212
+
1213
+ function resolveOptionSummary(option, lang) {
1214
+ if (!option || option.summary == null) {
1215
+ return "";
1216
+ }
1217
+ if (typeof option.summary === "string") {
1218
+ return option.summary;
1219
+ }
1220
+ return resolveLocalizedText(option.summary, lang);
1221
+ }
1222
+
1223
+ function clampOptionIndex(index, size) {
1224
+ if (!Number.isFinite(index)) {
1225
+ return 0;
1226
+ }
1227
+ if (size <= 0) {
1228
+ return 0;
1229
+ }
1230
+ const normalized = index % size;
1231
+ return normalized < 0 ? normalized + size : normalized;
1232
+ }
1233
+
1234
+ function renderPromptHeader(state, lang) {
1235
+ const width = resolveRenderWidth(state, {});
1236
+ const horizontalGutter = width >= 120 ? 3 : width >= 84 ? 2 : 1;
1237
+ const contentWidth = Math.max(24, width - horizontalGutter * 2);
1238
+ const headerLines = insetBlock(
1239
+ createPanel("", createHeaderLines(state, lang, contentWidth), contentWidth),
1240
+ width,
1241
+ contentWidth,
1242
+ { align: "left", gutter: horizontalGutter }
1243
+ );
1244
+ return `${headerLines.join("\n")}\n\n`;
1245
+ }
1246
+
1247
+ async function promptSelection(state, options = {}) {
1248
+ const lang = normalizeLanguage(state.lang);
1249
+ const zh = lang === "zh";
1250
+ const entries = Array.isArray(options.items) ? options.items.filter(Boolean) : [];
1251
+ if (entries.length === 0) {
1252
+ return null;
1253
+ }
1254
+
1255
+ const includeBack = options.includeBack === true;
1256
+ const backLabel = options.backLabel || (zh ? "上一步" : "Back");
1257
+ const showSummary = options.showSummary !== false;
1258
+ const showFooter = options.showFooter !== false;
1259
+ const menuSize = entries.length + (includeBack ? 1 : 0);
1260
+ let selectedIndex = clampOptionIndex(options.initialIndex, menuSize);
1261
+ const title = options.title || (zh ? "请选择" : "Select");
1262
+ const subtitle = options.subtitle || "";
1263
+ const footer = options.footer || (zh ? "Enter 确认,Esc 返回" : "Enter to confirm, Esc to go back");
1264
+
1265
+ const render = () => {
1266
+ clearScreen();
1267
+ process.stdout.write(renderPromptHeader(state, lang));
1268
+ process.stdout.write(`${title}\n\n`);
1269
+ if (subtitle) {
1270
+ process.stdout.write(`${subtitle}\n\n`);
1271
+ }
1272
+ for (let index = 0; index < entries.length; index += 1) {
1273
+ process.stdout.write(`${createMenuLine(resolveOptionLabel(entries[index], lang), index === selectedIndex)}\n`);
1274
+ }
1275
+ if (includeBack) {
1276
+ process.stdout.write("\n");
1277
+ process.stdout.write(`${createMenuLine(backLabel, selectedIndex === entries.length)}\n`);
1278
+ }
1279
+ if (showSummary && selectedIndex < entries.length) {
1280
+ const selected = entries[selectedIndex];
1281
+ const summary = resolveOptionSummary(selected, lang);
1282
+ if (summary) {
1283
+ process.stdout.write(`\n${summary}\n`);
1284
+ }
1285
+ }
1286
+ if (showFooter) {
1287
+ process.stdout.write(`\n${footer}\n`);
1288
+ }
1289
+ };
1290
+
1291
+ return await new Promise((resolve, reject) => {
1292
+ const cleanup = () => {
1293
+ process.stdin.off("keypress", onKeypress);
1294
+ };
1295
+
1296
+ const onKeypress = (str, key = {}) => {
1297
+ if (key.ctrl && key.name === "c") {
1298
+ cleanup();
1299
+ reject(new Error("Interrupted"));
1300
+ return;
1301
+ }
1302
+ if (key.name === "up" || (key.name === "k" && key.shift !== true)) {
1303
+ selectedIndex = (selectedIndex - 1 + menuSize) % menuSize;
1304
+ render();
1305
+ return;
1306
+ }
1307
+ if (key.name === "down" || (key.name === "j" && key.shift !== true)) {
1308
+ selectedIndex = (selectedIndex + 1) % menuSize;
1309
+ render();
1310
+ return;
1311
+ }
1312
+ if (key.name === "return" || key.name === "r") {
1313
+ cleanup();
1314
+ if (includeBack && selectedIndex === entries.length) {
1315
+ resolve(null);
1316
+ return;
1317
+ }
1318
+ resolve(entries[selectedIndex]);
1319
+ return;
1320
+ }
1321
+ if (key.name === "escape" || key.name === "b" || key.name === "q") {
1322
+ cleanup();
1323
+ resolve(null);
1324
+ }
1325
+ };
1326
+
1327
+ process.stdin.on("keypress", onKeypress);
1328
+ render();
1329
+ });
1330
+ }
1331
+
1332
+ function commandArgsWithOverrides(command, state, options = {}) {
1333
+ let args = buildCommandArgs(command, state);
1334
+ if (Array.isArray(options.appendArgs) && options.appendArgs.length > 0) {
1335
+ args = args.concat(options.appendArgs);
1336
+ }
1337
+ return args;
1338
+ }
1339
+
1340
+ function executeDaVinci(state, args) {
1341
+ return spawnSync(process.execPath, [DA_VINCI_BIN, ...args], {
1342
+ cwd: getExecutionCwd(state.projectPath),
1343
+ encoding: "utf8",
1344
+ maxBuffer: 16 * 1024 * 1024
1345
+ });
1346
+ }
1347
+
1348
+ function renderExecutionReport(lang, title, lines) {
1349
+ clearScreen();
1350
+ process.stdout.write(`${title}\n\n`);
1351
+ for (const line of lines) {
1352
+ process.stdout.write(`${line}\n`);
1353
+ }
1354
+ process.stdout.write(`\n${lang === "zh" ? "按任意键返回 TUI。" : "Press any key to return to the TUI."}\n`);
1355
+ }
1356
+
1357
+ function renderExecutionLoading(lang, title, lines = []) {
1358
+ clearScreen();
1359
+ process.stdout.write(`⏳ ${title}\n\n`);
1360
+ for (const line of lines) {
1361
+ process.stdout.write(`${line}\n`);
1362
+ }
1363
+ process.stdout.write(
1364
+ `\n${lang === "zh" ? "命令执行中,请稍候..." : "Command is running, please wait..."}\n`
1365
+ );
1366
+ }
1367
+
1368
+ async function runCommandById(state, commandId, options = {}) {
1369
+ const lang = normalizeLanguage(state.lang);
1370
+ const command = getCommandById(commandId);
1371
+ if (!command) {
1372
+ state.lastStatus = `${commandId} -> missing command`;
1373
+ return;
1374
+ }
1375
+
1376
+ let args = Array.isArray(options.overrideArgs) && options.overrideArgs.length > 0
1377
+ ? options.overrideArgs.slice()
1378
+ : commandArgsWithOverrides(command, state, options);
1379
+ let display = ["da-vinci", ...args].join(" ");
1380
+
1381
+ if (options.forceEdit || hasPlaceholders(args)) {
1382
+ renderState(state);
1383
+ const edited = await promptInput(
1384
+ state,
1385
+ lang === "zh" ? "编辑命令" : "Edit command",
1386
+ display
1387
+ );
1388
+ if (!edited) {
1389
+ state.lastStatus = `${commandId} -> skipped`;
1390
+ return;
1391
+ }
1392
+ const parsed = parseEditedCommandInput(edited);
1393
+ if (parsed.error) {
1394
+ clearScreen();
1395
+ process.stdout.write(
1396
+ `${lang === "zh" ? "命令编辑失败" : "Command edit failed"}\n\n${parsed.error.message || String(parsed.error)}\n\n`
1397
+ );
1398
+ process.stdout.write(`${lang === "zh" ? "按任意键返回 TUI。" : "Press any key to return to the TUI."}\n`);
1399
+ state.lastStatus = `${commandId} -> edit error`;
1400
+ await waitForAnyKey();
1401
+ return;
1402
+ }
1403
+ if (!parsed.args || parsed.args.length === 0) {
1404
+ state.lastStatus = `${commandId} -> skipped`;
1405
+ return;
1406
+ }
1407
+ args = parsed.args;
1408
+ display = ["da-vinci", ...args].join(" ");
1409
+ }
1410
+
1411
+ if (options.loadingView) {
1412
+ const loadingTitle =
1413
+ (lang === "zh" ? options.loadingView.titleZh : options.loadingView.titleEn) ||
1414
+ (lang === "zh" ? "正在执行命令" : "Running Command");
1415
+ const loadingLines = Array.isArray(options.loadingView.lines)
1416
+ ? options.loadingView.lines.filter((line) => String(line || "").trim().length > 0)
1417
+ : [];
1418
+ renderExecutionLoading(lang, loadingTitle, loadingLines);
1419
+ }
1420
+
1421
+ appendLogEvent(state, options.sceneId || "advanced", options.eventName || commandId, "start", display);
1422
+ const result = executeDaVinci(state, args);
1423
+ const stdout = String(result.stdout || "").trim();
1424
+ const stderr = String(result.stderr || "").trim();
1425
+ const merged = [stdout, stderr].filter(Boolean);
1426
+ const statusCode = result.status == null ? "null" : String(result.status);
1427
+ const success = result.status === 0;
1428
+ const reportLines = [
1429
+ lang === "zh" ? `结果: ${success ? "成功" : "失败"}` : `Result: ${success ? "PASS" : "FAIL"}`,
1430
+ `${lang === "zh" ? "退出码" : "Exit code"}: ${statusCode}`,
1431
+ "",
1432
+ ...(merged.length > 0 ? merged : [lang === "zh" ? "没有输出。" : "No output."])
1433
+ ];
1434
+
1435
+ renderExecutionReport(
1436
+ lang,
1437
+ `${lang === "zh" ? "执行结果" : "Execution Result"}: ${display}`,
1438
+ reportLines
1439
+ );
1440
+ await waitForAnyKey();
1441
+
1442
+ state.lastStatus = `${display} -> ${statusCode}`;
1443
+ appendLogEvent(
1444
+ state,
1445
+ options.sceneId || "advanced",
1446
+ options.eventName || commandId,
1447
+ result.status === 0 ? "ok" : "error",
1448
+ `exit=${statusCode}`
1449
+ );
1450
+
1451
+ return {
1452
+ status: result.status,
1453
+ stdout,
1454
+ stderr,
1455
+ args,
1456
+ display,
1457
+ json: parseJsonEnvelope(stdout)
1458
+ };
1459
+ }
1460
+
1461
+ async function runCommandChain(state, sceneId, chain) {
1462
+ const lang = normalizeLanguage(state.lang);
1463
+ const lines = [];
1464
+ let finalStatus = 0;
1465
+
1466
+ for (const step of chain) {
1467
+ const label = step.label;
1468
+ const command = getCommandById(step.commandId);
1469
+ if (!command) {
1470
+ lines.push(`- ${label}: missing command ${step.commandId}`);
1471
+ finalStatus = 1;
1472
+ if (step.stopOnFailure) {
1473
+ break;
1474
+ }
1475
+ continue;
1476
+ }
1477
+
1478
+ const args = commandArgsWithOverrides(command, state, step.options || {});
1479
+ const display = ["da-vinci", ...args].join(" ");
1480
+ appendLogEvent(state, sceneId, step.eventName || step.commandId, "start", display);
1481
+ const result = executeDaVinci(state, args);
1482
+ const statusCode = result.status == null ? "null" : String(result.status);
1483
+ const statusText = result.status === 0 ? "PASS" : "FAIL";
1484
+ lines.push(`- ${label}: ${statusText} (${display})`);
1485
+
1486
+ const stdout = String(result.stdout || "").trim();
1487
+ const stderr = String(result.stderr || "").trim();
1488
+ if (stdout) {
1489
+ for (const line of wrapText(stdout, 100).slice(0, 6)) {
1490
+ lines.push(` ${line}`);
1491
+ }
1492
+ }
1493
+ if (stderr) {
1494
+ for (const line of wrapText(stderr, 100).slice(0, 4)) {
1495
+ lines.push(` ${line}`);
1496
+ }
1497
+ }
1498
+
1499
+ appendLogEvent(state, sceneId, step.eventName || step.commandId, result.status === 0 ? "ok" : "error", `exit=${statusCode}`);
1500
+ if (result.status !== 0) {
1501
+ finalStatus = result.status || 1;
1502
+ if (step.stopOnFailure) {
1503
+ break;
1504
+ }
1505
+ }
1506
+ }
1507
+
1508
+ renderExecutionReport(
1509
+ lang,
1510
+ lang === "zh" ? "组合动作执行结果" : "Compound action execution report",
1511
+ lines
1512
+ );
1513
+ await waitForAnyKey();
1514
+ state.lastStatus = `${sceneId} chain -> ${finalStatus === 0 ? "PASS" : "FAIL"}`;
1515
+ }
1516
+
1517
+ function normalizeHeadingToken(text) {
1518
+ return String(text || "")
1519
+ .toLowerCase()
1520
+ .replace(/[^a-z0-9]+/g, " ")
1521
+ .trim();
1522
+ }
1523
+
1524
+ function findLevelTwoSectionRange(markdown, heading) {
1525
+ const text = String(markdown || "").replace(/\r\n?/g, "\n");
1526
+ const headingPattern = new RegExp(`^##\\s+${escapeRegExp(heading)}\\b.*$`, "m");
1527
+ const headingMatch = headingPattern.exec(text);
1528
+ if (!headingMatch || headingMatch.index == null) {
1529
+ return null;
1530
+ }
1531
+
1532
+ const start = headingMatch.index;
1533
+ const bodyStart = start + headingMatch[0].length;
1534
+ const remainder = text.slice(bodyStart);
1535
+ const nextHeadingMatch = /\n(?=##\s+)/.exec(remainder);
1536
+ const end = nextHeadingMatch ? bodyStart + nextHeadingMatch.index + 1 : text.length;
1537
+
1538
+ return {
1539
+ text,
1540
+ start,
1541
+ end
1542
+ };
1543
+ }
1544
+
1545
+ function readPromptPresetSection(platform, modeKey) {
1546
+ const fileName = PROMPT_PRESET_FILES[platform];
1547
+ const heading = PROMPT_MODE_TO_HEADING[modeKey];
1548
+ if (!fileName || !heading) {
1549
+ throw new Error(`Unknown platform/mode pair: ${platform}/${modeKey}`);
1550
+ }
1551
+
1552
+ const absolutePath = path.join(__dirname, "..", "docs", "prompt-presets", fileName);
1553
+ const markdown = fs.readFileSync(absolutePath, "utf8");
1554
+ const lines = markdown.split(/\r?\n/);
1555
+ const target = normalizeHeadingToken(heading);
1556
+ let start = -1;
1557
+ for (let index = 0; index < lines.length; index += 1) {
1558
+ const match = lines[index].match(/^##\s+(.+)$/);
1559
+ if (!match) {
1560
+ continue;
1561
+ }
1562
+ if (normalizeHeadingToken(match[1]) === target) {
1563
+ start = index + 1;
1564
+ break;
1565
+ }
1566
+ }
1567
+ if (start < 0) {
1568
+ throw new Error(`Preset heading not found: ${heading} in ${fileName}`);
1569
+ }
1570
+
1571
+ let end = lines.length;
1572
+ for (let index = start; index < lines.length; index += 1) {
1573
+ if (/^##\s+/.test(lines[index])) {
1574
+ end = index;
1575
+ break;
1576
+ }
1577
+ }
1578
+ const section = lines.slice(start, end).join("\n");
1579
+ const blockMatch = section.match(/```text\s*\n([\s\S]*?)\n```/i);
1580
+ if (!blockMatch) {
1581
+ throw new Error(`No text code block found in ${fileName} / ${heading}`);
1582
+ }
1583
+ return blockMatch[1].trim();
1584
+ }
1585
+
1586
+ function readVisualAssistVariant(platform, quality) {
1587
+ const fileName = PROMPT_PRESET_FILES[platform];
1588
+ const variant = QUALITY_TO_VARIANT[String(quality || "").toLowerCase()];
1589
+ if (!fileName || !variant) {
1590
+ throw new Error(`Unknown visual-assist selection: ${platform}/${quality}`);
1591
+ }
1592
+
1593
+ const absolutePath = path.join(__dirname, "..", "docs", "visual-assist-presets", fileName);
1594
+ const markdown = fs.readFileSync(absolutePath, "utf8");
1595
+ const lines = markdown.split(/\r?\n/);
1596
+ const headingPattern = new RegExp(`^###\\s+Variant\\s+${variant}\\b`, "i");
1597
+ let start = -1;
1598
+ for (let index = 0; index < lines.length; index += 1) {
1599
+ if (headingPattern.test(lines[index])) {
1600
+ start = index + 1;
1601
+ break;
1602
+ }
1603
+ }
1604
+ if (start < 0) {
1605
+ throw new Error(`Variant heading not found: ${variant} in ${fileName}`);
1606
+ }
1607
+
1608
+ let end = lines.length;
1609
+ for (let index = start; index < lines.length; index += 1) {
1610
+ if (/^###\s+Variant\s+\d+\b/i.test(lines[index])) {
1611
+ end = index;
1612
+ break;
1613
+ }
1614
+ }
1615
+ const section = lines.slice(start, end).join("\n");
1616
+ const blockMatch = section.match(/```md\s*\n([\s\S]*?)\n```/i);
1617
+ if (!blockMatch) {
1618
+ throw new Error(`No markdown code block found in ${fileName} / variant ${variant}`);
1619
+ }
1620
+ return blockMatch[1].trim();
1621
+ }
1622
+
1623
+ function parseVisualAssistOverrides(existingSection) {
1624
+ const overrides = {};
1625
+ const preserveKeys = [
1626
+ "Preferred adapters",
1627
+ "Design-supervisor reviewers",
1628
+ "Require Adapter",
1629
+ "Require Supervisor Review"
1630
+ ];
1631
+ const lines = String(existingSection || "").split(/\r?\n/);
1632
+ for (const key of preserveKeys) {
1633
+ const matcher = new RegExp(`^-\\s*${escapeRegExp(key)}\\s*:\\s*(.+)$`, "i");
1634
+ const matchLine = lines.find((line) => matcher.test(line.trim()));
1635
+ if (!matchLine) {
1636
+ continue;
1637
+ }
1638
+ const valueMatch = matchLine.trim().match(matcher);
1639
+ if (!valueMatch) {
1640
+ continue;
1641
+ }
1642
+ overrides[key] = String(valueMatch[1] || "").trim();
1643
+ }
1644
+ return overrides;
1645
+ }
1646
+
1647
+ function applyVisualAssistOverrides(blockText, overrides) {
1648
+ const lines = String(blockText || "").split(/\r?\n/);
1649
+ const keys = Object.keys(overrides || {});
1650
+ if (keys.length === 0) {
1651
+ return String(blockText || "").trim();
1652
+ }
1653
+ const updated = lines.map((line) => {
1654
+ for (const key of keys) {
1655
+ const matcher = new RegExp(`^(-\\s*${escapeRegExp(key)}\\s*:\\s*)(.+)$`, "i");
1656
+ const match = line.match(matcher);
1657
+ if (!match) {
1658
+ continue;
1659
+ }
1660
+ return `${match[1]}${overrides[key]}`;
1661
+ }
1662
+ return line;
1663
+ });
1664
+ return updated.join("\n").trim();
1665
+ }
1666
+
1667
+ function extractVisualAssistSection(markdown) {
1668
+ const range = findLevelTwoSectionRange(markdown, "Visual Assist");
1669
+ return range ? range.text.slice(range.start, range.end).trim() : "";
1670
+ }
1671
+
1672
+ function applyManagedVisualAssistBlock(originalContent, visualAssistBlock) {
1673
+ const original = String(originalContent || "").replace(/\r\n?/g, "\n");
1674
+ const normalizedBlock = String(visualAssistBlock || "").trim();
1675
+ if (!original.trim()) {
1676
+ return `# DA-VINCI\n\n${normalizedBlock}\n`;
1677
+ }
1678
+
1679
+ const range = findLevelTwoSectionRange(original, "Visual Assist");
1680
+ if (range) {
1681
+ const before = range.text.slice(0, range.start).trimEnd();
1682
+ const after = range.text.slice(range.end).trimStart();
1683
+ if (!before && !after) {
1684
+ return `${normalizedBlock}\n`;
1685
+ }
1686
+ if (!before) {
1687
+ return `${normalizedBlock}\n\n${after}`.replace(/\n{3,}/g, "\n\n").trimEnd() + "\n";
1688
+ }
1689
+ if (!after) {
1690
+ return `${before}\n\n${normalizedBlock}\n`;
1691
+ }
1692
+ return `${before}\n\n${normalizedBlock}\n\n${after}`.replace(/\n{3,}/g, "\n\n").trimEnd() + "\n";
1693
+ }
1694
+
1695
+ const base = original.trimEnd();
1696
+ return `${base}\n\n${normalizedBlock}\n`;
1697
+ }
1698
+
1699
+ function copyToClipboard(text) {
1700
+ const payload = String(text || "");
1701
+ const candidates = [
1702
+ { bin: "pbcopy", args: [] },
1703
+ { bin: "clip", args: [] },
1704
+ { bin: "xclip", args: ["-selection", "clipboard"] },
1705
+ { bin: "xsel", args: ["--clipboard", "--input"] }
1706
+ ];
1707
+ for (const candidate of candidates) {
1708
+ const result = spawnSync(candidate.bin, candidate.args, {
1709
+ input: payload,
1710
+ encoding: "utf8"
1711
+ });
1712
+ if (!result.error && result.status === 0) {
1713
+ return { ok: true, method: candidate.bin };
1714
+ }
1715
+ }
1716
+ return { ok: false, method: "" };
1717
+ }
1718
+
1719
+ async function runPlatformLifecycleFlow(state, commandId) {
1720
+ const lang = normalizeLanguage(state.lang);
1721
+ const zh = lang === "zh";
1722
+ const isInstall = commandId === "install";
1723
+ const selectedPlatforms = PLATFORM_INSTALL_OPTIONS.map((item) => item.value);
1724
+ const platformValue = selectedPlatforms.join(",");
1725
+ await runCommandById(state, commandId, {
1726
+ overrideArgs: [commandId, "--platform", platformValue],
1727
+ sceneId: "install-uninstall",
1728
+ eventName: `${commandId}_all_platforms`,
1729
+ loadingView: {
1730
+ titleZh: isInstall ? "正在安装资产" : "正在卸载资产",
1731
+ titleEn: isInstall ? "Installing Assets" : "Uninstalling Assets",
1732
+ lines: [
1733
+ `${zh ? "目标平台" : "Platforms"}: ${selectedPlatforms.join(", ")}`,
1734
+ zh
1735
+ ? "完成后会自动显示执行结果。"
1736
+ : "The execution result will be shown automatically when done."
1737
+ ]
1738
+ }
1739
+ });
1740
+ }
1741
+
1742
+ async function runPromptTemplateFlow(state, platform) {
1743
+ const lang = normalizeLanguage(state.lang);
1744
+ const zh = lang === "zh";
1745
+ appendLogEvent(state, "prompt-templates", "prompt_template_start", "ok", `platform=${platform}`);
1746
+
1747
+ const modeInput = await promptInput(
1748
+ state,
1749
+ zh ? "选择模式 (mode1-mode5)" : "Select mode (mode1-mode5)",
1750
+ "mode1"
1751
+ );
1752
+ const mode = String(modeInput || "").trim().toLowerCase();
1753
+ if (!PROMPT_MODE_TO_HEADING[mode]) {
1754
+ state.lastStatus = `${platform} prompt -> invalid mode`;
1755
+ appendLogEvent(state, "prompt-templates", "prompt_template_invalid_mode", "error", mode);
1756
+ return;
1757
+ }
1758
+
1759
+ let template = readPromptPresetSection(platform, mode);
1760
+ if (mode === "mode3") {
1761
+ const referenceDir = await promptInput(
1762
+ state,
1763
+ zh
1764
+ ? "<reference-directory>(可留空保留占位符)"
1765
+ : "<reference-directory> (leave blank to keep placeholder)",
1766
+ "",
1767
+ { allowBlank: true }
1768
+ );
1769
+ if (referenceDir) {
1770
+ template = template.replace(/<reference-directory>/g, referenceDir);
1771
+ }
1772
+ }
1773
+
1774
+ clearScreen();
1775
+ process.stdout.write(`${zh ? "模板预览" : "Template Preview"} (${platform} / ${mode})\n\n`);
1776
+ process.stdout.write(`${template}\n\n`);
1777
+ const copyAnswer = await promptInput(
1778
+ state,
1779
+ zh ? "复制到剪贴板? (Y/n)" : "Copy to clipboard? (Y/n)",
1780
+ "Y",
1781
+ { allowBlank: true }
1782
+ );
1783
+
1784
+ const wantsCopy = !String(copyAnswer || "").trim() || !/^n/i.test(String(copyAnswer || "").trim());
1785
+ if (wantsCopy) {
1786
+ const copied = copyToClipboard(template);
1787
+ if (copied.ok) {
1788
+ state.lastStatus = `${platform}/${mode} template copied via ${copied.method}`;
1789
+ appendLogEvent(state, "prompt-templates", "prompt_template_copy", "ok", `${platform}/${mode}`);
1790
+ } else {
1791
+ state.lastStatus = `${platform}/${mode} template ready (clipboard unavailable)`;
1792
+ appendLogEvent(state, "prompt-templates", "prompt_template_copy", "warn", "clipboard unavailable");
1793
+ }
1794
+ } else {
1795
+ state.lastStatus = `${platform}/${mode} template previewed`;
1796
+ }
1797
+
1798
+ process.stdout.write(`${zh ? "按任意键返回 TUI。" : "Press any key to return to the TUI."}\n`);
1799
+ await waitForAnyKey();
1800
+ }
1801
+
1802
+ async function runDesignQualityFlow(state) {
1803
+ const lang = normalizeLanguage(state.lang);
1804
+ const zh = lang === "zh";
1805
+
1806
+ while (true) {
1807
+ const platformChoice = await promptSelection(state, {
1808
+ title: zh ? "Visual Assist 预设:选择平台" : "Visual Assist Preset: Choose Platform",
1809
+ items: DESIGN_PLATFORM_OPTIONS,
1810
+ includeBack: true,
1811
+ showSummary: false,
1812
+ showFooter: false,
1813
+ initialIndex: Number.isFinite(state.designQualityPlatformSelectionIndex) ? state.designQualityPlatformSelectionIndex : 0
1814
+ });
1815
+ if (!platformChoice || !PROMPT_PRESET_FILES[platformChoice.value]) {
1816
+ state.lastStatus = zh ? "Visual Assist 预设已返回上一级" : "Visual Assist preset returned to previous level";
1817
+ appendLogEvent(state, "design-work", "visual_assist_platform_back", "ok", "back");
1818
+ return;
1819
+ }
1820
+ state.designQualityPlatformSelectionIndex = DESIGN_PLATFORM_OPTIONS.findIndex((item) => item.value === platformChoice.value);
1821
+
1822
+ const platform = platformChoice.value;
1823
+
1824
+ const qualityChoice = await promptSelection(state, {
1825
+ title: zh ? "Visual Assist 预设:选择质量" : "Visual Assist Preset: Choose Quality",
1826
+ items: DESIGN_QUALITY_OPTIONS,
1827
+ includeBack: true,
1828
+ showSummary: false,
1829
+ showFooter: false,
1830
+ initialIndex: Number.isFinite(state.designQualityQualitySelectionIndex) ? state.designQualityQualitySelectionIndex : 1
1831
+ });
1832
+ if (!qualityChoice || !QUALITY_TO_VARIANT[qualityChoice.value]) {
1833
+ state.lastStatus = zh ? "Visual Assist 预设:返回平台选择" : "Visual Assist preset: back to platform selection";
1834
+ appendLogEvent(state, "design-work", "visual_assist_quality_back", "ok", "back_to_platform");
1835
+ continue;
1836
+ }
1837
+ state.designQualityQualitySelectionIndex = DESIGN_QUALITY_OPTIONS.findIndex((item) => item.value === qualityChoice.value);
1838
+
1839
+ const quality = qualityChoice.value;
1840
+ const presetBlock = readVisualAssistVariant(platform, quality);
1841
+ const projectRoot = getExecutionCwd(state.projectPath);
1842
+ const daVinciPath = path.join(projectRoot, "DA-VINCI.md");
1843
+ const existingContent = fs.existsSync(daVinciPath) ? fs.readFileSync(daVinciPath, "utf8") : "";
1844
+ const existingSection = extractVisualAssistSection(existingContent);
1845
+ const overrides = parseVisualAssistOverrides(existingSection);
1846
+ const finalBlock = applyVisualAssistOverrides(presetBlock, overrides);
1847
+
1848
+ const nextContent = applyManagedVisualAssistBlock(existingContent, finalBlock);
1849
+ ensureDir(path.dirname(daVinciPath));
1850
+ fs.writeFileSync(daVinciPath, nextContent, "utf8");
1851
+ const variant = qualityChoice.variant || QUALITY_TO_VARIANT[quality];
1852
+ state.lastStatus = `Visual Assist updated (${platform}, variant ${variant}) -> ${daVinciPath}`;
1853
+ appendLogEvent(
1854
+ state,
1855
+ "design-work",
1856
+ "visual_assist_write",
1857
+ "ok",
1858
+ `platform=${platform};quality=${quality};variant=${variant};path=${daVinciPath}`
1859
+ );
1860
+ return;
1861
+ }
1862
+ }
1863
+
1864
+ function mapRouteToScene(route) {
1865
+ const value = String(route || "").trim();
1866
+ if (!value) {
1867
+ return "current-status";
1868
+ }
1869
+ if (value === "/dv:design") {
1870
+ return "design-work";
1871
+ }
1872
+ if (value === "/dv:verify") {
1873
+ return "pre-completion-validation";
1874
+ }
1875
+ if (value === "select-change") {
1876
+ return "switch-work-item";
1877
+ }
1878
+ if (
1879
+ value === "/dv:tasks" ||
1880
+ value === "/dv:build" ||
1881
+ value === "/dv:bootstrap" ||
1882
+ value === "bootstrap-project"
1883
+ ) {
1884
+ return null;
1885
+ }
1886
+ return "current-status";
1887
+ }
1888
+
1889
+ async function runCurrentStatusFlow(state) {
1890
+ const lang = normalizeLanguage(state.lang);
1891
+ const zh = lang === "zh";
1892
+
1893
+ const command = getCommandById("workflow-status");
1894
+ const statusArgs = commandArgsWithOverrides(command, state, { appendArgs: ["--json"] });
1895
+ const workflowStatusResult = executeDaVinci(state, statusArgs);
1896
+ const workflowPayload = parseJsonEnvelope(workflowStatusResult.stdout);
1897
+
1898
+ const nextCommand = getCommandById("next-step");
1899
+ const nextArgs = commandArgsWithOverrides(nextCommand, state, {});
1900
+ const nextStepResult = executeDaVinci(state, nextArgs);
1901
+
1902
+ const stage = workflowPayload && workflowPayload.stage ? String(workflowPayload.stage) : "unknown";
1903
+ const blockers = workflowPayload && Array.isArray(workflowPayload.failures) ? workflowPayload.failures : [];
1904
+ const nextRoute = workflowPayload && workflowPayload.nextStep && workflowPayload.nextStep.route
1905
+ ? String(workflowPayload.nextStep.route)
1906
+ : "";
1907
+ const recommendedScene = mapRouteToScene(nextRoute);
1908
+ const nextStepText = String(nextStepResult.stdout || "")
1909
+ .split(/\r?\n/)
1910
+ .map((line) => line.trimEnd())
1911
+ .filter((line) => line.trim().length > 0);
1912
+
1913
+ const lines = [];
1914
+ lines.push(`${zh ? "当前阶段" : "Stage"}: ${stage}`);
1915
+ lines.push(`${zh ? "阻塞项" : "Blockers"}: ${blockers.length}`);
1916
+ if (blockers.length > 0) {
1917
+ for (const blocker of blockers.slice(0, 5)) {
1918
+ lines.push(`- ${blocker}`);
1919
+ }
1920
+ }
1921
+ if (nextRoute) {
1922
+ lines.push(`${zh ? "建议路由" : "Suggested route"}: ${nextRoute}`);
1923
+ }
1924
+ const sceneRecord = recommendedScene ? getSceneById(recommendedScene) : null;
1925
+ const sceneTitle = sceneRecord
1926
+ ? resolveLocalizedText(sceneRecord.title, lang)
1927
+ : zh
1928
+ ? "直接执行命令"
1929
+ : "Run Recommended CLI Command";
1930
+ lines.push(`${zh ? "建议模块" : "Suggested module"}: ${sceneTitle}`);
1931
+ if (nextStepText.length > 0) {
1932
+ lines.push("");
1933
+ lines.push(zh ? "下一步总结:" : "Next-step summary:");
1934
+ for (const line of nextStepText.slice(0, 8)) {
1935
+ lines.push(`- ${line}`);
1936
+ }
1937
+ }
1938
+
1939
+ renderExecutionReport(
1940
+ lang,
1941
+ zh ? "当前状态" : "Current Status",
1942
+ lines
1943
+ );
1944
+ await waitForAnyKey();
1945
+
1946
+ state.lastStatus = `current-status -> stage=${stage}; route=${nextRoute || "n/a"}`;
1947
+ appendLogEvent(
1948
+ state,
1949
+ "current-status",
1950
+ "current_status",
1951
+ workflowStatusResult.status === 0 && nextStepResult.status === 0 ? "ok" : "error",
1952
+ `stage=${stage};route=${nextRoute}`
1953
+ );
1954
+ state.activeSceneId = "current-status";
1955
+ state.rootSelectionIndex = Math.max(0, SCENES.findIndex((scene) => scene.id === "current-status"));
1956
+ state.viewMode = "scene_list";
1957
+ }
1958
+
1959
+ function getArtifactReadiness(state) {
1960
+ const projectRoot = getExecutionCwd(state.projectPath);
1961
+ const changeId = resolveActiveChangeId(projectRoot, state.changeId);
1962
+ if (!changeId) {
1963
+ return {
1964
+ changeId: "",
1965
+ changeRoot: "",
1966
+ missingTasks: true,
1967
+ missingBindings: true,
1968
+ hasDesign: false,
1969
+ hasBindings: false,
1970
+ hasTasks: false
1971
+ };
1972
+ }
1973
+
1974
+ const changeRoot = path.join(projectRoot, ".da-vinci", "changes", changeId);
1975
+ const hasTasks = fs.existsSync(path.join(changeRoot, "tasks.md"));
1976
+ const hasDesign = fs.existsSync(path.join(changeRoot, "pencil-design.md"));
1977
+ const hasBindings = fs.existsSync(path.join(changeRoot, "pencil-bindings.md"));
1978
+
1979
+ const missingTasks = !hasTasks;
1980
+ const missingBindings = hasDesign && !hasBindings;
1981
+
1982
+ return {
1983
+ changeId,
1984
+ changeRoot,
1985
+ missingTasks,
1986
+ missingBindings,
1987
+ hasDesign,
1988
+ hasBindings,
1989
+ hasTasks
1990
+ };
1991
+ }
1992
+
1993
+ function resolvePreImplementationResult(readiness, stepResults) {
1994
+ const hasBlocker = Boolean(readiness && (readiness.missingTasks || readiness.missingBindings));
1995
+ const hasCommandFailure = (stepResults || []).some((step) => !step || step.status !== 0);
1996
+ const exitCode = hasBlocker || hasCommandFailure ? 1 : 0;
1997
+ const summaryStatus = hasBlocker ? "BLOCK" : hasCommandFailure ? "FAIL" : "PASS";
1998
+ return {
1999
+ hasBlocker,
2000
+ hasCommandFailure,
2001
+ exitCode,
2002
+ summaryStatus,
2003
+ logStatus: exitCode === 0 ? "ok" : "error"
2004
+ };
2005
+ }
2006
+
2007
+ async function runPreImplementationChecks(state) {
2008
+ const lang = normalizeLanguage(state.lang);
2009
+ const zh = lang === "zh";
2010
+ const readiness = getArtifactReadiness(state);
2011
+ const lines = [];
2012
+ const chain = [];
2013
+ const stepResults = [];
2014
+
2015
+ chain.push({
2016
+ commandId: "lint-spec",
2017
+ label: "lint-spec",
2018
+ eventName: "preimpl_lint_spec",
2019
+ options: {},
2020
+ stopOnFailure: false
2021
+ });
2022
+ chain.push({
2023
+ commandId: "scope-check",
2024
+ label: "scope-check",
2025
+ eventName: "preimpl_scope_check",
2026
+ options: {},
2027
+ stopOnFailure: false
2028
+ });
2029
+
2030
+ if (readiness.hasTasks) {
2031
+ chain.push({
2032
+ commandId: "lint-tasks",
2033
+ label: "lint-tasks",
2034
+ eventName: "preimpl_lint_tasks",
2035
+ options: {},
2036
+ stopOnFailure: false
2037
+ });
2038
+ } else {
2039
+ lines.push(zh ? "- BLOCK: 缺少 tasks.md,实施前检查不满足实现就绪。" : "- BLOCK: tasks.md is missing; implementation readiness is not met.");
2040
+ }
2041
+
2042
+ if (readiness.hasBindings) {
2043
+ chain.push({
2044
+ commandId: "lint-bindings",
2045
+ label: "lint-bindings",
2046
+ eventName: "preimpl_lint_bindings",
2047
+ options: {},
2048
+ stopOnFailure: false
2049
+ });
2050
+ } else if (readiness.missingBindings) {
2051
+ lines.push(
2052
+ zh
2053
+ ? "- BLOCK: 已存在 pencil-design.md 但缺少 pencil-bindings.md。"
2054
+ : "- BLOCK: pencil-design.md exists but pencil-bindings.md is missing."
2055
+ );
2056
+ } else {
2057
+ lines.push(
2058
+ zh
2059
+ ? "- SKIP: 当前没有 design/bindings 对,跳过 lint-bindings。"
2060
+ : "- SKIP: no design/bindings pair found, lint-bindings skipped safely."
2061
+ );
2062
+ }
2063
+
2064
+ const reportLines = [];
2065
+ for (const item of chain) {
2066
+ const command = getCommandById(item.commandId);
2067
+ const args = commandArgsWithOverrides(command, state, item.options || {});
2068
+ const display = ["da-vinci", ...args].join(" ");
2069
+ appendLogEvent(state, "pre-implementation-checks", item.eventName, "start", display);
2070
+ const result = executeDaVinci(state, args);
2071
+ const code = result.status == null ? "null" : String(result.status);
2072
+ stepResults.push({
2073
+ commandId: item.commandId,
2074
+ status: result.status
2075
+ });
2076
+ reportLines.push(`- ${item.label}: ${result.status === 0 ? "PASS" : "FAIL"} (${code})`);
2077
+ appendLogEvent(state, "pre-implementation-checks", item.eventName, result.status === 0 ? "ok" : "error", `exit=${code}`);
2078
+ }
2079
+
2080
+ const finalLines = [...lines, ...reportLines];
2081
+ const outcome = resolvePreImplementationResult(readiness, stepResults);
2082
+ renderExecutionReport(
2083
+ lang,
2084
+ zh ? "实施前检查结果" : "Pre-Implementation Check Result",
2085
+ finalLines
2086
+ );
2087
+ await waitForAnyKey();
2088
+
2089
+ state.lastStatus = `pre-implementation -> ${outcome.summaryStatus}`;
2090
+ appendLogEvent(
2091
+ state,
2092
+ "pre-implementation-checks",
2093
+ "preimpl_summary",
2094
+ outcome.logStatus,
2095
+ `missingTasks=${readiness.missingTasks};missingBindings=${readiness.missingBindings};commandFailures=${outcome.hasCommandFailure}`
2096
+ );
2097
+ }
2098
+
2099
+ async function runPreCompletionValidation(state) {
2100
+ await runCommandChain(state, "pre-completion-validation", [
2101
+ {
2102
+ commandId: "verify-bindings",
2103
+ label: "verify-bindings",
2104
+ eventName: "verify_bindings",
2105
+ options: {},
2106
+ stopOnFailure: false
2107
+ },
2108
+ {
2109
+ commandId: "verify-implementation",
2110
+ label: "verify-implementation",
2111
+ eventName: "verify_implementation",
2112
+ options: {},
2113
+ stopOnFailure: false
2114
+ },
2115
+ {
2116
+ commandId: "verify-structure",
2117
+ label: "verify-structure",
2118
+ eventName: "verify_structure",
2119
+ options: {},
2120
+ stopOnFailure: false
2121
+ },
2122
+ {
2123
+ commandId: "verify-coverage",
2124
+ label: "verify-coverage",
2125
+ eventName: "verify_coverage",
2126
+ options: {},
2127
+ stopOnFailure: false
2128
+ }
2129
+ ]);
2130
+ }
2131
+
2132
+ function listExistingChangeIds(projectPath) {
2133
+ const changesRoot = path.join(getExecutionCwd(projectPath), ".da-vinci", "changes");
2134
+ if (!fs.existsSync(changesRoot)) {
2135
+ return [];
2136
+ }
2137
+ try {
2138
+ return fs
2139
+ .readdirSync(changesRoot, { withFileTypes: true })
2140
+ .filter((entry) => entry && entry.isDirectory())
2141
+ .map((entry) => entry.name)
2142
+ .sort();
2143
+ } catch (_error) {
2144
+ return [];
2145
+ }
2146
+ }
2147
+
2148
+ async function chooseExistingChange(state) {
2149
+ const lang = normalizeLanguage(state.lang);
2150
+ const zh = lang === "zh";
2151
+ const changeIds = listExistingChangeIds(state.projectPath);
2152
+ if (changeIds.length === 0) {
2153
+ clearScreen();
2154
+ process.stdout.write(`${zh ? "没有可切换的工作项。" : "No work items found."}\n\n`);
2155
+ process.stdout.write(`${zh ? "当前项目下还没有可供切换的 change。" : "This project does not have any switchable changes yet."}\n\n`);
2156
+ process.stdout.write(`${zh ? "按任意键返回 TUI。" : "Press any key to return to the TUI."}\n`);
2157
+ state.lastStatus = zh ? "暂无可切换工作项" : "no work items available";
2158
+ appendLogEvent(state, "switch-work-item", "change_switch", "warn", "no work items");
2159
+ await waitForAnyKey();
2160
+ return;
2161
+ }
2162
+ state.workItemChoices = changeIds;
2163
+ state.workItemSelectionIndex = Math.max(0, changeIds.findIndex((item) => item === state.changeId));
2164
+ state.viewMode = "work_item_switch";
2165
+ appendLogEvent(state, "switch-work-item", "change_switch_open", "ok", `${changeIds.length} items`);
2166
+ }
2167
+
2168
+ async function chooseLanguage(state) {
2169
+ const lang = normalizeLanguage(state.lang);
2170
+ const zh = lang === "zh";
2171
+ const selected = await promptInput(
2172
+ state,
2173
+ zh ? "语言 (en/zh)" : "Language (en/zh)",
2174
+ state.lang
2175
+ );
2176
+ const next = normalizeLanguage(selected);
2177
+ state.lang = next;
2178
+ savePreferences({ lang: next });
2179
+ state.lastStatus = `language -> ${next}`;
2180
+ appendLogEvent(state, "more", "language_set", "ok", next);
2181
+ }
2182
+
2183
+ function listDailyLogFiles(projectPath) {
2184
+ const logsRoot = path.join(getExecutionCwd(projectPath), LOGS_DIR_RELATIVE);
2185
+ if (!fs.existsSync(logsRoot)) {
2186
+ return [];
2187
+ }
2188
+ let files = [];
2189
+ try {
2190
+ files = fs
2191
+ .readdirSync(logsRoot)
2192
+ .filter((name) => /^\d{4}-\d{2}-\d{2}\.ndjson$/.test(name))
2193
+ .sort()
2194
+ .reverse();
2195
+ } catch (_error) {
2196
+ return [];
2197
+ }
2198
+ return files.map((name) => path.join(logsRoot, name));
2199
+ }
2200
+
2201
+ async function showLogsFlow(state) {
2202
+ const lang = normalizeLanguage(state.lang);
2203
+ const zh = lang === "zh";
2204
+
2205
+ const files = listDailyLogFiles(state.projectPath);
2206
+ if (files.length === 0) {
2207
+ state.lastStatus = zh ? "日志为空" : "No logs found";
2208
+ return;
2209
+ }
2210
+
2211
+ const promptLines = files
2212
+ .slice(0, 10)
2213
+ .map((absolutePath, index) => `${index + 1}. ${path.basename(absolutePath)}`)
2214
+ .join("\n");
2215
+ clearScreen();
2216
+ process.stdout.write(`${zh ? "可用日志" : "Available logs"}\n\n${promptLines}\n\n`);
2217
+
2218
+ const indexRaw = await promptInput(
2219
+ state,
2220
+ zh ? "选择日志编号" : "Pick log number",
2221
+ "1"
2222
+ );
2223
+ const selectedIndex = Math.max(1, Number.parseInt(indexRaw, 10) || 1);
2224
+ const selectedPath = files[Math.min(files.length - 1, selectedIndex - 1)];
2225
+ const content = fs.readFileSync(selectedPath, "utf8");
2226
+
2227
+ clearScreen();
2228
+ process.stdout.write(`${path.basename(selectedPath)}\n\n${content}\n`);
2229
+
2230
+ const copyAnswer = await promptInput(
2231
+ state,
2232
+ zh ? "复制当天日志? (Y/n)" : "Copy this day log? (Y/n)",
2233
+ "Y",
2234
+ { allowBlank: true }
2235
+ );
2236
+
2237
+ const wantsCopy = !String(copyAnswer || "").trim() || !/^n/i.test(String(copyAnswer || "").trim());
2238
+ if (wantsCopy) {
2239
+ const copied = copyToClipboard(content);
2240
+ if (copied.ok) {
2241
+ state.lastStatus = `${path.basename(selectedPath)} copied via ${copied.method}`;
2242
+ appendLogEvent(state, "more", "logs_copy", "ok", path.basename(selectedPath));
2243
+ } else {
2244
+ state.lastStatus = `${path.basename(selectedPath)} ready (clipboard unavailable)`;
2245
+ appendLogEvent(state, "more", "logs_copy", "warn", "clipboard unavailable");
2246
+ }
2247
+ } else {
2248
+ state.lastStatus = `${path.basename(selectedPath)} viewed`;
2249
+ }
2250
+
2251
+ process.stdout.write(`\n${zh ? "按任意键返回 TUI。" : "Press any key to return to the TUI."}\n`);
2252
+ await waitForAnyKey();
2253
+ }
2254
+
2255
+ async function launchTui(options = {}) {
2256
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
2257
+ throw new Error("`da-vinci tui` requires an interactive terminal. Use `--help` for usage.");
2258
+ }
2259
+
2260
+ const preferences = loadPreferences();
2261
+ const initialLang = options.lang ? normalizeLanguage(options.lang) : normalizeLanguage(preferences.lang || getDefaultLanguage());
2262
+ const initialProject = String(options.projectPath || process.cwd()).trim() || process.cwd();
2263
+ const initialChange = String(options.changeId || "").trim();
2264
+ const initialLayoutWidth = normalizeLayoutWidth(
2265
+ options.tuiWidth != null ? options.tuiWidth : options.layoutWidth != null ? options.layoutWidth : preferences.layoutWidth
2266
+ );
2267
+ const useAltScreen = resolveAltScreenMode(options, preferences);
2268
+
2269
+ const state = {
2270
+ lang: initialLang,
2271
+ projectPath: initialProject,
2272
+ changeId: initialChange,
2273
+ layoutWidth: initialLayoutWidth,
2274
+ strict: options.strict === true,
2275
+ jsonOutput: options.jsonOutput === true,
2276
+ continueOnError: options.continueOnError === true,
2277
+ themeMode: THEME_MODE,
2278
+ viewMode: "scene_list",
2279
+ activeSceneId: SCENES[0].id,
2280
+ rootSelectionIndex: 0,
2281
+ sceneSelectionById: {},
2282
+ designQualityPlatformSelectionIndex: 0,
2283
+ designQualityQualitySelectionIndex: 1,
2284
+ helpVisible: false,
2285
+ lastStatus: "",
2286
+ rawModeEnabled: true,
2287
+ busy: false
2288
+ };
2289
+
2290
+ appendLogEvent(state, "system", "tui_start", "ok", `lang=${state.lang};theme=${state.themeMode}`);
2291
+
2292
+ readline.emitKeypressEvents(process.stdin);
2293
+ process.stdin.setRawMode(true);
2294
+ process.stdin.resume();
2295
+ if (useAltScreen) {
2296
+ enterAlternateScreen();
2297
+ }
2298
+ hideCursor();
2299
+
2300
+ let closed = false;
2301
+ return await new Promise((resolve) => {
2302
+ const close = () => {
2303
+ if (closed) {
2304
+ return;
2305
+ }
2306
+ closed = true;
2307
+ appendLogEvent(state, "system", "tui_exit", "ok", "quit");
2308
+ teardownInput(process.stdin, onKeypress);
2309
+ if (useAltScreen) {
2310
+ exitAlternateScreen();
2311
+ }
2312
+ showCursor();
2313
+ resolve();
2314
+ };
2315
+
2316
+ const moveSelection = (delta) => {
2317
+ if (state.viewMode === "scene_list") {
2318
+ const menuSize = SCENES.length + 1;
2319
+ const currentIndex = Number.isFinite(state.rootSelectionIndex) ? state.rootSelectionIndex : 0;
2320
+ const nextIndex = (currentIndex + delta + menuSize) % menuSize;
2321
+ state.rootSelectionIndex = nextIndex;
2322
+ if (nextIndex < SCENES.length) {
2323
+ state.activeSceneId = SCENES[nextIndex].id;
2324
+ }
2325
+ return;
2326
+ }
2327
+
2328
+ if (state.viewMode === "scene_actions") {
2329
+ const next = getSceneActionIndex(state) + delta;
2330
+ setSceneActionIndex(state, next);
2331
+ return;
2332
+ }
2333
+
2334
+ if (state.viewMode === "work_item_switch") {
2335
+ const next = getWorkItemSelectionIndex(state) + delta;
2336
+ setWorkItemSelectionIndex(state, next);
2337
+ }
2338
+ };
2339
+
2340
+ const onKeypress = async (str, key = {}) => {
2341
+ if (closed || state.busy) {
2342
+ return;
2343
+ }
2344
+ const action = getKeyAction(str, key);
2345
+ if (!action) {
2346
+ return;
2347
+ }
2348
+
2349
+ if (action === "quit") {
2350
+ close();
2351
+ return;
2352
+ }
2353
+ if (action === "move_up") {
2354
+ moveSelection(-1);
2355
+ renderState(state);
2356
+ return;
2357
+ }
2358
+ if (action === "move_down") {
2359
+ moveSelection(1);
2360
+ renderState(state);
2361
+ return;
2362
+ }
2363
+ if (action === "toggle_lang") {
2364
+ state.lang = state.lang === "zh" ? "en" : "zh";
2365
+ savePreferences({ lang: state.lang });
2366
+ state.lastStatus = `language -> ${state.lang}`;
2367
+ appendLogEvent(state, "system", "language_toggle", "ok", state.lang);
2368
+ renderState(state);
2369
+ return;
2370
+ }
2371
+ if (action === "toggle_theme") {
2372
+ state.themeMode = cycleThemeMode(state.themeMode);
2373
+ applyThemeMode(state.themeMode);
2374
+ state.lastStatus = `theme -> ${state.themeMode}`;
2375
+ appendLogEvent(state, "system", "theme_toggle", "ok", state.themeMode);
2376
+ renderState(state);
2377
+ return;
2378
+ }
2379
+ if (action === "toggle_strict") {
2380
+ state.strict = !state.strict;
2381
+ appendLogEvent(state, "system", "strict_toggle", "ok", String(state.strict));
2382
+ renderState(state);
2383
+ return;
2384
+ }
2385
+ if (action === "toggle_json") {
2386
+ state.jsonOutput = !state.jsonOutput;
2387
+ appendLogEvent(state, "system", "json_toggle", "ok", String(state.jsonOutput));
2388
+ renderState(state);
2389
+ return;
2390
+ }
2391
+ if (action === "toggle_continue_on_error") {
2392
+ state.continueOnError = !state.continueOnError;
2393
+ appendLogEvent(state, "system", "continue_toggle", "ok", String(state.continueOnError));
2394
+ renderState(state);
2395
+ return;
2396
+ }
2397
+ if (action === "set_project") {
2398
+ state.busy = true;
2399
+ try {
2400
+ const value = await promptInput(
2401
+ state,
2402
+ state.lang === "zh" ? "项目路径" : "Project path",
2403
+ state.projectPath
2404
+ );
2405
+ state.projectPath = value || process.cwd();
2406
+ state.lastStatus = `project -> ${state.projectPath}`;
2407
+ appendLogEvent(state, "system", "project_set", "ok", state.projectPath);
2408
+ } finally {
2409
+ state.busy = false;
2410
+ }
2411
+ renderState(state);
2412
+ return;
2413
+ }
2414
+ if (action === "set_change") {
2415
+ state.busy = true;
2416
+ try {
2417
+ const value = await promptInput(
2418
+ state,
2419
+ state.lang === "zh" ? "Change ID(留空自动推断)" : "Change id (leave blank for auto)",
2420
+ state.changeId,
2421
+ { allowBlank: true }
2422
+ );
2423
+ state.changeId = value;
2424
+ state.lastStatus = `change -> ${state.changeId || "auto"}`;
2425
+ appendLogEvent(state, "system", "change_set", "ok", state.changeId || "auto");
2426
+ } finally {
2427
+ state.busy = false;
2428
+ }
2429
+ renderState(state);
2430
+ return;
2431
+ }
2432
+ if (action === "run") {
2433
+ if (state.viewMode === "scene_list") {
2434
+ const menuSize = SCENES.length + 1;
2435
+ const rootIndex =
2436
+ Number.isFinite(state.rootSelectionIndex) && menuSize > 0
2437
+ ? ((state.rootSelectionIndex % menuSize) + menuSize) % menuSize
2438
+ : 0;
2439
+ state.rootSelectionIndex = rootIndex;
2440
+ if (rootIndex === SCENES.length) {
2441
+ close();
2442
+ return;
2443
+ }
2444
+ const selectedScene = SCENES[rootIndex] || SCENES[0];
2445
+ state.activeSceneId = selectedScene.id;
2446
+ if (
2447
+ selectedScene.id === "current-status" ||
2448
+ selectedScene.id === "switch-work-item" ||
2449
+ selectedScene.id === "pre-implementation-checks" ||
2450
+ selectedScene.id === "pre-completion-validation"
2451
+ ) {
2452
+ state.busy = true;
2453
+ try {
2454
+ if (selectedScene.id === "current-status") {
2455
+ appendLogEvent(state, state.activeSceneId, "scene_run", "ok", "run current status summary");
2456
+ await runCurrentStatusFlow(state);
2457
+ appendLogEvent(state, state.activeSceneId, "scene_complete", "ok", "run current status summary");
2458
+ } else if (selectedScene.id === "pre-implementation-checks") {
2459
+ appendLogEvent(state, state.activeSceneId, "scene_run", "ok", "run implementation readiness checks");
2460
+ await runPreImplementationChecks(state);
2461
+ appendLogEvent(state, state.activeSceneId, "scene_complete", "ok", "run implementation readiness checks");
2462
+ } else if (selectedScene.id === "pre-completion-validation") {
2463
+ appendLogEvent(state, state.activeSceneId, "scene_run", "ok", "run standard completion validation");
2464
+ await runPreCompletionValidation(state);
2465
+ appendLogEvent(state, state.activeSceneId, "scene_complete", "ok", "run standard completion validation");
2466
+ } else {
2467
+ appendLogEvent(state, state.activeSceneId, "scene_run", "ok", "open work item switcher");
2468
+ await chooseExistingChange(state);
2469
+ appendLogEvent(state, state.activeSceneId, "scene_complete", "ok", "open work item switcher");
2470
+ }
2471
+ } catch (error) {
2472
+ state.lastStatus = `${selectedScene.id} -> error: ${error.message || String(error)}`;
2473
+ appendLogEvent(state, state.activeSceneId, "scene_complete", "error", error.message || String(error));
2474
+ } finally {
2475
+ state.busy = false;
2476
+ }
2477
+ renderState(state);
2478
+ return;
2479
+ }
2480
+ state.viewMode = "scene_actions";
2481
+ appendLogEvent(state, state.activeSceneId, "scene_enter", "ok", "open scene actions");
2482
+ renderState(state);
2483
+ return;
2484
+ }
2485
+
2486
+ if (state.viewMode === "scene_actions") {
2487
+ const actions = getSceneActions(state, state.activeSceneId);
2488
+ const selectedIndex = getSceneActionIndex(state);
2489
+ if (selectedIndex >= actions.length) {
2490
+ state.viewMode = "scene_list";
2491
+ state.rootSelectionIndex = Math.max(0, SCENES.findIndex((scene) => scene.id === state.activeSceneId));
2492
+ renderState(state);
2493
+ return;
2494
+ }
2495
+ const actionItem = actions[selectedIndex] || null;
2496
+ if (!actionItem) {
2497
+ renderState(state);
2498
+ return;
2499
+ }
2500
+ state.busy = true;
2501
+ try {
2502
+ appendLogEvent(state, state.activeSceneId, "action_start", "ok", actionItem.id);
2503
+ await actionItem.run(state);
2504
+ appendLogEvent(state, state.activeSceneId, "action_complete", "ok", actionItem.id);
2505
+ } catch (error) {
2506
+ state.lastStatus = `${actionItem.id} -> error: ${error.message || String(error)}`;
2507
+ appendLogEvent(state, state.activeSceneId, "action_complete", "error", `${actionItem.id}: ${error.message || String(error)}`);
2508
+ } finally {
2509
+ state.busy = false;
2510
+ }
2511
+ renderState(state);
2512
+ return;
2513
+ }
2514
+
2515
+ if (state.viewMode === "work_item_switch") {
2516
+ const choices = Array.isArray(state.workItemChoices) ? state.workItemChoices : [];
2517
+ const selectedIndex = getWorkItemSelectionIndex(state);
2518
+ if (selectedIndex >= choices.length) {
2519
+ state.viewMode = "scene_list";
2520
+ state.activeSceneId = "switch-work-item";
2521
+ state.rootSelectionIndex = Math.max(0, SCENES.findIndex((scene) => scene.id === "switch-work-item"));
2522
+ renderState(state);
2523
+ return;
2524
+ }
2525
+ state.changeId = choices[selectedIndex];
2526
+ state.lastStatus = `change -> ${state.changeId || "auto"}`;
2527
+ appendLogEvent(state, "switch-work-item", "change_switch", "ok", state.changeId || "auto");
2528
+ state.viewMode = "scene_list";
2529
+ state.activeSceneId = "switch-work-item";
2530
+ state.rootSelectionIndex = Math.max(0, SCENES.findIndex((scene) => scene.id === "switch-work-item"));
2531
+ renderState(state);
2532
+ return;
2533
+ }
2534
+ }
2535
+ if (action === "toggle_help") {
2536
+ state.helpVisible = !state.helpVisible;
2537
+ renderState(state);
2538
+ return;
2539
+ }
2540
+ };
2541
+
2542
+ process.stdin.on("keypress", onKeypress);
2543
+ renderState(state);
2544
+ });
2545
+ }
2546
+
2547
+ module.exports = {
2548
+ SCENES,
2549
+ COMMANDS,
2550
+ buildCommandArgs,
2551
+ buildDisplayCommand,
2552
+ buildScreenLines,
2553
+ formatCommandParameterHelp,
2554
+ formatTuiHelp,
2555
+ getExecutionCwd,
2556
+ getCommandParameterItems,
2557
+ getCommandById,
2558
+ getDefaultLanguage,
2559
+ getKeyAction,
2560
+ launchTui,
2561
+ cycleThemeMode,
2562
+ detectTerminalThemeMode,
2563
+ enterAlternateScreen,
2564
+ exitAlternateScreen,
2565
+ hideCursor,
2566
+ showCursor,
2567
+ normalizeEditedCommand,
2568
+ parseEditedCommandInput,
2569
+ resolveActiveChangeId,
2570
+ resolveDailyLogPath,
2571
+ resolvePreImplementationResult,
2572
+ readPromptPresetSection,
2573
+ readVisualAssistVariant,
2574
+ applyManagedVisualAssistBlock,
2575
+ applyVisualAssistOverrides,
2576
+ extractVisualAssistSection,
2577
+ parseVisualAssistOverrides,
2578
+ resolveLocalizedText,
2579
+ mapRouteToScene,
2580
+ stripAnsi,
2581
+ teardownInput,
2582
+ tokenizeCommandLine
2583
+ };