@love-moon/conductor-cli 0.2.20 → 0.2.22
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-daemon.js +51 -0
- package/bin/conductor-fire.js +8 -0
- package/bin/conductor-update.js +15 -110
- package/bin/conductor.js +73 -52
- package/package.json +4 -4
- package/src/cli-update-notifier.js +241 -0
- package/src/daemon.js +534 -30
- package/src/version-check.js +240 -0
package/bin/conductor-daemon.js
CHANGED
|
@@ -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
|
});
|
package/bin/conductor-fire.js
CHANGED
|
@@ -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 {
|
|
@@ -527,6 +533,7 @@ async function main() {
|
|
|
527
533
|
conductor = await ConductorClient.connect({
|
|
528
534
|
projectPath: runtimeProjectPath,
|
|
529
535
|
extraEnv: env,
|
|
536
|
+
extraHeaders: buildConductorConnectHeaders(),
|
|
530
537
|
configFile: cliArgs.configFile,
|
|
531
538
|
onConnected: (event) => {
|
|
532
539
|
fireWatchdog.onConnected(event);
|
|
@@ -1220,6 +1227,7 @@ export async function applyWorkingDirectory(targetPath) {
|
|
|
1220
1227
|
}
|
|
1221
1228
|
try {
|
|
1222
1229
|
process.chdir(normalizedPath);
|
|
1230
|
+
process.env.PWD = process.cwd();
|
|
1223
1231
|
} catch (error) {
|
|
1224
1232
|
throw new Error(`Cannot switch working directory to ${normalizedPath}: ${error?.message || error}`);
|
|
1225
1233
|
}
|
package/bin/conductor-update.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
|
package/bin/conductor.js
CHANGED
|
@@ -13,16 +13,13 @@
|
|
|
13
13
|
* channel - Connect user-owned chat channel providers
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
import { fileURLToPath } from "node:url";
|
|
16
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
17
17
|
import path from "node:path";
|
|
18
|
-
import { createRequire } from "node:module";
|
|
19
18
|
import fs from "node:fs";
|
|
20
|
-
import
|
|
21
|
-
import { hideBin } from "yargs/helpers";
|
|
19
|
+
import { maybeCheckForUpdates } from "../src/cli-update-notifier.js";
|
|
22
20
|
|
|
23
21
|
const __filename = fileURLToPath(import.meta.url);
|
|
24
22
|
const __dirname = path.dirname(__filename);
|
|
25
|
-
const require = createRequire(import.meta.url);
|
|
26
23
|
const PKG_ROOT = path.join(__dirname, "..");
|
|
27
24
|
|
|
28
25
|
const pkgJson = JSON.parse(fs.readFileSync(path.join(PKG_ROOT, "package.json"), "utf-8"));
|
|
@@ -30,57 +27,81 @@ const pkgJson = JSON.parse(fs.readFileSync(path.join(PKG_ROOT, "package.json"),
|
|
|
30
27
|
// Parse command line arguments
|
|
31
28
|
const argv = process.argv.slice(2);
|
|
32
29
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
process.
|
|
30
|
+
export function runConductorCli(args = argv, deps = {}) {
|
|
31
|
+
const consoleImpl = deps.console || console;
|
|
32
|
+
const importModule = deps.importModule || ((subcommandPath) => import(subcommandPath));
|
|
33
|
+
const env = deps.env || process.env;
|
|
34
|
+
const processArgv = deps.processArgv || process.argv;
|
|
35
|
+
const fsExistsSync = deps.existsSync || fs.existsSync;
|
|
36
|
+
const checkForUpdates = deps.maybeCheckForUpdates || maybeCheckForUpdates;
|
|
37
|
+
const validSubcommands = ["fire", "daemon", "config", "update", "diagnose", "send-file", "channel"];
|
|
38
|
+
|
|
39
|
+
if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
|
|
40
|
+
showHelp(consoleImpl);
|
|
41
|
+
return { shouldExit: true, exitCode: 0 };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (args[0] === "--version" || args[0] === "-v") {
|
|
45
|
+
const commitId = pkgJson.gitCommitId || "unknown";
|
|
46
|
+
consoleImpl.log(`conductor version ${pkgJson.version} (${commitId})`);
|
|
47
|
+
return { shouldExit: true, exitCode: 0 };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const subcommand = args[0];
|
|
51
|
+
if (!validSubcommands.includes(subcommand)) {
|
|
52
|
+
consoleImpl.error(`Error: Unknown subcommand '${subcommand}'`);
|
|
53
|
+
consoleImpl.error(`Valid subcommands: ${validSubcommands.join(", ")}`);
|
|
54
|
+
consoleImpl.error(`Run 'conductor --help' for usage information.`);
|
|
55
|
+
return { shouldExit: true, exitCode: 1 };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
void checkForUpdates({
|
|
59
|
+
currentVersion: pkgJson.version,
|
|
60
|
+
subcommand,
|
|
61
|
+
env,
|
|
62
|
+
}).catch(() => {});
|
|
63
|
+
|
|
64
|
+
const subcommandArgs = args.slice(1);
|
|
65
|
+
env.CONDUCTOR_CLI_NAME = `conductor ${subcommand}`;
|
|
66
|
+
env.CONDUCTOR_LAUNCHER_SCRIPT = processArgv[1] || "";
|
|
67
|
+
env.CONDUCTOR_SUBCOMMAND = subcommand;
|
|
68
|
+
env.CONDUCTOR_SUBCOMMAND_ARGS_JSON = JSON.stringify(subcommandArgs);
|
|
69
|
+
|
|
70
|
+
const subcommandPath = path.join(__dirname, `conductor-${subcommand}.js`);
|
|
71
|
+
if (!fsExistsSync(subcommandPath)) {
|
|
72
|
+
consoleImpl.error(`Error: Subcommand implementation not found: ${subcommandPath}`);
|
|
73
|
+
return { shouldExit: true, exitCode: 1 };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
process.argv = [processArgv[0], subcommandPath, ...subcommandArgs];
|
|
77
|
+
importModule(subcommandPath).catch((error) => {
|
|
78
|
+
consoleImpl.error(`Error loading subcommand '${subcommand}': ${error.message}`);
|
|
79
|
+
process.exit(1);
|
|
80
|
+
});
|
|
81
|
+
return { shouldExit: false, exitCode: 0 };
|
|
37
82
|
}
|
|
38
83
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
84
|
+
const isDirectExecution = (() => {
|
|
85
|
+
const entryPath = process.argv[1];
|
|
86
|
+
if (!entryPath) {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
return pathToFileURL(entryPath).href === import.meta.url;
|
|
91
|
+
} catch {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
})();
|
|
95
|
+
|
|
96
|
+
if (isDirectExecution) {
|
|
97
|
+
const result = runConductorCli();
|
|
98
|
+
if (result.shouldExit) {
|
|
99
|
+
process.exit(result.exitCode);
|
|
100
|
+
}
|
|
44
101
|
}
|
|
45
102
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
// Valid subcommands
|
|
50
|
-
const validSubcommands = ["fire", "daemon", "config", "update", "diagnose", "send-file", "channel"];
|
|
51
|
-
|
|
52
|
-
if (!validSubcommands.includes(subcommand)) {
|
|
53
|
-
console.error(`Error: Unknown subcommand '${subcommand}'`);
|
|
54
|
-
console.error(`Valid subcommands: ${validSubcommands.join(", ")}`);
|
|
55
|
-
console.error(`Run 'conductor --help' for usage information.`);
|
|
56
|
-
process.exit(1);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// Route to the appropriate subcommand
|
|
60
|
-
const subcommandArgs = argv.slice(1);
|
|
61
|
-
|
|
62
|
-
// Set environment variable to track the CLI name for logging
|
|
63
|
-
process.env.CONDUCTOR_CLI_NAME = `conductor ${subcommand}`;
|
|
64
|
-
|
|
65
|
-
// Import and execute the subcommand
|
|
66
|
-
const subcommandPath = path.join(__dirname, `conductor-${subcommand}.js`);
|
|
67
|
-
|
|
68
|
-
if (!fs.existsSync(subcommandPath)) {
|
|
69
|
-
console.error(`Error: Subcommand implementation not found: ${subcommandPath}`);
|
|
70
|
-
process.exit(1);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// Replace process.argv to pass the correct arguments to the subcommand
|
|
74
|
-
process.argv = [process.argv[0], subcommandPath, ...subcommandArgs];
|
|
75
|
-
|
|
76
|
-
// Dynamically import and execute the subcommand
|
|
77
|
-
import(subcommandPath).catch((error) => {
|
|
78
|
-
console.error(`Error loading subcommand '${subcommand}': ${error.message}`);
|
|
79
|
-
process.exit(1);
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
function showHelp() {
|
|
83
|
-
console.log(`conductor - Conductor CLI tool
|
|
103
|
+
function showHelp(consoleImpl = console) {
|
|
104
|
+
consoleImpl.log(`conductor - Conductor CLI tool
|
|
84
105
|
|
|
85
106
|
Usage: conductor <subcommand> [options]
|
|
86
107
|
|
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.22",
|
|
4
|
+
"gitCommitId": "d04b18c",
|
|
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.22",
|
|
21
|
+
"@love-moon/conductor-sdk": "0.2.22",
|
|
22
22
|
"dotenv": "^16.4.5",
|
|
23
23
|
"enquirer": "^2.4.1",
|
|
24
24
|
"js-yaml": "^4.1.1",
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
import { fetchLatestVersion, isNewerVersion } from "./version-check.js";
|
|
6
|
+
|
|
7
|
+
export const DEFAULT_VERSION_CHECK_INTERVAL_MS = 12 * 60 * 60 * 1000;
|
|
8
|
+
export const DEFAULT_VERSION_NOTIFY_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
9
|
+
export const DEFAULT_VERSION_CHECK_TIMEOUT_MS = 800;
|
|
10
|
+
const DEFAULT_CACHE_FILE = "version-check.json";
|
|
11
|
+
|
|
12
|
+
function normalizeOptionalString(value) {
|
|
13
|
+
if (typeof value !== "string") {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
const normalized = value.trim();
|
|
17
|
+
return normalized || null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function parseBooleanLike(value) {
|
|
21
|
+
if (typeof value !== "string") {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
return ["1", "true", "yes", "on"].includes(value.trim().toLowerCase());
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function parseTimestamp(value) {
|
|
28
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
const parsed = Date.parse(value);
|
|
32
|
+
if (Number.isNaN(parsed)) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
return new Date(parsed).toISOString();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function isTimestampOlderThan(value, ageMs, nowMs) {
|
|
39
|
+
const parsed = value ? Date.parse(value) : NaN;
|
|
40
|
+
if (Number.isNaN(parsed)) {
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
return nowMs - parsed >= ageMs;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function resolveVersionCheckCachePath(options = {}) {
|
|
47
|
+
const homeDir = options.homeDir || process.env.HOME || os.homedir() || "/tmp";
|
|
48
|
+
return path.join(homeDir, ".conductor", DEFAULT_CACHE_FILE);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function normalizeVersionCheckCache(value) {
|
|
52
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
const latestVersion = normalizeOptionalString(value.latestVersion);
|
|
56
|
+
const lastNotifiedVersion = normalizeOptionalString(value.lastNotifiedVersion);
|
|
57
|
+
const normalized = {
|
|
58
|
+
lastCheckedAt: parseTimestamp(value.lastCheckedAt),
|
|
59
|
+
latestVersion,
|
|
60
|
+
latestCheckedAt: parseTimestamp(value.latestCheckedAt),
|
|
61
|
+
lastNotifiedVersion,
|
|
62
|
+
lastNotifiedAt: parseTimestamp(value.lastNotifiedAt),
|
|
63
|
+
};
|
|
64
|
+
if (
|
|
65
|
+
!normalized.lastCheckedAt &&
|
|
66
|
+
!normalized.latestVersion &&
|
|
67
|
+
!normalized.latestCheckedAt &&
|
|
68
|
+
!normalized.lastNotifiedVersion &&
|
|
69
|
+
!normalized.lastNotifiedAt
|
|
70
|
+
) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
return normalized;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function readVersionCheckCache(options = {}) {
|
|
77
|
+
const readFileFn = options.readFile || fs.readFile;
|
|
78
|
+
const cachePath = options.cachePath || resolveVersionCheckCachePath(options);
|
|
79
|
+
try {
|
|
80
|
+
const content = await readFileFn(cachePath, "utf8");
|
|
81
|
+
return normalizeVersionCheckCache(JSON.parse(content));
|
|
82
|
+
} catch {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export async function writeVersionCheckCache(cache, options = {}) {
|
|
88
|
+
const writeFileFn = options.writeFile || fs.writeFile;
|
|
89
|
+
const mkdirFn = options.mkdir || fs.mkdir;
|
|
90
|
+
const cachePath = options.cachePath || resolveVersionCheckCachePath(options);
|
|
91
|
+
await mkdirFn(path.dirname(cachePath), { recursive: true });
|
|
92
|
+
await writeFileFn(cachePath, JSON.stringify(cache, null, 2), "utf8");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function shouldSkipVersionCheck(options = {}) {
|
|
96
|
+
const env = options.env || process.env;
|
|
97
|
+
const subcommand = normalizeOptionalString(options.subcommand);
|
|
98
|
+
const stdoutIsTTY = options.stdoutIsTTY ?? Boolean(process.stdout?.isTTY);
|
|
99
|
+
|
|
100
|
+
if (parseBooleanLike(env.CONDUCTOR_SKIP_UPDATE_CHECK)) {
|
|
101
|
+
return { skip: true, reason: "disabled_by_env" };
|
|
102
|
+
}
|
|
103
|
+
if (subcommand === "update") {
|
|
104
|
+
return { skip: true, reason: "update_subcommand" };
|
|
105
|
+
}
|
|
106
|
+
if (!stdoutIsTTY) {
|
|
107
|
+
return { skip: true, reason: "non_tty" };
|
|
108
|
+
}
|
|
109
|
+
if (parseBooleanLike(env.CI)) {
|
|
110
|
+
return { skip: true, reason: "ci" };
|
|
111
|
+
}
|
|
112
|
+
if (normalizeOptionalString(env.CONDUCTOR_CLI_COMMAND)) {
|
|
113
|
+
return { skip: true, reason: "nested_cli" };
|
|
114
|
+
}
|
|
115
|
+
return { skip: false, reason: null };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function buildUpdateNotice({ currentVersion, latestVersion }) {
|
|
119
|
+
return `New conductor version available: ${currentVersion} -> ${latestVersion}. Run: conductor update`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function shouldNotifyVersion({ latestVersion, currentVersion, cache, nowMs, notifyIntervalMs }) {
|
|
123
|
+
if (!latestVersion || !isNewerVersion(latestVersion, currentVersion)) {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
if (!cache?.lastNotifiedVersion || cache.lastNotifiedVersion !== latestVersion) {
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
return isTimestampOlderThan(cache.lastNotifiedAt, notifyIntervalMs, nowMs);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function shouldRefreshVersion({ cache, nowMs, checkIntervalMs }) {
|
|
133
|
+
if (!cache?.lastCheckedAt) {
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
return isTimestampOlderThan(cache.lastCheckedAt, checkIntervalMs, nowMs);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function createUpdatedCache(previousCache, updates = {}) {
|
|
140
|
+
return normalizeVersionCheckCache({
|
|
141
|
+
...previousCache,
|
|
142
|
+
...updates,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export async function maybeCheckForUpdates(options = {}) {
|
|
147
|
+
const env = options.env || process.env;
|
|
148
|
+
const currentVersion = normalizeOptionalString(options.currentVersion);
|
|
149
|
+
const subcommand = normalizeOptionalString(options.subcommand);
|
|
150
|
+
const nowMs = options.nowMs ?? Date.now();
|
|
151
|
+
const checkIntervalMs = options.checkIntervalMs ?? DEFAULT_VERSION_CHECK_INTERVAL_MS;
|
|
152
|
+
const notifyIntervalMs = options.notifyIntervalMs ?? DEFAULT_VERSION_NOTIFY_INTERVAL_MS;
|
|
153
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_VERSION_CHECK_TIMEOUT_MS;
|
|
154
|
+
const skip = shouldSkipVersionCheck({
|
|
155
|
+
env,
|
|
156
|
+
subcommand,
|
|
157
|
+
stdoutIsTTY: options.stdoutIsTTY,
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
if (skip.skip || !currentVersion) {
|
|
161
|
+
return { skipped: true, reason: skip.reason || "missing_current_version" };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const writeNotice = options.writeNotice || ((message) => process.stderr.write(`${message}\n`));
|
|
165
|
+
const fetchLatestVersionFn = options.fetchLatestVersion || fetchLatestVersion;
|
|
166
|
+
const cacheOptions = {
|
|
167
|
+
cachePath: options.cachePath,
|
|
168
|
+
homeDir: options.homeDir || env.HOME,
|
|
169
|
+
readFile: options.readFile,
|
|
170
|
+
writeFile: options.writeFile,
|
|
171
|
+
mkdir: options.mkdir,
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
let cache = await readVersionCheckCache(cacheOptions);
|
|
175
|
+
const needsRefresh = shouldRefreshVersion({ cache, nowMs, checkIntervalMs });
|
|
176
|
+
|
|
177
|
+
if (!needsRefresh) {
|
|
178
|
+
if (cache?.latestVersion && shouldNotifyVersion({
|
|
179
|
+
latestVersion: cache.latestVersion,
|
|
180
|
+
currentVersion,
|
|
181
|
+
cache,
|
|
182
|
+
nowMs,
|
|
183
|
+
notifyIntervalMs,
|
|
184
|
+
})) {
|
|
185
|
+
writeNotice(buildUpdateNotice({ currentVersion, latestVersion: cache.latestVersion }));
|
|
186
|
+
cache = createUpdatedCache(cache, {
|
|
187
|
+
lastNotifiedVersion: cache.latestVersion,
|
|
188
|
+
lastNotifiedAt: new Date(nowMs).toISOString(),
|
|
189
|
+
});
|
|
190
|
+
if (cache) {
|
|
191
|
+
await writeVersionCheckCache(cache, cacheOptions);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return { skipped: false, refreshed: false, latestVersion: cache?.latestVersion || null };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
let latestVersion = null;
|
|
198
|
+
try {
|
|
199
|
+
latestVersion = await fetchLatestVersionFn(undefined, { timeoutMs });
|
|
200
|
+
} catch {
|
|
201
|
+
latestVersion = null;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
cache = createUpdatedCache(cache, {
|
|
205
|
+
lastCheckedAt: new Date(nowMs).toISOString(),
|
|
206
|
+
...(latestVersion
|
|
207
|
+
? {
|
|
208
|
+
latestVersion,
|
|
209
|
+
latestCheckedAt: new Date(nowMs).toISOString(),
|
|
210
|
+
}
|
|
211
|
+
: {}),
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
if (cache) {
|
|
215
|
+
await writeVersionCheckCache(cache, cacheOptions);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const versionToNotify = latestVersion || cache?.latestVersion || null;
|
|
219
|
+
if (versionToNotify && shouldNotifyVersion({
|
|
220
|
+
latestVersion: versionToNotify,
|
|
221
|
+
currentVersion,
|
|
222
|
+
cache,
|
|
223
|
+
nowMs,
|
|
224
|
+
notifyIntervalMs,
|
|
225
|
+
})) {
|
|
226
|
+
writeNotice(buildUpdateNotice({ currentVersion, latestVersion: versionToNotify }));
|
|
227
|
+
cache = createUpdatedCache(cache, {
|
|
228
|
+
lastNotifiedVersion: versionToNotify,
|
|
229
|
+
lastNotifiedAt: new Date(nowMs).toISOString(),
|
|
230
|
+
});
|
|
231
|
+
if (cache) {
|
|
232
|
+
await writeVersionCheckCache(cache, cacheOptions);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return {
|
|
237
|
+
skipped: false,
|
|
238
|
+
refreshed: true,
|
|
239
|
+
latestVersion,
|
|
240
|
+
};
|
|
241
|
+
}
|