@love-moon/conductor-cli 0.2.23 → 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.
- package/bin/conductor-update.js +91 -30
- package/bin/conductor-verify-node-pty.js +24 -0
- package/bin/conductor.js +3 -1
- package/package.json +4 -4
- package/src/daemon.js +182 -27
- package/src/native-deps.js +309 -0
package/bin/conductor-update.js
CHANGED
|
@@ -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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
|
|
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.
|
|
4
|
-
"gitCommitId": "
|
|
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.
|
|
21
|
-
"@love-moon/conductor-sdk": "0.2.
|
|
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 =
|
|
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
|
-
|
|
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
|
|
1285
|
+
// Step 5: graceful shutdown
|
|
1164
1286
|
await shutdownDaemon("auto-update");
|
|
1165
1287
|
|
|
1166
|
-
// Step
|
|
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
|
-
|
|
1661
|
-
process.env.SHELL
|
|
1662
|
-
|
|
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,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
|
+
}
|