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 { fileURLToPath } from "node:url"
|
||||||
import { log } from "../../../shared/logger"
|
import { log } from "../../../shared/logger"
|
||||||
import type { PackageJson } from "../types"
|
import type { PackageJson } from "../types"
|
||||||
import { INSTALLED_PACKAGE_JSON } from "../constants"
|
import { INSTALLED_PACKAGE_JSON_CANDIDATES } from "../constants"
|
||||||
import { findPackageJsonUp } from "./package-json-locator"
|
import { findPackageJsonUp } from "./package-json-locator"
|
||||||
|
|
||||||
export function getCachedVersion(): string | null {
|
export function getCachedVersion(): string | null {
|
||||||
try {
|
for (const candidate of INSTALLED_PACKAGE_JSON_CANDIDATES) {
|
||||||
if (fs.existsSync(INSTALLED_PACKAGE_JSON)) {
|
try {
|
||||||
const content = fs.readFileSync(INSTALLED_PACKAGE_JSON, "utf-8")
|
if (fs.existsSync(candidate)) {
|
||||||
const pkg = JSON.parse(content) as PackageJson
|
const content = fs.readFileSync(candidate, "utf-8")
|
||||||
if (pkg.version) return pkg.version
|
const pkg = JSON.parse(content) as PackageJson
|
||||||
|
if (pkg.version) return pkg.version
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore; try next candidate
|
||||||
}
|
}
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import * as fs from "node:fs"
|
import * as fs from "node:fs"
|
||||||
import { fileURLToPath } from "node:url"
|
import { fileURLToPath } from "node:url"
|
||||||
import type { OpencodeConfig } from "../types"
|
import type { OpencodeConfig } from "../types"
|
||||||
import { PACKAGE_NAME } from "../constants"
|
import { ACCEPTED_PACKAGE_NAMES } from "../constants"
|
||||||
import { getConfigPaths } from "./config-paths"
|
import { getConfigPaths } from "./config-paths"
|
||||||
import { stripJsonComments } from "./jsonc-strip"
|
import { stripJsonComments } from "./jsonc-strip"
|
||||||
|
|
||||||
@@ -18,12 +18,12 @@ export function getLocalDevPath(directory: string): string | null {
|
|||||||
const plugins = config.plugin ?? []
|
const plugins = config.plugin ?? []
|
||||||
|
|
||||||
for (const entry of plugins) {
|
for (const entry of plugins) {
|
||||||
if (entry.startsWith("file://") && entry.includes(PACKAGE_NAME)) {
|
if (!entry.startsWith("file://")) continue
|
||||||
try {
|
if (!ACCEPTED_PACKAGE_NAMES.some(name => entry.includes(name))) continue
|
||||||
return fileURLToPath(entry)
|
try {
|
||||||
} catch {
|
return fileURLToPath(entry)
|
||||||
return entry.replace("file://", "")
|
} catch {
|
||||||
}
|
return entry.replace("file://", "")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} 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 fs from "node:fs"
|
||||||
import * as path from "node:path"
|
import * as path from "node:path"
|
||||||
import type { PackageJson } from "../types"
|
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 {
|
export function findPackageJsonUp(startPath: string): string | null {
|
||||||
try {
|
try {
|
||||||
@@ -14,7 +16,7 @@ export function findPackageJsonUp(startPath: string): string | null {
|
|||||||
try {
|
try {
|
||||||
const content = fs.readFileSync(pkgPath, "utf-8")
|
const content = fs.readFileSync(pkgPath, "utf-8")
|
||||||
const pkg = JSON.parse(content) as PackageJson
|
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 {
|
} catch {
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,4 +26,24 @@ describe("auto-update-checker constants", () => {
|
|||||||
// then PACKAGE_NAME equals the actually published package name
|
// then PACKAGE_NAME equals the actually published package name
|
||||||
expect(PACKAGE_NAME).toBe(repoPackageJson.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"
|
import { getOpenCodeConfigDir } from "../../shared/opencode-config-dir"
|
||||||
|
|
||||||
export const PACKAGE_NAME = "oh-my-opencode"
|
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_REGISTRY_URL = `https://registry.npmjs.org/-/package/${PACKAGE_NAME}/dist-tags`
|
||||||
export const NPM_FETCH_TIMEOUT = 5000
|
export const NPM_FETCH_TIMEOUT = 5000
|
||||||
|
|
||||||
@@ -34,3 +44,11 @@ export const INSTALLED_PACKAGE_JSON = path.join(
|
|||||||
PACKAGE_NAME,
|
PACKAGE_NAME,
|
||||||
"package.json"
|
"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