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