flower-trellis 0.5.4 → 0.5.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  {
2
- "syncedAt": "2026-07-29T14:45:40.104Z",
2
+ "syncedAt": "2026-07-29T23:45:58.845Z",
3
3
  "syncedFrom": "vendor/skill-garden",
4
4
  "sourceCommit": "2aca31ca35fe6adb7d1e1932a0f201018f458188",
5
5
  "common": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flower-trellis",
3
- "version": "0.5.4",
3
+ "version": "0.5.5",
4
4
  "description": "一键安装/升级 Trellis 并自动融合 skill-garden 强化包(默认 Claude + agents)",
5
5
  "type": "module",
6
6
  "bin": {
@@ -68,9 +68,9 @@
68
68
  "commit-and-tag-version": "^12.7.3"
69
69
  },
70
70
  "flowerReleaseNotes": {
71
- "version": "0.5.4",
71
+ "version": "0.5.5",
72
72
  "source": "CHANGELOG.md",
73
- "body": "### ✨ 新功能 Features\n\n* **cli:** 优化 init 与 update 交互体验 ([1248456](https://github.com/SilentFlower/flower-trellis/commit/12484563bb9f5b7f6842879c38adca02c9697584))\n* **craft-rpa:** 同步网络采集与页面控制升级 ([4827e20](https://github.com/SilentFlower/flower-trellis/commit/4827e2043a2123ba331ff82c0b59ce03cbb29f6c))\n\n\n### 🐛 修复 Bug Fixes\n\n* **workflow:** 防止发布请求误触 trellis-release ([e3e4add](https://github.com/SilentFlower/flower-trellis/commit/e3e4addd953059bdaa5135ac527432f4c0f8838a))",
73
+ "body": "### 🐛 修复 Bug Fixes\n\n* **cli:** 修复 Windows PTY 交互状态泄漏 ([e14ffc9](https://github.com/SilentFlower/flower-trellis/commit/e14ffc922e052120a0b0993420e0b2bb5c11a8f2))\n* **cli:** 修复 Windows 完成页退出挂起 ([ae2fd30](https://github.com/SilentFlower/flower-trellis/commit/ae2fd30ed9e0f378ec34a60ad1167766979539ce))",
74
74
  "truncated": false
75
75
  },
76
76
  "optionalDependencies": {
package/src/cli.js CHANGED
@@ -7,6 +7,7 @@ import { readManifest } from "./lib/manifest.js";
7
7
  import { ProjectStore } from "./plugin/state/project-store.js";
8
8
  import { runTrellis } from "./lib/trellis-runner.js";
9
9
  import { parseCliArgs } from "./lib/cli-args.js";
10
+ import { installWindowsTerminalInputRecovery } from "./lib/terminal-state.js";
10
11
 
11
12
  /**
12
13
  * flower-trellis CLI 主入口。
@@ -105,6 +106,9 @@ init / update 启动时会顺带检测 flower-trellis 自身是否有新版(联
105
106
  }
106
107
 
107
108
  async function main() {
109
+ // 新进程先修复旧版或异常退出遗留的 ConPTY 输入模式,退出时再兜底恢复一次。
110
+ installWindowsTerminalInputRecovery();
111
+
108
112
  // Ctrl+C:父进程也立即退出,绝不在子进程被取消后继续叠加
109
113
  process.on("SIGINT", () => process.exit(130));
110
114
 
@@ -1,4 +1,5 @@
1
1
  import { select } from "@inquirer/prompts";
2
+ import { scheduleWindowsTerminalExit } from "./terminal-state.js";
2
3
 
3
4
  const SUCCESS_LABELS = {
4
5
  init: "安装成功",
@@ -16,7 +17,7 @@ const OUTCOME_LABELS = {
16
17
  *
17
18
  * @param {"init"|"update"} command 命令名
18
19
  * @param {string} target 目标项目根目录
19
- * @param {{passthrough?:string[],interactive?:boolean,outcome?:"success"|"preview",selectPrompt?:Function,output?:{log:(message:string)=>void}}} [options] 交互、结果类型与测试注入
20
+ * @param {{passthrough?:string[],interactive?:boolean,outcome?:"success"|"preview",selectPrompt?:Function,output?:{log:(message:string)=>void},terminalExit?:object}} [options] 交互、结果类型与测试注入
20
21
  * @returns {Promise<void>} 成功状态输出及可选退出确认完成后返回
21
22
  */
22
23
  export async function showCommandCompletion(command, target, options = {}) {
@@ -35,9 +36,10 @@ export async function showCommandCompletion(command, target, options = {}) {
35
36
  if (!interactive) return;
36
37
 
37
38
  const selectPrompt = options.selectPrompt || select;
38
- await selectPrompt({
39
+ const action = await selectPrompt({
39
40
  message: "操作已完成",
40
41
  choices: [{ name: "退出", value: "exit" }],
41
42
  loop: false,
42
43
  });
44
+ if (action === "exit") scheduleWindowsTerminalExit(options.terminalExit);
43
45
  }
@@ -0,0 +1,78 @@
1
+ const WIN32_INPUT_MODE_DISABLE = "\x1b[?9001l";
2
+ const CURSOR_SHOW = "\x1b[?25h";
3
+
4
+ /**
5
+ * 关闭 Windows Terminal 的 Win32 输入模式,恢复宿主 CLI 的普通键盘输入。
6
+ *
7
+ * ConPTY 子进程会通过 CSI ? 9001 h 请求 Windows Terminal 把按键编码成
8
+ * Win32 Input Mode 记录。Flower 透传子进程终端输出时必须在边界处关闭该模式,
9
+ * 否则后续 Inquirer 会收到形如 `CSI Vk;Sc;Uc;Kd;Cs;Rc _` 的残留输入。
10
+ *
11
+ * @param {{platform?:string,output?:{isTTY?:boolean,write:(value:string)=>unknown}}} [options] 平台与输出流测试注入
12
+ * @returns {boolean} 是否写出了终端恢复序列
13
+ */
14
+ export function disableWindowsTerminalWin32InputMode(options = {}) {
15
+ const platform = options.platform ?? process.platform;
16
+ const output = options.output ?? process.stdout;
17
+ if (platform !== "win32" || !output?.isTTY || typeof output.write !== "function") {
18
+ return false;
19
+ }
20
+ try {
21
+ output.write(WIN32_INPUT_MODE_DISABLE);
22
+ return true;
23
+ } catch {
24
+ // 终端恢复属于退出期兜底,输出流已关闭时不能覆盖原命令结果。
25
+ return false;
26
+ }
27
+ }
28
+
29
+ /**
30
+ * 在 CLI 启动时恢复一次 Windows 终端输入,并在进程退出时再次兜底恢复。
31
+ *
32
+ * @param {{platform?:string,output?:{isTTY?:boolean,write:(value:string)=>unknown},processEvents?:{once:(event:string,listener:()=>void)=>unknown,off:(event:string,listener:()=>void)=>unknown}}} [options] 平台、输出流与进程事件测试注入
33
+ * @returns {()=>void} 移除退出期恢复监听的清理函数
34
+ */
35
+ export function installWindowsTerminalInputRecovery(options = {}) {
36
+ const processEvents = options.processEvents ?? process;
37
+ const restore = () => disableWindowsTerminalWin32InputMode(options);
38
+ restore();
39
+ processEvents.once("exit", restore);
40
+ return () => processEvents.off("exit", restore);
41
+ }
42
+
43
+ /**
44
+ * 在 Windows 交互完成页选择“退出”后恢复终端,并显式结束 CLI 进程。
45
+ *
46
+ * node-pty 的 Windows ConPTY worker 在子进程自然退出后仍可能持有 MessagePort / Socket,
47
+ * 因此不能只依赖 Node 事件循环自然清空。这里仅在用户已经选择退出时执行,并把实际退出
48
+ * 延后到下一轮事件循环,让 Inquirer 完成当前输出清理。
49
+ *
50
+ * @param {{platform?:string,input?:{isTTY?:boolean,isRaw?:boolean,setRawMode?:(value:boolean)=>unknown,pause?:()=>unknown},output?:{isTTY?:boolean,write:(value:string)=>unknown},schedule?:(callback:()=>void)=>unknown,exitProcess?:(code:number)=>unknown}} [options] 终端、调度器与退出函数测试注入
51
+ * @returns {boolean} 是否安排了 Windows CLI 显式退出
52
+ */
53
+ export function scheduleWindowsTerminalExit(options = {}) {
54
+ const platform = options.platform ?? process.platform;
55
+ if (platform !== "win32") return false;
56
+
57
+ const input = options.input ?? process.stdin;
58
+ const output = options.output ?? process.stdout;
59
+ try {
60
+ if (input?.isTTY && input.isRaw && typeof input.setRawMode === "function") {
61
+ input.setRawMode(false);
62
+ }
63
+ if (typeof input?.pause === "function") input.pause();
64
+ } catch {
65
+ // 退出前终端恢复是 best-effort,不能把成功命令改成失败。
66
+ }
67
+ try {
68
+ if (output?.isTTY && typeof output.write === "function") output.write(CURSOR_SHOW);
69
+ } catch {
70
+ // 输出流关闭时继续执行 Win32 输入模式恢复与退出。
71
+ }
72
+ disableWindowsTerminalWin32InputMode({ platform, output });
73
+
74
+ const schedule = options.schedule ?? ((callback) => setTimeout(callback, 0));
75
+ const exitProcess = options.exitProcess ?? ((code) => process.exit(code));
76
+ schedule(() => exitProcess(0));
77
+ return true;
78
+ }
@@ -5,6 +5,7 @@ import { spawn } from "node:child_process";
5
5
  import { createInterface } from "node:readline";
6
6
  import figlet from "figlet";
7
7
  import * as pty from "node-pty";
8
+ import { disableWindowsTerminalWin32InputMode } from "./terminal-state.js";
8
9
 
9
10
  /**
10
11
  * 定位捆绑的 @mindfoldhq/trellis 可执行 bin 的绝对路径。
@@ -106,22 +107,27 @@ export function runTrellis(args, cwd, opts = {}) {
106
107
  *
107
108
  * @param {string[]} args
108
109
  * @param {string} cwd
109
- * @param {object} [opts] { stripBanner }
110
+ * @param {object} [opts] { stripBanner, ptySpawn, stdin, stdout, platform }
110
111
  * @returns {Promise<number>} 退出码(信号终止返回 128)
111
112
  */
112
113
  export function runTrellisPty(args, cwd, opts = {}) {
113
114
  const bin = resolveTrellisBin();
115
+ const stdin = opts.stdin || process.stdin;
116
+ const stdout = opts.stdout || process.stdout;
117
+ const platform = opts.platform ?? process.platform;
118
+ const ptySpawn = opts.ptySpawn || pty.spawn;
114
119
  return new Promise((resolve, reject) => {
115
120
  let child;
116
121
  try {
117
- child = pty.spawn(process.execPath, [bin, ...args], {
122
+ child = ptySpawn(process.execPath, [bin, ...args], {
118
123
  name: "xterm-256color",
119
- cols: process.stdout.columns || 80,
120
- rows: process.stdout.rows || 30,
124
+ cols: stdout.columns || 80,
125
+ rows: stdout.rows || 30,
121
126
  cwd,
122
127
  env: { ...process.env, FORCE_COLOR: "1" },
123
128
  });
124
129
  } catch (e) {
130
+ disableWindowsTerminalWin32InputMode({ platform, output: stdout });
125
131
  reject(e);
126
132
  return;
127
133
  }
@@ -130,15 +136,15 @@ export function runTrellisPty(args, cwd, opts = {}) {
130
136
  let filtering = !!opts.stripBanner;
131
137
  let buf = "";
132
138
 
133
- child.onData((data) => {
139
+ const dataSubscription = child.onData((data) => {
134
140
  if (!filtering) {
135
- process.stdout.write(data);
141
+ stdout.write(data);
136
142
  return;
137
143
  }
138
144
  // inquirer 开始渲染(隐藏光标)→ 立即停止过滤,整体透传,保住交互菜单
139
145
  if (data.includes("\x1b[?25l")) {
140
146
  filtering = false;
141
- process.stdout.write(buf + data);
147
+ stdout.write(buf + data);
142
148
  buf = "";
143
149
  return;
144
150
  }
@@ -149,21 +155,21 @@ export function runTrellisPty(args, cwd, opts = {}) {
149
155
  const kind = classifyHeaderLine(line, bannerSet);
150
156
  if (kind === "skip") continue;
151
157
  if (kind === "keep") {
152
- process.stdout.write(line + "\r\n");
158
+ stdout.write(line + "\r\n");
153
159
  continue;
154
160
  }
155
161
  filtering = false; // 正文开始
156
- process.stdout.write(line + "\r\n");
162
+ stdout.write(line + "\r\n");
157
163
  }
158
164
  if (!filtering && buf) {
159
- process.stdout.write(buf);
165
+ stdout.write(buf);
160
166
  buf = "";
161
167
  }
162
168
  });
163
169
 
164
170
  // 把真终端的输入转发进 pty(让用户能操作 trellis 的交互菜单)
165
- const stdin = process.stdin;
166
171
  const wasRaw = !!stdin.isRaw;
172
+ const wasFlowing = stdin.readableFlowing;
167
173
  if (stdin.isTTY) stdin.setRawMode(true);
168
174
  stdin.resume();
169
175
  const onStdin = (d) => child.write(d.toString("utf8"));
@@ -171,18 +177,22 @@ export function runTrellisPty(args, cwd, opts = {}) {
171
177
 
172
178
  const onResize = () => {
173
179
  try {
174
- child.resize(process.stdout.columns || 80, process.stdout.rows || 30);
180
+ child.resize(stdout.columns || 80, stdout.rows || 30);
175
181
  } catch {
176
182
  // 忽略 resize 失败
177
183
  }
178
184
  };
179
- process.stdout.on("resize", onResize);
185
+ stdout.on("resize", onResize);
180
186
 
181
187
  child.onExit(({ exitCode, signal }) => {
188
+ // 先停止接收子进程输出,再恢复宿主终端;否则迟到的 9001h 会重新污染父终端。
189
+ dataSubscription.dispose();
182
190
  stdin.off("data", onStdin);
183
191
  if (stdin.isTTY) stdin.setRawMode(wasRaw);
184
- stdin.pause();
185
- process.stdout.off("resize", onResize);
192
+ if (wasFlowing === true) stdin.resume();
193
+ else stdin.pause();
194
+ stdout.off("resize", onResize);
195
+ disableWindowsTerminalWin32InputMode({ platform, output: stdout });
186
196
  if (signal) resolve(128);
187
197
  else resolve(exitCode ?? 0);
188
198
  });