@love-moon/conductor-cli 0.2.19 → 0.2.21

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.
@@ -0,0 +1,130 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from "node:fs";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import process from "node:process";
7
+ import { fileURLToPath } from "node:url";
8
+
9
+ import yargs from "yargs/yargs";
10
+ import { hideBin } from "yargs/helpers";
11
+ import { loadConfig } from "@love-moon/conductor-sdk";
12
+
13
+ const isMainModule = (() => {
14
+ const currentFile = fileURLToPath(import.meta.url);
15
+ const entryFile = process.argv[1] ? path.resolve(process.argv[1]) : "";
16
+ return entryFile === currentFile;
17
+ })();
18
+
19
+ function resolveDefaultConfigPath(env = process.env) {
20
+ const home = env.HOME || env.USERPROFILE || os.homedir();
21
+ return path.join(home, ".conductor", "config.yaml");
22
+ }
23
+
24
+ function readTextFile(filePath) {
25
+ return fs.readFileSync(filePath, "utf8");
26
+ }
27
+
28
+ function ensureFeishuChannelConfig(config) {
29
+ const feishu = config?.channels?.feishu;
30
+ if (!feishu?.appId || !feishu?.appSecret || !feishu?.verificationToken) {
31
+ throw new Error("config.yaml is missing channels.feishu.app_id/app_secret/verification_token");
32
+ }
33
+ return feishu;
34
+ }
35
+
36
+ function formatErrorBody(text) {
37
+ const normalized = typeof text === "string" ? text.trim() : "";
38
+ if (!normalized) return "";
39
+ try {
40
+ const parsed = JSON.parse(normalized);
41
+ if (parsed && typeof parsed === "object" && typeof parsed.error === "string") {
42
+ return parsed.error;
43
+ }
44
+ } catch {
45
+ // ignore
46
+ }
47
+ return normalized;
48
+ }
49
+
50
+ export async function connectFeishuChannel(options = {}) {
51
+ const env = options.env ?? process.env;
52
+ const fetchImpl = options.fetchImpl ?? global.fetch;
53
+ if (typeof fetchImpl !== "function") {
54
+ throw new Error("fetch is not available");
55
+ }
56
+
57
+ const resolvedConfigPath = path.resolve(options.configFile || resolveDefaultConfigPath(env));
58
+ const rawYaml = readTextFile(resolvedConfigPath);
59
+ const config = loadConfig(resolvedConfigPath, { env });
60
+ const feishu = ensureFeishuChannelConfig(config);
61
+
62
+ const url = new URL("/api/channel/feishu/config", config.backendUrl);
63
+ const response = await fetchImpl(String(url), {
64
+ method: "POST",
65
+ headers: {
66
+ Authorization: `Bearer ${config.agentToken}`,
67
+ "Content-Type": "application/json",
68
+ Accept: "application/json",
69
+ },
70
+ body: JSON.stringify({ yaml: rawYaml }),
71
+ });
72
+
73
+ const rawText = await response.text();
74
+ if (!response.ok) {
75
+ const detail = formatErrorBody(rawText);
76
+ throw new Error(`Failed to connect Feishu channel (${response.status})${detail ? `: ${detail}` : ""}`);
77
+ }
78
+
79
+ const parsed = rawText ? JSON.parse(rawText) : {};
80
+ return {
81
+ configPath: resolvedConfigPath,
82
+ feishu,
83
+ config: parsed.config ?? null,
84
+ };
85
+ }
86
+
87
+ export async function main(argvInput = hideBin(process.argv), dependencies = {}) {
88
+ const log = dependencies.log ?? console.log;
89
+ const logError = dependencies.logError ?? console.error;
90
+
91
+ await yargs(argvInput)
92
+ .scriptName("conductor channel")
93
+ .command(
94
+ "connect feishu",
95
+ "Upload channels.feishu from config.yaml to Conductor backend",
96
+ (command) => command.option("config-file", {
97
+ type: "string",
98
+ describe: "Path to Conductor config file",
99
+ }),
100
+ async (argv) => {
101
+ try {
102
+ const result = await connectFeishuChannel({
103
+ configFile: argv.configFile,
104
+ });
105
+ const effective = result.config ?? {
106
+ provider: "FEISHU",
107
+ appId: result.feishu.appId,
108
+ verificationToken: result.feishu.verificationToken,
109
+ };
110
+ log(`Connected Feishu channel from ${result.configPath}`);
111
+ log(`app_id: ${effective.appId}`);
112
+ log(`verification_token: ${effective.verificationToken}`);
113
+ log("Configure the same verification_token in Feishu Open Platform webhook settings.");
114
+ } catch (error) {
115
+ logError(error instanceof Error ? error.message : String(error));
116
+ process.exitCode = 1;
117
+ }
118
+ },
119
+ )
120
+ .demandCommand(1)
121
+ .help()
122
+ .parseAsync();
123
+ }
124
+
125
+ if (isMainModule) {
126
+ main().catch((error) => {
127
+ console.error(error instanceof Error ? error.message : String(error));
128
+ process.exit(1);
129
+ });
130
+ }
@@ -23,7 +23,7 @@ const DEFAULT_CLIs = {
23
23
  },
