@superblocksteam/sdk 2.0.82 → 2.0.83-next.1

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.
Files changed (46) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/cli-replacement/automatic-upgrades.d.ts +0 -6
  3. package/dist/cli-replacement/automatic-upgrades.d.ts.map +1 -1
  4. package/dist/cli-replacement/automatic-upgrades.js +5 -27
  5. package/dist/cli-replacement/automatic-upgrades.js.map +1 -1
  6. package/dist/cli-replacement/dev.d.mts.map +1 -1
  7. package/dist/cli-replacement/dev.mjs +260 -217
  8. package/dist/cli-replacement/dev.mjs.map +1 -1
  9. package/dist/cli-replacement/version-detection.d.ts +71 -0
  10. package/dist/cli-replacement/version-detection.d.ts.map +1 -0
  11. package/dist/cli-replacement/version-detection.js +186 -0
  12. package/dist/cli-replacement/version-detection.js.map +1 -0
  13. package/dist/cli-replacement/version-detection.test.d.ts +5 -0
  14. package/dist/cli-replacement/version-detection.test.d.ts.map +1 -0
  15. package/dist/cli-replacement/version-detection.test.js +257 -0
  16. package/dist/cli-replacement/version-detection.test.js.map +1 -0
  17. package/dist/dev-utils/dev-server.mjs +133 -115
  18. package/dist/dev-utils/dev-server.mjs.map +1 -1
  19. package/dist/telemetry/index.d.ts +4 -4
  20. package/dist/telemetry/index.d.ts.map +1 -1
  21. package/dist/telemetry/index.js +92 -64
  22. package/dist/telemetry/index.js.map +1 -1
  23. package/dist/telemetry/index.test.d.ts +2 -0
  24. package/dist/telemetry/index.test.d.ts.map +1 -0
  25. package/dist/telemetry/index.test.js +93 -0
  26. package/dist/telemetry/index.test.js.map +1 -0
  27. package/dist/telemetry/local-obs.d.ts +73 -0
  28. package/dist/telemetry/local-obs.d.ts.map +1 -0
  29. package/dist/telemetry/local-obs.js +107 -0
  30. package/dist/telemetry/local-obs.js.map +1 -0
  31. package/dist/telemetry/util.d.ts +1 -2
  32. package/dist/telemetry/util.d.ts.map +1 -1
  33. package/dist/telemetry/util.js +26 -4
  34. package/dist/telemetry/util.js.map +1 -1
  35. package/package.json +6 -5
  36. package/src/cli-replacement/automatic-upgrades.ts +10 -42
  37. package/src/cli-replacement/dev.mts +336 -281
  38. package/src/cli-replacement/version-detection.test.ts +336 -0
  39. package/src/cli-replacement/version-detection.ts +220 -0
  40. package/src/dev-utils/dev-server.mts +149 -127
  41. package/src/telemetry/index.test.ts +130 -0
  42. package/src/telemetry/index.ts +105 -83
  43. package/src/telemetry/local-obs.ts +138 -0
  44. package/src/telemetry/util.ts +27 -4
  45. package/tsconfig.tsbuildinfo +1 -1
  46. package/turbo.json +20 -2
