Merge pull request #681 from aw338WoWmUI/fix/installer-version-pinning
fix(cli): write version-aware plugin entry during installation
This commit is contained in:
commit
b1f19cbfbd
@ -1,8 +1,174 @@
|
|||||||
import { describe, expect, test } from "bun:test"
|
import { describe, expect, test, mock, beforeEach, afterEach } from "bun:test"
|
||||||
|
|
||||||
import { ANTIGRAVITY_PROVIDER_CONFIG, generateOmoConfig } from "./config-manager"
|
import { ANTIGRAVITY_PROVIDER_CONFIG, getPluginNameWithVersion, fetchNpmDistTags, generateOmoConfig } from "./config-manager"
|
||||||
import type { InstallConfig } from "./types"
|
import type { InstallConfig } from "./types"
|
||||||
|
|
||||||
|
describe("getPluginNameWithVersion", () => {
|
||||||
|
const originalFetch = globalThis.fetch
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
globalThis.fetch = originalFetch
|
||||||
|
})
|
||||||
|
|
||||||
|
test("returns @latest when current version matches latest tag", async () => {
|
||||||
|
// #given npm dist-tags with latest=2.14.0
|
||||||
|
globalThis.fetch = mock(() =>
|
||||||
|
Promise.resolve({
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve({ latest: "2.14.0", beta: "3.0.0-beta.3" }),
|
||||||
|
} as Response)
|
||||||
|
) as unknown as typeof fetch
|
||||||
|
|
||||||
|
// #when current version is 2.14.0
|
||||||
|
const result = await getPluginNameWithVersion("2.14.0")
|
||||||
|
|
||||||
|
// #then should use @latest tag
|
||||||
|
expect(result).toBe("oh-my-opencode@latest")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("returns @beta when current version matches beta tag", async () => {
|
||||||
|
// #given npm dist-tags with beta=3.0.0-beta.3
|
||||||
|
globalThis.fetch = mock(() =>
|
||||||
|
Promise.resolve({
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve({ latest: "2.14.0", beta: "3.0.0-beta.3" }),
|
||||||
|
} as Response)
|
||||||
|
) as unknown as typeof fetch
|
||||||
|
|
||||||
|
// #when current version is 3.0.0-beta.3
|
||||||
|
const result = await getPluginNameWithVersion("3.0.0-beta.3")
|
||||||
|
|
||||||
|
// #then should use @beta tag
|
||||||
|
expect(result).toBe("oh-my-opencode@beta")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("returns @next when current version matches next tag", async () => {
|
||||||
|
// #given npm dist-tags with next=3.1.0-next.1
|
||||||
|
globalThis.fetch = mock(() =>
|
||||||
|
Promise.resolve({
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve({ latest: "2.14.0", beta: "3.0.0-beta.3", next: "3.1.0-next.1" }),
|
||||||
|
} as Response)
|
||||||
|
) as unknown as typeof fetch
|
||||||
|
|
||||||
|
// #when current version is 3.1.0-next.1
|
||||||
|
const result = await getPluginNameWithVersion("3.1.0-next.1")
|
||||||
|
|
||||||
|
// #then should use @next tag
|
||||||
|
expect(result).toBe("oh-my-opencode@next")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("returns pinned version when no tag matches", async () => {
|
||||||
|
// #given npm dist-tags with beta=3.0.0-beta.3
|
||||||
|
globalThis.fetch = mock(() =>
|
||||||
|
Promise.resolve({
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve({ latest: "2.14.0", beta: "3.0.0-beta.3" }),
|
||||||
|
} as Response)
|
||||||
|
) as unknown as typeof fetch
|
||||||
|
|
||||||
|
// #when current version is old beta 3.0.0-beta.2
|
||||||
|
const result = await getPluginNameWithVersion("3.0.0-beta.2")
|
||||||
|
|
||||||
|
// #then should pin to specific version
|
||||||
|
expect(result).toBe("oh-my-opencode@3.0.0-beta.2")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("returns pinned version when fetch fails", async () => {
|
||||||
|
// #given network failure
|
||||||
|
globalThis.fetch = mock(() => Promise.reject(new Error("Network error"))) as unknown as typeof fetch
|
||||||
|
|
||||||
|
// #when current version is 3.0.0-beta.3
|
||||||
|
const result = await getPluginNameWithVersion("3.0.0-beta.3")
|
||||||
|
|
||||||
|
// #then should fall back to pinned version
|
||||||
|
expect(result).toBe("oh-my-opencode@3.0.0-beta.3")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("returns pinned version when npm returns non-ok response", async () => {
|
||||||
|
// #given npm returns 404
|
||||||
|
globalThis.fetch = mock(() =>
|
||||||
|
Promise.resolve({
|
||||||
|
ok: false,
|
||||||
|
status: 404,
|
||||||
|
} as Response)
|
||||||
|
) as unknown as typeof fetch
|
||||||
|
|
||||||
|
// #when current version is 2.14.0
|
||||||
|
const result = await getPluginNameWithVersion("2.14.0")
|
||||||
|
|
||||||
|
// #then should fall back to pinned version
|
||||||
|
expect(result).toBe("oh-my-opencode@2.14.0")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("prioritizes latest over other tags when version matches multiple", async () => {
|
||||||
|
// #given version matches both latest and beta (during release promotion)
|
||||||
|
globalThis.fetch = mock(() =>
|
||||||
|
Promise.resolve({
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve({ beta: "3.0.0", latest: "3.0.0", next: "3.1.0-alpha.1" }),
|
||||||
|
} as Response)
|
||||||
|
) as unknown as typeof fetch
|
||||||
|
|
||||||
|
// #when current version matches both
|
||||||
|
const result = await getPluginNameWithVersion("3.0.0")
|
||||||
|
|
||||||
|
// #then should prioritize @latest
|
||||||
|
expect(result).toBe("oh-my-opencode@latest")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("fetchNpmDistTags", () => {
|
||||||
|
const originalFetch = globalThis.fetch
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
globalThis.fetch = originalFetch
|
||||||
|
})
|
||||||
|
|
||||||
|
test("returns dist-tags on success", async () => {
|
||||||
|
// #given npm returns dist-tags
|
||||||
|
globalThis.fetch = mock(() =>
|
||||||
|
Promise.resolve({
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve({ latest: "2.14.0", beta: "3.0.0-beta.3" }),
|
||||||
|
} as Response)
|
||||||
|
) as unknown as typeof fetch
|
||||||
|
|
||||||
|
// #when fetching dist-tags
|
||||||
|
const result = await fetchNpmDistTags("oh-my-opencode")
|
||||||
|
|
||||||
|
// #then should return the tags
|
||||||
|
expect(result).toEqual({ latest: "2.14.0", beta: "3.0.0-beta.3" })
|
||||||
|
})
|
||||||
|
|
||||||
|
test("returns null on network failure", async () => {
|
||||||
|
// #given network failure
|
||||||
|
globalThis.fetch = mock(() => Promise.reject(new Error("Network error"))) as unknown as typeof fetch
|
||||||
|
|
||||||
|
// #when fetching dist-tags
|
||||||
|
const result = await fetchNpmDistTags("oh-my-opencode")
|
||||||
|
|
||||||
|
// #then should return null
|
||||||
|
expect(result).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
test("returns null on non-ok response", async () => {
|
||||||
|
// #given npm returns 404
|
||||||
|
globalThis.fetch = mock(() =>
|
||||||
|
Promise.resolve({
|
||||||
|
ok: false,
|
||||||
|
status: 404,
|
||||||
|
} as Response)
|
||||||
|
) as unknown as typeof fetch
|
||||||
|
|
||||||
|
// #when fetching dist-tags
|
||||||
|
const result = await fetchNpmDistTags("oh-my-opencode")
|
||||||
|
|
||||||
|
// #then should return null
|
||||||
|
expect(result).toBeNull()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe("config-manager ANTIGRAVITY_PROVIDER_CONFIG", () => {
|
describe("config-manager ANTIGRAVITY_PROVIDER_CONFIG", () => {
|
||||||
test("Gemini models include full spec (limit + modalities)", () => {
|
test("Gemini models include full spec (limit + modalities)", () => {
|
||||||
const google = (ANTIGRAVITY_PROVIDER_CONFIG as any).google
|
const google = (ANTIGRAVITY_PROVIDER_CONFIG as any).google
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import { existsSync, mkdirSync, readFileSync, writeFileSync, statSync } from "node:fs"
|
import { existsSync, mkdirSync, readFileSync, writeFileSync, statSync } from "node:fs"
|
||||||
import { join } from "node:path"
|
|
||||||
import {
|
import {
|
||||||
parseJsonc,
|
parseJsonc,
|
||||||
getOpenCodeConfigPaths,
|
getOpenCodeConfigPaths,
|
||||||
@ -109,6 +108,47 @@ export async function fetchLatestVersion(packageName: string): Promise<string |
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface NpmDistTags {
|
||||||
|
latest?: string
|
||||||
|
beta?: string
|
||||||
|
next?: string
|
||||||
|
[tag: string]: string | undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const NPM_FETCH_TIMEOUT_MS = 5000
|
||||||
|
|
||||||
|
export async function fetchNpmDistTags(packageName: string): Promise<NpmDistTags | null> {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`https://registry.npmjs.org/-/package/${packageName}/dist-tags`, {
|
||||||
|
signal: AbortSignal.timeout(NPM_FETCH_TIMEOUT_MS),
|
||||||
|
})
|
||||||
|
if (!res.ok) return null
|
||||||
|
const data = await res.json() as NpmDistTags
|
||||||
|
return data
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const PACKAGE_NAME = "oh-my-opencode"
|
||||||
|
|
||||||
|
const PRIORITIZED_TAGS = ["latest", "beta", "next"] as const
|
||||||
|
|
||||||
|
export async function getPluginNameWithVersion(currentVersion: string): Promise<string> {
|
||||||
|
const distTags = await fetchNpmDistTags(PACKAGE_NAME)
|
||||||
|
|
||||||
|
if (distTags) {
|
||||||
|
const allTags = new Set([...PRIORITIZED_TAGS, ...Object.keys(distTags)])
|
||||||
|
for (const tag of allTags) {
|
||||||
|
if (distTags[tag] === currentVersion) {
|
||||||
|
return `${PACKAGE_NAME}@${tag}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${PACKAGE_NAME}@${currentVersion}`
|
||||||
|
}
|
||||||
|
|
||||||
type ConfigFormat = "json" | "jsonc" | "none"
|
type ConfigFormat = "json" | "jsonc" | "none"
|
||||||
|
|
||||||
interface OpenCodeConfig {
|
interface OpenCodeConfig {
|
||||||
@ -179,7 +219,7 @@ function ensureConfigDir(): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function addPluginToOpenCodeConfig(): ConfigMergeResult {
|
export async function addPluginToOpenCodeConfig(currentVersion: string): Promise<ConfigMergeResult> {
|
||||||
try {
|
try {
|
||||||
ensureConfigDir()
|
ensureConfigDir()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -187,11 +227,11 @@ export function addPluginToOpenCodeConfig(): ConfigMergeResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { format, path } = detectConfigFormat()
|
const { format, path } = detectConfigFormat()
|
||||||
const pluginName = "oh-my-opencode"
|
const pluginEntry = await getPluginNameWithVersion(currentVersion)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (format === "none") {
|
if (format === "none") {
|
||||||
const config: OpenCodeConfig = { plugin: [pluginName] }
|
const config: OpenCodeConfig = { plugin: [pluginEntry] }
|
||||||
writeFileSync(path, JSON.stringify(config, null, 2) + "\n")
|
writeFileSync(path, JSON.stringify(config, null, 2) + "\n")
|
||||||
return { success: true, configPath: path }
|
return { success: true, configPath: path }
|
||||||
}
|
}
|
||||||
@ -203,11 +243,18 @@ export function addPluginToOpenCodeConfig(): ConfigMergeResult {
|
|||||||
|
|
||||||
const config = parseResult.config
|
const config = parseResult.config
|
||||||
const plugins = config.plugin ?? []
|
const plugins = config.plugin ?? []
|
||||||
if (plugins.some((p) => p.startsWith(pluginName))) {
|
const existingIndex = plugins.findIndex((p) => p === PACKAGE_NAME || p.startsWith(`${PACKAGE_NAME}@`))
|
||||||
return { success: true, configPath: path }
|
|
||||||
|
if (existingIndex !== -1) {
|
||||||
|
if (plugins[existingIndex] === pluginEntry) {
|
||||||
|
return { success: true, configPath: path }
|
||||||
|
}
|
||||||
|
plugins[existingIndex] = pluginEntry
|
||||||
|
} else {
|
||||||
|
plugins.push(pluginEntry)
|
||||||
}
|
}
|
||||||
|
|
||||||
config.plugin = [...plugins, pluginName]
|
config.plugin = plugins
|
||||||
|
|
||||||
if (format === "jsonc") {
|
if (format === "jsonc") {
|
||||||
const content = readFileSync(path, "utf-8")
|
const content = readFileSync(path, "utf-8")
|
||||||
@ -215,14 +262,11 @@ export function addPluginToOpenCodeConfig(): ConfigMergeResult {
|
|||||||
const match = content.match(pluginArrayRegex)
|
const match = content.match(pluginArrayRegex)
|
||||||
|
|
||||||
if (match) {
|
if (match) {
|
||||||
const arrayContent = match[1].trim()
|
const formattedPlugins = plugins.map((p) => `"${p}"`).join(",\n ")
|
||||||
const newArrayContent = arrayContent
|
const newContent = content.replace(pluginArrayRegex, `"plugin": [\n ${formattedPlugins}\n ]`)
|
||||||
? `${arrayContent},\n "${pluginName}"`
|
|
||||||
: `"${pluginName}"`
|
|
||||||
const newContent = content.replace(pluginArrayRegex, `"plugin": [\n ${newArrayContent}\n ]`)
|
|
||||||
writeFileSync(path, newContent)
|
writeFileSync(path, newContent)
|
||||||
} else {
|
} else {
|
||||||
const newContent = content.replace(/^(\s*\{)/, `$1\n "plugin": ["${pluginName}"],`)
|
const newContent = content.replace(/^(\s*\{)/, `$1\n "plugin": ["${pluginEntry}"],`)
|
||||||
writeFileSync(path, newContent)
|
writeFileSync(path, newContent)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -11,6 +11,9 @@ import {
|
|||||||
detectCurrentConfig,
|
detectCurrentConfig,
|
||||||
} from "./config-manager"
|
} from "./config-manager"
|
||||||
|
|
||||||
|
const packageJson = await import("../../package.json")
|
||||||
|
const VERSION = packageJson.version
|
||||||
|
|
||||||
const SYMBOLS = {
|
const SYMBOLS = {
|
||||||
check: color.green("✓"),
|
check: color.green("✓"),
|
||||||
cross: color.red("✗"),
|
cross: color.red("✗"),
|
||||||
@ -274,7 +277,7 @@ async function runNonTuiInstall(args: InstallArgs): Promise<number> {
|
|||||||
const config = argsToConfig(args)
|
const config = argsToConfig(args)
|
||||||
|
|
||||||
printStep(step++, totalSteps, "Adding oh-my-opencode plugin...")
|
printStep(step++, totalSteps, "Adding oh-my-opencode plugin...")
|
||||||
const pluginResult = addPluginToOpenCodeConfig()
|
const pluginResult = await addPluginToOpenCodeConfig(VERSION)
|
||||||
if (!pluginResult.success) {
|
if (!pluginResult.success) {
|
||||||
printError(`Failed: ${pluginResult.error}`)
|
printError(`Failed: ${pluginResult.error}`)
|
||||||
return 1
|
return 1
|
||||||
@ -380,7 +383,7 @@ export async function install(args: InstallArgs): Promise<number> {
|
|||||||
if (!config) return 1
|
if (!config) return 1
|
||||||
|
|
||||||
s.start("Adding oh-my-opencode to OpenCode config")
|
s.start("Adding oh-my-opencode to OpenCode config")
|
||||||
const pluginResult = addPluginToOpenCodeConfig()
|
const pluginResult = await addPluginToOpenCodeConfig(VERSION)
|
||||||
if (!pluginResult.success) {
|
if (!pluginResult.success) {
|
||||||
s.stop(`Failed to add plugin: ${pluginResult.error}`)
|
s.stop(`Failed to add plugin: ${pluginResult.error}`)
|
||||||
p.outro(color.red("Installation failed."))
|
p.outro(color.red("Installation failed."))
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user