24
24
  codex: {
25
25
  command: "codex",
26
- execArgs: "exec --dangerously-bypass-approvals-and-sandbox --skip-git-repo-check",
26
+ execArgs: "--dangerously-bypass-approvals-and-sandbox --ask-for-approval never",
27
27
  description: "OpenAI Codex CLI"
28
28
  },
29
29
  // gemini: {
@@ -14,6 +14,56 @@ const argv = hideBin(process.argv);
14
14
 
15
15
  const CLI_NAME = process.env.CONDUCTOR_CLI_NAME || "conductor-daemon";
16
16
 
17
+ function parseJsonArrayEnv(value) {
18
+ if (typeof value !== "string" || !value.trim()) {
19
+ return null;
20
+ }
21
+ try {
22
+ const parsed = JSON.parse(value);
23
+ if (!Array.isArray(parsed)) {
24
+ return null;
25
+ }
26
+ return parsed.filter((entry) => typeof entry === "string");
27
+ } catch {
28
+ return null;
29
+ }
30
+ }
31
+
32
+ function stripNohupArgs(args) {
33
+ return args.filter((arg) => !(arg === "--nohup" || arg.startsWith("--nohup=")));
34
+ }
35
+
36
+ function resolveLauncherConfig() {
37
+ const inheritedLauncherScript =
38
+ typeof process.env.CONDUCTOR_LAUNCHER_SCRIPT === "string" &&
39
+ process.env.CONDUCTOR_LAUNCHER_SCRIPT.trim()
40
+ ? process.env.CONDUCTOR_LAUNCHER_SCRIPT.trim()
41
+ : null;
42
+ const inheritedSubcommand =
43
+ typeof process.env.CONDUCTOR_SUBCOMMAND === "string" &&
44
+ process.env.CONDUCTOR_SUBCOMMAND.trim()
45
+ ? process.env.CONDUCTOR_SUBCOMMAND.trim()
46
+ : null;
47
+ const inheritedSubcommandArgs = parseJsonArrayEnv(process.env.CONDUCTOR_SUBCOMMAND_ARGS_JSON);
48
+
49
+ if (inheritedLauncherScript && inheritedSubcommand === "daemon" && inheritedSubcommandArgs) {
50
+ return {
51
+ restartLauncherScript: inheritedLauncherScript,
52
+ restartLauncherArgs: ["daemon", ...stripNohupArgs(inheritedSubcommandArgs)],
53
+ versionCheckScript: inheritedLauncherScript,
54
+ versionCheckArgs: ["--version"],
55
+ };
56
+ }
57
+
58
+ const daemonScript = path.resolve(process.argv[1]);
59
+ return {
60
+ restartLauncherScript: daemonScript,
61
+ restartLauncherArgs: argv,
62
+ versionCheckScript: inheritedLauncherScript,
63
+ versionCheckArgs: ["--version"],
64
+ };
65
+ }
66
+
17
67
  function formatBeijingTimestampForFile(date = new Date()) {
18
68
  const base = date
19
69
  .toLocaleString("sv-SE", { timeZone: "Asia/Shanghai", hour12: false })
@@ -149,4 +199,5 @@ startDaemon({
149
199
  CLEAN_ALL: args.cleanAll,
150
200
  CONFIG_FILE: args.configFile,
151
201
  FORCE: args.force,
202
+ ...resolveLauncherConfig(),
152
203
  });
@@ -244,6 +244,7 @@ function printReport(taskId, report) {
244
244
  const diagnosis = payload?.diagnosis || {};
245
245
  const task = payload?.task || {};
246
246
  const realtime = payload?.realtime || {};
247
+ const ptyTransport = payload?.pty_transport || payload?.ptyTransport || {};
247
248
  const messages = payload?.messages || {};
248
249
 
249
250
  process.stdout.write(`Task: ${task?.id || taskId}\n`);
@@ -273,6 +274,25 @@ function printReport(taskId, report) {
273
274
  );
274
275
  }
275
276
  }
