kubeagent 0.1.31 → 0.1.33
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/dist/cli.js +11 -0
- package/dist/onboard/index.js +15 -0
- package/dist/version-check.d.ts +14 -0
- package/dist/version-check.js +108 -0
- package/dist/version-check.test.d.ts +1 -0
- package/dist/version-check.test.js +179 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -25,6 +25,7 @@ import { scanCluster } from "./onboard/cluster-scan.js";
|
|
|
25
25
|
import { formatProjectMarkdown } from "./onboard/code-scan.js";
|
|
26
26
|
import { writeProjectKb, ensureKbDir } from "./kb/writer.js";
|
|
27
27
|
import { checkForUpdate } from "./update-notifier.js";
|
|
28
|
+
import { enforceMinVersion } from "./version-check.js";
|
|
28
29
|
import { sendTelemetry } from "./telemetry.js";
|
|
29
30
|
const program = new Command();
|
|
30
31
|
program
|
|
@@ -530,6 +531,16 @@ program
|
|
|
530
531
|
}
|
|
531
532
|
}
|
|
532
533
|
});
|
|
534
|
+
// Mandatory version check — runs before each action so users on
|
|
535
|
+
// API-incompatible CLI versions can't proceed. The hook fires AFTER arg
|
|
536
|
+
// parsing, so `kubeagent login --server <url>` validates against the
|
|
537
|
+
// requested server, not the default. Cached per-server for 24h; network
|
|
538
|
+
// failure with no cache falls through silently (see version-check.ts).
|
|
539
|
+
program.hook("preAction", async (_thisCommand, actionCommand) => {
|
|
540
|
+
const opts = actionCommand.opts();
|
|
541
|
+
const serverUrl = opts.server ?? loadAuth()?.serverUrl ?? DEFAULT_SERVER;
|
|
542
|
+
await enforceMinVersion(version, serverUrl);
|
|
543
|
+
});
|
|
533
544
|
program.parse();
|
|
534
545
|
// Non-blocking update check — runs after command completes
|
|
535
546
|
checkForUpdate();
|
package/dist/onboard/index.js
CHANGED
|
@@ -8,9 +8,20 @@ import { writeClusterKb, writeProjectKb, ensureKbDir } from "../kb/writer.js";
|
|
|
8
8
|
import { saveConfig, loadConfig, configDir, ALL_ACTIONS, DEFAULT_SAFE_ACTIONS } from "../config.js";
|
|
9
9
|
import { interactiveAddChannel } from "../notify/setup.js";
|
|
10
10
|
import { pickContext } from "../kubectl-config.js";
|
|
11
|
+
import { loadAuth } from "../auth.js";
|
|
11
12
|
import { join } from "node:path";
|
|
12
13
|
import { homedir } from "node:os";
|
|
13
14
|
import readline from "node:readline";
|
|
15
|
+
function pingOnboardComplete() {
|
|
16
|
+
const auth = loadAuth();
|
|
17
|
+
if (!auth?.apiKey)
|
|
18
|
+
return;
|
|
19
|
+
fetch(`${auth.serverUrl}/v1/onboard/complete`, {
|
|
20
|
+
method: "POST",
|
|
21
|
+
headers: { Authorization: `ApiKey ${auth.apiKey}` },
|
|
22
|
+
signal: AbortSignal.timeout(5000),
|
|
23
|
+
}).catch(() => { });
|
|
24
|
+
}
|
|
14
25
|
function expandPath(p) {
|
|
15
26
|
return p.startsWith("~/") ? homedir() + p.slice(1) : p;
|
|
16
27
|
}
|
|
@@ -147,6 +158,8 @@ export async function onboard(opts = {}) {
|
|
|
147
158
|
notifications: { ...existingConfig.notifications, channels: newChannels },
|
|
148
159
|
clusters: existingConfig.clusters.map((c) => c.context === contextName ? { ...c, codepaths: codePaths.map((p) => p.path) } : c),
|
|
149
160
|
});
|
|
161
|
+
// fallow-ignore-next-line code-duplication
|
|
162
|
+
pingOnboardComplete();
|
|
150
163
|
console.log(chalk.green(`\nKnowledge base written to ${kbDir}`));
|
|
151
164
|
console.log(chalk.green(`Config saved to ${configDir()}/config.yaml`));
|
|
152
165
|
console.log(chalk.dim("\nRun `kubeagent watch` to start monitoring."));
|
|
@@ -341,6 +354,8 @@ export async function onboard(opts = {}) {
|
|
|
341
354
|
config.clusters.push(clusterConfig);
|
|
342
355
|
}
|
|
343
356
|
saveConfig(config);
|
|
357
|
+
// fallow-ignore-next-line code-duplication
|
|
358
|
+
pingOnboardComplete();
|
|
344
359
|
console.log(chalk.green(`\nKnowledge base written to ${kbDir}`));
|
|
345
360
|
console.log(chalk.green(`Config saved to ${configDir()}/config.yaml`));
|
|
346
361
|
console.log(chalk.dim("\nRun `kubeagent watch` to start monitoring."));
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare function isBelow(version: string, floor: string): boolean;
|
|
2
|
+
/**
|
|
3
|
+
* Verifies that the running CLI is still supported by the server at
|
|
4
|
+
* `serverUrl`.
|
|
5
|
+
*
|
|
6
|
+
* Behaviour:
|
|
7
|
+
* - Fetches the server's `minSupported` version (cached per-server for 24h).
|
|
8
|
+
* - If the running version is strictly below the floor, prints a mandatory-
|
|
9
|
+
* update message and exits the process with code 1.
|
|
10
|
+
* - On network failure with no cached value, returns silently — we don't
|
|
11
|
+
* want to lock users out when the server is unreachable for transient
|
|
12
|
+
* reasons. A stale cache is preferred over no cache when offline.
|
|
13
|
+
*/
|
|
14
|
+
export declare function enforceMinVersion(currentVersion: string, serverUrl: string): Promise<void>;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
6
|
+
const FETCH_TIMEOUT_MS = 3000;
|
|
7
|
+
function cacheFilePath() {
|
|
8
|
+
return join(homedir(), ".kubeagent", "version-check.json");
|
|
9
|
+
}
|
|
10
|
+
function readCache() {
|
|
11
|
+
try {
|
|
12
|
+
const data = JSON.parse(readFileSync(cacheFilePath(), "utf-8"));
|
|
13
|
+
if (!data || typeof data !== "object")
|
|
14
|
+
return {};
|
|
15
|
+
// Reject the legacy single-entry shape ({checkedAt, minSupported}) — we
|
|
16
|
+
// can't know which server it came from, so a refresh is safer than
|
|
17
|
+
// potentially applying it to the wrong server.
|
|
18
|
+
if ("checkedAt" in data && "minSupported" in data)
|
|
19
|
+
return {};
|
|
20
|
+
return data;
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return {};
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function writeCache(cache) {
|
|
27
|
+
try {
|
|
28
|
+
mkdirSync(join(homedir(), ".kubeagent"), { recursive: true });
|
|
29
|
+
writeFileSync(cacheFilePath(), JSON.stringify(cache), "utf-8");
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
// Non-fatal
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
// Returns true when `version` is strictly older than `floor` (semver-like, 3 parts).
|
|
36
|
+
export function isBelow(version, floor) {
|
|
37
|
+
const parse = (v) => v.replace(/^v/, "").split(".").map((n) => parseInt(n, 10));
|
|
38
|
+
const [vMaj, vMin, vPatch] = parse(version);
|
|
39
|
+
const [fMaj, fMin, fPatch] = parse(floor);
|
|
40
|
+
if (vMaj !== fMaj)
|
|
41
|
+
return vMaj < fMaj;
|
|
42
|
+
if (vMin !== fMin)
|
|
43
|
+
return vMin < fMin;
|
|
44
|
+
return vPatch < fPatch;
|
|
45
|
+
}
|
|
46
|
+
async function fetchMinSupported(serverUrl) {
|
|
47
|
+
const controller = new AbortController();
|
|
48
|
+
const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
49
|
+
try {
|
|
50
|
+
const res = await fetch(`${serverUrl}/v1/version`, { signal: controller.signal });
|
|
51
|
+
if (!res.ok)
|
|
52
|
+
return undefined;
|
|
53
|
+
const data = (await res.json());
|
|
54
|
+
return data.minSupported;
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
finally {
|
|
60
|
+
clearTimeout(timeout);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Verifies that the running CLI is still supported by the server at
|
|
65
|
+
* `serverUrl`.
|
|
66
|
+
*
|
|
67
|
+
* Behaviour:
|
|
68
|
+
* - Fetches the server's `minSupported` version (cached per-server for 24h).
|
|
69
|
+
* - If the running version is strictly below the floor, prints a mandatory-
|
|
70
|
+
* update message and exits the process with code 1.
|
|
71
|
+
* - On network failure with no cached value, returns silently — we don't
|
|
72
|
+
* want to lock users out when the server is unreachable for transient
|
|
73
|
+
* reasons. A stale cache is preferred over no cache when offline.
|
|
74
|
+
*/
|
|
75
|
+
export async function enforceMinVersion(currentVersion, serverUrl) {
|
|
76
|
+
const now = Date.now();
|
|
77
|
+
const cache = readCache();
|
|
78
|
+
const cached = cache[serverUrl];
|
|
79
|
+
let minSupported;
|
|
80
|
+
if (cached && now - cached.checkedAt < CACHE_TTL_MS) {
|
|
81
|
+
minSupported = cached.minSupported;
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
minSupported = await fetchMinSupported(serverUrl);
|
|
85
|
+
if (minSupported) {
|
|
86
|
+
cache[serverUrl] = { checkedAt: now, minSupported };
|
|
87
|
+
writeCache(cache);
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
// Network error or bad response — fall back to whatever was last cached
|
|
91
|
+
// for THIS server. Floors from other servers must not leak across.
|
|
92
|
+
minSupported = cached?.minSupported;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (!minSupported)
|
|
96
|
+
return;
|
|
97
|
+
if (!isBelow(currentVersion, minSupported))
|
|
98
|
+
return;
|
|
99
|
+
console.error("\n" +
|
|
100
|
+
chalk.red(` This CLI version (${currentVersion}) is no longer supported.`) +
|
|
101
|
+
"\n" +
|
|
102
|
+
chalk.red(` Minimum supported version is ${minSupported}.`) +
|
|
103
|
+
"\n\n" +
|
|
104
|
+
chalk.dim(" Run ") +
|
|
105
|
+
chalk.cyan("npm install -g kubeagent@latest") +
|
|
106
|
+
chalk.dim(" to update.\n"));
|
|
107
|
+
process.exit(1);
|
|
108
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { mkdtempSync, rmSync, writeFileSync, mkdirSync, existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
// Mock chalk to return plain strings
|
|
6
|
+
vi.mock("chalk", () => {
|
|
7
|
+
const identity = (s) => s;
|
|
8
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
9
|
+
const handler = {
|
|
10
|
+
get: () => new Proxy(identity, handler),
|
|
11
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
12
|
+
apply: (_t, _this, args) => args[0],
|
|
13
|
+
};
|
|
14
|
+
return { default: new Proxy(identity, handler) };
|
|
15
|
+
});
|
|
16
|
+
let tmpHome;
|
|
17
|
+
let originalHome;
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
tmpHome = mkdtempSync(join(tmpdir(), "kubeagent-version-test-"));
|
|
20
|
+
originalHome = process.env.HOME;
|
|
21
|
+
process.env.HOME = tmpHome;
|
|
22
|
+
});
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
if (originalHome !== undefined)
|
|
25
|
+
process.env.HOME = originalHome;
|
|
26
|
+
else
|
|
27
|
+
delete process.env.HOME;
|
|
28
|
+
rmSync(tmpHome, { recursive: true, force: true });
|
|
29
|
+
vi.restoreAllMocks();
|
|
30
|
+
});
|
|
31
|
+
describe("isBelow", () => {
|
|
32
|
+
it("returns true when current is older", async () => {
|
|
33
|
+
const { isBelow } = await import("./version-check.js");
|
|
34
|
+
expect(isBelow("0.1.5", "0.2.0")).toBe(true);
|
|
35
|
+
expect(isBelow("0.1.5", "0.1.6")).toBe(true);
|
|
36
|
+
expect(isBelow("0.0.9", "1.0.0")).toBe(true);
|
|
37
|
+
});
|
|
38
|
+
it("returns false when current is equal or newer", async () => {
|
|
39
|
+
const { isBelow } = await import("./version-check.js");
|
|
40
|
+
expect(isBelow("0.2.0", "0.2.0")).toBe(false);
|
|
41
|
+
expect(isBelow("0.2.1", "0.2.0")).toBe(false);
|
|
42
|
+
expect(isBelow("1.0.0", "0.9.99")).toBe(false);
|
|
43
|
+
});
|
|
44
|
+
it("strips a leading v", async () => {
|
|
45
|
+
const { isBelow } = await import("./version-check.js");
|
|
46
|
+
expect(isBelow("v0.1.0", "0.2.0")).toBe(true);
|
|
47
|
+
expect(isBelow("0.1.0", "v0.2.0")).toBe(true);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
describe("enforceMinVersion", () => {
|
|
51
|
+
it("exits with code 1 when current version is below min", async () => {
|
|
52
|
+
globalThis.fetch = vi.fn().mockResolvedValue({
|
|
53
|
+
ok: true,
|
|
54
|
+
json: () => Promise.resolve({ minSupported: "0.5.0" }),
|
|
55
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
56
|
+
});
|
|
57
|
+
const errSpy = vi.spyOn(console, "error").mockImplementation(() => { });
|
|
58
|
+
const exitSpy = vi.spyOn(process, "exit").mockImplementation(((_code) => {
|
|
59
|
+
throw new Error("__exit__");
|
|
60
|
+
}));
|
|
61
|
+
const { enforceMinVersion } = await import("./version-check.js");
|
|
62
|
+
await expect(enforceMinVersion("0.1.0", "https://api.example.com")).rejects.toThrow("__exit__");
|
|
63
|
+
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
64
|
+
const output = errSpy.mock.calls.map((c) => c.join(" ")).join("\n");
|
|
65
|
+
expect(output).toContain("no longer supported");
|
|
66
|
+
expect(output).toContain("0.5.0");
|
|
67
|
+
});
|
|
68
|
+
it("does nothing when current version meets the floor", async () => {
|
|
69
|
+
globalThis.fetch = vi.fn().mockResolvedValue({
|
|
70
|
+
ok: true,
|
|
71
|
+
json: () => Promise.resolve({ minSupported: "0.1.0" }),
|
|
72
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
73
|
+
});
|
|
74
|
+
const errSpy = vi.spyOn(console, "error").mockImplementation(() => { });
|
|
75
|
+
const exitSpy = vi.spyOn(process, "exit").mockImplementation((() => { }));
|
|
76
|
+
const { enforceMinVersion } = await import("./version-check.js");
|
|
77
|
+
await enforceMinVersion("0.1.0", "https://api.example.com");
|
|
78
|
+
expect(exitSpy).not.toHaveBeenCalled();
|
|
79
|
+
expect(errSpy).not.toHaveBeenCalled();
|
|
80
|
+
});
|
|
81
|
+
it("falls back to stale cache on network error", async () => {
|
|
82
|
+
// Seed an older cached floor for THIS server URL that already proved
|
|
83
|
+
// this version stale.
|
|
84
|
+
const url = "https://api.example.com";
|
|
85
|
+
mkdirSync(join(tmpHome, ".kubeagent"), { recursive: true });
|
|
86
|
+
writeFileSync(join(tmpHome, ".kubeagent", "version-check.json"), JSON.stringify({
|
|
87
|
+
[url]: {
|
|
88
|
+
// checkedAt > 24h ago so we attempt a refetch
|
|
89
|
+
checkedAt: Date.now() - 48 * 60 * 60 * 1000,
|
|
90
|
+
minSupported: "0.5.0",
|
|
91
|
+
},
|
|
92
|
+
}), "utf-8");
|
|
93
|
+
globalThis.fetch = vi.fn().mockRejectedValue(new Error("offline"));
|
|
94
|
+
const errSpy = vi.spyOn(console, "error").mockImplementation(() => { });
|
|
95
|
+
const exitSpy = vi.spyOn(process, "exit").mockImplementation(((_code) => {
|
|
96
|
+
throw new Error("__exit__");
|
|
97
|
+
}));
|
|
98
|
+
const { enforceMinVersion } = await import("./version-check.js");
|
|
99
|
+
await expect(enforceMinVersion("0.1.0", url)).rejects.toThrow("__exit__");
|
|
100
|
+
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
101
|
+
expect(errSpy).toHaveBeenCalled();
|
|
102
|
+
});
|
|
103
|
+
it("returns silently with no cache and a network error", async () => {
|
|
104
|
+
globalThis.fetch = vi.fn().mockRejectedValue(new Error("offline"));
|
|
105
|
+
const errSpy = vi.spyOn(console, "error").mockImplementation(() => { });
|
|
106
|
+
const exitSpy = vi.spyOn(process, "exit").mockImplementation((() => { }));
|
|
107
|
+
const { enforceMinVersion } = await import("./version-check.js");
|
|
108
|
+
await enforceMinVersion("0.1.0", "https://api.example.com");
|
|
109
|
+
expect(exitSpy).not.toHaveBeenCalled();
|
|
110
|
+
expect(errSpy).not.toHaveBeenCalled();
|
|
111
|
+
});
|
|
112
|
+
it("uses cached value within 24h without refetching", async () => {
|
|
113
|
+
const url = "https://api.example.com";
|
|
114
|
+
mkdirSync(join(tmpHome, ".kubeagent"), { recursive: true });
|
|
115
|
+
writeFileSync(join(tmpHome, ".kubeagent", "version-check.json"), JSON.stringify({ [url]: { checkedAt: Date.now(), minSupported: "0.2.0" } }), "utf-8");
|
|
116
|
+
const fetchSpy = vi.fn();
|
|
117
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
118
|
+
globalThis.fetch = fetchSpy;
|
|
119
|
+
const exitSpy = vi.spyOn(process, "exit").mockImplementation(((_code) => {
|
|
120
|
+
throw new Error("__exit__");
|
|
121
|
+
}));
|
|
122
|
+
vi.spyOn(console, "error").mockImplementation(() => { });
|
|
123
|
+
const { enforceMinVersion } = await import("./version-check.js");
|
|
124
|
+
await expect(enforceMinVersion("0.1.0", url)).rejects.toThrow("__exit__");
|
|
125
|
+
expect(fetchSpy).not.toHaveBeenCalled();
|
|
126
|
+
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
127
|
+
});
|
|
128
|
+
it("writes cache after a successful fetch", async () => {
|
|
129
|
+
globalThis.fetch = vi.fn().mockResolvedValue({
|
|
130
|
+
ok: true,
|
|
131
|
+
json: () => Promise.resolve({ minSupported: "0.1.0" }),
|
|
132
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
133
|
+
});
|
|
134
|
+
vi.spyOn(process, "exit").mockImplementation((() => { }));
|
|
135
|
+
vi.spyOn(console, "error").mockImplementation(() => { });
|
|
136
|
+
const { enforceMinVersion } = await import("./version-check.js");
|
|
137
|
+
await enforceMinVersion("0.1.5", "https://api.example.com");
|
|
138
|
+
const cachePath = join(tmpHome, ".kubeagent", "version-check.json");
|
|
139
|
+
expect(existsSync(cachePath)).toBe(true);
|
|
140
|
+
const cache = JSON.parse(readFileSync(cachePath, "utf-8"));
|
|
141
|
+
expect(cache["https://api.example.com"].minSupported).toBe("0.1.0");
|
|
142
|
+
});
|
|
143
|
+
it("does not apply a floor cached for a different server URL", async () => {
|
|
144
|
+
const otherUrl = "https://other-server.example.com";
|
|
145
|
+
mkdirSync(join(tmpHome, ".kubeagent"), { recursive: true });
|
|
146
|
+
writeFileSync(join(tmpHome, ".kubeagent", "version-check.json"), JSON.stringify({
|
|
147
|
+
[otherUrl]: { checkedAt: Date.now(), minSupported: "99.0.0" },
|
|
148
|
+
}), "utf-8");
|
|
149
|
+
// Target server returns a permissive floor; other-server's strict floor
|
|
150
|
+
// must NOT be applied here.
|
|
151
|
+
globalThis.fetch = vi.fn().mockResolvedValue({
|
|
152
|
+
ok: true,
|
|
153
|
+
json: () => Promise.resolve({ minSupported: "0.1.0" }),
|
|
154
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
155
|
+
});
|
|
156
|
+
const exitSpy = vi.spyOn(process, "exit").mockImplementation((() => { }));
|
|
157
|
+
vi.spyOn(console, "error").mockImplementation(() => { });
|
|
158
|
+
const { enforceMinVersion } = await import("./version-check.js");
|
|
159
|
+
await enforceMinVersion("0.1.0", "https://api.example.com");
|
|
160
|
+
expect(exitSpy).not.toHaveBeenCalled();
|
|
161
|
+
});
|
|
162
|
+
it("ignores the legacy single-entry cache shape", async () => {
|
|
163
|
+
// Old format: {checkedAt, minSupported} at the root. Should be discarded
|
|
164
|
+
// since we don't know which server it came from.
|
|
165
|
+
mkdirSync(join(tmpHome, ".kubeagent"), { recursive: true });
|
|
166
|
+
writeFileSync(join(tmpHome, ".kubeagent", "version-check.json"), JSON.stringify({ checkedAt: Date.now(), minSupported: "99.0.0" }), "utf-8");
|
|
167
|
+
globalThis.fetch = vi.fn().mockResolvedValue({
|
|
168
|
+
ok: true,
|
|
169
|
+
json: () => Promise.resolve({ minSupported: "0.1.0" }),
|
|
170
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
171
|
+
});
|
|
172
|
+
const exitSpy = vi.spyOn(process, "exit").mockImplementation((() => { }));
|
|
173
|
+
vi.spyOn(console, "error").mockImplementation(() => { });
|
|
174
|
+
const { enforceMinVersion } = await import("./version-check.js");
|
|
175
|
+
await enforceMinVersion("0.1.0", "https://api.example.com");
|
|
176
|
+
// Legacy floor (99.0.0) ignored, fresh fetch (0.1.0) applied — no exit.
|
|
177
|
+
expect(exitSpy).not.toHaveBeenCalled();
|
|
178
|
+
});
|
|
179
|
+
});
|