diff --git a/packages/types/src/providers/roo.ts b/packages/types/src/providers/roo.ts index af88d145ee5..1306d244a5b 100644 --- a/packages/types/src/providers/roo.ts +++ b/packages/types/src/providers/roo.ts @@ -41,7 +41,14 @@ export const RooModelSchema = z.object({ default_temperature: z.number().optional(), // Dynamic settings that map directly to ModelInfo properties // Allows the API to configure model-specific defaults like includedTools, excludedTools, reasoningEffort, etc. + // These are always direct values (e.g., includedTools: ['search_replace']) for backward compatibility with old clients. settings: z.record(z.string(), z.unknown()).optional(), + // Versioned settings keyed by version number (e.g., '3.36.4'). + // Each version key maps to a settings object that is used when plugin version >= that version. + // New clients find the highest version key <= current version and use those settings. + // Old clients ignore this field and use plain values from `settings`. + // Example: { '3.36.4': { includedTools: ['search_replace'] }, '3.35.0': { ... } } + versionedSettings: z.record(z.string(), z.record(z.string(), z.unknown())).optional(), }) export const RooModelsResponseSchema = z.object({ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ff3e592377e..8542674e140 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -795,6 +795,9 @@ importers: say: specifier: ^0.16.0 version: 0.16.0 + semver-compare: + specifier: ^1.0.0 + version: 1.0.0 serialize-error: specifier: ^12.0.0 version: 12.0.0 @@ -898,6 +901,9 @@ importers: '@types/ps-tree': specifier: ^1.1.6 version: 1.1.6 + '@types/semver-compare': + specifier: ^1.0.3 + version: 1.0.3 '@types/shell-quote': specifier: ^1.7.5 version: 1.7.5 @@ -4157,6 +4163,9 @@ packages: '@types/retry@0.12.5': resolution: {integrity: sha512-3xSjTp3v03X/lSQLkczaN9UIEwJMoMCA1+Nb5HfbJEQWogdeQIyVtTvxPXDQjZ5zws8rFQfVfRdz03ARihPJgw==} + '@types/semver-compare@1.0.3': + resolution: {integrity: sha512-mVZkB2QjXmZhh+MrtwMlJ8BqUnmbiSkpd88uOWskfwB8yitBT0tBRAKt+41VRgZD9zr9Sc+Xs02qGgvzd1Rq/Q==} + '@types/shell-quote@1.7.5': resolution: {integrity: sha512-+UE8GAGRPbJVQDdxi16dgadcBfQ+KG2vgZhV1+3A1XmHbmwcdwhCUwIdy+d3pAGrbvgRoVSjeI9vOWyq376Yzw==} @@ -8906,6 +8915,9 @@ packages: seed-random@2.2.0: resolution: {integrity: sha512-34EQV6AAHQGhoc0tn/96a9Fsi6v2xdqe/dMUwljGRaFOzR3EgRmECvD0O8vi8X+/uQ50LGHfkNu/Eue5TPKZkQ==} + semver-compare@1.0.0: + resolution: {integrity: sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==} + semver@5.7.2: resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} hasBin: true @@ -13895,6 +13907,8 @@ snapshots: '@types/retry@0.12.5': {} + '@types/semver-compare@1.0.3': {} + '@types/shell-quote@1.7.5': {} '@types/stack-utils@2.0.3': {} @@ -14114,7 +14128,7 @@ snapshots: sirv: 3.0.1 tinyglobby: 0.2.14 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.2.1)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0) + vitest: 3.2.4(@types/debug@4.1.12)(@types/node@20.17.50)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(tsx@4.19.4)(yaml@2.8.0) '@vitest/utils@3.2.4': dependencies: @@ -19476,6 +19490,8 @@ snapshots: seed-random@2.2.0: {} + semver-compare@1.0.0: {} + semver@5.7.2: {} semver@6.3.1: {} diff --git a/src/api/providers/fetchers/__tests__/roo.spec.ts b/src/api/providers/fetchers/__tests__/roo.spec.ts index 7cc479f9ba4..0ee7d8e3ed8 100644 --- a/src/api/providers/fetchers/__tests__/roo.spec.ts +++ b/src/api/providers/fetchers/__tests__/roo.spec.ts @@ -801,4 +801,179 @@ describe("getRooModels", () => { expect(model.anotherSetting).toBe(42) expect(model.nestedConfig).toEqual({ key: "value" }) }) + + it("should apply versioned settings when version matches", async () => { + const mockResponse = { + object: "list", + data: [ + { + id: "test/versioned-model", + object: "model", + created: 1234567890, + owned_by: "test", + name: "Model with Versioned Settings", + description: "Model with versioned settings", + context_window: 128000, + max_tokens: 8192, + type: "language", + tags: ["tool-use"], + pricing: { + input: "0.0001", + output: "0.0002", + }, + // Plain settings for backward compatibility with old clients + settings: { + includedTools: ["apply_patch"], + excludedTools: ["write_to_file"], + }, + // Versioned settings keyed by version number (low version - always met) + versionedSettings: { + "1.0.0": { + includedTools: ["apply_patch", "search_replace"], + excludedTools: ["apply_diff", "write_to_file"], + }, + }, + }, + ], + } + + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => mockResponse, + }) + + const models = await getRooModels(baseUrl, apiKey) + + // Versioned settings should be used instead of plain settings + expect(models["test/versioned-model"].includedTools).toEqual(["apply_patch", "search_replace"]) + expect(models["test/versioned-model"].excludedTools).toEqual(["apply_diff", "write_to_file"]) + }) + + it("should use plain settings when no versioned settings version matches", async () => { + const mockResponse = { + object: "list", + data: [ + { + id: "test/old-version-model", + object: "model", + created: 1234567890, + owned_by: "test", + name: "Model for Old Version", + description: "Model with versioned settings for newer version", + context_window: 128000, + max_tokens: 8192, + type: "language", + tags: ["tool-use"], + pricing: { + input: "0.0001", + output: "0.0002", + }, + settings: { + includedTools: ["apply_patch"], + }, + // Versioned settings keyed by very high version - never met + versionedSettings: { + "99.0.0": { + includedTools: ["apply_patch", "search_replace"], + }, + }, + }, + ], + } + + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => mockResponse, + }) + + const models = await getRooModels(baseUrl, apiKey) + + // Should use plain settings since no versioned settings match current version + expect(models["test/old-version-model"].includedTools).toEqual(["apply_patch"]) + }) + + it("should handle model with only versionedSettings and no plain settings", async () => { + const mockResponse = { + object: "list", + data: [ + { + id: "test/versioned-only-model", + object: "model", + created: 1234567890, + owned_by: "test", + name: "Model with Only Versioned Settings", + description: "Model with only versioned settings", + context_window: 128000, + max_tokens: 8192, + type: "language", + tags: [], + pricing: { + input: "0.0001", + output: "0.0002", + }, + // No plain settings, only versionedSettings keyed by version + versionedSettings: { + "1.0.0": { + customFeature: true, + }, + }, + }, + ], + } + + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => mockResponse, + }) + + const models = await getRooModels(baseUrl, apiKey) + const model = models["test/versioned-only-model"] as Record + + expect(model.customFeature).toBe(true) + }) + + it("should select highest matching version from versionedSettings", async () => { + const mockResponse = { + object: "list", + data: [ + { + id: "test/multi-version-model", + object: "model", + created: 1234567890, + owned_by: "test", + name: "Model with Multiple Versions", + description: "Model with multiple version settings", + context_window: 128000, + max_tokens: 8192, + type: "language", + tags: [], + pricing: { + input: "0.0001", + output: "0.0002", + }, + settings: { + feature: "default", + }, + // Multiple version keys - should use highest one <= current version + versionedSettings: { + "99.0.0": { feature: "future" }, + "3.0.0": { feature: "current" }, + "2.0.0": { feature: "old" }, + "1.0.0": { feature: "very_old" }, + }, + }, + ], + } + + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => mockResponse, + }) + + const models = await getRooModels(baseUrl, apiKey) + const model = models["test/multi-version-model"] as Record + + // Should use 3.0.0 version settings (highest that's <= current plugin version) + expect(model.feature).toBe("current") + }) }) diff --git a/src/api/providers/fetchers/__tests__/versionedSettings.spec.ts b/src/api/providers/fetchers/__tests__/versionedSettings.spec.ts new file mode 100644 index 00000000000..029f92b3017 --- /dev/null +++ b/src/api/providers/fetchers/__tests__/versionedSettings.spec.ts @@ -0,0 +1,263 @@ +import { + compareSemver, + meetsMinimumVersion, + findHighestMatchingVersion, + resolveVersionedSettings, + type VersionedSettings, +} from "../versionedSettings" + +describe("versionedSettings", () => { + describe("compareSemver", () => { + it("should return 0 for equal versions", () => { + expect(compareSemver("1.0.0", "1.0.0")).toBe(0) + expect(compareSemver("3.36.4", "3.36.4")).toBe(0) + expect(compareSemver("0.0.1", "0.0.1")).toBe(0) + }) + + it("should return positive when first version is greater", () => { + expect(compareSemver("2.0.0", "1.0.0")).toBeGreaterThan(0) + expect(compareSemver("1.1.0", "1.0.0")).toBeGreaterThan(0) + expect(compareSemver("1.0.1", "1.0.0")).toBeGreaterThan(0) + expect(compareSemver("3.36.5", "3.36.4")).toBeGreaterThan(0) + expect(compareSemver("3.37.0", "3.36.4")).toBeGreaterThan(0) + expect(compareSemver("4.0.0", "3.36.4")).toBeGreaterThan(0) + }) + + it("should return negative when first version is smaller", () => { + expect(compareSemver("1.0.0", "2.0.0")).toBeLessThan(0) + expect(compareSemver("1.0.0", "1.1.0")).toBeLessThan(0) + expect(compareSemver("1.0.0", "1.0.1")).toBeLessThan(0) + expect(compareSemver("3.36.3", "3.36.4")).toBeLessThan(0) + expect(compareSemver("3.35.0", "3.36.4")).toBeLessThan(0) + expect(compareSemver("2.0.0", "3.36.4")).toBeLessThan(0) + }) + + it("should handle pre-release versions by ignoring pre-release suffix", () => { + expect(compareSemver("3.36.4-beta.1", "3.36.4")).toBe(0) + expect(compareSemver("3.36.4-rc.2", "3.36.4")).toBe(0) + expect(compareSemver("3.36.5-alpha", "3.36.4")).toBeGreaterThan(0) + expect(compareSemver("3.36.3-beta", "3.36.4")).toBeLessThan(0) + }) + + it("should handle edge cases", () => { + expect(compareSemver("0.0.0", "0.0.0")).toBe(0) + expect(compareSemver("10.20.30", "10.20.30")).toBe(0) + expect(compareSemver("10.0.0", "9.99.99")).toBeGreaterThan(0) + }) + }) + + describe("meetsMinimumVersion", () => { + it("should return true when current version equals minimum", () => { + expect(meetsMinimumVersion("3.36.4", "3.36.4")).toBe(true) + }) + + it("should return true when current version exceeds minimum", () => { + expect(meetsMinimumVersion("3.36.4", "3.36.5")).toBe(true) + expect(meetsMinimumVersion("3.36.4", "3.37.0")).toBe(true) + expect(meetsMinimumVersion("3.36.4", "4.0.0")).toBe(true) + }) + + it("should return false when current version is below minimum", () => { + expect(meetsMinimumVersion("3.36.4", "3.36.3")).toBe(false) + expect(meetsMinimumVersion("3.36.4", "3.35.0")).toBe(false) + expect(meetsMinimumVersion("3.36.4", "2.0.0")).toBe(false) + }) + }) + + describe("findHighestMatchingVersion", () => { + it("should return undefined when no versions match", () => { + const versionedSettings: VersionedSettings = { + "4.0.0": { includedTools: ["apply_diff"] }, + "5.0.0": { includedTools: ["apply_diff", "search_replace"] }, + } + + const result = findHighestMatchingVersion(versionedSettings, "3.36.4") + expect(result).toBeUndefined() + }) + + it("should return the exact version when it matches", () => { + const versionedSettings: VersionedSettings = { + "3.36.4": { includedTools: ["apply_diff"] }, + "3.35.0": { includedTools: ["search_replace"] }, + } + + const result = findHighestMatchingVersion(versionedSettings, "3.36.4") + expect(result).toBe("3.36.4") + }) + + it("should return the highest version that is <= current version", () => { + const versionedSettings: VersionedSettings = { + "3.37.0": { includedTools: ["future_tool"] }, + "3.36.4": { includedTools: ["apply_diff"] }, + "3.35.0": { includedTools: ["search_replace"] }, + "3.34.0": { includedTools: ["basic_tool"] }, + } + + // Current version is 3.36.5, should match 3.36.4 (highest <= 3.36.5) + const result = findHighestMatchingVersion(versionedSettings, "3.36.5") + expect(result).toBe("3.36.4") + }) + + it("should handle single version", () => { + const versionedSettings: VersionedSettings = { + "3.35.0": { includedTools: ["search_replace"] }, + } + + expect(findHighestMatchingVersion(versionedSettings, "3.36.4")).toBe("3.35.0") + expect(findHighestMatchingVersion(versionedSettings, "3.34.0")).toBeUndefined() + }) + + it("should handle empty versionedSettings", () => { + const versionedSettings: VersionedSettings = {} + + const result = findHighestMatchingVersion(versionedSettings, "3.36.4") + expect(result).toBeUndefined() + }) + }) + + describe("resolveVersionedSettings", () => { + const currentVersion = "3.36.4" + + it("should return settings for exact version match", () => { + const versionedSettings: VersionedSettings = { + "3.36.4": { + includedTools: ["search_replace"], + excludedTools: ["apply_diff"], + }, + } + + const resolved = resolveVersionedSettings(versionedSettings, currentVersion) + + expect(resolved).toEqual({ + includedTools: ["search_replace"], + excludedTools: ["apply_diff"], + }) + }) + + it("should return settings for highest matching version", () => { + const versionedSettings: VersionedSettings = { + "4.0.0": { + includedTools: ["future_tool"], + }, + "3.36.0": { + includedTools: ["search_replace"], + excludedTools: ["apply_diff"], + }, + "3.35.0": { + includedTools: ["old_tool"], + }, + } + + const resolved = resolveVersionedSettings(versionedSettings, currentVersion) + + expect(resolved).toEqual({ + includedTools: ["search_replace"], + excludedTools: ["apply_diff"], + }) + }) + + it("should return empty object when no versions match", () => { + const versionedSettings: VersionedSettings = { + "4.0.0": { + includedTools: ["future_tool"], + }, + "3.37.0": { + includedTools: ["newer_tool"], + }, + } + + const resolved = resolveVersionedSettings(versionedSettings, currentVersion) + + expect(resolved).toEqual({}) + }) + + it("should handle empty versionedSettings", () => { + const resolved = resolveVersionedSettings({}, currentVersion) + expect(resolved).toEqual({}) + }) + + it("should handle versioned boolean values", () => { + const versionedSettings: VersionedSettings = { + "3.36.0": { + supportsNativeTools: true, + }, + } + + const resolved = resolveVersionedSettings(versionedSettings, currentVersion) + + expect(resolved).toEqual({ + supportsNativeTools: true, + }) + }) + + it("should handle versioned null values", () => { + const versionedSettings: VersionedSettings = { + "3.36.0": { + defaultTemperature: null, + }, + } + + const resolved = resolveVersionedSettings(versionedSettings, currentVersion) + + expect(resolved).toEqual({ + defaultTemperature: null, + }) + }) + + it("should handle versioned nested objects", () => { + const versionedSettings: VersionedSettings = { + "3.36.0": { + complexSetting: { nested: { deeply: true } }, + }, + } + + const resolved = resolveVersionedSettings(versionedSettings, currentVersion) + + expect(resolved).toEqual({ + complexSetting: { nested: { deeply: true } }, + }) + }) + + it("should use all settings from the matching version", () => { + const versionedSettings: VersionedSettings = { + "3.36.4": { + includedTools: ["search_replace", "apply_diff"], + excludedTools: ["write_to_file"], + supportsReasoningEffort: true, + description: "Updated model", + }, + "3.35.0": { + includedTools: ["search_replace"], + description: "Old model", + }, + } + + const resolved = resolveVersionedSettings(versionedSettings, "3.36.4") + + expect(resolved).toEqual({ + includedTools: ["search_replace", "apply_diff"], + excludedTools: ["write_to_file"], + supportsReasoningEffort: true, + description: "Updated model", + }) + }) + + it("should handle multiple versions and select correct one", () => { + const versionedSettings: VersionedSettings = { + "3.38.0": { feature: "very_new" }, + "3.37.0": { feature: "new" }, + "3.36.0": { feature: "current" }, + "3.35.0": { feature: "old" }, + "3.34.0": { feature: "very_old" }, + } + + // Test different current versions + expect(resolveVersionedSettings(versionedSettings, "3.40.0")).toEqual({ feature: "very_new" }) + expect(resolveVersionedSettings(versionedSettings, "3.37.5")).toEqual({ feature: "new" }) + expect(resolveVersionedSettings(versionedSettings, "3.36.5")).toEqual({ feature: "current" }) + expect(resolveVersionedSettings(versionedSettings, "3.35.5")).toEqual({ feature: "old" }) + expect(resolveVersionedSettings(versionedSettings, "3.34.5")).toEqual({ feature: "very_old" }) + expect(resolveVersionedSettings(versionedSettings, "3.33.0")).toEqual({}) + }) + }) +}) diff --git a/src/api/providers/fetchers/roo.ts b/src/api/providers/fetchers/roo.ts index 735d4bed922..65a2db77c39 100644 --- a/src/api/providers/fetchers/roo.ts +++ b/src/api/providers/fetchers/roo.ts @@ -4,6 +4,7 @@ import type { ModelRecord } from "../../../shared/api" import { parseApiPrice } from "../../../shared/cost" import { DEFAULT_HEADERS } from "../constants" +import { resolveVersionedSettings, type VersionedSettings } from "./versionedSettings" /** * Fetches available models from the Roo Code Cloud provider @@ -128,9 +129,37 @@ export async function getRooModels(baseUrl: string, apiKey?: string): Promise | undefined + // + // Two fields are used for backward compatibility: + // - `settings`: Plain values that work with all client versions (e.g., { includedTools: ['search_replace'] }) + // - `versionedSettings`: Version-keyed settings (e.g., { '3.36.4': { includedTools: ['search_replace'] } }) + // + // New clients check versionedSettings first - if a matching version is found, those settings are used. + // Otherwise, falls back to plain `settings`. Old clients only see `settings`. + const apiSettings = model.settings as Record | undefined + const apiVersionedSettings = model.versionedSettings as VersionedSettings | undefined + + // Start with base model info + let modelInfo: ModelInfo = { ...baseModelInfo } + + // Try to resolve versioned settings first (finds highest version <= current plugin version) + // If versioned settings match, use them exclusively (they contain all necessary settings) + // Otherwise fall back to plain settings for backward compatibility + if (apiVersionedSettings) { + const resolvedVersionedSettings = resolveVersionedSettings>(apiVersionedSettings) + if (Object.keys(resolvedVersionedSettings).length > 0) { + // Versioned settings found - use them exclusively + modelInfo = { ...modelInfo, ...resolvedVersionedSettings } + } else if (apiSettings) { + // No matching versioned settings - fall back to plain settings + modelInfo = { ...modelInfo, ...(apiSettings as Partial) } + } + } else if (apiSettings) { + // No versioned settings at all - use plain settings + modelInfo = { ...modelInfo, ...(apiSettings as Partial) } + } - models[modelId] = apiSettings ? { ...baseModelInfo, ...apiSettings } : baseModelInfo + models[modelId] = modelInfo } return models diff --git a/src/api/providers/fetchers/versionedSettings.ts b/src/api/providers/fetchers/versionedSettings.ts new file mode 100644 index 00000000000..50ec0b89732 --- /dev/null +++ b/src/api/providers/fetchers/versionedSettings.ts @@ -0,0 +1,113 @@ +import cmp from "semver-compare" + +import { Package } from "../../../shared/package" + +/** + * Type for versioned settings where the version is the key. + * Each version key maps to a settings object that should be used + * when the current plugin version is >= that version. + * + * Example API response: + * ``` + * { + * settings: { + * includedTools: ['search_replace'] // Plain value for old clients + * }, + * versionedSettings: { + * '3.36.4': { + * includedTools: ['search_replace', 'apply_diff'], // Enhanced value for 3.36.4+ + * excludedTools: ['write_to_file'], + * }, + * '3.35.0': { + * includedTools: ['search_replace'], // Value for 3.35.0 - 3.36.3 + * }, + * } + * } + * ``` + * + * The resolver will find the highest version key that is <= the current plugin version + * and use those settings. If no version matches, falls back to plain `settings`. + */ +export type VersionedSettings = Record> + +/** + * Compares two semantic version strings using semver-compare. + * + * @param version1 First version string (e.g., "3.36.4") + * @param version2 Second version string (e.g., "3.36.0") + * @returns -1 if version1 < version2, 0 if equal, 1 if version1 > version2 + */ +export function compareSemver(version1: string, version2: string): number { + // Handle pre-release versions by stripping the suffix + // semver-compare doesn't handle pre-release properly + const stripPrerelease = (v: string): string => v.split("-")[0] + return cmp(stripPrerelease(version1), stripPrerelease(version2)) +} + +/** + * Checks if the current plugin version meets or exceeds the required minimum version. + * + * @param minPluginVersion The minimum required version + * @param currentVersion The current plugin version (defaults to Package.version) + * @returns true if current version >= minPluginVersion + */ +export function meetsMinimumVersion(minPluginVersion: string, currentVersion: string = Package.version): boolean { + return compareSemver(currentVersion, minPluginVersion) >= 0 +} + +/** + * Finds the highest version from versionedSettings that is <= the current plugin version. + * + * @param versionedSettings The versioned settings object with version keys + * @param currentVersion The current plugin version (defaults to Package.version) + * @returns The highest matching version key, or undefined if none match + */ +export function findHighestMatchingVersion( + versionedSettings: VersionedSettings, + currentVersion: string = Package.version, +): string | undefined { + const versions = Object.keys(versionedSettings) + + // Filter to versions that are <= currentVersion + const matchingVersions = versions.filter((version) => meetsMinimumVersion(version, currentVersion)) + + if (matchingVersions.length === 0) { + return undefined + } + + // Sort in descending order and return the highest + matchingVersions.sort((a, b) => compareSemver(b, a)) + return matchingVersions[0] +} + +/** + * Resolves versioned settings by finding the highest version that is <= the current + * plugin version and returning those settings. + * + * The versionedSettings structure uses version numbers as keys: + * ``` + * versionedSettings: { + * '3.36.4': { includedTools: ['search_replace'], excludedTools: ['apply_diff'] }, + * '3.35.0': { includedTools: ['search_replace'] }, + * } + * ``` + * + * This function finds the highest version key that is <= currentVersion and returns + * the corresponding settings object. If no version matches, returns an empty object. + * + * @param versionedSettings The versioned settings object with version keys + * @param currentVersion The current plugin version (defaults to Package.version) + * @returns The settings object for the highest matching version, or empty object if none match + */ +export function resolveVersionedSettings>( + versionedSettings: VersionedSettings, + currentVersion: string = Package.version, +): Partial { + const matchingVersion = findHighestMatchingVersion(versionedSettings, currentVersion) + + if (!matchingVersion) { + return {} + } + + return versionedSettings[matchingVersion] as Partial +} diff --git a/src/package.json b/src/package.json index c1e38199aaf..e296d91f833 100644 --- a/src/package.json +++ b/src/package.json @@ -488,6 +488,7 @@ "safe-stable-stringify": "^2.5.0", "sanitize-filename": "^1.6.3", "say": "^0.16.0", + "semver-compare": "^1.0.0", "serialize-error": "^12.0.0", "shell-quote": "^1.8.2", "simple-git": "^3.27.0", @@ -524,6 +525,7 @@ "@types/node-ipc": "^9.2.3", "@types/proper-lockfile": "^4.1.4", "@types/ps-tree": "^1.1.6", + "@types/semver-compare": "^1.0.3", "@types/shell-quote": "^1.7.5", "@types/stream-json": "^1.7.8", "@types/string-similarity": "^4.0.2",