277
+ const latestLatency = ptyTransport?.latest_latency_sample || ptyTransport?.latestLatencySample;
278
+ if (latestLatency) {
279
+ process.stdout.write(
280
+ `- pty.latest_latency: client->server=${formatLatencyMs(
281
+ latestLatency.client_input_to_server_received_ms ?? latestLatency.clientInputToServerReceivedMs,
282
+ )}, server->daemon=${formatLatencyMs(
283
+ latestLatency.server_received_to_daemon_received_ms ?? latestLatency.serverReceivedToDaemonReceivedMs,
284
+ )}, daemon->first_output=${formatLatencyMs(
285
+ latestLatency.daemon_input_to_first_output_ms ?? latestLatency.daemonInputToFirstOutputMs,
286
+ )}, client->first_output=${formatLatencyMs(
287
+ latestLatency.client_input_to_first_output_ms ?? latestLatency.clientInputToFirstOutputMs,
288
+ )}\n`,
289
+ );
290
+ process.stdout.write(
291
+ `- pty.primary_bottleneck: ${String(
292
+ ptyTransport?.primary_bottleneck || ptyTransport?.primaryBottleneck || "unknown",
293
+ )}\n`,
294
+ );
295
+ }
276
296
 
277
297
  const reasons = Array.isArray(diagnosis.reasons) ? diagnosis.reasons : [];
