@love-moon/conductor-cli 0.2.22 → 0.2.24

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.
@@ -12,9 +12,10 @@ import { RUNTIME_SUPPORTED_BACKENDS } from "../src/runtime-backends.js";
12
12
 
13
13
  const CONFIG_DIR = path.join(os.homedir(), ".conductor");
14
14
  const CONFIG_FILE = path.join(CONFIG_DIR, "config.yaml");
15
+ const packageJson = JSON.parse(
16
+ fs.readFileSync(new URL("../package.json", import.meta.url), "utf-8"),
17
+ );
15
18
 
16
- // 市面上主流 Coding CLI 配置
17
- // 格式: { name: { command: string, description: string, execArgs?: string } }
18
19
  const DEFAULT_CLIs = {
19
20
  claude: {
20
21
  command: "claude",
@@ -26,21 +27,11 @@ const DEFAULT_CLIs = {
26
27
  execArgs: "--dangerously-bypass-approvals-and-sandbox --ask-for-approval never",
27
28
  description: "OpenAI Codex CLI"
28
29
  },
29
- // gemini: {
30
- // command: "gemini",
31
- // execArgs: "",
32
- // description: "Google Gemini CLI"
33
- // },
34
30
  opencode: {
35
31
  command: "opencode",
36
32
  execArgs: "",
37
33
  description: "OpenCode CLI (Conductor runs opencode serve with permission=allow)"
38
34
  },
39
- // kimi: {
40
- // command: "kimi",
41
- // execArgs: "--yolo --print --prompt",
42
- // description: "Kimi CLI"
43
- // },
44
35
  };
45
36
 
46
37
  const backendUrl =
@@ -48,8 +39,8 @@ const backendUrl =
48
39
  process.env.BACKEND_URL ||
49
40
  "https://conductor-ai.top";
50
41
  const defaultDaemonName = os.hostname() || "my-daemon";
42
+ const cliVersion = packageJson.version || "unknown";
51
43
 
52
- // ANSI 颜色代码
53
44
  const COLORS = {
54
45
  yellow: "\x1b[33m",
55
46
  green: "\x1b[32m",
@@ -58,6 +49,8 @@ const COLORS = {
58
49
  bold: "\x1b[1m"
59
50
  };
60
51
 
52
+ let lastDeviceAuthConfig = null;
53
+
61
54
  function colorize(text, color) {
62
55
  return `${COLORS[color] || ""}${text}${COLORS.reset}`;
63
56
  }
@@ -79,11 +72,15 @@ function buildConfigEntryLines(cli, info, { commented = false } = {}) {
79
72
  }
80
73
 
81
74
  async function main() {
82
- // 解析命令行参数
83
75
  const argv = yargs(hideBin(process.argv))
84
76
  .option("token", {
85
77
  type: "string",
86
- description: "Conductor token (optional, will prompt if not provided)"
78
+ description: "Conductor token"
79
+ })
80
+ .option("manual", {
81
+ type: "boolean",
82
+ default: false,
83
+ description: "Enter token manually instead of browser device authorization"
87
84
  })
88
85
  .option("force", {
89
86
  type: "boolean",
@@ -96,13 +93,13 @@ async function main() {
96
93
  description: "Show help"
97
94
  })
98
95
  .usage("Usage: conductor config [options]")
99
- .example("conductor config", "Interactive configuration")
100
- .example("conductor config --token <your-token>", "Configure with token")
96
+ .example("conductor config", "Authorize this device in the browser and write config")
97
+ .example("conductor config --manual", "Enter token manually")
98
+ .example("conductor config --token <token>", "Configure with token")
101
99
  .example("conductor config --token <token> --force", "Force overwrite existing config")
102
100
  .help()
103
101
  .argv;
104
102
 
105
- // 检查配置文件是否存在
106
103
  if (fs.existsSync(CONFIG_FILE) && !argv.force) {
107
104
  process.stderr.write(
108
105
  colorize(`Config already exists at ${CONFIG_FILE}. Use --force to overwrite.\n`, "yellow")
@@ -110,66 +107,68 @@ async function main() {
110
107
  process.exit(1);
111
108
  }
112
109
 
113
- // 获取 token
114
- let token = argv.token;
115
- if (!token) {
116
- token = await promptForToken();
117
- if (!token) {
118
- process.stderr.write(colorize("No token provided. Aborting.\n", "yellow"));
119
- process.exit(1);
120
- }
121
- }
122
-
123
- // 检测已安装的 CLI
110
+ let resolvedBackendUrl = backendUrl;
111
+ let resolvedWebsocketUrl = null;
124
112
  const detectedCLIs = detectInstalledCLIs();
125
113
 
126
- // 如果没有检测到任何 CLI,显示警告
127
114
  if (detectedCLIs.length === 0) {
128
115
  console.log("");
129
116
  console.log(colorize("=".repeat(70), "yellow"));
130
117
  console.log(colorize("⚠️ WARNING: No coding CLI detected!", "yellow"));
131
118
  console.log(colorize("=".repeat(70), "yellow"));
132
- console.log(colorize("", "yellow"));
119
+ console.log("");
133
120
  console.log(colorize("Conductor requires at least one coding CLI to work properly.", "yellow"));
134
- console.log(colorize("", "yellow"));
121
+ console.log("");
135
122
  console.log(colorize("Please install one of the following CLIs first:", "yellow"));
136
123
  console.log("");
137
-
138
- Object.entries(DEFAULT_CLIs).forEach(([key, info]) => {
124
+
125
+ Object.entries(DEFAULT_CLIs).forEach(([, info]) => {
139
126
  console.log(` • ${colorize(info.command, "cyan")} - ${info.description}`);
140
127
  });
141
-
128
+
142
129
  console.log("");
143
130
  console.log(colorize("After installing a CLI, run 'conductor config' again.", "yellow"));
144
131
  console.log(colorize("=".repeat(70), "yellow"));
145
132
  console.log("");
146
-
147
- // 询问是否继续创建配置
133
+
148
134
  const shouldContinue = await promptYesNo(
149
135
  "Do you want to continue creating the config anyway? (y/N): "
150
136
  );
151
-
137
+
152
138
  if (!shouldContinue) {
153
139
  process.exit(1);
154
140
  }
155
141
  } else {
156
- // 显示检测到的 CLI
157
142
  console.log("");
158
143
  console.log(colorize("✓ Detected the following coding CLIs:", "green"));
159
- detectedCLIs.forEach(cli => {
144
+ detectedCLIs.forEach((cli) => {
160
145
  const info = DEFAULT_CLIs[cli];
161
146
  console.log(` • ${colorize(info.command, "cyan")} - ${info.description}`);
162
147
  });
163
148
  console.log("");
164
149
  }
165
150
 
166
- // 创建配置目录
151
+ let token = argv.token;
152
+ if (!token) {
153
+ if (argv.manual) {
154
+ token = await promptForToken();
155
+ if (!token) {
156
+ process.stderr.write(colorize("No token provided. Aborting.\n", "yellow"));
157
+ process.exit(1);
158
+ }
159
+ } else {
160
+ const authResult = await authorizeDeviceAndGetToken();
161
+ token = authResult.agentToken;
162
+ resolvedBackendUrl = authResult.backendUrl || resolvedBackendUrl;
163
+ resolvedWebsocketUrl = authResult.websocketUrl || null;
164
+ }
165
+ }
166
+
167
167
  fs.mkdirSync(CONFIG_DIR, { recursive: true });
168
168
 
169
- // 构建配置内容
170
169
  const lines = [
171
170
  `agent_token: ${yamlQuote(token)}`,
172
- `backend_url: ${yamlQuote(backendUrl)}`,
171
+ `backend_url: ${yamlQuote(resolvedBackendUrl)}`,
173
172
  `daemon_name: ${yamlQuote(defaultDaemonName)}`,
174
173
  "log_level: debug",
175
174
  "workspace: '~/ws/fires'",
@@ -178,14 +177,16 @@ async function main() {
178
177
  "allow_cli_list:"
179
178
  ];
180
179
 
181
- // 添加检测到的 CLI 到配置
180
+ if (resolvedWebsocketUrl) {
181
+ lines.splice(2, 0, `websocket_url: ${yamlQuote(resolvedWebsocketUrl)}`);
182
+ }
183
+
182
184
  if (detectedCLIs.length > 0) {
183
- detectedCLIs.forEach(cli => {
185
+ detectedCLIs.forEach((cli) => {
184
186
  const info = DEFAULT_CLIs[cli];
185
187
  lines.push(...buildConfigEntryLines(cli, info));
186
188
  });
187
189
  } else {
188
- // 如果没有检测到任何 CLI,添加示例注释
189
190
  lines.push(" # No CLI detected. Add your installed CLI here:");
190
191
  Object.entries(DEFAULT_CLIs).forEach(([key, info]) => {
191
192
  lines.push(...buildConfigEntryLines(key, info, { commented: true }));
@@ -202,22 +203,18 @@ async function main() {
202
203
  );
203
204
 
204
205
  fs.writeFileSync(CONFIG_FILE, lines.join("\n"), "utf-8");
205
-
206
+
206
207
  console.log(colorize(`✓ Wrote Conductor config to ${CONFIG_FILE}`, "green"));
207
-
208
+
208
209
  if (detectedCLIs.length === 0) {
209
210
  console.log("");
210
211
  console.log(colorize("⚠️ Remember to install a coding CLI before using Conductor!", "yellow"));
211
212
  }
212
213
  }
213
214
 
214
- /**
215
- * 检测系统中已安装的 CLI
216
- * @returns {string[]} 已安装的 CLI key 列表
217
- */
218
215
  function detectInstalledCLIs() {
219
216
  const detected = [];
220
-
217
+
221
218
  for (const [key, info] of Object.entries(DEFAULT_CLIs)) {
222
219
  if (!RUNTIME_SUPPORTED_BACKENDS.includes(key)) {
223
220
  continue;
@@ -226,52 +223,29 @@ function detectInstalledCLIs() {
226
223
  detected.push(key);
227
224
  }
228
225
  }
229
-
226
+
230
227
  return detected;
231
228
  }
232
229
 
233
- /**
234
- * 检查命令是否在系统 PATH 中可用
235
- * @param {string} command - 命令名称
236
- * @returns {boolean}
237
- */
238
230
  function isCommandAvailable(command) {
239
231
  try {
240
232
  const platform = os.platform();
241
- let checkCmd;
242
-
243
- if (platform === "win32") {
244
- // Windows: 使用 where 命令
245
- checkCmd = `where ${command}`;
246
- } else {
247
- // Unix/Linux/macOS: 使用 which 或 command -v
248
- checkCmd = `command -v ${command}`;
249
- }
250
-
251
- execSync(checkCmd, {
233
+ const checkCmd = platform === "win32" ? `where ${command}` : `command -v ${command}`;
234
+ execSync(checkCmd, {
252
235
  stdio: "pipe",
253
- timeout: 5000
236
+ timeout: 5000
254
237
  });
255
238
  return true;
256
- } catch (error) {
257
- // 对于某些 CLI,可能有特定的检测方式
258
- // 例如检查特定的配置文件或目录
239
+ } catch {
259
240
  return checkAlternativeInstallations(command);
260
241
  }
261
242
  }
262
243
 
263
- /**
264
- * 检查 CLI 的替代安装方式
265
- * @param {string} command - 命令名称
266
- * @returns {boolean}
267
- */
268
244
  function checkAlternativeInstallations(command) {
269
- // 检查常见的全局安装路径
270
245
  const homeDir = os.homedir();
271
246
  const platform = os.platform();
272
-
273
247
  const commonPaths = [];
274
-
248
+
275
249
  if (platform === "win32") {
276
250
  commonPaths.push(
277
251
  path.join(homeDir, "AppData", "Roaming", "npm", `${command}.cmd`),
@@ -290,15 +264,89 @@ function checkAlternativeInstallations(command) {
290
264
  `/opt/${command}/bin/${command}`
291
265
  );
292
266
  }
293
-
294
- // 检查文件是否存在
295
- for (const checkPath of commonPaths) {
296
- if (fs.existsSync(checkPath)) {
297
- return true;
267
+
268
+ return commonPaths.some((checkPath) => fs.existsSync(checkPath));
269
+ }
270
+
271
+ async function authorizeDeviceAndGetToken() {
272
+ const startResponse = await fetch(new URL("/api/auth/device/start", backendUrl), {
273
+ method: "POST",
274
+ headers: { "Content-Type": "application/json" },
275
+ body: JSON.stringify({
276
+ cli_version: cliVersion,
277
+ hostname: defaultDaemonName,
278
+ platform: os.platform(),
279
+ backend_url: backendUrl,
280
+ }),
281
+ });
282
+ const startData = await parseJsonResponse(startResponse);
283
+ if (!startResponse.ok) {
284
+ throw new Error(startData.error || `Failed to start device authorization (${startResponse.status})`);
285
+ }
286
+
287
+ console.log("");
288
+ console.log(colorize("Open this link in your browser to authorize this device:", "cyan"));
289
+ console.log("");
290
+ console.log(`Device code: ${colorize(startData.user_code, "bold")}`);
291
+ console.log(`Direct link: ${startData.verification_uri_complete}`);
292
+ console.log("");
293
+ console.log("Only approve the request if the web page shows the same device code.");
294
+ console.log("");
295
+ console.log("Waiting for authorization...");
296
+
297
+ const intervalSeconds =
298
+ typeof startData.interval === "number" && Number.isFinite(startData.interval)
299
+ ? Math.max(1, startData.interval)
300
+ : 3;
301
+ const expiresInSeconds =
302
+ typeof startData.expires_in === "number" && Number.isFinite(startData.expires_in)
303
+ ? Math.max(intervalSeconds, startData.expires_in)
304
+ : 600;
305
+ const deadlineAt = Date.now() + expiresInSeconds * 1000;
306
+
307
+ while (Date.now() <= deadlineAt) {
308
+ await sleep(intervalSeconds * 1000);
309
+
310
+ const pollResponse = await fetch(new URL("/api/auth/device/poll", backendUrl), {
311
+ method: "POST",
312
+ headers: { "Content-Type": "application/json" },
313
+ body: JSON.stringify({ device_code: startData.device_code }),
314
+ });
315
+ const pollData = await parseJsonResponse(pollResponse);
316
+ if (!pollResponse.ok) {
317
+ throw new Error(pollData.error || `Failed to poll device authorization (${pollResponse.status})`);
298
318
  }
319
+
320
+ if (pollData.status === "pending") {
321
+ continue;
322
+ }
323
+ if (pollData.status === "approved") {
324
+ const result = {
325
+ agentToken: pollData.agent_token,
326
+ backendUrl: pollData.backend_url || backendUrl,
327
+ websocketUrl: pollData.websocket_url || null,
328
+ };
329
+ lastDeviceAuthConfig = result;
330
+ console.log(colorize("✓ Device authorized", "green"));
331
+ return result;
332
+ }
333
+ if (pollData.status === "denied" || pollData.status === "expired" || pollData.status === "consumed") {
334
+ throw new Error(pollData.message || `Device authorization ${pollData.status}`);
335
+ }
336
+ if (pollData.error) {
337
+ throw new Error(pollData.error);
338
+ }
339
+ }
340
+
341
+ throw new Error("Device authorization timed out");
342
+ }
343
+
344
+ async function parseJsonResponse(response) {
345
+ try {
346
+ return await response.json();
347
+ } catch {
348
+ return {};
299
349
  }
300
-
301
- return false;
302
350
  }
303
351
 
304
352
  async function promptForToken() {
@@ -330,6 +378,12 @@ async function promptYesNo(question) {
330
378
  }
331
379
  }
332
380
 
381
+ function sleep(ms) {
382
+ return new Promise((resolve) => {
383
+ setTimeout(resolve, ms);
384
+ });
385
+ }
386
+
333
387
  function yamlQuote(value) {
334
388
  return JSON.stringify(value);
335
389
  }
@@ -17,6 +17,10 @@ import {
17
17
  isNewerVersion,
18
18
  detectPackageManager,
19
19
  } from "../src/version-check.js";
20
+ import {
21
+ ensurePnpmOnlyBuiltDependencies,
22
+ repairAndVerifyGlobalNodePty,
23
+ } from "../src/native-deps.js";
20
24
 
21
25
  const __filename = fileURLToPath(import.meta.url);
22
26
  const __dirname = path.dirname(__filename);
@@ -140,41 +144,50 @@ async function confirmUpdate(version) {
140
144
  }
141
145
 
142
146
  async function performUpdate() {
143
- return new Promise((resolve, reject) => {
144
- // 检测使用的包管理器
145
- const packageManager = detectPackageManager({
146
- launcherPath: process.env.CONDUCTOR_LAUNCHER_SCRIPT || process.argv[1],
147
- packageRoot: PKG_ROOT,
147
+ const packageManager = detectPackageManager({
148
+ launcherPath: process.env.CONDUCTOR_LAUNCHER_SCRIPT || process.argv[1],
149
+ packageRoot: PKG_ROOT,
150
+ });
151
+ console.log(` Using package manager: ${colorize(packageManager, "cyan")}`);
152
+ console.log("");
153
+
154
+ if (packageManager === "pnpm") {
155
+ console.log(" Preparing pnpm native dependency allowlist...");
156
+ await ensurePnpmOnlyBuiltDependencies({
157
+ runCommand: runBufferedCommand,
158
+ dependencies: ["node-pty"],
159
+ global: true,
148
160
  });
149
- console.log(` Using package manager: ${colorize(packageManager, "cyan")}`);
150
161
  console.log("");
151
-
152
- let cmd, args;
153
-
154
- switch (packageManager) {
155
- case "pnpm":
156
- cmd = "pnpm";
157
- args = ["add", "-g", `${PACKAGE_NAME}@latest`];
158
- break;
159
- case "yarn":
160
- cmd = "yarn";
161
- args = ["global", "add", `${PACKAGE_NAME}@latest`];
162
- break;
163
- case "npm":
164
- default:
165
- cmd = "npm";
166
- args = ["install", "-g", `${PACKAGE_NAME}@latest`];
167
- break;
168
- }
169
-
170
- console.log(` Running: ${colorize(`${cmd} ${args.join(" ")}`, "cyan")}`);
171
- console.log("");
172
-
162
+ }
163
+
164
+ let cmd, args;
165
+
166
+ switch (packageManager) {
167
+ case "pnpm":
168
+ cmd = "pnpm";
169
+ args = ["add", "-g", `${PACKAGE_NAME}@latest`];
170
+ break;
171
+ case "yarn":
172
+ cmd = "yarn";
173
+ args = ["global", "add", `${PACKAGE_NAME}@latest`];
174
+ break;
175
+ case "npm":
176
+ default:
177
+ cmd = "npm";
178
+ args = ["install", "-g", `${PACKAGE_NAME}@latest`];
179
+ break;
180
+ }
181
+
182
+ console.log(` Running: ${colorize(`${cmd} ${args.join(" ")}`, "cyan")}`);
183
+ console.log("");
184
+
185
+ await new Promise((resolve, reject) => {
173
186
  const child = spawn(cmd, args, {
174
187
  stdio: "inherit",
175
188
  shell: true
176
189
  });
177
-
190
+
178
191
  child.on("close", (code) => {
179
192
  if (code === 0) {
180
193
  resolve();
@@ -182,11 +195,59 @@ async function performUpdate() {
182
195
  reject(new Error(`Exit code ${code}`));
183
196
  }
184
197
  });
185
-
198
+
186
199
  child.on("error", (error) => {
187
200
  reject(error);
188
201
  });
189
202
  });
203
+
204
+ console.log(" Repairing and verifying node-pty native binding...");
205
+ await repairAndVerifyGlobalNodePty({
206
+ packageManager,
207
+ packageName: PACKAGE_NAME,
208
+ runCommand: runBufferedCommand,
209
+ nodeExecutable: process.execPath,
210
+ });
211
+ }
212
+
213
+ function runBufferedCommand(command, args, options = {}) {
214
+ return new Promise((resolve) => {
215
+ let stdout = "";
216
+ let stderr = "";
217
+ const child = spawn(command, args, {
218
+ stdio: ["ignore", "pipe", "pipe"],
219
+ shell: false,
220
+ env: options.env || process.env,
221
+ cwd: options.cwd || process.cwd(),
222
+ });
223
+ const timer = setTimeout(() => {
224
+ try {
225
+ child.kill("SIGTERM");
226
+ } catch {
227
+ // ignore timeout failures
228
+ }
229
+ }, options.timeoutMs || 20_000);
230
+
231
+ child.stdout?.on("data", (chunk) => {
232
+ if (stdout.length < 16_000) stdout += chunk.toString();
233
+ });
234
+ child.stderr?.on("data", (chunk) => {
235
+ if (stderr.length < 16_000) stderr += chunk.toString();
236
+ });
237
+ child.on("close", (code) => {
238
+ clearTimeout(timer);
239
+ resolve({ success: code === 0, code, stdout, stderr });
240
+ });
241
+ child.on("error", (error) => {
242
+ clearTimeout(timer);
243
+ resolve({
244
+ success: false,
245
+ code: -1,
246
+ stdout,
247
+ stderr: error instanceof Error ? error.message : String(error),
248
+ });
249
+ });
250
+ });
190
251
  }
191
252
 
192
253
  function showHelpMessage() {
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env node
2
+
3
+ import process from "node:process";
4
+
5
+ import { verifyNodePtyForPackageDirectory } from "../src/native-deps.js";
6
+
7
+ async function main() {
8
+ const packageDirectory = process.argv[2];
9
+ if (!packageDirectory) {
10
+ process.stderr.write("Usage: conductor-verify-node-pty <package-directory>\n");
11
+ process.exit(1);
12
+ return;
13
+ }
14
+
15
+ await verifyNodePtyForPackageDirectory({
16
+ packageDirectory,
17
+ });
18
+ process.stdout.write("Verified node-pty native binding\n");
19
+ }
20
+
21
+ main().catch((error) => {
22
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
23
+ process.exit(1);
24
+ });
package/bin/conductor.js CHANGED
@@ -87,7 +87,9 @@ const isDirectExecution = (() => {
87
87
  return false;
88
88
  }
89
89
  try {
90
- return pathToFileURL(entryPath).href === import.meta.url;
90
+ const entryRealPath = fs.realpathSync(entryPath);
91
+ const currentRealPath = fs.realpathSync(__filename);
92
+ return pathToFileURL(entryRealPath).href === pathToFileURL(currentRealPath).href;
91
93
  } catch {
92
94
  return false;
93
95
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@love-moon/conductor-cli",
3
- "version": "0.2.22",
4
- "gitCommitId": "d04b18c",
3
+ "version": "0.2.24",
4
+ "gitCommitId": "1774cf6",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "conductor": "bin/conductor.js"
@@ -17,8 +17,8 @@
17
17
  "test": "node --test test/*.test.js"
18
18
  },
19
19
  "dependencies": {
20
- "@love-moon/ai-sdk": "0.2.22",
21
- "@love-moon/conductor-sdk": "0.2.22",
20
+ "@love-moon/ai-sdk": "0.2.24",
21
+ "@love-moon/conductor-sdk": "0.2.24",
22
22
  "dotenv": "^16.4.5",
23
23
  "enquirer": "^2.4.1",
24
24
  "js-yaml": "^4.1.1",
package/src/daemon.js CHANGED
@@ -20,6 +20,10 @@ import {
20
20
  isInUpdateWindow,
21
21
  isManagedInstallPath,
22
22
  } from "./version-check.js";
23
+ import {
24
+ ensurePnpmOnlyBuiltDependencies,
25
+ repairAndVerifyGlobalNodePty,
26
+ } from "./native-deps.js";
23
27
 
24
28
  dotenv.config();
25
29
 
@@ -48,6 +52,39 @@ const DEFAULT_TERMINAL_RING_BUFFER_MAX_BYTES = 2 * 1024 * 1024;
48
52
  const DEFAULT_RTC_MODULE_CANDIDATES = ["@roamhq/wrtc", "wrtc"];
49
53
  let nodePtySpawnPromise = null;
50
54
 
55
+ function resolveNodePtySpawnExport(mod) {
56
+ if (typeof mod?.spawn === "function") {
57
+ return mod.spawn;
58
+ }
59
+ if (mod?.default && typeof mod.default.spawn === "function") {
60
+ return mod.default.spawn.bind(mod.default);
61
+ }
62
+ throw new Error("node-pty spawn export not found");
63
+ }
64
+
65
+ export function probePtyTaskCapability({
66
+ requireFn = moduleRequire,
67
+ ensureSpawnHelperExecutableFn = ensureNodePtySpawnHelperExecutable,
68
+ } = {}) {
69
+ try {
70
+ const spawnHelperInfo = ensureSpawnHelperExecutableFn();
71
+ const spawnPty = resolveNodePtySpawnExport(requireFn("node-pty"));
72
+ return {
73
+ enabled: true,
74
+ reason: null,
75
+ spawnHelperInfo,
76
+ spawnPty,
77
+ };
78
+ } catch (error) {
79
+ return {
80
+ enabled: false,
81
+ reason: error instanceof Error ? error.message : String(error),
82
+ spawnHelperInfo: null,
83
+ spawnPty: null,
84
+ };
85
+ }
86
+ }
87
+
51
88
  function appendDaemonLog(line) {
52
89
  try {
53
90
  fs.mkdirSync(DAEMON_LOG_DIR, { recursive: true });
@@ -154,20 +191,48 @@ async function defaultCreatePty(command, args, options) {
154
191
  if (spawnHelperInfo?.updated) {
155
192
  log(`Enabled execute permission on node-pty spawn-helper: ${spawnHelperInfo.helperPath}`);
156
193
  }
157
- nodePtySpawnPromise = import("node-pty").then((mod) => {
158
- if (typeof mod.spawn === "function") {
159
- return mod.spawn;
160
- }
161
- if (mod.default && typeof mod.default.spawn === "function") {
162
- return mod.default.spawn.bind(mod.default);
163
- }
164
- throw new Error("node-pty spawn export not found");
165
- });
194
+ nodePtySpawnPromise = Promise.resolve(resolveNodePtySpawnExport(moduleRequire("node-pty")));
166
195
  }
167
196
  const spawnPty = await nodePtySpawnPromise;
168
197
  return spawnPty(command, args, options);
169
198
  }
170
199
 
200
+ export function resolveDefaultPtyShell({
201
+ explicitShell,
202
+ envShell = process.env.SHELL,
203
+ comspec = process.env.COMSPEC,
204
+ platform = process.platform,
205
+ existsSync = fs.existsSync,
206
+ } = {}) {
207
+ const normalizedExplicitShell = normalizeOptionalString(explicitShell);
208
+ if (normalizedExplicitShell) {
209
+ return normalizedExplicitShell;
210
+ }
211
+
212
+ const normalizedEnvShell = normalizeOptionalString(envShell);
213
+ if (normalizedEnvShell) {
214
+ return normalizedEnvShell;
215
+ }
216
+
217
+ if (platform === "win32") {
218
+ return normalizeOptionalString(comspec) || "cmd.exe";
219
+ }
220
+
221
+ if (platform === "darwin") {
222
+ return "/bin/zsh";
223
+ }
224
+
225
+ if (existsSync("/bin/bash")) {
226
+ return "/bin/bash";
227
+ }
228
+
229
+ if (existsSync("/bin/sh")) {
230
+ return "/bin/sh";
231
+ }
232
+
233
+ return "/bin/bash";
234
+ }
235
+
171
236
  export function ensureNodePtySpawnHelperExecutable(deps = {}) {
172
237
  const platform = deps.platform || process.platform;
173
238
  if (platform === "win32") {
@@ -284,6 +349,15 @@ export function startDaemon(config = {}, deps = {}) {
284
349
  let requestShutdown = async () => {};
285
350
  let shutdownSignalHandled = false;
286
351
  let forcedSignalExitHandled = false;
352
+ let processHandlersAttached = false;
353
+
354
+ const removeProcessListener = (eventName, handler) => {
355
+ if (typeof process.off === "function") {
356
+ process.off(eventName, handler);
357
+ return;
358
+ }
359
+ process.removeListener(eventName, handler);
360
+ };
287
361
 
288
362
  const exitAndReturn = (code) => {
289
363
  exitFn(code);
@@ -597,7 +671,6 @@ export function startDaemon(config = {}, deps = {}) {
597
671
  );
598
672
  };
599
673
 
600
- process.on("exit", cleanupLock);
601
674
  const signalExitCode = (signal) => (signal === "SIGINT" ? 130 : 143);
602
675
  const handleSignal = (signal) => {
603
676
  if (shutdownSignalHandled) {
@@ -621,17 +694,33 @@ export function startDaemon(config = {}, deps = {}) {
621
694
  }
622
695
  })();
623
696
  };
624
- process.on("SIGINT", () => {
697
+ const onSigInt = () => {
625
698
  handleSignal("SIGINT");
626
- });
627
- process.on("SIGTERM", () => {
699
+ };
700
+ const onSigTerm = () => {
628
701
  handleSignal("SIGTERM");
629
- });
630
- process.on("uncaughtException", (err) => {
702
+ };
703
+ const onUncaughtException = (err) => {
631
704
  logError(`Uncaught exception: ${err}`);
632
705
  cleanupLock();
633
706
  exitFn(1);
634
- });
707
+ };
708
+ const detachProcessHandlers = () => {
709
+ if (!processHandlersAttached) {
710
+ return;
711
+ }
712
+ processHandlersAttached = false;
713
+ removeProcessListener("exit", cleanupLock);
714
+ removeProcessListener("SIGINT", onSigInt);
715
+ removeProcessListener("SIGTERM", onSigTerm);
716
+ removeProcessListener("uncaughtException", onUncaughtException);
717
+ };
718
+
719
+ process.on("exit", cleanupLock);
720
+ process.on("SIGINT", onSigInt);
721
+ process.on("SIGTERM", onSigTerm);
722
+ process.on("uncaughtException", onUncaughtException);
723
+ processHandlersAttached = true;
635
724
 
636
725
  if (config.CLEAN_ALL) {
637
726
  cleanAllAgents(BACKEND_HTTP, AGENT_TOKEN, fetchFn)
@@ -641,8 +730,11 @@ export function startDaemon(config = {}, deps = {}) {
641
730
  .catch((err) => {
642
731
  log(`Failed to clean daemons: ${err.message}`);
643
732
  })
644
- .finally(() => exitFn(0));
645
- return { close: () => {} };
733
+ .finally(() => {
734
+ detachProcessHandlers();
735
+ exitFn(0);
736
+ });
737
+ return { close: detachProcessHandlers };
646
738
  }
647
739
 
648
740
  log("Daemon starting...");
@@ -692,13 +784,40 @@ export function startDaemon(config = {}, deps = {}) {
692
784
  let rtcAvailabilityLogKey = null;
693
785
  const logCollector = createLogCollector(BACKEND_HTTP);
694
786
  const createPtyFn = deps.createPty || defaultCreatePty;
787
+ const resolvePtyTaskCapabilityFn =
788
+ deps.resolvePtyTaskCapability ||
789
+ (deps.createPty
790
+ ? (() => ({ enabled: true, reason: null, spawnHelperInfo: null, spawnPty: null }))
791
+ : probePtyTaskCapability);
792
+ let ptyTaskCapability;
793
+ try {
794
+ ptyTaskCapability = resolvePtyTaskCapabilityFn();
795
+ } catch (error) {
796
+ ptyTaskCapability = {
797
+ enabled: false,
798
+ reason: error instanceof Error ? error.message : String(error),
799
+ spawnHelperInfo: null,
800
+ spawnPty: null,
801
+ };
802
+ }
803
+ const ptyTaskCapabilityEnabled = ptyTaskCapability?.enabled !== false;
804
+ const ptyTaskCapabilityError = normalizeOptionalString(ptyTaskCapability?.reason);
805
+ if (ptyTaskCapability?.spawnHelperInfo?.updated) {
806
+ log(`Enabled execute permission on node-pty spawn-helper: ${ptyTaskCapability.spawnHelperInfo.helperPath}`);
807
+ }
808
+ if (!ptyTaskCapabilityEnabled) {
809
+ logError(`[pty] Disabled PTY capability: ${ptyTaskCapabilityError || "unknown error"}`);
810
+ }
811
+ const extraHeaders = {
812
+ "x-conductor-host": AGENT_NAME,
813
+ "x-conductor-backends": SUPPORTED_BACKENDS.join(","),
814
+ "x-conductor-version": cliVersion,
815
+ };
816
+ if (ptyTaskCapabilityEnabled) {
817
+ extraHeaders["x-conductor-capabilities"] = "pty_task";
818
+ }
695
819
  const client = createWebSocketClient(sdkConfig, {
696
- extraHeaders: {
697
- "x-conductor-host": AGENT_NAME,
698
- "x-conductor-backends": SUPPORTED_BACKENDS.join(","),
699
- "x-conductor-capabilities": "pty_task",
700
- "x-conductor-version": cliVersion,
701
- },
820
+ extraHeaders,
702
821
  onConnected: ({ isReconnect, connectedAt } = { isReconnect: false, connectedAt: Date.now() }) => {
703
822
  wsConnected = true;
704
823
  lastConnectedAt = connectedAt || Date.now();
@@ -1054,6 +1173,14 @@ export function startDaemon(config = {}, deps = {}) {
1054
1173
  });
1055
1174
  }
1056
1175
 
1176
+ function runBufferedCommand(command, args, options = {}) {
1177
+ return runCommand(
1178
+ command,
1179
+ args,
1180
+ typeof options === "number" ? options : options?.timeoutMs ?? 120_000,
1181
+ );
1182
+ }
1183
+
1057
1184
  async function readInstalledCliVersion() {
1058
1185
  const commandAttempts = versionCheckScript
1059
1186
  ? [{
@@ -1090,6 +1217,16 @@ export function startDaemon(config = {}, deps = {}) {
1090
1217
  packageRoot: installedPackageRoot,
1091
1218
  });
1092
1219
  const pkgSpec = `${PACKAGE_NAME}@${targetVersion}`;
1220
+
1221
+ if (pm === "pnpm") {
1222
+ log("[auto-update] Preparing pnpm native dependency allowlist for node-pty");
1223
+ await ensurePnpmOnlyBuiltDependencies({
1224
+ runCommand: runBufferedCommand,
1225
+ dependencies: ["node-pty"],
1226
+ global: true,
1227
+ });
1228
+ }
1229
+
1093
1230
  log(`[auto-update] Installing ${pkgSpec} via ${pm}...`);
1094
1231
 
1095
1232
  // Step 1: install
@@ -1118,7 +1255,19 @@ export function startDaemon(config = {}, deps = {}) {
1118
1255
  throw new Error(`Version verification failed: ${verifyErr?.message || verifyErr}`);
1119
1256
  }
1120
1257
 
1121
- log(`[auto-update] Verified ${targetVersion}. Restarting daemon...`);
1258
+ // Step 4: repair and verify native dependencies before shutting down the healthy daemon.
1259
+ try {
1260
+ await repairAndVerifyGlobalNodePty({
1261
+ packageManager: pm,
1262
+ packageName: PACKAGE_NAME,
1263
+ runCommand: runBufferedCommand,
1264
+ nodeExecutable: process.execPath,
1265
+ });
1266
+ } catch (verifyErr) {
1267
+ throw new Error(`Native dependency verification failed: ${verifyErr?.message || verifyErr}`);
1268
+ }
1269
+
1270
+ log(`[auto-update] Verified ${targetVersion} and node-pty. Restarting daemon...`);
1122
1271
 
1123
1272
  let logFd = null;
1124
1273
  if (isBackgroundProcess) {
@@ -1133,10 +1282,10 @@ export function startDaemon(config = {}, deps = {}) {
1133
1282
  logFd = fs.openSync(DAEMON_LOG_PATH, "a");
1134
1283
  }
1135
1284
 
1136
- // Step 4: graceful shutdown
1285
+ // Step 5: graceful shutdown
1137
1286
  await shutdownDaemon("auto-update");
1138
1287
 
1139
- // Step 5: re-spawn (only in background/nohup mode)
1288
+ // Step 6: re-spawn (only in background/nohup mode)
1140
1289
  if (isBackgroundProcess) {
1141
1290
  const handoffToken = `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
1142
1291
  const handoffExpiresAt = Date.now() + 15_000;
@@ -1403,6 +1552,31 @@ export function startDaemon(config = {}, deps = {}) {
1403
1552
  });
1404
1553
  }
1405
1554
 
1555
+ function rejectCreatePtyTaskUnavailable(payload) {
1556
+ const taskId = payload?.task_id ? String(payload.task_id) : "";
1557
+ const projectId = payload?.project_id ? String(payload.project_id) : "";
1558
+ const ptySessionId = payload?.pty_session_id ? String(payload.pty_session_id) : null;
1559
+ const requestId = payload?.request_id ? String(payload.request_id) : "";
1560
+ const message = ptyTaskCapabilityError
1561
+ ? `pty runtime unavailable: ${ptyTaskCapabilityError}`
1562
+ : "pty runtime unavailable";
1563
+ log(`Rejecting create_pty_task for ${taskId || "unknown"}: ${message}`);
1564
+ sendAgentCommandAck({
1565
+ requestId,
1566
+ taskId,
1567
+ eventType: "create_pty_task",
1568
+ accepted: false,
1569
+ }).catch(() => {});
1570
+ sendTerminalEvent("terminal_error", {
1571
+ task_id: taskId || undefined,
1572
+ project_id: projectId || undefined,
1573
+ pty_session_id: ptySessionId,
1574
+ message,
1575
+ }).catch((err) => {
1576
+ logError(`Failed to report PTY capability rejection for ${taskId || "unknown"}: ${err?.message || err}`);
1577
+ });
1578
+ }
1579
+
1406
1580
  function sendPtyTransportSignal(payload) {
1407
1581
  return client.sendJson({
1408
1582
  type: "pty_transport_signal",
@@ -1629,10 +1803,13 @@ export function startDaemon(config = {}, deps = {}) {
1629
1803
  normalizeOptionalString(normalizedLaunchConfig.toolPreset)
1630
1804
  ? "tool_preset"
1631
1805
  : "shell");
1632
- const preferredShell =
1633
- normalizeOptionalString(normalizedLaunchConfig.shell) ||
1634
- process.env.SHELL ||
1635
- "/bin/zsh";
1806
+ const preferredShell = resolveDefaultPtyShell({
1807
+ explicitShell: normalizedLaunchConfig.shell,
1808
+ envShell: process.env.SHELL,
1809
+ comspec: process.env.COMSPEC,
1810
+ platform: process.platform,
1811
+ existsSync: existsSyncFn,
1812
+ });
1636
1813
  const cwd =
1637
1814
  normalizeOptionalString(normalizedLaunchConfig.cwd) ||
1638
1815
  fallbackCwd;
@@ -1880,6 +2057,16 @@ export function startDaemon(config = {}, deps = {}) {
1880
2057
  return;
1881
2058
  }
1882
2059
 
2060
+ if (daemonShuttingDown) {
2061
+ rejectCreatePtyTaskDuringShutdown(payload);
2062
+ return;
2063
+ }
2064
+
2065
+ if (!ptyTaskCapabilityEnabled) {
2066
+ rejectCreatePtyTaskUnavailable(payload);
2067
+ return;
2068
+ }
2069
+
1883
2070
  if (requestId && !markRequestSeen(requestId)) {
1884
2071
  log(`Duplicate create_pty_task ignored for ${taskId} (request_id=${requestId})`);
1885
2072
  sendAgentCommandAck({
@@ -1891,11 +2078,6 @@ export function startDaemon(config = {}, deps = {}) {
1891
2078
  return;
1892
2079
  }
1893
2080
 
1894
- if (daemonShuttingDown) {
1895
- rejectCreatePtyTaskDuringShutdown(payload);
1896
- return;
1897
- }
1898
-
1899
2081
  if (activeTaskProcesses.has(taskId) || activePtySessions.has(taskId)) {
1900
2082
  log(`Duplicate create_pty_task ignored for ${taskId}: task already active`);
1901
2083
  sendAgentCommandAck({
@@ -2946,6 +3128,7 @@ export function startDaemon(config = {}, deps = {}) {
2946
3128
 
2947
3129
  return {
2948
3130
  close: () => {
3131
+ detachProcessHandlers();
2949
3132
  void shutdownDaemon();
2950
3133
  },
2951
3134
  };
@@ -0,0 +1,309 @@
1
+ import path from "node:path";
2
+ import process from "node:process";
3
+ import { spawn as spawnProcess } from "node:child_process";
4
+
5
+ function defaultRunCommand(command, args, options = {}) {
6
+ return new Promise((resolve) => {
7
+ let stdout = "";
8
+ let stderr = "";
9
+ const child = spawnProcess(command, args, {
10
+ stdio: ["ignore", "pipe", "pipe"],
11
+ env: options.env || { ...process.env },
12
+ cwd: options.cwd || process.cwd(),
13
+ });
14
+ const timer = setTimeout(() => {
15
+ try {
16
+ child.kill("SIGTERM");
17
+ } catch {
18
+ // ignore timeout kill failures
19
+ }
20
+ }, options.timeoutMs || 20_000);
21
+ child.stdout?.on("data", (chunk) => {
22
+ if (stdout.length < 16_000) {
23
+ stdout += chunk.toString();
24
+ }
25
+ });
26
+ child.stderr?.on("data", (chunk) => {
27
+ if (stderr.length < 16_000) {
28
+ stderr += chunk.toString();
29
+ }
30
+ });
31
+ child.on("close", (code) => {
32
+ clearTimeout(timer);
33
+ resolve({ success: code === 0, code, stdout, stderr });
34
+ });
35
+ child.on("error", (error) => {
36
+ clearTimeout(timer);
37
+ resolve({
38
+ success: false,
39
+ code: -1,
40
+ stdout,
41
+ stderr: error instanceof Error ? error.message : String(error),
42
+ });
43
+ });
44
+ });
45
+ }
46
+
47
+ function quoteForSingleQuotedShell(value) {
48
+ return String(value).replace(/\\/g, "\\\\").replace(/'/g, "\\'");
49
+ }
50
+
51
+ export function normalizeBuiltDependencyList(value) {
52
+ if (Array.isArray(value)) {
53
+ return value
54
+ .map((entry) => (typeof entry === "string" ? entry.trim() : ""))
55
+ .filter(Boolean);
56
+ }
57
+ if (typeof value !== "string") {
58
+ return [];
59
+ }
60
+ const trimmed = value.trim();
61
+ if (!trimmed || trimmed === "undefined" || trimmed === "null") {
62
+ return [];
63
+ }
64
+ try {
65
+ return normalizeBuiltDependencyList(JSON.parse(trimmed));
66
+ } catch {
67
+ return trimmed
68
+ .split(",")
69
+ .map((entry) => entry.trim())
70
+ .filter(Boolean);
71
+ }
72
+ }
73
+
74
+ export function mergeBuiltDependencies(existing, required) {
75
+ const merged = new Set(normalizeBuiltDependencyList(existing));
76
+ for (const dependency of normalizeBuiltDependencyList(required)) {
77
+ merged.add(dependency);
78
+ }
79
+ return [...merged];
80
+ }
81
+
82
+ export async function ensurePnpmOnlyBuiltDependencies({
83
+ runCommand = defaultRunCommand,
84
+ dependencies = ["node-pty"],
85
+ global = true,
86
+ } = {}) {
87
+ const scopeArgs = global ? ["--global"] : ["--location=project"];
88
+ const currentResult = await runCommand("pnpm", [
89
+ "config",
90
+ "get",
91
+ ...scopeArgs,
92
+ "onlyBuiltDependencies",
93
+ "--json",
94
+ ]);
95
+ const current = normalizeBuiltDependencyList(currentResult.stdout);
96
+ const merged = mergeBuiltDependencies(current, dependencies);
97
+ if (merged.length === current.length && merged.every((entry, index) => entry === current[index])) {
98
+ return merged;
99
+ }
100
+ const setResult = await runCommand("pnpm", [
101
+ "config",
102
+ "set",
103
+ ...scopeArgs,
104
+ "onlyBuiltDependencies",
105
+ JSON.stringify(merged),
106
+ ]);
107
+ if (!setResult.success) {
108
+ throw new Error(
109
+ `Failed to configure pnpm onlyBuiltDependencies: ${String(
110
+ setResult.stderr || setResult.stdout || "unknown error",
111
+ ).trim()}`,
112
+ );
113
+ }
114
+ return merged;
115
+ }
116
+
117
+ export async function resolveGlobalPackageDirectory({
118
+ packageManager,
119
+ packageName,
120
+ runCommand = defaultRunCommand,
121
+ } = {}) {
122
+ if (!packageManager || !packageName) {
123
+ throw new Error("packageManager and packageName are required");
124
+ }
125
+
126
+ let command;
127
+ let args;
128
+ let normalizeRoot = (value) => value;
129
+
130
+ if (packageManager === "pnpm" || packageManager === "npm") {
131
+ command = packageManager;
132
+ args = ["root", "-g"];
133
+ } else if (packageManager === "yarn") {
134
+ command = "yarn";
135
+ args = ["global", "dir"];
136
+ normalizeRoot = (value) => path.join(value, "node_modules");
137
+ } else {
138
+ throw new Error(`Unsupported package manager: ${packageManager}`);
139
+ }
140
+
141
+ const result = await runCommand(command, args);
142
+ if (!result.success) {
143
+ throw new Error(
144
+ `Failed to resolve global package root via ${packageManager}: ${String(
145
+ result.stderr || result.stdout || "unknown error",
146
+ ).trim()}`,
147
+ );
148
+ }
149
+
150
+ const rawRoot = String(result.stdout || "")
151
+ .split(/\r?\n/)
152
+ .map((line) => line.trim())
153
+ .filter(Boolean)
154
+ .at(-1);
155
+ if (!rawRoot) {
156
+ throw new Error(`Global package root for ${packageManager} is empty`);
157
+ }
158
+
159
+ return path.join(normalizeRoot(rawRoot), packageName);
160
+ }
161
+
162
+ export function buildNodePtyVerificationScript() {
163
+ return String.raw`
164
+ const fs = require('node:fs');
165
+ const path = require('node:path');
166
+ const { createRequire } = require('node:module');
167
+
168
+ const packageDir = process.argv[1];
169
+ if (!packageDir) {
170
+ throw new Error('package directory is required');
171
+ }
172
+ const packageJsonPath = path.join(packageDir, 'package.json');
173
+ const req = createRequire(packageJsonPath);
174
+ const nodePty = req('node-pty');
175
+ const spawn = typeof nodePty.spawn === 'function'
176
+ ? nodePty.spawn
177
+ : (nodePty.default && typeof nodePty.default.spawn === 'function'
178
+ ? nodePty.default.spawn.bind(nodePty.default)
179
+ : null);
180
+
181
+ if (!spawn) {
182
+ throw new Error('node-pty spawn export not found');
183
+ }
184
+
185
+ const shell = process.platform === 'win32'
186
+ ? (process.env.COMSPEC || 'cmd.exe')
187
+ : (fs.existsSync('/bin/bash') ? '/bin/bash' : '/bin/sh');
188
+ const shellArgs = process.platform === 'win32'
189
+ ? ['/d', '/s', '/c', 'exit 0']
190
+ : ['-lc', 'exit 0'];
191
+
192
+ const child = spawn(shell, shellArgs, {
193
+ name: 'conductor-node-pty-check',
194
+ cols: 80,
195
+ rows: 24,
196
+ cwd: process.cwd(),
197
+ env: process.env,
198
+ });
199
+
200
+ let settled = false;
201
+ const finish = (code, error) => {
202
+ if (settled) return;
203
+ settled = true;
204
+ clearTimeout(timer);
205
+ if (error) {
206
+ console.error(error instanceof Error ? error.message : String(error));
207
+ process.exit(1);
208
+ return;
209
+ }
210
+ if (typeof code === 'number' && code !== 0) {
211
+ console.error('node-pty smoke test exited with code ' + code);
212
+ process.exit(1);
213
+ return;
214
+ }
215
+ console.log('Verified node-pty native binding');
216
+ process.exit(0);
217
+ };
218
+
219
+ const timer = setTimeout(() => {
220
+ try {
221
+ child.kill();
222
+ } catch {
223
+ // ignore kill failures
224
+ }
225
+ finish(null, new Error('node-pty smoke test timed out'));
226
+ }, 5000);
227
+
228
+ child.on('exit', (code) => finish(code, null));
229
+ child.on('error', (error) => finish(null, error));
230
+ `;
231
+ }
232
+
233
+ export async function verifyNodePtyForPackageDirectory({
234
+ packageDirectory,
235
+ runCommand = defaultRunCommand,
236
+ nodeExecutable = process.execPath,
237
+ } = {}) {
238
+ if (!packageDirectory) {
239
+ throw new Error("packageDirectory is required");
240
+ }
241
+ const result = await runCommand(nodeExecutable, ["-e", buildNodePtyVerificationScript(), packageDirectory], {
242
+ timeoutMs: 15_000,
243
+ });
244
+ if (!result.success) {
245
+ throw new Error(
246
+ `node-pty verification failed for ${packageDirectory}: ${String(
247
+ result.stderr || result.stdout || "unknown error",
248
+ ).trim()}`,
249
+ );
250
+ }
251
+ return result;
252
+ }
253
+
254
+ export async function repairAndVerifyGlobalNodePty({
255
+ packageManager,
256
+ packageName,
257
+ runCommand = defaultRunCommand,
258
+ nodeExecutable = process.execPath,
259
+ dependencies = ["node-pty"],
260
+ packageSpec = null,
261
+ } = {}) {
262
+ if (!packageManager || !packageName) {
263
+ throw new Error("packageManager and packageName are required");
264
+ }
265
+
266
+ if (packageManager === "pnpm") {
267
+ await ensurePnpmOnlyBuiltDependencies({ runCommand, dependencies, global: true });
268
+ }
269
+
270
+ if (packageManager === "pnpm") {
271
+ const rebuildResult = await runCommand("pnpm", ["rebuild", "-g", ...dependencies]);
272
+ if (!rebuildResult.success) {
273
+ throw new Error(
274
+ `pnpm rebuild failed: ${String(rebuildResult.stderr || rebuildResult.stdout || "unknown error").trim()}`,
275
+ );
276
+ }
277
+ } else if (packageManager === "npm") {
278
+ const rebuildArgs = ["rebuild", "-g"];
279
+ if (packageSpec) {
280
+ rebuildArgs.push(packageSpec);
281
+ } else {
282
+ rebuildArgs.push(packageName);
283
+ }
284
+ const rebuildResult = await runCommand("npm", rebuildArgs);
285
+ if (!rebuildResult.success) {
286
+ throw new Error(
287
+ `npm rebuild failed: ${String(rebuildResult.stderr || rebuildResult.stdout || "unknown error").trim()}`,
288
+ );
289
+ }
290
+ }
291
+
292
+ const packageDirectory = await resolveGlobalPackageDirectory({
293
+ packageManager,
294
+ packageName,
295
+ runCommand,
296
+ });
297
+ await verifyNodePtyForPackageDirectory({
298
+ packageDirectory,
299
+ runCommand,
300
+ nodeExecutable,
301
+ });
302
+ return packageDirectory;
303
+ }
304
+
305
+ export function buildNodePtyShellVerificationCommand(scriptPath, packageDirectory) {
306
+ const quotedScriptPath = quoteForSingleQuotedShell(scriptPath);
307
+ const quotedPackageDirectory = quoteForSingleQuotedShell(packageDirectory);
308
+ return `node '${quotedScriptPath}' '${quotedPackageDirectory}'`;
309
+ }