@love-moon/conductor-cli 0.2.23 → 0.2.25

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.
@@ -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.23",
4
- "gitCommitId": "e1d19e3",
3
+ "version": "0.2.25",
4
+ "gitCommitId": "6d7b348",
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.23",
21
- "@love-moon/conductor-sdk": "0.2.23",
20
+ "@love-moon/ai-sdk": "0.2.25",
21
+ "@love-moon/conductor-sdk": "0.2.25",
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") {
@@ -719,13 +784,40 @@ export function startDaemon(config = {}, deps = {}) {
719
784
  let rtcAvailabilityLogKey = null;
720
785
  const logCollector = createLogCollector(BACKEND_HTTP);
721
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
+ }
722
819
  const client = createWebSocketClient(sdkConfig, {
723
- extraHeaders: {
724
- "x-conductor-host": AGENT_NAME,
725
- "x-conductor-backends": SUPPORTED_BACKENDS.join(","),
726
- "x-conductor-capabilities": "pty_task",
727
- "x-conductor-version": cliVersion,
728
- },
820
+ extraHeaders,
729
821
  onConnected: ({ isReconnect, connectedAt } = { isReconnect: false, connectedAt: Date.now() }) => {
730
822
  wsConnected = true;
731
823
  lastConnectedAt = connectedAt || Date.now();
@@ -1081,6 +1173,14 @@ export function startDaemon(config = {}, deps = {}) {
1081
1173
  });
1082
1174
  }
1083
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
+
1084
1184
  async function readInstalledCliVersion() {
1085
1185
  const commandAttempts = versionCheckScript
1086
1186
  ? [{
@@ -1117,6 +1217,16 @@ export function startDaemon(config = {}, deps = {}) {
1117
1217
  packageRoot: installedPackageRoot,
1118
1218
  });
1119
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
+
1120
1230
  log(`[auto-update] Installing ${pkgSpec} via ${pm}...`);
1121
1231
 
1122
1232
  // Step 1: install
@@ -1145,7 +1255,19 @@ export function startDaemon(config = {}, deps = {}) {
1145
1255
  throw new Error(`Version verification failed: ${verifyErr?.message || verifyErr}`);
1146
1256
  }
1147
1257
 
1148
- 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...`);
1149
1271
 
1150
1272
  let logFd = null;
1151
1273
  if (isBackgroundProcess) {
@@ -1160,10 +1282,10 @@ export function startDaemon(config = {}, deps = {}) {
1160
1282
  logFd = fs.openSync(DAEMON_LOG_PATH, "a");
1161
1283
  }
1162
1284
 
1163
- // Step 4: graceful shutdown
1285
+ // Step 5: graceful shutdown
1164
1286
  await shutdownDaemon("auto-update");
1165
1287
 
1166
- // Step 5: re-spawn (only in background/nohup mode)
1288
+ // Step 6: re-spawn (only in background/nohup mode)
1167
1289
  if (isBackgroundProcess) {
1168
1290
  const handoffToken = `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
1169
1291
  const handoffExpiresAt = Date.now() + 15_000;
@@ -1430,6 +1552,31 @@ export function startDaemon(config = {}, deps = {}) {
1430
1552
  });
1431
1553
  }
1432
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
+
1433
1580
  function sendPtyTransportSignal(payload) {
1434
1581
  return client.sendJson({
1435
1582
  type: "pty_transport_signal",
@@ -1656,10 +1803,13 @@ export function startDaemon(config = {}, deps = {}) {
1656
1803
  normalizeOptionalString(normalizedLaunchConfig.toolPreset)
1657
1804
  ? "tool_preset"
1658
1805
  : "shell");
1659
- const preferredShell =
1660
- normalizeOptionalString(normalizedLaunchConfig.shell) ||
1661
- process.env.SHELL ||
1662
- "/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
+ });
1663
1813
  const cwd =
1664
1814
  normalizeOptionalString(normalizedLaunchConfig.cwd) ||
1665
1815
  fallbackCwd;
@@ -1907,6 +2057,16 @@ export function startDaemon(config = {}, deps = {}) {
1907
2057
  return;
1908
2058
  }
1909
2059
 
2060
+ if (daemonShuttingDown) {
2061
+ rejectCreatePtyTaskDuringShutdown(payload);
2062
+ return;
2063
+ }
2064
+
2065
+ if (!ptyTaskCapabilityEnabled) {
2066
+ rejectCreatePtyTaskUnavailable(payload);
2067
+ return;
2068
+ }
2069
+
1910
2070
  if (requestId && !markRequestSeen(requestId)) {
1911
2071
  log(`Duplicate create_pty_task ignored for ${taskId} (request_id=${requestId})`);
1912
2072
  sendAgentCommandAck({
@@ -1918,11 +2078,6 @@ export function startDaemon(config = {}, deps = {}) {
1918
2078
  return;
1919
2079
  }
1920
2080
 
1921
- if (daemonShuttingDown) {
1922
- rejectCreatePtyTaskDuringShutdown(payload);
1923
- return;
1924
- }
1925
-
1926
2081
  if (activeTaskProcesses.has(taskId) || activePtySessions.has(taskId)) {
1927
2082
  log(`Duplicate create_pty_task ignored for ${taskId}: task already active`);
1928
2083
  sendAgentCommandAck({
@@ -0,0 +1,323 @@
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 shouldIgnoreNodePtyVerificationErrorMessage(message) {
52
+ const normalized = String(message || "")
53
+ .trim()
54
+ .toLowerCase();
55
+ return normalized === "read eio" || normalized.endsWith(": read eio");
56
+ }
57
+
58
+ export function normalizeBuiltDependencyList(value) {
59
+ if (Array.isArray(value)) {
60
+ return value
61
+ .map((entry) => (typeof entry === "string" ? entry.trim() : ""))
62
+ .filter(Boolean);
63
+ }
64
+ if (typeof value !== "string") {
65
+ return [];
66
+ }
67
+ const trimmed = value.trim();
68
+ if (!trimmed || trimmed === "undefined" || trimmed === "null") {
69
+ return [];
70
+ }
71
+ try {
72
+ return normalizeBuiltDependencyList(JSON.parse(trimmed));
73
+ } catch {
74
+ return trimmed
75
+ .split(",")
76
+ .map((entry) => entry.trim())
77
+ .filter(Boolean);
78
+ }
79
+ }
80
+
81
+ export function mergeBuiltDependencies(existing, required) {
82
+ const merged = new Set(normalizeBuiltDependencyList(existing));
83
+ for (const dependency of normalizeBuiltDependencyList(required)) {
84
+ merged.add(dependency);
85
+ }
86
+ return [...merged];
87
+ }
88
+
89
+ export async function ensurePnpmOnlyBuiltDependencies({
90
+ runCommand = defaultRunCommand,
91
+ dependencies = ["node-pty"],
92
+ global = true,
93
+ } = {}) {
94
+ const scopeArgs = global ? ["--global"] : ["--location=project"];
95
+ const currentResult = await runCommand("pnpm", [
96
+ "config",
97
+ "get",
98
+ ...scopeArgs,
99
+ "onlyBuiltDependencies",
100
+ "--json",
101
+ ]);
102
+ const current = normalizeBuiltDependencyList(currentResult.stdout);
103
+ const merged = mergeBuiltDependencies(current, dependencies);
104
+ if (merged.length === current.length && merged.every((entry, index) => entry === current[index])) {
105
+ return merged;
106
+ }
107
+ const setResult = await runCommand("pnpm", [
108
+ "config",
109
+ "set",
110
+ ...scopeArgs,
111
+ "onlyBuiltDependencies",
112
+ JSON.stringify(merged),
113
+ ]);
114
+ if (!setResult.success) {
115
+ throw new Error(
116
+ `Failed to configure pnpm onlyBuiltDependencies: ${String(
117
+ setResult.stderr || setResult.stdout || "unknown error",
118
+ ).trim()}`,
119
+ );
120
+ }
121
+ return merged;
122
+ }
123
+
124
+ export async function resolveGlobalPackageDirectory({
125
+ packageManager,
126
+ packageName,
127
+ runCommand = defaultRunCommand,
128
+ } = {}) {
129
+ if (!packageManager || !packageName) {
130
+ throw new Error("packageManager and packageName are required");
131
+ }
132
+
133
+ let command;
134
+ let args;
135
+ let normalizeRoot = (value) => value;
136
+
137
+ if (packageManager === "pnpm" || packageManager === "npm") {
138
+ command = packageManager;
139
+ args = ["root", "-g"];
140
+ } else if (packageManager === "yarn") {
141
+ command = "yarn";
142
+ args = ["global", "dir"];
143
+ normalizeRoot = (value) => path.join(value, "node_modules");
144
+ } else {
145
+ throw new Error(`Unsupported package manager: ${packageManager}`);
146
+ }
147
+
148
+ const result = await runCommand(command, args);
149
+ if (!result.success) {
150
+ throw new Error(
151
+ `Failed to resolve global package root via ${packageManager}: ${String(
152
+ result.stderr || result.stdout || "unknown error",
153
+ ).trim()}`,
154
+ );
155
+ }
156
+
157
+ const rawRoot = String(result.stdout || "")
158
+ .split(/\r?\n/)
159
+ .map((line) => line.trim())
160
+ .filter(Boolean)
161
+ .at(-1);
162
+ if (!rawRoot) {
163
+ throw new Error(`Global package root for ${packageManager} is empty`);
164
+ }
165
+
166
+ return path.join(normalizeRoot(rawRoot), packageName);
167
+ }
168
+
169
+ export function buildNodePtyVerificationScript() {
170
+ return String.raw`
171
+ const fs = require('node:fs');
172
+ const path = require('node:path');
173
+ const { createRequire } = require('node:module');
174
+ const shouldIgnoreNodePtyVerificationErrorMessage = ${shouldIgnoreNodePtyVerificationErrorMessage.toString()};
175
+
176
+ const packageDir = process.argv[1];
177
+ if (!packageDir) {
178
+ throw new Error('package directory is required');
179
+ }
180
+ const packageJsonPath = path.join(packageDir, 'package.json');
181
+ const req = createRequire(packageJsonPath);
182
+ const nodePty = req('node-pty');
183
+ const spawn = typeof nodePty.spawn === 'function'
184
+ ? nodePty.spawn
185
+ : (nodePty.default && typeof nodePty.default.spawn === 'function'
186
+ ? nodePty.default.spawn.bind(nodePty.default)
187
+ : null);
188
+
189
+ if (!spawn) {
190
+ throw new Error('node-pty spawn export not found');
191
+ }
192
+
193
+ const shell = process.platform === 'win32'
194
+ ? (process.env.COMSPEC || 'cmd.exe')
195
+ : (fs.existsSync('/bin/bash') ? '/bin/bash' : '/bin/sh');
196
+ const shellArgs = process.platform === 'win32'
197
+ ? ['/d', '/s', '/c', 'exit 0']
198
+ : ['-lc', 'exit 0'];
199
+
200
+ const child = spawn(shell, shellArgs, {
201
+ name: 'conductor-node-pty-check',
202
+ cols: 80,
203
+ rows: 24,
204
+ cwd: process.cwd(),
205
+ env: process.env,
206
+ });
207
+
208
+ let settled = false;
209
+ const finish = (code, error) => {
210
+ if (settled) return;
211
+ settled = true;
212
+ clearTimeout(timer);
213
+ if (error) {
214
+ console.error(error instanceof Error ? error.message : String(error));
215
+ process.exit(1);
216
+ return;
217
+ }
218
+ if (typeof code === 'number' && code !== 0) {
219
+ console.error('node-pty smoke test exited with code ' + code);
220
+ process.exit(1);
221
+ return;
222
+ }
223
+ console.log('Verified node-pty native binding');
224
+ process.exit(0);
225
+ };
226
+
227
+ const timer = setTimeout(() => {
228
+ try {
229
+ child.kill();
230
+ } catch {
231
+ // ignore kill failures
232
+ }
233
+ finish(null, new Error('node-pty smoke test timed out'));
234
+ }, 5000);
235
+
236
+ child.on('exit', (code) => finish(code, null));
237
+ child.on('error', (error) => {
238
+ const message = error instanceof Error ? error.message : String(error);
239
+ if (shouldIgnoreNodePtyVerificationErrorMessage(message)) {
240
+ return;
241
+ }
242
+ finish(null, error);
243
+ });
244
+ `;
245
+ }
246
+
247
+ export async function verifyNodePtyForPackageDirectory({
248
+ packageDirectory,
249
+ runCommand = defaultRunCommand,
250
+ nodeExecutable = process.execPath,
251
+ } = {}) {
252
+ if (!packageDirectory) {
253
+ throw new Error("packageDirectory is required");
254
+ }
255
+ const result = await runCommand(nodeExecutable, ["-e", buildNodePtyVerificationScript(), packageDirectory], {
256
+ timeoutMs: 15_000,
257
+ });
258
+ if (!result.success) {
259
+ throw new Error(
260
+ `node-pty verification failed for ${packageDirectory}: ${String(
261
+ result.stderr || result.stdout || "unknown error",
262
+ ).trim()}`,
263
+ );
264
+ }
265
+ return result;
266
+ }
267
+
268
+ export async function repairAndVerifyGlobalNodePty({
269
+ packageManager,
270
+ packageName,
271
+ runCommand = defaultRunCommand,
272
+ nodeExecutable = process.execPath,
273
+ dependencies = ["node-pty"],
274
+ packageSpec = null,
275
+ } = {}) {
276
+ if (!packageManager || !packageName) {
277
+ throw new Error("packageManager and packageName are required");
278
+ }
279
+
280
+ if (packageManager === "pnpm") {
281
+ await ensurePnpmOnlyBuiltDependencies({ runCommand, dependencies, global: true });
282
+ }
283
+
284
+ if (packageManager === "pnpm") {
285
+ const rebuildResult = await runCommand("pnpm", ["rebuild", "-g", ...dependencies]);
286
+ if (!rebuildResult.success) {
287
+ throw new Error(
288
+ `pnpm rebuild failed: ${String(rebuildResult.stderr || rebuildResult.stdout || "unknown error").trim()}`,
289
+ );
290
+ }
291
+ } else if (packageManager === "npm") {
292
+ const rebuildArgs = ["rebuild", "-g"];
293
+ if (packageSpec) {
294
+ rebuildArgs.push(packageSpec);
295
+ } else {
296
+ rebuildArgs.push(packageName);
297
+ }
298
+ const rebuildResult = await runCommand("npm", rebuildArgs);
299
+ if (!rebuildResult.success) {
300
+ throw new Error(
301
+ `npm rebuild failed: ${String(rebuildResult.stderr || rebuildResult.stdout || "unknown error").trim()}`,
302
+ );
303
+ }
304
+ }
305
+
306
+ const packageDirectory = await resolveGlobalPackageDirectory({
307
+ packageManager,
308
+ packageName,
309
+ runCommand,
310
+ });
311
+ await verifyNodePtyForPackageDirectory({
312
+ packageDirectory,
313
+ runCommand,
314
+ nodeExecutable,
315
+ });
316
+ return packageDirectory;
317
+ }
318
+
319
+ export function buildNodePtyShellVerificationCommand(scriptPath, packageDirectory) {
320
+ const quotedScriptPath = quoteForSingleQuotedShell(scriptPath);
321
+ const quotedPackageDirectory = quoteForSingleQuotedShell(packageDirectory);
322
+ return `node '${quotedScriptPath}' '${quotedPackageDirectory}'`;
323
+ }