@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.
@@ -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
  });
@@ -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
  }
@@ -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
 
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 yargs from "yargs/yargs";
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
- // If no arguments or help flag, show help
34
- if (argv.length === 0 || argv[0] === "--help" || argv[0] === "-h") {
35
- showHelp();
36
- process.exit(0);
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
- // If version flag, show version
40
- if (argv[0] === "--version" || argv[0] === "-v") {
41
- const commitId = pkgJson.gitCommitId || "unknown";
42
- console.log(`conductor version ${pkgJson.version} (${commitId})`);
43
- process.exit(0);
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
- // Get the subcommand
47
- const subcommand = argv[0];
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.20",
4
- "gitCommitId": "d622756",
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.20",
21
- "@love-moon/conductor-sdk": "0.2.20",
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
+ }