@@ -0,0 +1,336 @@
1
+ /**
2
+ * Tests for CLI version detection with caching.
3
+ */
4
+
5
+ import * as child_process from "node:child_process";
6
+ import { describe, it, expect, beforeEach, vi, afterEach } from "vitest";
7
+ import {
8
+ getCurrentCliVersion,
9
+ clearCliVersionCache,
10
+ } from "./version-detection.js";
11
+
12
+ // Mock child_process.exec and execFile
13
+ vi.mock("node:child_process", () => ({
14
+ exec: vi.fn(),
15
+ execFile: vi.fn(),
16
+ }));
17
+
18
+ // Mock logger to suppress console output during tests
19
+ vi.mock("../telemetry/logging.js", () => ({
20
+ getLogger: () => ({
21
+ debug: vi.fn(),
22
+ warn: vi.fn(),
23
+ error: vi.fn(),
24
+ }),
25
+ }));
26
+
27
+ describe("version-detection", () => {
28
+ const originalEnv = process.env;
29
+
30
+ beforeEach(() => {
31
+ // Reset environment and cache before each test
32
+ process.env = { ...originalEnv };
33
+ clearCliVersionCache(); // Clear cache
34
+ vi.clearAllMocks();
35
+ });
36
+
37
+ afterEach(() => {
38
+ // Restore original environment
39
+ process.env = originalEnv;
40
+ });
41
+
42
+ describe("Docker/CSB environment", () => {
43
+ it("should read version from SUPERBLOCKS_CLI_VERSION env var", async () => {
44
+ process.env.SUPERBLOCKS_IS_CSB = "true";
45
+ process.env.SUPERBLOCKS_CLI_VERSION = "2.0.0-next.1";
46
+
47
+ const result = await getCurrentCliVersion();
48
+
49
+ expect(result).toEqual({
50
+ version: "2.0.0-next.1",
51
+ alias: undefined,
52
+ });
53
+ // Should not call subprocess
54
+ expect(child_process.exec).not.toHaveBeenCalled();
55
+ });
56
+
57
+ it("should detect ephemeral builds from SNAPSHOT in version", async () => {
58
+ process.env.SUPERBLOCKS_IS_CSB = "true";
59
+ process.env.SUPERBLOCKS_CLI_VERSION = "2.0.0-SNAPSHOT.ebd2b86d643331f5";
60
+
61
+ const result = await getCurrentCliVersion();
62
+
63
+ expect(result).toEqual({
64
+ version: "2.0.0-SNAPSHOT.ebd2b86d643331f5",
65
+ alias: "@superblocksteam/cli-ephemeral",
66
+ });
67
+ expect(child_process.exec).not.toHaveBeenCalled();
68
+ });
69
+
70
+ it("should fall back to subprocess if env var not set", async () => {
71
+ process.env.SUPERBLOCKS_IS_CSB = "true";
72
+ // No SUPERBLOCKS_CLI_VERSION set
73
+
74
+ // Mock subprocess calls
75
+ const execMock = child_process.exec as unknown as ReturnType<
76
+ typeof vi.fn
77
+ >;
78
+ const execFileMock = child_process.execFile as unknown as ReturnType<
79
+ typeof vi.fn
80
+ >;
81
+ execMock.mockImplementationOnce((_cmd, callback: any) => {
82
+ // which/where command
83
+ callback(null, { stdout: "/usr/local/bin/superblocks\n" });
84
+ });
85
+ execFileMock.mockImplementationOnce((_path, _args, callback: any) => {
86
+ // version command
87
+ callback(null, {
88
+ stdout: JSON.stringify({
89
+ cliVersion: "@superblocksteam/cli/2.0.0-next.1",
90
+ }),
91
+ });
92
+ });
93
+
94
+ const result = await getCurrentCliVersion();
95
+
96
+ expect(result).toEqual({
97
+ version: "2.0.0-next.1",
98
+ });
99
+ expect(child_process.exec).toHaveBeenCalledTimes(1);
100
+ expect(child_process.execFile).toHaveBeenCalledTimes(1);
101
+ });
102
+ });
103
+
104
+ describe("Local environment", () => {
105
+ beforeEach(() => {
106
+ process.env.SUPERBLOCKS_IS_CSB = "false";
107
+ });
108
+
109
+ it("should detect version via subprocess", async () => {
110
+ const execMock = child_process.exec as unknown as ReturnType<
111
+ typeof vi.fn
112
+ >;
113
+ const execFileMock = child_process.execFile as unknown as ReturnType<
114
+ typeof vi.fn
115
+ >;
116
+ execMock.mockImplementationOnce((_cmd, callback: any) => {
117
+ // which/where command
118
+ callback(null, { stdout: "/usr/local/bin/superblocks\n" });
119
+ });
120
+ execFileMock.mockImplementationOnce((_path, _args, callback: any) => {
121
+ // version command
122
+ callback(null, {
123
+ stdout: JSON.stringify({
124
+ cliVersion: "@superblocksteam/cli/2.0.0-next.1",
125
+ }),
126
+ });
127
+ });
128
+
129
+ const result = await getCurrentCliVersion();
130
+
131
+ expect(result).toEqual({
132
+ version: "2.0.0-next.1",
133
+ });
134
+ expect(child_process.exec).toHaveBeenCalledTimes(1);
135
+ expect(child_process.execFile).toHaveBeenCalledTimes(1);
136
+ });
137
+
138
+ it("should detect ephemeral builds with alias", async () => {
139
+ const execMock = child_process.exec as unknown as ReturnType<
140
+ typeof vi.fn
141
+ >;
142
+ const execFileMock = child_process.execFile as unknown as ReturnType<
143
+ typeof vi.fn
144
+ >;
145
+ execMock.mockImplementationOnce((_cmd, callback: any) => {
146
+ callback(null, { stdout: "/usr/local/bin/superblocks\n" });
147
+ });
148
+ execFileMock.mockImplementationOnce((_path, _args, callback: any) => {
149
+ callback(null, {
150
+ stdout: JSON.stringify({
151
+ cliVersion:
152
+ "@superblocksteam/cli-ephemeral/2.0.0-SNAPSHOT.ebd2b86d643331f5",
153
+ }),
154
+ });
155
+ });
156
+
157
+ const result = await getCurrentCliVersion();
158
+
159
+ expect(result).toEqual({
160
+ version: "2.0.0-SNAPSHOT.ebd2b86d643331f5",
161
+ alias: "@superblocksteam/cli-ephemeral",
162
+ });
163
+ });
164
+
165
+ it("should use Windows 'where' command on win32", async () => {
166
+ const originalPlatform = process.platform;
167
+ Object.defineProperty(process, "platform", {
168
+ value: "win32",
169
+ configurable: true,
170
+ });
171
+
172
+ try {
173
+ const execMock = child_process.exec as unknown as ReturnType<
174
+ typeof vi.fn
175
+ >;
176
+ const execFileMock = child_process.execFile as unknown as ReturnType<
177
+ typeof vi.fn
178
+ >;
179
+ execMock.mockImplementationOnce((cmd, callback: any) => {
180
+ expect(cmd).toContain("where superblocks");
181
+ callback(null, { stdout: "C:\\Program Files\\superblocks.exe\n" });
182
+ });
183
+ execFileMock.mockImplementationOnce((_path, _args, callback: any) => {
184
+ callback(null, {
185
+ stdout: JSON.stringify({
186
+ cliVersion: "@superblocksteam/cli/2.0.0-next.1",
187
+ }),
188
+ });
189
+ });
190
+
191
+ await getCurrentCliVersion();
192
+ } finally {
193
+ // Restore platform
194
+ Object.defineProperty(process, "platform", {
195
+ value: originalPlatform,
196
+ configurable: true,
197
+ });
198
+ }
199
+ });
200
+
201
+ it("should return undefined if CLI not found", async () => {
202
+ const execMock = child_process.exec as unknown as ReturnType<
203
+ typeof vi.fn
204
+ >;
205
+ execMock.mockImplementationOnce((_cmd, callback: any) => {
206
+ callback(null, { stdout: "" }); // Empty output = not found
207
+ });
208
+
209
+ const result = await getCurrentCliVersion();
210
+
211
+ expect(result).toBeUndefined();
212
+ // Should only call which/where, not version command
213
+ expect(child_process.exec).toHaveBeenCalledTimes(1);
214
+ });
215
+
216
+ it("should return undefined on error", async () => {
217
+ const execMock = child_process.exec as unknown as ReturnType<
218
+ typeof vi.fn
219
+ >;
220
+ execMock.mockImplementationOnce((_cmd, callback: any) => {
221
+ callback(new Error("Command not found"));
222
+ });
223
+
224
+ const result = await getCurrentCliVersion();
225
+
226
+ expect(result).toBeUndefined();
227
+ });
228
+ });
229
+
230
+ describe("Caching behavior", () => {
231
+ beforeEach(() => {
232
+ process.env.SUPERBLOCKS_IS_CSB = "false";
233
+ });
234
+
235
+ it("should cache successful subprocess result", async () => {
236
+ const execMock = child_process.exec as unknown as ReturnType<
237
+ typeof vi.fn
238
+ >;
239
+ const execFileMock = child_process.execFile as unknown as ReturnType<
240
+ typeof vi.fn
241
+ >;
242
+ execMock.mockImplementationOnce((_cmd, callback: any) => {
243
+ callback(null, { stdout: "/usr/local/bin/superblocks\n" });
244
+ });
245
+ execFileMock.mockImplementationOnce((_path, _args, callback: any) => {
246
+ callback(null, {
247
+ stdout: JSON.stringify({
248
+ cliVersion: "@superblocksteam/cli/2.0.0-next.1",
249
+ }),
250
+ });
251
+ });
252
+
253
+ // First call - should execute subprocess
254
+ const result1 = await getCurrentCliVersion();
255
+ expect(result1).toEqual({ version: "2.0.0-next.1" });
256
+ expect(child_process.exec).toHaveBeenCalledTimes(1);
257
+ expect(child_process.execFile).toHaveBeenCalledTimes(1);
258
+
259
+ // Second call - should use cache
260
+ const result2 = await getCurrentCliVersion();
261
+ expect(result2).toEqual({ version: "2.0.0-next.1" });
262
+ expect(child_process.exec).toHaveBeenCalledTimes(1); // Still 1, not 2
263
+ expect(child_process.execFile).toHaveBeenCalledTimes(1); // Still 1, not 2
264
+ });
265
+
266
+ it("should cache Docker env var result", async () => {
267
+ process.env.SUPERBLOCKS_IS_CSB = "true";
268
+ process.env.SUPERBLOCKS_CLI_VERSION = "2.0.0-next.1";
269
+
270
+ // First call
271
+ const result1 = await getCurrentCliVersion();
272
+ expect(result1).toEqual({ version: "2.0.0-next.1", alias: undefined });
273
+
274
+ // Second call - should use cache
275
+ const result2 = await getCurrentCliVersion();
276
+ expect(result2).toEqual({ version: "2.0.0-next.1", alias: undefined });
277
+
278
+ expect(child_process.exec).not.toHaveBeenCalled();
279
+ });
280
+
281
+ it("should NOT cache failures (allows retry on transient errors)", async () => {
282
+ const execMock = child_process.exec as unknown as ReturnType<
283
+ typeof vi.fn
284
+ >;
285
+ execMock
286
+ .mockImplementationOnce((_cmd, callback: any) => {
287
+ callback(new Error("Not found"));
288
+ })
289
+ .mockImplementationOnce((_cmd, callback: any) => {
290
+ callback(new Error("Still not found"));
291
+ });
292
+
293
+ // First call - should attempt detection and fail
294
+ const result1 = await getCurrentCliVersion();
295
+ expect(result1).toBeUndefined();
296
+ expect(child_process.exec).toHaveBeenCalledTimes(1);
297
+
298
+ // Second call - should retry (not cached) since failures aren't cached
299
+ const result2 = await getCurrentCliVersion();
300
+ expect(result2).toBeUndefined();
301
+ expect(child_process.exec).toHaveBeenCalledTimes(2); // Retried!
302
+ });
303
+
304
+ it("should re-detect after cache is cleared", async () => {
305
+ const execMock = child_process.exec as unknown as ReturnType<
306
+ typeof vi.fn
307
+ >;
308
+ const execFileMock = child_process.execFile as unknown as ReturnType<
309
+ typeof vi.fn
310
+ >;
311
+ execMock.mockImplementation((_cmd, callback: any) => {
312
+ callback(null, { stdout: "/usr/local/bin/superblocks\n" });
313
+ });
314
+ execFileMock.mockImplementation((_path, _args, callback: any) => {
315
+ callback(null, {
316
+ stdout: JSON.stringify({
317
+ cliVersion: "@superblocksteam/cli/2.0.0-next.1",
318
+ }),
319
+ });
320
+ });
321
+
322
+ // First detection
323
+ await getCurrentCliVersion();
324
+ expect(child_process.exec).toHaveBeenCalledTimes(1);
325
+ expect(child_process.execFile).toHaveBeenCalledTimes(1);
326
+
327
+ // Clear cache
328
+ clearCliVersionCache();
329
+
330
+ // Should re-detect
331
+ await getCurrentCliVersion();
332
+ expect(child_process.exec).toHaveBeenCalledTimes(2); // 1 + 1
333
+ expect(child_process.execFile).toHaveBeenCalledTimes(2); // 1 + 1
334
+ });
335
+ });
336
+ });
@@ -0,0 +1,220 @@
1
+ /**
2
+ * CLI version detection with environment-aware caching.
3
+ *
4
+ * This module provides cached CLI version detection optimized for different
5
+ * environments:
6
+ * - Docker/CSB: Reads from SUPERBLOCKS_CLI_VERSION env var (instant, ~0ms)
7
+ * - Local: Caches subprocess result (6s first call, 0ms subsequent calls)
8
+ *
9
+ * The caching strategy eliminates duplicate subprocess executions that previously
10
+ * caused ~12s delays during dev server startup.
11
+ */
12
+
13
+ import * as child_process from "node:child_process";
14
+ import { promisify } from "node:util";
15
+ import { isNativeError } from "node:util/types";
16
+ import { getLogger } from "../telemetry/logging.js";
17
+
18
+ const exec = promisify(child_process.exec);
19
+ const execFile = promisify(child_process.execFile);
20
+ const logger = getLogger();
21
+
22
+ /**
23
+ * Package version information returned by version detection.
24
+ */
25
+ export interface PackageVersionInfo {
26
+ /** Semantic version string (e.g., "2.0.0-next.1") */
27
+ version: string;
28
+ /** Optional package alias for ephemeral builds (e.g., "@superblocksteam/cli-ephemeral") */
29
+ alias?: string;
30
+ }
31
+
32
+ /**
33
+ * Module-level cache for CLI version.
34
+ * - undefined: Not yet fetched
35
+ * - null: Fetch attempted but failed
36
+ * - PackageVersionInfo: Successfully fetched
37
+ */
38
+ let cachedCliVersion: PackageVersionInfo | undefined | null = undefined;
39
+
40
+ /**
41
+ * Detects if running in Docker/CSB environment.
42
+ *
43
+ * @returns true if SUPERBLOCKS_IS_CSB environment variable is set to "true"
44
+ */
45
+ function isDockerEnvironment(): boolean {
46
+ return process.env.SUPERBLOCKS_IS_CSB === "true";
47
+ }
48
+
49
+ /**
50
+ * Reads CLI version from SUPERBLOCKS_CLI_VERSION environment variable.
51
+ *
52
+ * This is the fast path for Docker/CSB environments where the version
53
+ * is injected at build time.
54
+ *
55
+ * @returns PackageVersionInfo if env var is set, undefined otherwise
56
+ */
57
+ function getVersionFromEnvironment(): PackageVersionInfo | undefined {
58
+ const envVersion = process.env.SUPERBLOCKS_CLI_VERSION;
59
+ if (!envVersion) return undefined;
60
+
61
+ // Detect ephemeral builds by SNAPSHOT in version string
62
+ const isEphemeral = envVersion.includes("SNAPSHOT");
63
+
64
+ return {
65
+ version: envVersion,
66
+ alias: isEphemeral ? "@superblocksteam/cli-ephemeral" : undefined,
67
+ };
68
+ }
69
+
70
+ /**
71
+ * Detects CLI version by spawning subprocesses.
72
+ *
73
+ * This is the slow path (~6s) used in local development environments.
74
+ * It executes two commands:
75
+ * 1. `which superblocks` (or `where` on Windows) - Find CLI binary in PATH
76
+ * 2. `superblocks version --json` - Execute CLI to get version
77
+ *
78
+ * @returns PackageVersionInfo if CLI is found and version can be read, undefined otherwise
79
+ */
80
+ async function getVersionFromSubprocess(): Promise<
81
+ PackageVersionInfo | undefined
82
+ > {
83
+ try {
84
+ const command = process.platform === "win32" ? "where" : "which";
85
+ const { stdout } = await exec(`${command} superblocks`);
86
+ const superblocksPath = stdout.trim().split("\n")[0]; // Take first line only
87
+
88
+ if (!superblocksPath) return undefined;
89
+
90
+ // Use execFile to properly handle paths with spaces (e.g., "C:\Program Files\...")
91
+ const { stdout: versionOutput } = await execFile(superblocksPath, [
92
+ "version",
93
+ "--json",
94
+ ]);
95
+ const json = JSON.parse(versionOutput) as Record<string, string>;
96
+
97
+ // Extract version from string like:
98
+ // - "@superblocksteam/cli/2.0.0-next.1"
99
+ // - "@superblocksteam/cli-ephemeral/2.0.0-SNAPSHOT.ebd2b86d643331f5"
100
+ const version = json.cliVersion?.replace(
101
+ /@superblocksteam\/cli(-ephemeral)?\//,
102
+ "",
103
+ );
104
+
105
+ // Detect alias (e.g., cli-ephemeral) for ephemeral builds
106
+ const aliasMatch = json.cliVersion?.match(/@superblocksteam\/([^\/]+)/);
107
+ if (aliasMatch && aliasMatch[1] !== "cli") {
108
+ return {
109
+ version: version,
110
+ alias: `@superblocksteam/${aliasMatch[1]}`,
111
+ };
112
+ }
113
+
114
+ return { version };
115
+ } catch (error) {
116
+ if (isNativeError(error)) {
117
+ // Expected: CLI not installed (which/where returns empty or command not found)
118
+ if (
119
+ error.message.includes("not found") ||
120
+ error.message.includes("command not found")
121
+ ) {
122
+ logger.debug("CLI binary not found in PATH");
123
+ return undefined;
124
+ }
125
+ // Unexpected: permission errors, malformed output, etc.
126
+ logger.warn(
127
+ `Failed to detect CLI version: ${error.message}. Automatic upgrades will be disabled.`,
128
+ );
129
+ }
130
+ return undefined;
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Gets the current CLI version with caching.
136
+ *
137
+ * Detection strategy (in order):
138
+ * 1. Return cached value if available (only successful detections are cached)
139
+ * 2. Docker/CSB: Read from SUPERBLOCKS_CLI_VERSION env var (~0ms)
140
+ * 3. Local: Execute subprocess and cache result (~6s first call, ~0ms subsequent)
141
+ *
142
+ * The cache is shared across all callers, eliminating duplicate subprocess
143
+ * executions. This reduces dev server startup from ~12s to ~6s (local) or
144
+ * ~0s (Docker).
145
+ *
146
+ * Note: Failures are NOT cached, allowing recovery from transient issues
147
+ * (network glitches, filesystem races) without requiring a dev server restart.
148
+ *
149
+ * @returns PackageVersionInfo if CLI version can be detected, undefined otherwise
150
+ *
151
+ * @example
152
+ * // First call in Docker - instant
153
+ * const version = await getCurrentCliVersion();
154
+ * // Returns: { version: "2.0.0-next.1" }
155
+ *
156
+ * @example
157
+ * // First call locally - slow (~6s)
158
+ * const version1 = await getCurrentCliVersion();
159
+ * // Second call - instant (cached)
160
+ * const version2 = await getCurrentCliVersion();
161
+ */
162
+ export async function getCurrentCliVersion(): Promise<
163
+ PackageVersionInfo | undefined
164
+ > {
165
+ // Return cached value if available (only successful detections are cached)
166
+ if (cachedCliVersion !== undefined && cachedCliVersion !== null) {
167
+ return cachedCliVersion;
168
+ }
169
+
170
+ // Docker/CSB: Try environment variable first (fast path)
171
+ if (isDockerEnvironment()) {
172
+ cachedCliVersion = getVersionFromEnvironment();
173
+ if (cachedCliVersion) {
174
+ logger.debug(`CLI version from environment: ${cachedCliVersion.version}`);
175
+ return cachedCliVersion;
176
+ }
177
+ }
178
+
179
+ // Local: Use subprocess detection (slow path, but cached on success)
180
+ logger.debug("Detecting CLI version via subprocess...");
181
+ const detectedVersion = await getVersionFromSubprocess();
182
+
183
+ if (detectedVersion) {
184
+ logger.debug(`CLI version from subprocess: ${detectedVersion.version}`);
185
+ cachedCliVersion = detectedVersion; // Cache successful detection
186
+ } else {
187
+ logger.debug("Could not detect CLI version");
188
+ // Don't cache failures - allow retry on next call in case of transient issues
189
+ }
190
+
191
+ return detectedVersion;
192
+ }
193
+
194
+ /**
195
+ * Clears the cached CLI version, optionally setting it to a known value.
196
+ *
197
+ * This is used after CLI upgrades to update the cache with the newly installed
198
+ * version, avoiding the need for subprocess detection. If a version is provided,
199
+ * the cache is set to that version; otherwise, the cache is cleared and the next
200
+ * call to getCurrentCliVersion() will re-detect.
201
+ *
202
+ * @param newVersion - Optional version info to set the cache to (e.g., after upgrade)
203
+ *
204
+ * @example
205
+ * // After upgrading CLI to a known version
206
+ * clearCliVersionCache({ version: "2.0.0-next.2" });
207
+ * const version = await getCurrentCliVersion(); // Returns cached version instantly
208
+ *
209
+ * @example
210
+ * // Clear cache completely (for tests)
211
+ * clearCliVersionCache();
212
+ * const version = await getCurrentCliVersion(); // Will re-detect
213
+ */
214
+ export function clearCliVersionCache(newVersion?: PackageVersionInfo): void {
215
+ if (newVersion) {
216
+ cachedCliVersion = newVersion;
217
+ } else {
218
+ cachedCliVersion = undefined;
219
+ }
220
+ }