@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
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared version-check utilities used by both `conductor update` and the daemon auto-update flow.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import http from "node:http";
|
|
6
|
+
import https from "node:https";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { execFileSync, execSync } from "node:child_process";
|
|
9
|
+
|
|
10
|
+
export const PACKAGE_NAME = "@love-moon/conductor-cli";
|
|
11
|
+
const DEFAULT_UPDATE_WINDOW = { startMinutes: 120, endMinutes: 240 };
|
|
12
|
+
const REQUEST_TIMEOUT_MS = 10_000;
|
|
13
|
+
|
|
14
|
+
function resolveTimeoutMs(value) {
|
|
15
|
+
const parsed = Number.parseInt(String(value ?? ""), 10);
|
|
16
|
+
if (Number.isFinite(parsed) && parsed > 0) {
|
|
17
|
+
return parsed;
|
|
18
|
+
}
|
|
19
|
+
return REQUEST_TIMEOUT_MS;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Fetch the latest published version from the npm registry.
|
|
24
|
+
* Prefers `npm view` so local registry / proxy / CA config is respected,
|
|
25
|
+
* then falls back to a direct registry request.
|
|
26
|
+
* Returns the version string on success, or `null` on any failure.
|
|
27
|
+
*/
|
|
28
|
+
export async function fetchLatestVersion(packageName = PACKAGE_NAME, deps = {}) {
|
|
29
|
+
const timeoutMs = resolveTimeoutMs(deps.timeoutMs);
|
|
30
|
+
const npmVersion = getLatestVersionFromNpm(packageName, deps);
|
|
31
|
+
if (npmVersion) {
|
|
32
|
+
return npmVersion;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const registryBase = getRegistryBaseUrl(deps.registryUrl);
|
|
36
|
+
const requestUrl = new URL(`${registryBase.replace(/\/$/, "")}/${encodeURIComponent(packageName)}/latest`);
|
|
37
|
+
const requestImpl =
|
|
38
|
+
requestUrl.protocol === "http:"
|
|
39
|
+
? (deps.httpGet || http.get)
|
|
40
|
+
: (deps.httpsGet || https.get);
|
|
41
|
+
|
|
42
|
+
return new Promise((resolve) => {
|
|
43
|
+
const req = requestImpl(requestUrl, { timeout: timeoutMs }, (res) => {
|
|
44
|
+
let data = "";
|
|
45
|
+
res.on("data", (chunk) => {
|
|
46
|
+
data += chunk;
|
|
47
|
+
});
|
|
48
|
+
res.on("end", () => {
|
|
49
|
+
if (res.statusCode && (res.statusCode < 200 || res.statusCode >= 300)) {
|
|
50
|
+
resolve(null);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
const json = JSON.parse(data);
|
|
55
|
+
resolve(json.version || null);
|
|
56
|
+
} catch {
|
|
57
|
+
resolve(null);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
req.on("error", () => resolve(null));
|
|
62
|
+
req.on("timeout", () => {
|
|
63
|
+
req.destroy();
|
|
64
|
+
resolve(null);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function getLatestVersionFromNpm(packageName, deps = {}) {
|
|
70
|
+
const execFileSyncFn = deps.execFileSync || execFileSync;
|
|
71
|
+
const platform = deps.platform || process.platform;
|
|
72
|
+
const npmCommand = platform === "win32" ? "npm.cmd" : "npm";
|
|
73
|
+
const timeoutMs = resolveTimeoutMs(deps.timeoutMs);
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
const output = execFileSyncFn(
|
|
77
|
+
npmCommand,
|
|
78
|
+
["view", packageName, "version", "--json"],
|
|
79
|
+
{
|
|
80
|
+
encoding: "utf-8",
|
|
81
|
+
timeout: timeoutMs,
|
|
82
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
83
|
+
},
|
|
84
|
+
);
|
|
85
|
+
const parsed = JSON.parse(String(output).trim());
|
|
86
|
+
return typeof parsed === "string" && parsed.trim() ? parsed.trim() : null;
|
|
87
|
+
} catch {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function getRegistryBaseUrl(overrideRegistryUrl) {
|
|
93
|
+
const fromEnv = typeof process.env.npm_config_registry === "string"
|
|
94
|
+
? process.env.npm_config_registry.trim()
|
|
95
|
+
: "";
|
|
96
|
+
const candidate = String(overrideRegistryUrl || fromEnv || "https://registry.npmjs.org").trim();
|
|
97
|
+
if (!candidate) {
|
|
98
|
+
return "https://registry.npmjs.org";
|
|
99
|
+
}
|
|
100
|
+
return candidate;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Compare two semver-like version strings.
|
|
105
|
+
* Returns `true` when `latest` is strictly newer than `current`.
|
|
106
|
+
*/
|
|
107
|
+
export function isNewerVersion(latest, current) {
|
|
108
|
+
const parse = (v) =>
|
|
109
|
+
String(v || "")
|
|
110
|
+
.replace(/^v/, "")
|
|
111
|
+
.split(".")
|
|
112
|
+
.map(Number);
|
|
113
|
+
|
|
114
|
+
const latestParts = parse(latest);
|
|
115
|
+
const currentParts = parse(current);
|
|
116
|
+
|
|
117
|
+
for (let i = 0; i < Math.max(latestParts.length, currentParts.length); i++) {
|
|
118
|
+
const l = latestParts[i] || 0;
|
|
119
|
+
const c = currentParts[i] || 0;
|
|
120
|
+
if (l > c) return true;
|
|
121
|
+
if (l < c) return false;
|
|
122
|
+
}
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Detect the package manager that was used to install the CLI globally.
|
|
128
|
+
* Returns `"pnpm"`, `"yarn"`, or `"npm"` (default fallback).
|
|
129
|
+
*/
|
|
130
|
+
export function detectPackageManager(options = {}) {
|
|
131
|
+
const hintSources = [options.launcherPath, options.packageRoot];
|
|
132
|
+
for (const hint of hintSources) {
|
|
133
|
+
const detected = detectPackageManagerFromHint(hint);
|
|
134
|
+
if (detected) {
|
|
135
|
+
return detected;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
const conductorPath = execSync("which conductor || where conductor", {
|
|
141
|
+
encoding: "utf-8",
|
|
142
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
143
|
+
}).trim();
|
|
144
|
+
|
|
145
|
+
const detected = detectPackageManagerFromHint(conductorPath);
|
|
146
|
+
if (detected) {
|
|
147
|
+
return detected;
|
|
148
|
+
}
|
|
149
|
+
} catch {
|
|
150
|
+
/* ignore */
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
execSync("pnpm --version", { stdio: "pipe" });
|
|
155
|
+
return "pnpm";
|
|
156
|
+
} catch {
|
|
157
|
+
/* not available */
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
execSync("yarn --version", { stdio: "pipe" });
|
|
162
|
+
return "yarn";
|
|
163
|
+
} catch {
|
|
164
|
+
/* not available */
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return "npm";
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function detectPackageManagerFromHint(value) {
|
|
171
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
const normalized = path.resolve(value).toLowerCase().replace(/\\/g, "/");
|
|
175
|
+
if (
|
|
176
|
+
normalized.includes("/pnpm/") ||
|
|
177
|
+
normalized.includes(".pnpm/") ||
|
|
178
|
+
normalized.endsWith("/pnpm")
|
|
179
|
+
) {
|
|
180
|
+
return "pnpm";
|
|
181
|
+
}
|
|
182
|
+
if (normalized.includes("/yarn/") || normalized.includes("/.yarn/")) {
|
|
183
|
+
return "yarn";
|
|
184
|
+
}
|
|
185
|
+
if (normalized.includes("/node_modules/")) {
|
|
186
|
+
return "npm";
|
|
187
|
+
}
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Parse an "HH:MM-HH:MM" window string into start/end minute-of-day values.
|
|
193
|
+
* Falls back to 02:00-04:00 on invalid input.
|
|
194
|
+
*/
|
|
195
|
+
export function parseUpdateWindow(str) {
|
|
196
|
+
const m = String(str || "").match(/^(\d{1,2}):(\d{2})-(\d{1,2}):(\d{2})$/);
|
|
197
|
+
if (!m) return { ...DEFAULT_UPDATE_WINDOW };
|
|
198
|
+
const startHour = parseInt(m[1], 10);
|
|
199
|
+
const startMinute = parseInt(m[2], 10);
|
|
200
|
+
const endHour = parseInt(m[3], 10);
|
|
201
|
+
const endMinute = parseInt(m[4], 10);
|
|
202
|
+
if (
|
|
203
|
+
startHour > 23 ||
|
|
204
|
+
endHour > 23 ||
|
|
205
|
+
startMinute > 59 ||
|
|
206
|
+
endMinute > 59
|
|
207
|
+
) {
|
|
208
|
+
return { ...DEFAULT_UPDATE_WINDOW };
|
|
209
|
+
}
|
|
210
|
+
return {
|
|
211
|
+
startMinutes: startHour * 60 + startMinute,
|
|
212
|
+
endMinutes: endHour * 60 + endMinute,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Auto-update should only mutate managed/global installs. Local repo runs and pnpm-linked
|
|
218
|
+
* worktrees are treated as development installs and are skipped by default.
|
|
219
|
+
*/
|
|
220
|
+
export function isManagedInstallPath(packageRoot) {
|
|
221
|
+
if (typeof packageRoot !== "string" || !packageRoot.trim()) {
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
const normalized = path.resolve(packageRoot);
|
|
225
|
+
return normalized.includes(`${path.sep}node_modules${path.sep}`);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Check whether the current local time falls inside the given update window.
|
|
230
|
+
* Handles windows that wrap around midnight (e.g. "23:00-05:00").
|
|
231
|
+
*/
|
|
232
|
+
export function isInUpdateWindow(window) {
|
|
233
|
+
const now = new Date();
|
|
234
|
+
const current = now.getHours() * 60 + now.getMinutes();
|
|
235
|
+
if (window.startMinutes <= window.endMinutes) {
|
|
236
|
+
return current >= window.startMinutes && current < window.endMinutes;
|
|
237
|
+
}
|
|
238
|
+
// wraps midnight
|
|
239
|
+
return current >= window.startMinutes || current < window.endMinutes;
|
|
240
|
+
}
|