278
298
  if (reasons.length > 0) {
@@ -364,6 +384,11 @@ function detectExecutionFailureLoopKey(value) {
364
384
  return null;
365
385
  }
366
386
 
387
+ function formatLatencyMs(value) {
388
+ const num = typeof value === "number" ? value : Number(value);
389
+ return Number.isFinite(num) && num >= 0 ? `${Math.round(num)}ms` : "n/a";
390
+ }
391
+
367
392
  function normalizePositiveInt(value, fallback) {
368
393
  const parsed = Number.parseInt(String(value ?? ""), 10);
369
394
  return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
@@ -42,6 +42,12 @@ const CLI_NAME = (process.env.CONDUCTOR_CLI_NAME || path.basename(process.argv[1
42
42
  "",
43
43
  );
44
44
 
45
+ export function buildConductorConnectHeaders(version = pkgJson.version) {
46
+ return {
47
+ "x-conductor-version": version,
48
+ };
49
+ }
50
+
45
51
  // Load allow_cli_list from config file (no defaults - must be configured)
46
52
  function loadAllowCliList(configFilePath) {
47
53
  try {
@@ -98,6 +104,30 @@ const DEFAULT_ERROR_LOOP_BACKOFF_MS = 3 * 60 * 1000;
98
104
  const DEFAULT_ERROR_LOOP_THRESHOLD = 3;
99
105
  const SESSION_BOOTSTRAP_LOCK_TIMEOUT_MS = 15_000;
100
106
  const SESSION_BOOTSTRAP_LOCK_RETRY_MS = 50;
107
+ const FIRE_WATCHDOG_INTERVAL_MS = getBoundedEnvInt(
108
+ "CONDUCTOR_FIRE_WATCHDOG_INTERVAL_MS",
109
+ 10_000,
110
+ 1_000,
111
+ 60_000,
112
+ );
113
+ const FIRE_WATCHDOG_STALE_WS_MS = getBoundedEnvInt(
114
+ "CONDUCTOR_FIRE_WATCHDOG_STALE_WS_MS",
115
+ 45_000,
116
+ 5_000,
117
+ 5 * 60_000,
118
+ );
119
+ const FIRE_WATCHDOG_CONNECT_GRACE_MS = getBoundedEnvInt(
120
+ "CONDUCTOR_FIRE_WATCHDOG_CONNECT_GRACE_MS",
121
+ 15_000,
122
+ 1_000,
123
+ 60_000,
124
+ );
125
+ const FIRE_WATCHDOG_RECONNECT_COOLDOWN_MS = getBoundedEnvInt(
126
+ "CONDUCTOR_FIRE_WATCHDOG_RECONNECT_COOLDOWN_MS",
127
+ 15_000,
128
+ 1_000,
129
+ 2 * 60_000,
130
+ );
101
131
 
102
132
  function sleepSync(ms) {
103
133
  if (!Number.isFinite(ms) || ms <= 0) {
@@ -191,6 +221,176 @@ function appendFireLocalLog(line) {
191
221
  }
192
222
  }
193
223
 
224
+ function formatFireDisconnectDiagnostics(event = {}) {
225
+ const parts = [];
226
+ if (event.reason) {
227
+ parts.push(`reason=${event.reason}`);
228
+ }
229
+ if (typeof event.closeCode === "number") {
230
+ parts.push(`close_code=${event.closeCode}`);
231
+ }
232
+ if (event.closeReason) {
233
+ parts.push(`close_reason=${sanitizeForLog(event.closeReason, 120)}`);
234
+ }
235
+ if (event.socketError) {
236
+ parts.push(`socket_error=${sanitizeForLog(event.socketError, 120)}`);
237
+ }
238
+ if (typeof event.missedPongs === "number" && event.missedPongs > 0) {
239
+ parts.push(`missed_pongs=${event.missedPongs}`);
240
+ }
241
+ return parts.join(" ") || "reason=connection_lost";
242
+ }
243
+
244
+ function formatIsoTimestamp(value) {
245
+ if (!Number.isFinite(value) || value <= 0) {
246
+ return "n/a";
247
+ }
248
+ return new Date(value).toISOString();
249
+ }
250
+
251
+ function formatFireWatchdogState({ connectedAt, lastPongAt, lastInboundAt }) {
252
+ return [
253
+ `connected_at=${formatIsoTimestamp(connectedAt)}`,
254
+ `last_pong_at=${formatIsoTimestamp(lastPongAt)}`,
255
+ `last_inbound_at=${formatIsoTimestamp(lastInboundAt)}`,
256
+ ].join(" ");
257
+ }
258
+
259
+ export class FireWatchdog {
260
+ constructor({
261
+ intervalMs = FIRE_WATCHDOG_INTERVAL_MS,
262
+ staleWsMs = FIRE_WATCHDOG_STALE_WS_MS,
263
+ connectGraceMs = FIRE_WATCHDOG_CONNECT_GRACE_MS,
264
+ reconnectCooldownMs = FIRE_WATCHDOG_RECONNECT_COOLDOWN_MS,
265
+ onForceReconnect,
266
+ logger = () => {},
267
+ now = () => Date.now(),
268
+ } = {}) {
269
+ this.intervalMs = intervalMs;
270
+ this.staleWsMs = staleWsMs;
271
+ this.connectGraceMs = connectGraceMs;
272
+ this.reconnectCooldownMs = reconnectCooldownMs;
273
+ this.onForceReconnect = onForceReconnect;
274
+ this.logger = logger;
275
+ this.now = now;
276
+ this.wsConnected = false;
277
+ this.lastConnectedAt = null;
278
+ this.lastPongAt = null;
279
+ this.lastInboundAt = null;
280
+ this.lastHealAt = 0;
281
+ this.healAttempts = 0;
282
+ this.awaitingHealthySignalAt = null;
283
+ this.timer = null;
284
+ }
285
+
286
+ start() {
287
+ if (this.timer) {
288
+ return;
289
+ }
290
+ this.timer = setInterval(() => {
291
+ void this.runOnce();
292
+ }, this.intervalMs);
293
+ if (typeof this.timer.unref === "function") {
294
+ this.timer.unref();
295
+ }
296
+ }
297
+
298
+ stop() {
299
+ if (!this.timer) {
300
+ return;
301
+ }
302
+ clearInterval(this.timer);
303
+ this.timer = null;
304
+ }
305
+
306
+ onConnected({ isReconnect = false, connectedAt = this.now() } = {}) {
307
+ this.wsConnected = true;
308
+ this.lastConnectedAt = connectedAt;
309
+ this.lastPongAt =
310
+ Number.isFinite(this.lastPongAt) && this.lastPongAt > connectedAt ? this.lastPongAt : connectedAt;
311
+ if (isReconnect && this.healAttempts > 0) {
312
+ this.awaitingHealthySignalAt = connectedAt;
313
+ } else if (!isReconnect) {
314
+ this.awaitingHealthySignalAt = null;
315
+ }
316
+ }
317
+
318
+ onDisconnected() {
319
+ this.wsConnected = false;
320
+ }
321
+
322
+ onPong({ at = this.now() } = {}) {
323
+ this.lastPongAt = at;
324
+ this.markHealthy("pong", at);
325
+ }
326
+
327
+ onInbound(at = this.now()) {
328
+ this.lastInboundAt = at;
329
+ this.markHealthy("inbound", at);
330
+ }
331
+
332
+ markHealthy(signal, at = this.now()) {
333
+ if (!this.awaitingHealthySignalAt || this.healAttempts === 0) {
334
+ return;
335
+ }
336
+ if (at < this.awaitingHealthySignalAt) {
337
+ return;
338
+ }
339
+ this.logger(
340
+ `[watchdog] Backend websocket healthy again after self-heal via ${signal} (${formatFireWatchdogState({
341
+ connectedAt: this.lastConnectedAt,
342
+ lastPongAt: this.lastPongAt,
343
+ lastInboundAt: this.lastInboundAt,
344
+ })})`,
345
+ );
346
+ this.awaitingHealthySignalAt = null;
347
+ this.healAttempts = 0;
348
+ }
349
+
350
+ async runOnce() {
351
+ if (!this.wsConnected || typeof this.onForceReconnect !== "function") {
352
+ return false;
353
+ }
354
+ const now = this.now();
355
+ if (!Number.isFinite(this.lastConnectedAt) || now - this.lastConnectedAt < this.connectGraceMs) {
356
+ return false;
357
+ }
358
+ const lastWsHealthAt = Math.max(this.lastPongAt || 0, this.lastInboundAt || 0, this.lastConnectedAt || 0);
359
+ if (lastWsHealthAt && now - lastWsHealthAt <= this.staleWsMs) {
360
+ return false;
361
+ }
362
+ if (this.lastHealAt && now - this.lastHealAt < this.reconnectCooldownMs) {
363
+ return false;
364
+ }
365
+
366
+ this.lastHealAt = now;
367
+ this.healAttempts += 1;
368
+ this.awaitingHealthySignalAt = null;
369
+ this.wsConnected = false;
370
+ this.logger(
371
+ `[watchdog] stale_ws_health; restarting fire websocket (${this.healAttempts}) (${formatFireWatchdogState({
372
+ connectedAt: this.lastConnectedAt,
373
+ lastPongAt: this.lastPongAt,
374
+ lastInboundAt: this.lastInboundAt,
375
+ })})`,
376
+ );
377
+ await this.onForceReconnect("watchdog:stale_ws_health");
378
+ return true;
379
+ }
380
+
381
+ getDebugState() {
382
+ return {
383
+ wsConnected: this.wsConnected,
384
+ lastConnectedAt: this.lastConnectedAt,
385
+ lastPongAt: this.lastPongAt,
386
+ lastInboundAt: this.lastInboundAt,
387
+ lastHealAt: this.lastHealAt,
388
+ healAttempts: this.healAttempts,
389
+ awaitingHealthySignalAt: this.awaitingHealthySignalAt,
390
+ };
391
+ }
392
+ }
393
+
194
394
  async function main() {
195
395
  const cliArgs = parseCliArgs();
196
396
  let runtimeProjectPath = process.cwd();
@@ -239,6 +439,17 @@ async function main() {
239
439
  let pendingRemoteStopEvent = null;
240
440
  let conductor = null;
241
441
  let reconnectResumeInFlight = false;
442
+ let fireShuttingDown = false;
443
+ const fireWatchdog = new FireWatchdog({
444
+ onForceReconnect: async (reason) => {
445
+ if (!conductor || typeof conductor.forceReconnect !== "function") {
446
+ return;
447
+ }
448
+ await conductor.forceReconnect(reason);
449
+ },
450
+ logger: log,
451
+ });
452
+ fireWatchdog.start();
242
453
 
243
454
  const scheduleReconnectRecovery = ({ isReconnect }) => {
244
455
  if (!isReconnect) {
@@ -274,6 +485,7 @@ async function main() {
274
485
  };
275
486
 
276
487
  const handleStopTaskCommand = async (event) => {
488
+ fireWatchdog.onInbound();
277
489
  if (!event || typeof event !== "object") {
278
490
  return;
279
491
  }
@@ -321,8 +533,21 @@ async function main() {
321
533
  conductor = await ConductorClient.connect({
322
534
  projectPath: runtimeProjectPath,
323
535
  extraEnv: env,
536
+ extraHeaders: buildConductorConnectHeaders(),
324
537
  configFile: cliArgs.configFile,
325
- onConnected: scheduleReconnectRecovery,
538
+ onConnected: (event) => {
539
+ fireWatchdog.onConnected(event);
540
+ scheduleReconnectRecovery(event);
541
+ },
542
+ onDisconnected: (event) => {
543
+ fireWatchdog.onDisconnected();
544
+ if (!fireShuttingDown) {
545
+ log(`[fire-ws] Disconnected from backend: ${formatFireDisconnectDiagnostics(event)}`);
546
+ }
547
+ },
548
+ onPong: (event) => {
549
+ fireWatchdog.onPong(event);
550
+ },
326
551
  onStopTask: handleStopTaskCommand,
327
552
  });
328
553
 
@@ -425,11 +650,13 @@ async function main() {
425
650
  };
426
651
  const onSigint = () => {
427
652
  shutdownSignal = shutdownSignal || "SIGINT";
653
+ fireShuttingDown = true;
428
654
  signals.abort();
429
655
  requestBackendShutdown("SIGINT");
430
656
  };
431
657
  const onSigterm = () => {
432
658
  shutdownSignal = shutdownSignal || "SIGTERM";
659
+ fireShuttingDown = true;
433
660
  signals.abort();
434
661
  requestBackendShutdown("SIGTERM");
435
662
  };
@@ -494,6 +721,8 @@ async function main() {
494
721
  }
495
722
  }
496
723
  } finally {
724
+ fireShuttingDown = true;
725
+ fireWatchdog.stop();
497
726
  if (backendSession && typeof backendSession.close === "function") {
498
727
  try {
499
728
  await backendSession.close();
@@ -8,9 +8,15 @@ import { fileURLToPath } from "node:url";
8
8
  import path from "node:path";
9
9
  import { createRequire } from "node:module";
10
10
  import fs from "node:fs";
11
- import { execSync, spawn } from "node:child_process";
11
+ import { spawn } from "node:child_process";
12
12
  import process from "node:process";
13
13
  import readline from "node:readline/promises";
14
+ import {
15
+ PACKAGE_NAME,
16
+ fetchLatestVersion,
17
+ isNewerVersion,
18
+ detectPackageManager,
19
+ } from "../src/version-check.js";
14
20
 
15
21
  const __filename = fileURLToPath(import.meta.url);
16
22
  const __dirname = path.dirname(__filename);
@@ -18,7 +24,6 @@ const require = createRequire(import.meta.url);
18
24
  const PKG_ROOT = path.join(__dirname, "..");
19
25
 
20
26
  const pkgJson = JSON.parse(fs.readFileSync(path.join(PKG_ROOT, "package.json"), "utf-8"));
21
- const PACKAGE_NAME = pkgJson.name;
22
27
  const CURRENT_VERSION = pkgJson.version;
23
28
 
24
29
  // ANSI 颜色代码
@@ -110,75 +115,11 @@ async function main() {
110
115
  }
111
116
 
112
117
  async function getLatestVersion() {
113
- return new Promise((resolve, reject) => {
114
- // 使用 npm view 获取最新版本
115
- try {
116
- const result = execSync(`npm view ${PACKAGE_NAME} version --json`, {
117
- encoding: "utf-8",
118
- timeout: 10000,
119
- stdio: ["pipe", "pipe", "pipe"]
120
- });
121
-
122
- // npm view 返回的是带引号的字符串 JSON
123
- const version = JSON.parse(result.trim());
124
- resolve(version);
125
- } catch (error) {
126
- // 如果失败,尝试从 registry API 获取
127
- fetchLatestFromRegistry()
128
- .then(resolve)
129
- .catch(reject);
130
- }
131
- });
132
- }
133
-
134
- async function fetchLatestFromRegistry() {
135
- return new Promise((resolve, reject) => {
136
- const https = require("https");
137
- const url = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
138
-
139
- https.get(url, { timeout: 10000 }, (res) => {
140
- let data = "";
141
-
142
- res.on("data", (chunk) => {
143
- data += chunk;
144
- });
145
-
146
- res.on("end", () => {
147
- try {
148
- const json = JSON.parse(data);
149
- if (json.version) {
150
- resolve(json.version);
151
- } else {
152
- reject(new Error("Invalid response from registry"));
153
- }
154
- } catch (error) {
155
- reject(new Error(`Failed to parse registry response: ${error.message}`));
156
- }
157
- });
158
- }).on("error", (error) => {
159
- reject(new Error(`Network error: ${error.message}`));
160
- }).on("timeout", () => {
161
- reject(new Error("Request timed out"));
162
- });
163
- });
164
- }
165
-
166
- function isNewerVersion(latest, current) {
167
- // 简单的版本比较
168
- const parseVersion = (v) => v.replace(/^v/, "").split(".").map(Number);
169
-
170
- const latestParts = parseVersion(latest);
171
- const currentParts = parseVersion(current);
172
-
173
- for (let i = 0; i < Math.max(latestParts.length, currentParts.length); i++) {
174
- const l = latestParts[i] || 0;
175
- const c = currentParts[i] || 0;
176
-
177
- if (l > c) return true;
178
- if (l < c) return false;
118
+ const version = await fetchLatestVersion();
119
+ if (!version) {
120
+ throw new Error("Could not fetch latest version from npm registry");
179
121
  }
180
-
181
- return false; // 版本相同
122
+ return version;
182
123
  }
183
124
 
184
125
  async function confirmUpdate(version) {
@@ -201,7 +142,10 @@ async function confirmUpdate(version) {
201
142
  async function performUpdate() {
202
143
  return new Promise((resolve, reject) => {
203
144
  // 检测使用的包管理器
204
- const packageManager = detectPackageManager();
145
+ const packageManager = detectPackageManager({
146
+ launcherPath: process.env.CONDUCTOR_LAUNCHER_SCRIPT || process.argv[1],
147
+ packageRoot: PKG_ROOT,
148
+ });
205
149
  console.log(` Using package manager: ${colorize(packageManager, "cyan")}`);
206
150
  console.log("");
207
151
 
@@ -245,45 +189,6 @@ async function performUpdate() {
245
189
  });
246
190
  }
247
191
 
248
- function detectPackageManager() {
249
- // 通过分析 conductor 命令的路径来推断包管理器
250
- try {
251
- const conductorPath = execSync("which conductor || where conductor", {
252
- encoding: "utf-8",
253
- stdio: ["pipe", "pipe", "ignore"]
254
- }).trim();
255
-
256
- if (conductorPath.includes("pnpm")) {
257
- return "pnpm";
258
- }
259
- if (conductorPath.includes("yarn")) {
260
- return "yarn";
261
- }
262
- if (conductorPath.includes(".npm") || conductorPath.includes("npm")) {
263
- return "npm";
264
- }
265
- } catch {
266
- // 忽略错误,使用默认检测
267
- }
268
-
269
- // 检查哪个包管理器可用
270
- try {
271
- execSync("pnpm --version", { stdio: "pipe" });
272
- return "pnpm";
273
- } catch {
274
- // pnpm 不可用
275
- }
276
-
277
- try {
278
- execSync("yarn --version", { stdio: "pipe" });
279
- return "yarn";
280
- } catch {
281
- // yarn 不可用
282
- }
283
-
284
- return "npm"; // 默认使用 npm
285
- }
286
-
287
192
  function showHelpMessage() {
288
193
  console.log(`conductor update - Update the CLI to the latest version
289
194