Merge pull request #3258 from code-yeongyu/fix/auto-update-openagent-name-match
fix(auto-update): resolve cached version when installed as oh-my-openagent (#3257)
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, mock } from "bun:test"
|
||||
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"
|
||||
import { tmpdir } from "node:os"
|
||||
import { join } from "node:path"
|
||||
|
||||
// Hold mutable mock state so beforeEach can swap the cache root for each test.
|
||||
const mockState: { candidates: string[] } = { candidates: [] }
|
||||
|
||||
mock.module("../constants", () => ({
|
||||
INSTALLED_PACKAGE_JSON_CANDIDATES: new Proxy([], {
|
||||
get(_, prop) {
|
||||
const current = mockState.candidates
|
||||
// Forward array methods/properties to the mutable candidates list
|
||||
// so getCachedVersion's `for (... of ...)` sees fresh data per test.
|
||||
const value = (current as unknown as Record<PropertyKey, unknown>)[prop]
|
||||
if (typeof value === "function") {
|
||||
return (value as (...args: unknown[]) => unknown).bind(current)
|
||||
}
|
||||
return value
|
||||
},
|
||||
}),
|
||||
}))
|
||||
|
||||
mock.module("./package-json-locator", () => ({
|
||||
findPackageJsonUp: () => null,
|
||||
}))
|
||||
|
||||
import { getCachedVersion } from "./cached-version"
|
||||
|
||||
describe("getCachedVersion (GH-3257)", () => {
|
||||
let cacheRoot: string
|
||||
|
||||
beforeEach(() => {
|
||||
cacheRoot = mkdtempSync(join(tmpdir(), "omo-cached-version-"))
|
||||
mockState.candidates = [
|
||||
join(cacheRoot, "node_modules", "oh-my-opencode", "package.json"),
|
||||
join(cacheRoot, "node_modules", "oh-my-openagent", "package.json"),
|
||||
]
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
rmSync(cacheRoot, { recursive: true, force: true })
|
||||
mockState.candidates = []
|
||||
})
|
||||
|
||||
it("returns the version when the package is installed under oh-my-opencode", () => {
|
||||
const pkgDir = join(cacheRoot, "node_modules", "oh-my-opencode")
|
||||
mkdirSync(pkgDir, { recursive: true })
|
||||
writeFileSync(join(pkgDir, "package.json"), JSON.stringify({ name: "oh-my-opencode", version: "3.16.0" }))
|
||||
|
||||
expect(getCachedVersion()).toBe("3.16.0")
|
||||
})
|
||||
|
||||
it("returns the version when the package is installed under oh-my-openagent", () => {
|
||||
// GH-3257: npm users who install the aliased `oh-my-openagent` package get
|
||||
// node_modules/oh-my-openagent/package.json, not the canonical oh-my-opencode
|
||||
// path. The cached version resolver must check both.
|
||||
const pkgDir = join(cacheRoot, "node_modules", "oh-my-openagent")
|
||||
mkdirSync(pkgDir, { recursive: true })
|
||||
writeFileSync(join(pkgDir, "package.json"), JSON.stringify({ name: "oh-my-openagent", version: "3.16.0" }))
|
||||
|
||||
expect(getCachedVersion()).toBe("3.16.0")
|
||||
})
|
||||
|
||||
it("prefers oh-my-opencode when both are installed", () => {
|
||||
const legacyDir = join(cacheRoot, "node_modules", "oh-my-opencode")
|
||||
mkdirSync(legacyDir, { recursive: true })
|
||||
writeFileSync(join(legacyDir, "package.json"), JSON.stringify({ name: "oh-my-opencode", version: "3.16.0" }))
|
||||
|
||||
const aliasDir = join(cacheRoot, "node_modules", "oh-my-openagent")
|
||||
mkdirSync(aliasDir, { recursive: true })
|
||||
writeFileSync(join(aliasDir, "package.json"), JSON.stringify({ name: "oh-my-openagent", version: "3.15.0" }))
|
||||
|
||||
expect(getCachedVersion()).toBe("3.16.0")
|
||||
})
|
||||
|
||||
it("returns null when neither candidate exists and fallbacks find nothing", () => {
|
||||
expect(getCachedVersion()).toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -3,18 +3,20 @@ import * as path from "node:path"
|
||||
import { fileURLToPath } from "node:url"
|
||||
import { log } from "../../../shared/logger"
|
||||
import type { PackageJson } from "../types"
|
||||
import { INSTALLED_PACKAGE_JSON } from "../constants"
|
||||
import { INSTALLED_PACKAGE_JSON_CANDIDATES } from "../constants"
|
||||
import { findPackageJsonUp } from "./package-json-locator"
|
||||
|
||||
export function getCachedVersion(): string | null {
|
||||
try {
|
||||
if (fs.existsSync(INSTALLED_PACKAGE_JSON)) {
|
||||
const content = fs.readFileSync(INSTALLED_PACKAGE_JSON, "utf-8")
|
||||
const pkg = JSON.parse(content) as PackageJson
|
||||
if (pkg.version) return pkg.version
|
||||
for (const candidate of INSTALLED_PACKAGE_JSON_CANDIDATES) {
|
||||
try {
|
||||
if (fs.existsSync(candidate)) {
|
||||
const content = fs.readFileSync(candidate, "utf-8")
|
||||
const pkg = JSON.parse(content) as PackageJson
|
||||
if (pkg.version) return pkg.version
|
||||
}
|
||||
} catch {
|
||||
// ignore; try next candidate
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as fs from "node:fs"
|
||||
import { fileURLToPath } from "node:url"
|
||||
import type { OpencodeConfig } from "../types"
|
||||
import { PACKAGE_NAME } from "../constants"
|
||||
import { ACCEPTED_PACKAGE_NAMES } from "../constants"
|
||||
import { getConfigPaths } from "./config-paths"
|
||||
import { stripJsonComments } from "./jsonc-strip"
|
||||
|
||||
@@ -18,12 +18,12 @@ export function getLocalDevPath(directory: string): string | null {
|
||||
const plugins = config.plugin ?? []
|
||||
|
||||
for (const entry of plugins) {
|
||||
if (entry.startsWith("file://") && entry.includes(PACKAGE_NAME)) {
|
||||
try {
|
||||
return fileURLToPath(entry)
|
||||
} catch {
|
||||
return entry.replace("file://", "")
|
||||
}
|
||||
if (!entry.startsWith("file://")) continue
|
||||
if (!ACCEPTED_PACKAGE_NAMES.some(name => entry.includes(name))) continue
|
||||
try {
|
||||
return fileURLToPath(entry)
|
||||
} catch {
|
||||
return entry.replace("file://", "")
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import { afterEach, beforeEach, describe, expect, it } from "bun:test"
|
||||
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"
|
||||
import { tmpdir } from "node:os"
|
||||
import { join } from "node:path"
|
||||
import { findPackageJsonUp } from "./package-json-locator"
|
||||
|
||||
describe("findPackageJsonUp", () => {
|
||||
let workdir: string
|
||||
|
||||
beforeEach(() => {
|
||||
workdir = mkdtempSync(join(tmpdir(), "omo-pkg-locator-"))
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
rmSync(workdir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
it("finds a package.json whose name is the canonical oh-my-opencode", () => {
|
||||
const pkgPath = join(workdir, "package.json")
|
||||
writeFileSync(pkgPath, JSON.stringify({ name: "oh-my-opencode", version: "3.16.0" }))
|
||||
|
||||
const found = findPackageJsonUp(workdir)
|
||||
|
||||
expect(found).toBe(pkgPath)
|
||||
})
|
||||
|
||||
it("finds a package.json whose name is the aliased oh-my-openagent (GH-3257)", () => {
|
||||
// A user who installed `oh-my-openagent` from npm gets a node_modules entry
|
||||
// whose package.json has `name: "oh-my-openagent"`. The auto-update-checker
|
||||
// must still resolve it so the startup toast shows a real version instead
|
||||
// of "unknown".
|
||||
const pkgPath = join(workdir, "package.json")
|
||||
writeFileSync(pkgPath, JSON.stringify({ name: "oh-my-openagent", version: "3.16.0" }))
|
||||
|
||||
const found = findPackageJsonUp(workdir)
|
||||
|
||||
expect(found).toBe(pkgPath)
|
||||
})
|
||||
|
||||
it("walks up directories to find the matching package.json", () => {
|
||||
const nested = join(workdir, "dist", "checker")
|
||||
mkdirSync(nested, { recursive: true })
|
||||
const pkgPath = join(workdir, "package.json")
|
||||
writeFileSync(pkgPath, JSON.stringify({ name: "oh-my-openagent", version: "3.16.0" }))
|
||||
|
||||
const found = findPackageJsonUp(nested)
|
||||
|
||||
expect(found).toBe(pkgPath)
|
||||
})
|
||||
|
||||
it("ignores unrelated package.json files", () => {
|
||||
const pkgPath = join(workdir, "package.json")
|
||||
writeFileSync(pkgPath, JSON.stringify({ name: "some-other-package", version: "1.0.0" }))
|
||||
|
||||
const found = findPackageJsonUp(workdir)
|
||||
|
||||
expect(found).toBeNull()
|
||||
})
|
||||
|
||||
it("returns null when no package.json exists", () => {
|
||||
const found = findPackageJsonUp(workdir)
|
||||
|
||||
expect(found).toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -1,7 +1,9 @@
|
||||
import * as fs from "node:fs"
|
||||
import * as path from "node:path"
|
||||
import type { PackageJson } from "../types"
|
||||
import { PACKAGE_NAME } from "../constants"
|
||||
import { ACCEPTED_PACKAGE_NAMES } from "../constants"
|
||||
|
||||
const ACCEPTED_NAME_SET = new Set<string>(ACCEPTED_PACKAGE_NAMES)
|
||||
|
||||
export function findPackageJsonUp(startPath: string): string | null {
|
||||
try {
|
||||
@@ -14,7 +16,7 @@ export function findPackageJsonUp(startPath: string): string | null {
|
||||
try {
|
||||
const content = fs.readFileSync(pkgPath, "utf-8")
|
||||
const pkg = JSON.parse(content) as PackageJson
|
||||
if (pkg.name === PACKAGE_NAME) return pkgPath
|
||||
if (pkg.name && ACCEPTED_NAME_SET.has(pkg.name)) return pkgPath
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
@@ -26,4 +26,24 @@ describe("auto-update-checker constants", () => {
|
||||
// then PACKAGE_NAME equals the actually published package name
|
||||
expect(PACKAGE_NAME).toBe(repoPackageJson.name)
|
||||
})
|
||||
|
||||
it("ACCEPTED_PACKAGE_NAMES contains both the canonical and aliased npm names (GH-3257)", async () => {
|
||||
const { ACCEPTED_PACKAGE_NAMES } = await import(`./constants?test=${Date.now()}`)
|
||||
|
||||
expect(ACCEPTED_PACKAGE_NAMES).toContain("oh-my-opencode")
|
||||
expect(ACCEPTED_PACKAGE_NAMES).toContain("oh-my-openagent")
|
||||
})
|
||||
|
||||
it("INSTALLED_PACKAGE_JSON_CANDIDATES covers every accepted package name (GH-3257)", async () => {
|
||||
const { ACCEPTED_PACKAGE_NAMES, INSTALLED_PACKAGE_JSON_CANDIDATES, CACHE_DIR } = await import(
|
||||
`./constants?test=${Date.now()}`
|
||||
)
|
||||
|
||||
expect(INSTALLED_PACKAGE_JSON_CANDIDATES).toHaveLength(ACCEPTED_PACKAGE_NAMES.length)
|
||||
for (const name of ACCEPTED_PACKAGE_NAMES) {
|
||||
expect(INSTALLED_PACKAGE_JSON_CANDIDATES).toContain(
|
||||
join(CACHE_DIR, "node_modules", name, "package.json")
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -4,6 +4,16 @@ import { getOpenCodeCacheDir } from "../../shared/data-path"
|
||||
import { getOpenCodeConfigDir } from "../../shared/opencode-config-dir"
|
||||
|
||||
export const PACKAGE_NAME = "oh-my-opencode"
|
||||
/**
|
||||
* All package names the canonical plugin may be published under.
|
||||
*
|
||||
* The package is published to npm as both `oh-my-opencode` (legacy canonical)
|
||||
* and `oh-my-openagent` (current canonical). Any code that *reads* an
|
||||
* installed package.json or walks up from an import path must accept both,
|
||||
* because the installed name depends on which package the user added to
|
||||
* their config. Code that *writes* continues to use {@link PACKAGE_NAME}.
|
||||
*/
|
||||
export const ACCEPTED_PACKAGE_NAMES = ["oh-my-opencode", "oh-my-openagent"] as const
|
||||
export const NPM_REGISTRY_URL = `https://registry.npmjs.org/-/package/${PACKAGE_NAME}/dist-tags`
|
||||
export const NPM_FETCH_TIMEOUT = 5000
|
||||
|
||||
@@ -34,3 +44,11 @@ export const INSTALLED_PACKAGE_JSON = path.join(
|
||||
PACKAGE_NAME,
|
||||
"package.json"
|
||||
)
|
||||
|
||||
/**
|
||||
* Candidate paths where the installed package.json may live, in priority order.
|
||||
* Readers should try each path in order and stop on the first success.
|
||||
*/
|
||||
export const INSTALLED_PACKAGE_JSON_CANDIDATES = ACCEPTED_PACKAGE_NAMES.map(
|
||||
name => path.join(CACHE_DIR, "node_modules", name, "package.json")
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user