jetbrains-pi-acp 0.1.3 → 0.1.6
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/README.md +7 -4
- package/README.zh-CN.md +7 -4
- package/extensions/acp.ts +10 -1
- package/package.json +1 -1
- package/src/agent.js +18 -0
- package/src/commands.js +12 -1
- package/src/file-changes.js +22 -2
- package/src/i18n.js +4 -0
- package/src/terminal-input.js +7 -6
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ Requires **Node.js 22.19.0+** and a JetBrains IDE with ACP support, such as Inte
|
|
|
11
11
|
1. Sign in to your model provider from a terminal:
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
|
-
npx --yes jetbrains-pi-acp@0.1.
|
|
14
|
+
npx --yes jetbrains-pi-acp@0.1.6 --setup
|
|
15
15
|
```
|
|
16
16
|
|
|
17
17
|
In pi, run `/login`, complete sign-in, then exit pi and return to the IDE. Credentials are managed by pi.
|
|
@@ -23,7 +23,7 @@ Requires **Node.js 22.19.0+** and a JetBrains IDE with ACP support, such as Inte
|
|
|
23
23
|
"agent_servers": {
|
|
24
24
|
"pi": {
|
|
25
25
|
"command": "npx",
|
|
26
|
-
"args": ["--yes", "jetbrains-pi-acp@0.1.
|
|
26
|
+
"args": ["--yes", "jetbrains-pi-acp@0.1.6"],
|
|
27
27
|
"use_idea_mcp": true,
|
|
28
28
|
"use_custom_mcp": true
|
|
29
29
|
}
|
|
@@ -41,7 +41,7 @@ The IDE must be able to find `npx`; use its absolute path if needed. Menu locati
|
|
|
41
41
|
- Native pi session history: list, load, resume, rename, fork, and delete sessions.
|
|
42
42
|
- Model and reasoning-level selection, plus full-access, standard, read-only, and plan modes.
|
|
43
43
|
- UI text (config names, mode and command descriptions, permission options, errors) follows the system language: Chinese for `zh*` locales, English otherwise, based on the first non-empty `LC_ALL`, `LC_MESSAGES`, or `LANG`, falling back to the system locale. Tool descriptions and system prompts sent to the model are always English. Restart the adapter after changing the language.
|
|
44
|
-
- File diffs for built-in `edit`/`write` tools, with optional Air Git patches and per-turn file reports.
|
|
44
|
+
- File diffs for built-in `edit`/`write` tools, with optional Air Git patches and per-turn file reports. File report snapshots follow Git ignore rules, including `.gitignore`, while retaining tracked files; non-Git projects or systems without Git use filesystem scanning that skips common dependency, build, and cache directories such as `node_modules/`, `vendor/`, `target/`, `build/`, `dist/`, `coverage/`, `.cache/`, and `.venv/`, including nested directories. Snapshots always skip `.DS_Store`.
|
|
45
45
|
- stdio/HTTP MCP forwarding, including IDE tools; subagents and background shell tasks with targeted cancellation.
|
|
46
46
|
- Steering, terminal stdin, forms, and URL interactions when supported by the client. Air v1 provides `/stdin text` and `/eof` during command execution.
|
|
47
47
|
|
|
@@ -52,7 +52,7 @@ The IDE must be able to find `npx`; use its absolute path if needed. Menu locati
|
|
|
52
52
|
Append pi options after `--` in the agent's `args`, for example:
|
|
53
53
|
|
|
54
54
|
```json
|
|
55
|
-
["--yes", "jetbrains-pi-acp@0.1.
|
|
55
|
+
["--yes", "jetbrains-pi-acp@0.1.6", "--", "--model", "provider/model-id", "--thinking", "medium"]
|
|
56
56
|
```
|
|
57
57
|
|
|
58
58
|
Use configuration options here, not initial prompts, interactive mode, or session-selection arguments. The adapter manages RPC mode and sessions.
|
|
@@ -61,12 +61,15 @@ To use an external pi executable, add `--pi-command /absolute/path/to/pi` before
|
|
|
61
61
|
|
|
62
62
|
Sessions use pi's native storage, normally `~/.pi/agent/sessions/`, and respect pi's session-directory settings. History is filtered by the project working directory. Avoid accessing the same session simultaneously from multiple adapters or from pi and the IDE.
|
|
63
63
|
|
|
64
|
+
The first time you open a historical session after restarting ACP, pi reads its current configuration, including newly added models, extensions, skills, and prompts. For an open session, send `/reload` to refresh configuration and the IDE's model and command lists. Historical sessions retain their selected model and reasoning level; changing the default model in `settings.json` does not switch an existing session. Select the new model in the IDE's model menu. Custom models normally live in `~/.pi/agent/models.json`, or under `PI_CODING_AGENT_DIR` when set. Authentication for added models must also be available to the pi process launched by the IDE.
|
|
65
|
+
|
|
64
66
|
Restart the agent connection after updating the adapter or changing startup options.
|
|
65
67
|
|
|
66
68
|
## Limitations
|
|
67
69
|
|
|
68
70
|
- Tools execute locally through pi; ACP client-side `fs/*` and `terminal/*` execution is not implemented. Save editor buffers before asking the agent to read or change files.
|
|
69
71
|
- Terminal input uses stdin pipes, not a PTY. Full-screen apps and TTY password prompts are unsupported; background tasks have no stdin input.
|
|
72
|
+
- Only commands the model runs with `interactive: true` accept input. Other commands get an empty stdin (`/dev/null`) like native pi, so commands that read stdin by mistake exit instead of hanging.
|
|
70
73
|
- Diffs have size limits and may be unavailable for binary files or older history. File reports scan at most 20,000 entries or two seconds per snapshot and cannot reliably attribute concurrent external changes.
|
|
71
74
|
- MCP SSE, audio, and binary embedded resources are unsupported. Image support depends on the model.
|
|
72
75
|
- ACP v2 follows a draft schema. Client UI behavior, real provider sign-in, and Windows hardware still need verification.
|
package/README.zh-CN.md
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
1. 在终端中登录模型提供商:
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
|
-
npx --yes jetbrains-pi-acp@0.1.
|
|
14
|
+
npx --yes jetbrains-pi-acp@0.1.6 --setup
|
|
15
15
|
```
|
|
16
16
|
|
|
17
17
|
在 pi 中运行 `/login`,完成登录后退出 pi 并返回 IDE。凭据由 pi 管理。
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"agent_servers": {
|
|
24
24
|
"pi": {
|
|
25
25
|
"command": "npx",
|
|
26
|
-
"args": ["--yes", "jetbrains-pi-acp@0.1.
|
|
26
|
+
"args": ["--yes", "jetbrains-pi-acp@0.1.6"],
|
|
27
27
|
"use_idea_mcp": true,
|
|
28
28
|
"use_custom_mcp": true
|
|
29
29
|
}
|
|
@@ -42,7 +42,7 @@ IDE 必须能找到 `npx`,必要时使用其绝对路径。菜单位置随 IDE
|
|
|
42
42
|
- 使用 pi 原生历史,支持会话列表、加载、恢复、改名、分叉和删除。
|
|
43
43
|
- 支持切换模型与推理级别,以及完全访问、标准访问、只读和规划模式。
|
|
44
44
|
- 界面文案(配置名称、模式说明、命令说明、授权选项、报错等)按系统语言自动切换中英文,给模型看的工具描述和系统提示固定使用英文。语言判断依次读取非空的 `LC_ALL`、`LC_MESSAGES`、`LANG`,未设置时使用系统默认语言;中文使用中文,其余语言使用英文。更改系统语言后需重启适配器。
|
|
45
|
-
- 展示内置 `edit/write` 工具的文件 diff,可协商 Air Git patch
|
|
45
|
+
- 展示内置 `edit/write` 工具的文件 diff,可协商 Air Git patch 和每轮文件报告。文件报告快照遵守 `.gitignore` 等 Git 忽略规则,保留已跟踪文件;非 Git 项目或 Git 不可用时使用文件系统扫描,跳过 `node_modules/`、`vendor/`、`target/`、`build/`、`dist/`、`coverage/`、`.cache/`、`.venv/` 等常见依赖、构建和缓存目录(包括嵌套目录)。快照始终跳过 `.DS_Store`。
|
|
46
46
|
- 转发 stdio/HTTP MCP 配置及 IDE 工具;支持子 agent、后台 shell 任务和定向停止。
|
|
47
47
|
- 按客户端能力提供 steering、终端 stdin、表单和 URL 交互。Air v1 可在命令执行期间发送 `/stdin 文本` 和 `/eof`。
|
|
48
48
|
|
|
@@ -53,7 +53,7 @@ IDE 必须能找到 `npx`,必要时使用其绝对路径。菜单位置随 IDE
|
|
|
53
53
|
在 Agent 的 `args` 末尾添加 `--`,随后传递 pi 参数,例如:
|
|
54
54
|
|
|
55
55
|
```json
|
|
56
|
-
["--yes", "jetbrains-pi-acp@0.1.
|
|
56
|
+
["--yes", "jetbrains-pi-acp@0.1.6", "--", "--model", "provider/model-id", "--thinking", "medium"]
|
|
57
57
|
```
|
|
58
58
|
|
|
59
59
|
这里只传配置选项,不传初始提示、交互模式或会话选择参数。RPC 模式和会话由适配器管理。
|
|
@@ -62,12 +62,15 @@ IDE 必须能找到 `npx`,必要时使用其绝对路径。菜单位置随 IDE
|
|
|
62
62
|
|
|
63
63
|
历史使用 pi 原生存储,通常位于 `~/.pi/agent/sessions/`,同时遵循 pi 的会话目录设置。列表按项目工作目录过滤。避免多个适配器,或终端 pi 与 IDE,同时操作同一会话。
|
|
64
64
|
|
|
65
|
+
历史会话在 ACP 重启后首次打开时读取当前 pi 配置,可使用新增的模型、扩展、skills 和提示词。已打开的会话可发送 `/reload`,刷新配置及 IDE 中的模型、命令列表。历史会话会保留上次选择的模型和推理级别;修改 `settings.json` 的默认模型不会自动切换历史会话,请在 IDE 模型菜单中选择新模型。自定义模型通常配置在 `~/.pi/agent/models.json`;设置了 `PI_CODING_AGENT_DIR` 时,以该目录为准,新增模型的认证也必须对 IDE 启动的 pi 可用。
|
|
66
|
+
|
|
65
67
|
更新适配器或调整启动参数后,需要重启 Agent 连接。
|
|
66
68
|
|
|
67
69
|
## 限制
|
|
68
70
|
|
|
69
71
|
- 工具由本机 pi 执行,尚未接入客户端 `fs/*` 和 `terminal/*` 执行接口。让 Agent 读取或修改文件前,请保存编辑器内容。
|
|
70
72
|
- 终端输入使用 stdin 管道,不提供 PTY,不支持全屏终端应用和 TTY 密码提示;后台任务不支持 stdin 输入。
|
|
73
|
+
- 只有模型以 `interactive: true` 调用的命令可以接收输入;其他命令与 pi 原生一致,stdin 为空(`/dev/null`),误读 stdin 的命令会直接结束,不会卡住。
|
|
71
74
|
- diff 有大小限制,二进制文件或旧历史可能无法展示。每轮文件报告的单次快照最多扫描 20,000 项或两秒,无法准确区分外部并发修改。
|
|
72
75
|
- 不支持 MCP SSE、音频和二进制内嵌资源,图片支持取决于模型。
|
|
73
76
|
- ACP v2 仍为草案,客户端界面展示、真实提供商账号登录及 Windows 实机仍需验证。
|
package/extensions/acp.ts
CHANGED
|
@@ -79,13 +79,17 @@ export default function (pi: any) {
|
|
|
79
79
|
const createTool = name === 'bash' ? createBashToolDefinition : createPowerShellToolDefinition;
|
|
80
80
|
const definition = createTool(process.cwd());
|
|
81
81
|
pi.registerTool({ ...definition,
|
|
82
|
+
parameters: Type.Object({ ...definition.parameters.properties,
|
|
83
|
+
interactive: Type.Optional(Type.Boolean({ description: 'Set true only when the command must read input typed by the user on stdin. ' +
|
|
84
|
+
'Default false: stdin is empty (/dev/null) and no input can be sent while it runs.' })) }),
|
|
82
85
|
execute: async (id: string, args: any, signal: AbortSignal, update: any, ctx: any) => {
|
|
86
|
+
const interactive = args.interactive === true;
|
|
83
87
|
const settings = SettingsManager.create(ctx.cwd);
|
|
84
88
|
const tool = createTool(ctx.cwd, {
|
|
85
89
|
commandPrefix: settings.getShellCommandPrefix(),
|
|
86
90
|
operations: { exec: (command: string, cwd: string, options: any) => terminals.exec(id,
|
|
87
91
|
name === 'bash' ? getShellConfig(settings.getShellPath()) : getPowerShellConfig(),
|
|
88
|
-
command, cwd, options, () => ctx.ui.notify(`PI_ACP_TERMINAL_READY:${id}`, 'info')) },
|
|
92
|
+
command, cwd, { ...options, interactive }, () => ctx.ui.notify(`PI_ACP_TERMINAL_READY:${id}`, 'info')) },
|
|
89
93
|
});
|
|
90
94
|
return tool.execute(id, args, signal, update, ctx);
|
|
91
95
|
},
|
|
@@ -187,6 +191,11 @@ export default function (pi: any) {
|
|
|
187
191
|
ctx.ui.notify('PI_ACP_READY', 'info');
|
|
188
192
|
},
|
|
189
193
|
});
|
|
194
|
+
pi.registerCommand('__acp_reload', {
|
|
195
|
+
description: 'Reload models.json, extensions, skills and prompts',
|
|
196
|
+
// reload 不会刷新 models.json,需先刷新模型注册表。
|
|
197
|
+
handler: async (_args: string, ctx: any) => { await ctx.modelRegistry.refresh(); await ctx.reload(); },
|
|
198
|
+
});
|
|
190
199
|
pi.registerCommand('__acp_archive', {
|
|
191
200
|
description: 'Sync the ACP archive state',
|
|
192
201
|
handler: async (args: string) => { pi.appendEntry('acp_archive', args.trim() === 'true'); },
|
package/package.json
CHANGED
package/src/agent.js
CHANGED
|
@@ -763,6 +763,24 @@ export class Agent {
|
|
|
763
763
|
}
|
|
764
764
|
}
|
|
765
765
|
|
|
766
|
+
// 重新加载 pi 配置后同步模型列表、命令和扩展内存状态(计划模式、目标)。
|
|
767
|
+
async reloadSession(session) {
|
|
768
|
+
await session.pi.request('prompt', { message: '/__acp_reload' });
|
|
769
|
+
if (session.mode === 'plan') await session.pi.request('prompt', { message: '/__acp_mode plan' });
|
|
770
|
+
await session.pi.request('prompt', { message: '/__acp_check' });
|
|
771
|
+
const state = await session.pi.request('get_state');
|
|
772
|
+
const available = await session.pi.request('get_available_models');
|
|
773
|
+
session.availableModels = available?.models ?? [];
|
|
774
|
+
const current = state?.model;
|
|
775
|
+
if (current && !session.availableModels.some(model => modelKey(model) === modelKey(current))) {
|
|
776
|
+
session.availableModels.push(current);
|
|
777
|
+
}
|
|
778
|
+
if (current) session.currentModelId = modelKey(current);
|
|
779
|
+
await this.refreshThinking(session);
|
|
780
|
+
this.update(session, { sessionUpdate: 'config_option_update', configOptions: this.modelConfig(session) });
|
|
781
|
+
await this.commands.publish(session);
|
|
782
|
+
}
|
|
783
|
+
|
|
766
784
|
async refreshThinking(session) {
|
|
767
785
|
const [state, available] = await Promise.all([
|
|
768
786
|
session.pi.request('get_state'), session.pi.request('get_available_thinking_levels'),
|
package/src/commands.js
CHANGED
|
@@ -8,7 +8,7 @@ export class Commands {
|
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
isLocal(text) {
|
|
11
|
-
return text === '/status' || /^\/(compact|goal)(?:\s|$)/.test(text);
|
|
11
|
+
return text === '/status' || text === '/reload' || /^\/(compact|goal)(?:\s|$)/.test(text);
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
// Air 聊天框中的 /stdin、/eof 转为唯一可输入终端的输入;返回 true 表示已处理。
|
|
@@ -57,6 +57,16 @@ export class Commands {
|
|
|
57
57
|
await this.agent.sendUsage(session, stats);
|
|
58
58
|
return finish({ stopReason: 'end_turn' });
|
|
59
59
|
}
|
|
60
|
+
if (text === '/reload') {
|
|
61
|
+
session.changingConfig = true;
|
|
62
|
+
try {
|
|
63
|
+
await this.agent.reloadSession(session);
|
|
64
|
+
this.agent.update(session, { sessionUpdate: 'agent_message_chunk', content: { type: 'text', text: t('reloadDone') } });
|
|
65
|
+
return finish({ stopReason: 'end_turn' });
|
|
66
|
+
} finally {
|
|
67
|
+
session.changingConfig = false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
60
70
|
const customInstructions = text.slice('/compact'.length).trim() || undefined;
|
|
61
71
|
const command = { cancelled: false };
|
|
62
72
|
session.command = command;
|
|
@@ -87,6 +97,7 @@ export class Commands {
|
|
|
87
97
|
const availableCommands = new Map([
|
|
88
98
|
['status', { name: 'status', description: t('cmdStatus'), input: null }],
|
|
89
99
|
// Air 的 ACP v1 解析器需要 input 的类型判别字段。
|
|
100
|
+
['reload', { name: 'reload', description: t('cmdReload'), input: null }],
|
|
90
101
|
['compact', { name: 'compact', description: t('cmdCompact'), input: { type: 'unstructured', hint: t('cmdCompactHint') } }],
|
|
91
102
|
['goal', { name: 'goal', description: t('cmdGoal'),
|
|
92
103
|
input: { type: 'unstructured', hint: t('cmdGoalHint') } }],
|
package/src/file-changes.js
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
1
2
|
import { lstat, opendir } from 'node:fs/promises';
|
|
2
3
|
import { join } from 'node:path';
|
|
4
|
+
import { promisify } from 'node:util';
|
|
5
|
+
|
|
6
|
+
const executeFile = promisify(execFile);
|
|
7
|
+
const fallbackIgnoredDirectories = new Set([
|
|
8
|
+
'node_modules', 'vendor', 'target', 'build', 'dist', 'out', 'coverage',
|
|
9
|
+
'.gradle', '.next', '.nuxt', '.output', '.cache',
|
|
10
|
+
'.venv', 'venv', '__pycache__', '.pytest_cache', '.mypy_cache', '.ruff_cache', '.tox',
|
|
11
|
+
]);
|
|
3
12
|
|
|
4
13
|
export async function fileSnapshot(cwd) {
|
|
5
14
|
const files = new Map();
|
|
@@ -7,16 +16,27 @@ export async function fileSnapshot(cwd) {
|
|
|
7
16
|
const deadline = Date.now() + 2000;
|
|
8
17
|
let entries = 0;
|
|
9
18
|
let incomplete = false;
|
|
19
|
+
const ignored = new Set();
|
|
20
|
+
let fallback = false;
|
|
21
|
+
try {
|
|
22
|
+
// Git 保留已跟踪文件,并处理嵌套 .gitignore、否定规则及仓库本地忽略规则。
|
|
23
|
+
const { stdout } = await executeFile('git',
|
|
24
|
+
['ls-files', '--others', '--ignored', '--exclude-standard', '--directory', '-z'],
|
|
25
|
+
{ cwd, timeout: 2000, maxBuffer: 4 * 1024 * 1024 });
|
|
26
|
+
for (const path of stdout.split('\0')) if (path) ignored.add(join(cwd, path.replace(/\/$/, '')));
|
|
27
|
+
} catch { fallback = true; }
|
|
10
28
|
while (directories.length) {
|
|
11
29
|
if (Date.now() > deadline) return { files, incomplete: true };
|
|
12
30
|
const directory = directories.pop();
|
|
13
31
|
try {
|
|
14
32
|
if ((await lstat(directory)).isSymbolicLink()) { incomplete = true; continue; }
|
|
15
33
|
for await (const entry of await opendir(directory)) {
|
|
16
|
-
if (entry.name === '.git'
|
|
34
|
+
if (entry.name === '.git' || entry.name === '.DS_Store' ||
|
|
35
|
+
(fallback && entry.isDirectory() && fallbackIgnoredDirectories.has(entry.name))) continue;
|
|
36
|
+
const path = join(directory, entry.name);
|
|
37
|
+
if (ignored.has(path)) continue;
|
|
17
38
|
// shortcut: 每次最多扫描 20000 项或 2 秒,大项目需要增量文件监听时再升级。
|
|
18
39
|
if (++entries > 20000 || Date.now() > deadline) return { files, incomplete: true };
|
|
19
|
-
const path = join(directory, entry.name);
|
|
20
40
|
try {
|
|
21
41
|
const info = await lstat(path, { bigint: true });
|
|
22
42
|
if (info.isDirectory()) directories.push(path);
|
package/src/i18n.js
CHANGED
|
@@ -142,6 +142,8 @@ const zh = {
|
|
|
142
142
|
|
|
143
143
|
// 斜杠命令
|
|
144
144
|
cmdStatus: '显示当前模型、token、成本和上下文使用情况',
|
|
145
|
+
cmdReload: '重新加载 pi 的模型、扩展、skills 和提示词配置',
|
|
146
|
+
reloadDone: '已重新加载 pi 配置',
|
|
145
147
|
cmdCompact: '压缩对话上下文,可附带摘要要求',
|
|
146
148
|
cmdCompactHint: '可选摘要要求',
|
|
147
149
|
cmdGoal: '设置或查看长期目标,也支持 pause、resume、clear',
|
|
@@ -342,6 +344,8 @@ const en = {
|
|
|
342
344
|
promptContentUnsupported: 'Unsupported prompt content: {type}',
|
|
343
345
|
|
|
344
346
|
cmdStatus: 'Show the current model, tokens, cost, and context usage',
|
|
347
|
+
cmdReload: 'Reload pi models, extensions, skills and prompts',
|
|
348
|
+
reloadDone: 'pi configuration reloaded',
|
|
345
349
|
cmdCompact: 'Compact the conversation context, optionally with summary instructions',
|
|
346
350
|
cmdCompactHint: 'Optional summary instructions',
|
|
347
351
|
cmdGoal: 'Set or view the goal; also supports pause, resume, and clear',
|
package/src/terminal-input.js
CHANGED
|
@@ -17,7 +17,7 @@ export class TerminalProcesses {
|
|
|
17
17
|
async input(params) {
|
|
18
18
|
validateTerminalInput(params);
|
|
19
19
|
const child = this.processes.get(params.terminalId);
|
|
20
|
-
if (!child || child.exitCode !== null || child.signalCode !== null || !child.stdin
|
|
20
|
+
if (!child || child.exitCode !== null || child.signalCode !== null || !child.stdin?.writable) {
|
|
21
21
|
throw new Error(t('terminalUnavailable'));
|
|
22
22
|
}
|
|
23
23
|
await new Promise((resolve, reject) => {
|
|
@@ -43,18 +43,19 @@ export class TerminalProcesses {
|
|
|
43
43
|
for (const child of this.processes.values()) this.kill(child);
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
async exec(terminalId, shell, command, cwd, { onData, signal, timeout, env }, ready) {
|
|
46
|
+
async exec(terminalId, shell, command, cwd, { onData, signal, timeout, env, interactive }, ready) {
|
|
47
47
|
if (signal?.aborted) throw new Error('aborted');
|
|
48
48
|
if (timeout !== undefined && (!Number.isFinite(timeout) || timeout <= 0 || timeout * 1000 > 2147483647)) {
|
|
49
49
|
throw new Error('Invalid timeout');
|
|
50
50
|
}
|
|
51
51
|
// shortcut: stdin 管道不提供 PTY;需要全屏程序、终端尺寸或 tty 密码提示时再接入 PTY。
|
|
52
52
|
if (shell.commandTransport === 'stdin') throw new Error(t('shellStdinTransport'));
|
|
53
|
+
// 非交互命令的 stdin 用 /dev/null,与 pi 原生一致,避免误读 stdin 的命令一直卡住。
|
|
53
54
|
const child = spawn(shell.shell, [...shell.args, command], {
|
|
54
|
-
cwd, env, stdio: ['pipe', 'pipe', 'pipe'], detached: process.platform !== 'win32', windowsHide: true,
|
|
55
|
+
cwd, env, stdio: [interactive ? 'pipe' : 'ignore', 'pipe', 'pipe'], detached: process.platform !== 'win32', windowsHide: true,
|
|
55
56
|
});
|
|
56
57
|
this.processes.set(terminalId, child);
|
|
57
|
-
child.stdin
|
|
58
|
+
child.stdin?.on('error', () => {});
|
|
58
59
|
child.stdout.on('data', data => onData(data, 'stdout'));
|
|
59
60
|
child.stderr.on('data', data => onData(data, 'stderr'));
|
|
60
61
|
let timedOut = false;
|
|
@@ -70,7 +71,7 @@ export class TerminalProcesses {
|
|
|
70
71
|
clearTimeout(idleTimer);
|
|
71
72
|
child.stdout.destroy();
|
|
72
73
|
child.stderr.destroy();
|
|
73
|
-
child.stdin
|
|
74
|
+
child.stdin?.destroy();
|
|
74
75
|
resolve(code ?? 128 + (constants.signals[child.signalCode] ?? 0));
|
|
75
76
|
};
|
|
76
77
|
const drain = () => {
|
|
@@ -81,7 +82,7 @@ export class TerminalProcesses {
|
|
|
81
82
|
child.stdout.on('data', drain);
|
|
82
83
|
child.stderr.on('data', drain);
|
|
83
84
|
child.once('error', reject);
|
|
84
|
-
child.once('spawn', ready);
|
|
85
|
+
if (interactive) child.once('spawn', ready);
|
|
85
86
|
child.once('close', finish);
|
|
86
87
|
child.once('exit', () => { exited = true; drain(); });
|
|
87
88
|
});
|