feat(installer): add version compatibility checking utilities
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -0,0 +1,82 @@
|
|||||||
|
import { describe, expect, it } from "bun:test"
|
||||||
|
import {
|
||||||
|
checkVersionCompatibility,
|
||||||
|
extractVersionFromPluginEntry,
|
||||||
|
} from "./version-compatibility"
|
||||||
|
|
||||||
|
describe("checkVersionCompatibility", () => {
|
||||||
|
it("allows fresh install when no current version", () => {
|
||||||
|
const result = checkVersionCompatibility(null, "3.15.0")
|
||||||
|
expect(result.canUpgrade).toBe(true)
|
||||||
|
expect(result.isDowngrade).toBe(false)
|
||||||
|
expect(result.requiresMigration).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("detects same version as already installed", () => {
|
||||||
|
const result = checkVersionCompatibility("3.15.0", "3.15.0")
|
||||||
|
expect(result.canUpgrade).toBe(true)
|
||||||
|
expect(result.reason).toContain("already installed")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("blocks downgrade from higher to lower version", () => {
|
||||||
|
const result = checkVersionCompatibility("3.15.0", "3.14.0")
|
||||||
|
expect(result.canUpgrade).toBe(false)
|
||||||
|
expect(result.isDowngrade).toBe(true)
|
||||||
|
expect(result.reason).toContain("Downgrade")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("allows patch version upgrade", () => {
|
||||||
|
const result = checkVersionCompatibility("3.15.0", "3.15.1")
|
||||||
|
expect(result.canUpgrade).toBe(true)
|
||||||
|
expect(result.isMajorBump).toBe(false)
|
||||||
|
expect(result.requiresMigration).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("allows minor version upgrade", () => {
|
||||||
|
const result = checkVersionCompatibility("3.15.0", "3.16.0")
|
||||||
|
expect(result.canUpgrade).toBe(true)
|
||||||
|
expect(result.isMajorBump).toBe(false)
|
||||||
|
expect(result.requiresMigration).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("detects major version bump requiring migration", () => {
|
||||||
|
const result = checkVersionCompatibility("3.15.0", "4.0.0")
|
||||||
|
expect(result.canUpgrade).toBe(true)
|
||||||
|
expect(result.isMajorBump).toBe(true)
|
||||||
|
expect(result.requiresMigration).toBe(true)
|
||||||
|
expect(result.reason).toContain("Major version upgrade")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("handles v prefix in versions", () => {
|
||||||
|
const result = checkVersionCompatibility("v3.15.0", "v3.16.0")
|
||||||
|
expect(result.canUpgrade).toBe(true)
|
||||||
|
expect(result.isDowngrade).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("handles mixed v prefix", () => {
|
||||||
|
const result = checkVersionCompatibility("3.15.0", "v3.16.0")
|
||||||
|
expect(result.canUpgrade).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("extractVersionFromPluginEntry", () => {
|
||||||
|
it("extracts version from canonical plugin entry", () => {
|
||||||
|
const version = extractVersionFromPluginEntry("oh-my-openagent@3.15.0")
|
||||||
|
expect(version).toBe("3.15.0")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("extracts version from legacy plugin entry", () => {
|
||||||
|
const version = extractVersionFromPluginEntry("oh-my-opencode@3.14.0")
|
||||||
|
expect(version).toBe("3.14.0")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("returns null for bare plugin entry", () => {
|
||||||
|
const version = extractVersionFromPluginEntry("oh-my-openagent")
|
||||||
|
expect(version).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("handles prerelease versions", () => {
|
||||||
|
const version = extractVersionFromPluginEntry("oh-my-openagent@3.16.0-beta.1")
|
||||||
|
expect(version).toBe("3.16.0-beta.1")
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
export interface VersionCompatibility {
|
||||||
|
canUpgrade: boolean
|
||||||
|
reason?: string
|
||||||
|
isDowngrade: boolean
|
||||||
|
isMajorBump: boolean
|
||||||
|
requiresMigration: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseVersion(version: string): number[] {
|
||||||
|
const clean = version.replace(/^v/, "").split("-")[0]
|
||||||
|
return clean.split(".").map(Number)
|
||||||
|
}
|
||||||
|
|
||||||
|
function compareVersions(a: string, b: string): number {
|
||||||
|
const partsA = parseVersion(a)
|
||||||
|
const partsB = parseVersion(b)
|
||||||
|
const maxLen = Math.max(partsA.length, partsB.length)
|
||||||
|
|
||||||
|
for (let i = 0; i < maxLen; i++) {
|
||||||
|
const numA = partsA[i] ?? 0
|
||||||
|
const numB = partsB[i] ?? 0
|
||||||
|
if (numA !== numB) {
|
||||||
|
return numA - numB
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
export function checkVersionCompatibility(
|
||||||
|
currentVersion: string | null,
|
||||||
|
newVersion: string
|
||||||
|
): VersionCompatibility {
|
||||||
|
if (!currentVersion) {
|
||||||
|
return {
|
||||||
|
canUpgrade: true,
|
||||||
|
isDowngrade: false,
|
||||||
|
isMajorBump: false,
|
||||||
|
requiresMigration: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const cleanCurrent = currentVersion.replace(/^v/, "")
|
||||||
|
const cleanNew = newVersion.replace(/^v/, "")
|
||||||
|
|
||||||
|
try {
|
||||||
|
const comparison = compareVersions(cleanNew, cleanCurrent)
|
||||||
|
|
||||||
|
if (comparison < 0) {
|
||||||
|
return {
|
||||||
|
canUpgrade: false,
|
||||||
|
reason: `Downgrade from ${currentVersion} to ${newVersion} is not allowed`,
|
||||||
|
isDowngrade: true,
|
||||||
|
isMajorBump: false,
|
||||||
|
requiresMigration: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (comparison === 0) {
|
||||||
|
return {
|
||||||
|
canUpgrade: true,
|
||||||
|
reason: `Version ${newVersion} is already installed`,
|
||||||
|
isDowngrade: false,
|
||||||
|
isMajorBump: false,
|
||||||
|
requiresMigration: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentMajor = cleanCurrent.split(".")[0]
|
||||||
|
const newMajor = cleanNew.split(".")[0]
|
||||||
|
const isMajorBump = currentMajor !== newMajor
|
||||||
|
|
||||||
|
if (isMajorBump) {
|
||||||
|
return {
|
||||||
|
canUpgrade: true,
|
||||||
|
reason: `Major version upgrade from ${currentVersion} to ${newVersion} - configuration migration may be required`,
|
||||||
|
isDowngrade: false,
|
||||||
|
isMajorBump: true,
|
||||||
|
requiresMigration: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
canUpgrade: true,
|
||||||
|
isDowngrade: false,
|
||||||
|
isMajorBump: false,
|
||||||
|
requiresMigration: false,
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return {
|
||||||
|
canUpgrade: true,
|
||||||
|
reason: `Unable to compare versions ${currentVersion} and ${newVersion} - proceeding with caution`,
|
||||||
|
isDowngrade: false,
|
||||||
|
isMajorBump: false,
|
||||||
|
requiresMigration: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function extractVersionFromPluginEntry(entry: string): string | null {
|
||||||
|
const match = entry.match(/@(.+)$/)
|
||||||
|
return match ? match[1] : null
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user