@bli-cockpit/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.
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { autostartStatus, installAutostartAgent, } from "./autostart.js";
|
|
4
|
+
export const AUTOSTART_REPAIR_THROTTLE_MARKER = ".last-autostart-repair";
|
|
5
|
+
const AUTOSTART_REPAIR_MIN_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
6
|
+
const DETAIL_MAX_CHARS = 300;
|
|
7
|
+
export async function runAutostartSelfHeal(paths, options) {
|
|
8
|
+
const platform = options.platform ?? process.platform;
|
|
9
|
+
if (platform !== "win32")
|
|
10
|
+
return null;
|
|
11
|
+
if (options.repoRoots.length === 0)
|
|
12
|
+
return null;
|
|
13
|
+
const status = await autostartStatus({
|
|
14
|
+
homeDir: options.homeDir,
|
|
15
|
+
repoRoot: options.repoRoots[0],
|
|
16
|
+
repoRoots: options.repoRoots,
|
|
17
|
+
dashboardUrl: options.dashboardUrl,
|
|
18
|
+
exec: options.exec,
|
|
19
|
+
platform,
|
|
20
|
+
});
|
|
21
|
+
// Healthy is the steady state and stays silent; absent means the operator
|
|
22
|
+
// (or onboarding) owns the decision, not this tick.
|
|
23
|
+
if (status.status !== "not_loaded")
|
|
24
|
+
return null;
|
|
25
|
+
// Attempts are throttled like the self-update's (marker mtime, written for
|
|
26
|
+
// the attempt not the outcome) so a persistently failing repair cannot spawn
|
|
27
|
+
// a registration every 15 minutes. The status probe above still runs every
|
|
28
|
+
// tick — it is one schtasks query.
|
|
29
|
+
const now = options.now ?? new Date();
|
|
30
|
+
const marker = path.join(paths.state_dir, AUTOSTART_REPAIR_THROTTLE_MARKER);
|
|
31
|
+
const lastAttempt = await fs.stat(marker).catch(() => null);
|
|
32
|
+
if (lastAttempt &&
|
|
33
|
+
now.getTime() - lastAttempt.mtimeMs < AUTOSTART_REPAIR_MIN_INTERVAL_MS) {
|
|
34
|
+
return { status: "skipped", reason: "repair_throttled_recent_attempt" };
|
|
35
|
+
}
|
|
36
|
+
await fs.mkdir(paths.state_dir, { recursive: true }).catch(() => undefined);
|
|
37
|
+
await fs.writeFile(marker, now.toISOString()).catch(() => undefined);
|
|
38
|
+
const problem = (status.message ?? "task not loaded").slice(0, DETAIL_MAX_CHARS);
|
|
39
|
+
const repaired = await installAutostartAgent({
|
|
40
|
+
homeDir: options.homeDir,
|
|
41
|
+
repoRoot: options.repoRoots[0],
|
|
42
|
+
repoRoots: options.repoRoots,
|
|
43
|
+
dashboardUrl: options.dashboardUrl,
|
|
44
|
+
exec: options.exec,
|
|
45
|
+
platform,
|
|
46
|
+
});
|
|
47
|
+
if (repaired.loaded) {
|
|
48
|
+
return { status: "ok", reason: "autostart_repaired", detail: problem };
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
status: "fail",
|
|
52
|
+
reason: "autostart_repair_failed",
|
|
53
|
+
detail: (repaired.message ?? problem).slice(0, DETAIL_MAX_CHARS),
|
|
54
|
+
};
|
|
55
|
+
}
|
package/dist/commands/local.js
CHANGED
|
@@ -22,6 +22,7 @@ import { runAttributedWorktreeSync, matchesLiveSyncWorktree, } from "./session-s
|
|
|
22
22
|
import { COLLECTION_ROOT_REQUIRED, missingCollectionRootMessage, normalizeRootsDetailed, resolveOnboardingRoots, rootRejectionExplanation, } from "../onboarding-roots.js";
|
|
23
23
|
import { rawEvidenceGcSummary, runRawEvidenceLocalGc, } from "../raw-evidence-gc.js";
|
|
24
24
|
import { envWithNodeRuntimeOnPath, runScheduledSelfUpdate, } from "../scheduled-self-update.js";
|
|
25
|
+
import { runAutostartSelfHeal, } from "../autostart-self-heal.js";
|
|
25
26
|
import { enqueueInstallEventEntry, readPendingInstallEventEntries, recordInstallEventAttemptFailure, removeInstallEventEntry, } from "../spool/install-event-outbox.js";
|
|
26
27
|
import { createCapturedExecRunner, createInteractiveExecRunner, } from "../process-runner.js";
|
|
27
28
|
import { normalizeCollectionRoots } from "../root-normalization.js";
|
|
@@ -711,9 +712,16 @@ function sanitizeInstallErrorCode(value) {
|
|
|
711
712
|
.slice(0, 120);
|
|
712
713
|
return normalized || "unknown";
|
|
713
714
|
}
|
|
715
|
+
/**
|
|
716
|
+
* Posts pending install events. Also the collector's only per-tick listening
|
|
717
|
+
* post: the response carries the server-published `min_cli_version` floor
|
|
718
|
+
* (BLI-2678), so the last one observed is returned for the scheduled
|
|
719
|
+
* self-update step to act on. Every early-out returns null — no receipt, no
|
|
720
|
+
* floor.
|
|
721
|
+
*/
|
|
714
722
|
export async function reportInstallEventsBestEffort(options) {
|
|
715
723
|
if (options.events.length === 0)
|
|
716
|
-
return;
|
|
724
|
+
return null;
|
|
717
725
|
const paths = getCollectorRuntimePaths(options.homeDir);
|
|
718
726
|
try {
|
|
719
727
|
await enqueueInstallEventEntry(paths, {
|
|
@@ -744,17 +752,18 @@ export async function reportInstallEventsBestEffort(options) {
|
|
|
744
752
|
if (options.json) {
|
|
745
753
|
writeLine(options.io.stderr, "Install event outbox unavailable: local_write_failed");
|
|
746
754
|
}
|
|
747
|
-
return;
|
|
755
|
+
return null;
|
|
748
756
|
}
|
|
749
757
|
const session = await readLocalCollectorSessionFile(paths).catch(() => null);
|
|
750
758
|
if (!session ||
|
|
751
759
|
session.session_state !== "valid" ||
|
|
752
760
|
typeof session.device_token !== "string" ||
|
|
753
761
|
!session.device_token) {
|
|
754
|
-
return;
|
|
762
|
+
return null;
|
|
755
763
|
}
|
|
756
764
|
const pending = (await readPendingInstallEventEntries(paths)).slice(0, 20);
|
|
757
765
|
const failures = [];
|
|
766
|
+
let observedMinCliVersion = null;
|
|
758
767
|
for (let offset = 0; offset < pending.length; offset += 5) {
|
|
759
768
|
await Promise.all(pending.slice(offset, offset + 5).map(async (entry) => {
|
|
760
769
|
const controller = new AbortController();
|
|
@@ -777,6 +786,13 @@ export async function reportInstallEventsBestEffort(options) {
|
|
|
777
786
|
if (!response.ok) {
|
|
778
787
|
throw new Error(`http_${response.status}`);
|
|
779
788
|
}
|
|
789
|
+
const receipt = (await response
|
|
790
|
+
.json()
|
|
791
|
+
.catch(() => null));
|
|
792
|
+
if (typeof receipt?.min_cli_version === "string" &&
|
|
793
|
+
receipt.min_cli_version.trim()) {
|
|
794
|
+
observedMinCliVersion = receipt.min_cli_version.trim();
|
|
795
|
+
}
|
|
780
796
|
await removeInstallEventEntry(paths, entry.outbox_id);
|
|
781
797
|
}
|
|
782
798
|
catch (error) {
|
|
@@ -795,6 +811,7 @@ export async function reportInstallEventsBestEffort(options) {
|
|
|
795
811
|
if (options.json && failures.length > 0) {
|
|
796
812
|
writeLine(options.io.stderr, `Install event telemetry queued for retry: ${[...new Set(failures)].join(",")}`);
|
|
797
813
|
}
|
|
814
|
+
return observedMinCliVersion;
|
|
798
815
|
}
|
|
799
816
|
function classifyInstallTelemetryError(error) {
|
|
800
817
|
if (error instanceof Error && error.name === "AbortError") {
|
|
@@ -2135,7 +2152,7 @@ async function runSync(command, io) {
|
|
|
2135
2152
|
const paths = getCollectorRuntimePaths(command.homeDir);
|
|
2136
2153
|
const config = await readLocalCollectorConfig(paths).catch(() => null);
|
|
2137
2154
|
const dashboardUrl = command.dashboardUrl ?? config?.dashboard_url ?? DEFAULT_DASHBOARD_URL;
|
|
2138
|
-
await reportInstallEventsBestEffort({
|
|
2155
|
+
const minCliVersionAtStart = await reportInstallEventsBestEffort({
|
|
2139
2156
|
homeDir: command.homeDir,
|
|
2140
2157
|
dashboardUrl,
|
|
2141
2158
|
command: "sync",
|
|
@@ -2145,7 +2162,7 @@ async function runSync(command, io) {
|
|
|
2145
2162
|
});
|
|
2146
2163
|
try {
|
|
2147
2164
|
const result = await runSyncWithHealthReceipt(command, io);
|
|
2148
|
-
await reportInstallEventsBestEffort({
|
|
2165
|
+
const minCliVersion = await reportInstallEventsBestEffort({
|
|
2149
2166
|
homeDir: command.homeDir,
|
|
2150
2167
|
dashboardUrl,
|
|
2151
2168
|
command: "sync",
|
|
@@ -2155,11 +2172,12 @@ async function runSync(command, io) {
|
|
|
2155
2172
|
});
|
|
2156
2173
|
// BLI-2601: self-update runs only after collection's own outcome above is
|
|
2157
2174
|
// already decided and reported, win or lose. See the function doc.
|
|
2158
|
-
await runScheduledSelfUpdateAfterSync(command, io, dashboardUrl);
|
|
2175
|
+
await runScheduledSelfUpdateAfterSync(command, io, dashboardUrl, minCliVersion ?? minCliVersionAtStart);
|
|
2176
|
+
await runAutostartSelfHealAfterSync(command, io, dashboardUrl);
|
|
2159
2177
|
return result.exitCode;
|
|
2160
2178
|
}
|
|
2161
2179
|
catch (error) {
|
|
2162
|
-
await reportInstallEventsBestEffort({
|
|
2180
|
+
const minCliVersion = await reportInstallEventsBestEffort({
|
|
2163
2181
|
homeDir: command.homeDir,
|
|
2164
2182
|
dashboardUrl,
|
|
2165
2183
|
command: "sync",
|
|
@@ -2174,10 +2192,60 @@ async function runSync(command, io) {
|
|
|
2174
2192
|
json: command.json,
|
|
2175
2193
|
io,
|
|
2176
2194
|
});
|
|
2177
|
-
await runScheduledSelfUpdateAfterSync(command, io, dashboardUrl);
|
|
2195
|
+
await runScheduledSelfUpdateAfterSync(command, io, dashboardUrl, minCliVersion ?? minCliVersionAtStart);
|
|
2196
|
+
await runAutostartSelfHealAfterSync(command, io, dashboardUrl);
|
|
2178
2197
|
throw error;
|
|
2179
2198
|
}
|
|
2180
2199
|
}
|
|
2200
|
+
/**
|
|
2201
|
+
* BLI-2721: after the tick's collection and self-update are done and
|
|
2202
|
+
* reported, repair a broken/legacy autostart registration in place (Windows
|
|
2203
|
+
* only — see autostart-self-heal.ts for why macOS is excluded). Every error
|
|
2204
|
+
* path is swallowed like the self-update's: heal outcomes are their own
|
|
2205
|
+
* receipts, never a sync failure.
|
|
2206
|
+
*/
|
|
2207
|
+
async function runAutostartSelfHealAfterSync(command, io, dashboardUrl) {
|
|
2208
|
+
let result;
|
|
2209
|
+
try {
|
|
2210
|
+
const rawExec = io.exec;
|
|
2211
|
+
if (!rawExec)
|
|
2212
|
+
return;
|
|
2213
|
+
const spawnEnv = envWithNodeRuntimeOnPath(io.env ?? process.env);
|
|
2214
|
+
const exec = (cmd, args, options) => rawExec(cmd, args, { ...options, env: options?.env ?? spawnEnv });
|
|
2215
|
+
result = await runAutostartSelfHeal(getCollectorRuntimePaths(command.homeDir), {
|
|
2216
|
+
homeDir: command.homeDir,
|
|
2217
|
+
repoRoots: await resolveAutostartRoots(command.homeDir, undefined),
|
|
2218
|
+
dashboardUrl: command.dashboardUrl,
|
|
2219
|
+
exec,
|
|
2220
|
+
});
|
|
2221
|
+
}
|
|
2222
|
+
catch (error) {
|
|
2223
|
+
result = {
|
|
2224
|
+
status: "fail",
|
|
2225
|
+
reason: "autostart_self_heal_threw",
|
|
2226
|
+
detail: redactedSyncErrorDetail(error),
|
|
2227
|
+
};
|
|
2228
|
+
}
|
|
2229
|
+
// Steady state (healthy, absent, non-Windows, no roots) and the daily
|
|
2230
|
+
// throttle are silent; an actual repair attempt reports either way.
|
|
2231
|
+
if (!result || result.reason === "repair_throttled_recent_attempt")
|
|
2232
|
+
return;
|
|
2233
|
+
await reportInstallEventsBestEffort({
|
|
2234
|
+
homeDir: command.homeDir,
|
|
2235
|
+
dashboardUrl,
|
|
2236
|
+
command: "sync",
|
|
2237
|
+
events: [
|
|
2238
|
+
{
|
|
2239
|
+
step: "autostart_repair",
|
|
2240
|
+
status: result.status,
|
|
2241
|
+
...(result.status === "ok" ? {} : { error_code: result.reason }),
|
|
2242
|
+
...(result.detail ? { error_detail: result.detail } : {}),
|
|
2243
|
+
},
|
|
2244
|
+
],
|
|
2245
|
+
json: command.json,
|
|
2246
|
+
io,
|
|
2247
|
+
});
|
|
2248
|
+
}
|
|
2181
2249
|
/**
|
|
2182
2250
|
* BLI-2601: the fleet keeps itself current on npm `latest` without anyone
|
|
2183
2251
|
* re-running `npm i -g @bli-cockpit/cli` by hand after day 0. This always
|
|
@@ -2188,10 +2256,10 @@ async function runSync(command, io) {
|
|
|
2188
2256
|
* reported as its own named `update` receipt, never surfaced as a `sync`
|
|
2189
2257
|
* failure or thrown from this function.
|
|
2190
2258
|
*/
|
|
2191
|
-
async function runScheduledSelfUpdateAfterSync(command, io, dashboardUrl) {
|
|
2259
|
+
async function runScheduledSelfUpdateAfterSync(command, io, dashboardUrl, minCliVersion) {
|
|
2192
2260
|
let event;
|
|
2193
2261
|
try {
|
|
2194
|
-
event = await runScheduledSelfUpdateForSync(command, io);
|
|
2262
|
+
event = await runScheduledSelfUpdateForSync(command, io, minCliVersion);
|
|
2195
2263
|
}
|
|
2196
2264
|
catch (error) {
|
|
2197
2265
|
// The throttle/probe/install machinery below is defensive already; this
|
|
@@ -2215,7 +2283,7 @@ async function runScheduledSelfUpdateAfterSync(command, io, dashboardUrl) {
|
|
|
2215
2283
|
io,
|
|
2216
2284
|
});
|
|
2217
2285
|
}
|
|
2218
|
-
async function runScheduledSelfUpdateForSync(command, io) {
|
|
2286
|
+
async function runScheduledSelfUpdateForSync(command, io, minCliVersion) {
|
|
2219
2287
|
const rawExec = io.exec;
|
|
2220
2288
|
if (!rawExec) {
|
|
2221
2289
|
// Only the real production `defaultIo()` supplies a process runner. A
|
|
@@ -2235,7 +2303,7 @@ async function runScheduledSelfUpdateForSync(command, io) {
|
|
|
2235
2303
|
exec,
|
|
2236
2304
|
currentVersion: LOCAL_COLLECTOR_VERSION,
|
|
2237
2305
|
install: (tag) => attemptScheduledSelfUpdateInstall(scheduledIo, tag),
|
|
2238
|
-
}, { env: io.env });
|
|
2306
|
+
}, { env: io.env, minVersion: minCliVersion });
|
|
2239
2307
|
return scheduledSelfUpdateInstallEvent(result);
|
|
2240
2308
|
}
|
|
2241
2309
|
async function attemptScheduledSelfUpdateInstall(io, tag) {
|
|
@@ -2260,9 +2328,21 @@ function scheduledSelfUpdateInstallEvent(result) {
|
|
|
2260
2328
|
// produces a receipt.
|
|
2261
2329
|
if (result.reason === "throttled_recent_attempt")
|
|
2262
2330
|
return null;
|
|
2263
|
-
|
|
2264
|
-
|
|
2331
|
+
// A forced attempt names its trigger in the receipt either way, so the
|
|
2332
|
+
// ledger can tell "converged on the daily cadence" from "the floor pulled
|
|
2333
|
+
// this machine forward" (BLI-2678).
|
|
2334
|
+
const forcedDetail = result.forced && result.min_version
|
|
2335
|
+
? `forced_min_version ${result.min_version}`
|
|
2336
|
+
: null;
|
|
2337
|
+
if (result.status === "ok") {
|
|
2338
|
+
return {
|
|
2339
|
+
step: "update",
|
|
2340
|
+
status: "ok",
|
|
2341
|
+
...(forcedDetail ? { error_detail: forcedDetail } : {}),
|
|
2342
|
+
};
|
|
2343
|
+
}
|
|
2265
2344
|
const detail = [
|
|
2345
|
+
forcedDetail,
|
|
2266
2346
|
result.target_version ? `target ${result.target_version}` : null,
|
|
2267
2347
|
result.installed_version ? `installed ${result.installed_version}` : null,
|
|
2268
2348
|
]
|
|
@@ -15,7 +15,7 @@ export async function runCockpitCli(argv, io) {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
if (command === "--version" || command === "-V" || command === "version") {
|
|
18
|
-
writeLine(io?.stdout ?? process.stdout, "0.2.
|
|
18
|
+
writeLine(io?.stdout ?? process.stdout, "0.2.24");
|
|
19
19
|
return 0;
|
|
20
20
|
}
|
|
21
21
|
|
|
@@ -37,9 +37,22 @@ export async function runScheduledSelfUpdate(paths, deps, options = {}) {
|
|
|
37
37
|
const env = options.env ?? process.env;
|
|
38
38
|
const now = options.now ?? new Date();
|
|
39
39
|
const tag = options.tag ?? "latest";
|
|
40
|
+
// BLI-2678: the fleet forced-update floor. When the server says the minimum
|
|
41
|
+
// is above what this process is running, the daily throttle stops applying —
|
|
42
|
+
// the whole point of the floor is converging faster than once a day. This is
|
|
43
|
+
// NOT remote code execution: being below the floor only makes the exact
|
|
44
|
+
// same npm self-update that runs daily anyway run now. An unparseable floor
|
|
45
|
+
// is ignored rather than obeyed — a typo must fail toward the safe default.
|
|
46
|
+
const minVersion = parseSemverTriple(options.minVersion ?? null)
|
|
47
|
+
? options.minVersion.trim()
|
|
48
|
+
: null;
|
|
49
|
+
const forced = minVersion !== null && isSemverBelow(deps.currentVersion, minVersion);
|
|
50
|
+
const forcedFields = forced ? { forced: true, min_version: minVersion } : {};
|
|
40
51
|
const marker = path.join(paths.state_dir, SELF_UPDATE_THROTTLE_MARKER);
|
|
41
52
|
const lastCheck = await fs.stat(marker).catch(() => null);
|
|
42
|
-
if (
|
|
53
|
+
if (!forced &&
|
|
54
|
+
lastCheck &&
|
|
55
|
+
now.getTime() - lastCheck.mtimeMs < SELF_UPDATE_MIN_INTERVAL_MS) {
|
|
43
56
|
// The steady-state case: already checked today, nothing to do. Not
|
|
44
57
|
// reported as an "attempt" by the caller — this fires on ~95 of every 96
|
|
45
58
|
// ticks and would otherwise be pure noise.
|
|
@@ -56,11 +69,33 @@ export async function runScheduledSelfUpdate(paths, deps, options = {}) {
|
|
|
56
69
|
// disabled machine reports itself at most once per day too, instead of on
|
|
57
70
|
// every tick.
|
|
58
71
|
if (env["COCKPIT_DISABLE_AUTO_UPDATE"] === "1") {
|
|
59
|
-
|
|
72
|
+
// The incident-triage freeze outranks the forced floor: a human pinned
|
|
73
|
+
// this machine on purpose, and the server must not be able to unpin it.
|
|
74
|
+
return { status: "skipped", reason: "disabled_by_env", ...forcedFields };
|
|
60
75
|
}
|
|
61
76
|
const targetVersion = await latestCliVersionFromNpm(deps.exec, tag);
|
|
77
|
+
if (forced &&
|
|
78
|
+
targetVersion &&
|
|
79
|
+
isSemverBelow(targetVersion, minVersion)) {
|
|
80
|
+
// The floor is above what npm actually serves — a typo'd floor, or one set
|
|
81
|
+
// before the release finished publishing. Installing would not clear the
|
|
82
|
+
// floor, so every forced tick would run `npm install` forever. Skip loudly
|
|
83
|
+
// instead: this reason posts a receipt on every tick until the floor is
|
|
84
|
+
// corrected, which is exactly the alarm a misconfig should raise.
|
|
85
|
+
return {
|
|
86
|
+
status: "skipped",
|
|
87
|
+
reason: "min_version_above_npm_latest",
|
|
88
|
+
target_version: targetVersion,
|
|
89
|
+
...forcedFields,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
62
92
|
if (targetVersion && targetVersion === deps.currentVersion) {
|
|
63
|
-
return {
|
|
93
|
+
return {
|
|
94
|
+
status: "ok",
|
|
95
|
+
reason: "already_latest",
|
|
96
|
+
target_version: targetVersion,
|
|
97
|
+
...forcedFields,
|
|
98
|
+
};
|
|
64
99
|
}
|
|
65
100
|
const installed = await deps.install(tag);
|
|
66
101
|
if (!installed.ok) {
|
|
@@ -68,6 +103,7 @@ export async function runScheduledSelfUpdate(paths, deps, options = {}) {
|
|
|
68
103
|
status: "fail",
|
|
69
104
|
reason: installed.eacces ? "eacces_needs_chown" : "npm_install_failed",
|
|
70
105
|
...(targetVersion ? { target_version: targetVersion } : {}),
|
|
106
|
+
...forcedFields,
|
|
71
107
|
};
|
|
72
108
|
}
|
|
73
109
|
// Verify against what the platform returns, not the exit code npm handed
|
|
@@ -82,6 +118,7 @@ export async function runScheduledSelfUpdate(paths, deps, options = {}) {
|
|
|
82
118
|
reason: "stale_after_self_update",
|
|
83
119
|
...(targetVersion ? { target_version: targetVersion } : {}),
|
|
84
120
|
...(installedVersion ? { installed_version: installedVersion } : {}),
|
|
121
|
+
...forcedFields,
|
|
85
122
|
};
|
|
86
123
|
}
|
|
87
124
|
return {
|
|
@@ -89,8 +126,37 @@ export async function runScheduledSelfUpdate(paths, deps, options = {}) {
|
|
|
89
126
|
reason: "updated",
|
|
90
127
|
target_version: targetVersion ?? installedVersion,
|
|
91
128
|
installed_version: installedVersion,
|
|
129
|
+
...forcedFields,
|
|
92
130
|
};
|
|
93
131
|
}
|
|
132
|
+
/**
|
|
133
|
+
* Ordering comparison for the forced-update floor. Mirrors the dashboard's
|
|
134
|
+
* parseSemver/compareParsedSemver (apps/dashboard/src/lib/ambient/rollups.ts)
|
|
135
|
+
* rather than adding a `semver` dependency for two ten-line functions. A
|
|
136
|
+
* version either side fails to parse → false: an unreadable floor or version
|
|
137
|
+
* must never force anything.
|
|
138
|
+
*/
|
|
139
|
+
function parseSemverTriple(value) {
|
|
140
|
+
if (typeof value !== "string")
|
|
141
|
+
return null;
|
|
142
|
+
const match = value.trim().match(/^(\d+)\.(\d+)\.(\d+)/u);
|
|
143
|
+
if (!match)
|
|
144
|
+
return null;
|
|
145
|
+
return [Number(match[1]), Number(match[2]), Number(match[3])];
|
|
146
|
+
}
|
|
147
|
+
function isSemverBelow(left, right) {
|
|
148
|
+
const parsedLeft = parseSemverTriple(left);
|
|
149
|
+
const parsedRight = parseSemverTriple(right);
|
|
150
|
+
if (!parsedLeft || !parsedRight)
|
|
151
|
+
return false;
|
|
152
|
+
for (let part = 0; part < 3; part += 1) {
|
|
153
|
+
if ((parsedLeft[part] ?? 0) < (parsedRight[part] ?? 0))
|
|
154
|
+
return true;
|
|
155
|
+
if ((parsedLeft[part] ?? 0) > (parsedRight[part] ?? 0))
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
94
160
|
async function latestCliVersionFromNpm(exec, tag) {
|
|
95
161
|
const result = await exec("npm", [
|
|
96
162
|
"view",
|