@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.
- package/bin/conductor-config.js +143 -89
- 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 +219 -36
- package/src/native-deps.js +309 -0
package/bin/conductor-config.js
CHANGED
|
@@ -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
|
|
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", "
|
|
100
|
-
.example("conductor config --
|
|
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
|
-
|
|
114
|
-
let
|
|
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(
|
|
119
|
+
console.log("");
|
|
133
120
|
console.log(colorize("Conductor requires at least one coding CLI to work properly.", "yellow"));
|
|
134
|
-
console.log(
|
|
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(([
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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
|
}
|
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") {
|
|
@@ -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
|
-
|
|
697
|
+
const onSigInt = () => {
|
|
625
698
|
handleSignal("SIGINT");
|
|
626
|
-
}
|
|
627
|
-
|
|
699
|
+
};
|
|
700
|
+
const onSigTerm = () => {
|
|
628
701
|
handleSignal("SIGTERM");
|
|
629
|
-
}
|
|
630
|
-
|
|
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(() =>
|
|
645
|
-
|
|
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
|
-
|
|
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
|
|
1285
|
+
// Step 5: graceful shutdown
|
|
1137
1286
|
await shutdownDaemon("auto-update");
|
|
1138
1287
|
|
|
1139
|
-
// Step
|
|
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
|
-
|
|
1634
|
-
process.env.SHELL
|
|
1635
|
-
|
|
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
|
+
}
|