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.
- package/enhancements/MANIFEST.json +1 -1
- package/package.json +3 -3
- package/src/cli.js +4 -0
- package/src/lib/command-completion.js +4 -2
- package/src/lib/terminal-state.js +78 -0
- package/src/lib/trellis-runner.js +25 -15
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flower-trellis",
|
|
3
|
-
"version": "0.5.
|
|
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.
|
|
71
|
+
"version": "0.5.5",
|
|
72
72
|
"source": "CHANGELOG.md",
|
|
73
|
-
"body": "###
|
|
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 =
|
|
122
|
+
child = ptySpawn(process.execPath, [bin, ...args], {
|
|
118
123
|
name: "xterm-256color",
|
|
119
|
-
cols:
|
|
120
|
-
rows:
|
|
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
|
-
|
|
141
|
+
stdout.write(data);
|
|
136
142
|
return;
|
|
137
143
|
}
|
|
138
144
|
// inquirer 开始渲染(隐藏光标)→ 立即停止过滤,整体透传,保住交互菜单
|
|
139
145
|
if (data.includes("\x1b[?25l")) {
|
|
140
146
|
filtering = false;
|
|
141
|
-
|
|
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
|
-
|
|
158
|
+
stdout.write(line + "\r\n");
|
|
153
159
|
continue;
|
|
154
160
|
}
|
|
155
161
|
filtering = false; // 正文开始
|
|
156
|
-
|
|
162
|
+
stdout.write(line + "\r\n");
|
|
157
163
|
}
|
|
158
164
|
if (!filtering && buf) {
|
|
159
|
-
|
|
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(
|
|
180
|
+
child.resize(stdout.columns || 80, stdout.rows || 30);
|
|
175
181
|
} catch {
|
|
176
182
|
// 忽略 resize 失败
|
|
177
183
|
}
|
|
178
184
|
};
|
|
179
|
-
|
|
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.
|
|
185
|
-
|
|
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
|
});
|