Merge remote-tracking branch 'origin/dev' into fix/sync-package-json-to-opencode-intent

This commit is contained in:
acamq
2026-03-11 08:04:16 -06:00
185 changed files with 8236 additions and 1041 deletions
+1 -1
View File
@@ -3,7 +3,7 @@ export { getLocalDevVersion } from "./checker/local-dev-version"
export { findPluginEntry } from "./checker/plugin-entry"
export type { PluginEntryInfo } from "./checker/plugin-entry"
export { getCachedVersion } from "./checker/cached-version"
export { updatePinnedVersion, revertPinnedVersion } from "./checker/pinned-version-updater"
export { updatePinnedVersion } from "./checker/pinned-version-updater"
export { getLatestVersion } from "./checker/latest-version"
export { checkForUpdate } from "./checker/check-for-update"
export { syncCachePackageJsonToIntent } from "./checker/sync-package-json"
@@ -10,11 +10,8 @@ interface CachePackageJson {
}
export interface SyncResult {
/** Whether the package.json was successfully synced/updated */
synced: boolean
/** Whether there was an error during sync (null if no error) */
error: "file_not_found" | "plugin_not_in_deps" | "parse_error" | "write_error" | null
/** Human-readable message describing what happened */
message?: string
}
@@ -28,35 +25,13 @@ function safeUnlink(filePath: string): void {
}
}
/**
* Determine the version specifier to use in cache package.json based on opencode.json intent.
*
* - "oh-my-opencode" (no version) → "latest"
* - "oh-my-opencode@latest" → "latest"
* - "oh-my-opencode@next" → "next"
* - "oh-my-opencode@3.10.0" → "3.10.0" (pinned, use as-is)
*/
function getIntentVersion(pluginInfo: PluginEntryInfo): string {
if (!pluginInfo.pinnedVersion) {
// No version specified in opencode.json, default to latest
return "latest"
}
return pluginInfo.pinnedVersion
}
/**
* Sync the cache package.json to match the opencode.json plugin intent.
*
* OpenCode pins resolved versions in cache package.json (e.g., "3.11.0" instead of "latest").
* This causes issues when users switch from pinned to tag in opencode.json:
* - User changes opencode.json from "oh-my-opencode@3.10.0" to "oh-my-opencode@latest"
* - Cache package.json still has "3.10.0"
* - bun install reinstalls 3.10.0 instead of resolving @latest
*
* This function updates cache package.json to match the user's intent before bun install.
*
* @returns SyncResult with synced status and any error information
*/
export function syncCachePackageJsonToIntent(pluginInfo: PluginEntryInfo): SyncResult {
const cachePackageJsonPath = path.join(CACHE_DIR, "package.json")
@@ -95,9 +70,6 @@ export function syncCachePackageJsonToIntent(pluginInfo: PluginEntryInfo): SyncR
return { synced: false, error: null, message: `Already matches intent: ${intentVersion}` }
}
// Check if this is a meaningful change:
// - If intent is a tag (latest, next, beta) and current is semver, we need to update
// - If both are semver but different, user explicitly changed versions
const intentIsTag = !EXACT_SEMVER_REGEX.test(intentVersion.trim())
const currentIsSemver = EXACT_SEMVER_REGEX.test(currentVersion.trim())
+34 -44
View File
@@ -54,6 +54,26 @@ function createPluginInput() {
} as never
}
async function flushScheduledWork(): Promise<void> {
await new Promise<void>((resolve) => {
setTimeout(resolve, 0)
})
await Promise.resolve()
await Promise.resolve()
}
function runSessionCreatedEvent(
hook: ReturnType<HookFactory>,
properties?: { info?: { parentID?: string } }
): void {
hook.event({
event: {
type: "session.created",
properties,
},
})
}
beforeEach(() => {
mockShowConfigErrorsIfAny.mockClear()
mockShowModelCacheWarningIfNeeded.mockClear()
@@ -85,13 +105,8 @@ describe("createAutoUpdateCheckerHook", () => {
})
//#when - session.created event arrives
hook.event({
event: {
type: "session.created",
properties: { info: { parentID: undefined } },
},
})
await new Promise((resolve) => setTimeout(resolve, 50))
runSessionCreatedEvent(hook, { info: { parentID: undefined } })
await flushScheduledWork()
//#then - no update checker side effects run
expect(mockShowConfigErrorsIfAny).not.toHaveBeenCalled()
@@ -108,12 +123,8 @@ describe("createAutoUpdateCheckerHook", () => {
const hook = createAutoUpdateCheckerHook(createPluginInput())
//#when - session.created event arrives on primary session
hook.event({
event: {
type: "session.created",
},
})
await new Promise((resolve) => setTimeout(resolve, 50))
runSessionCreatedEvent(hook)
await flushScheduledWork()
//#then - startup checks, toast, and background check run
expect(mockShowConfigErrorsIfAny).toHaveBeenCalledTimes(1)
@@ -129,13 +140,8 @@ describe("createAutoUpdateCheckerHook", () => {
const hook = createAutoUpdateCheckerHook(createPluginInput())
//#when - session.created event contains parentID
hook.event({
event: {
type: "session.created",
properties: { info: { parentID: "parent-123" } },
},
})
await new Promise((resolve) => setTimeout(resolve, 50))
runSessionCreatedEvent(hook, { info: { parentID: "parent-123" } })
await flushScheduledWork()
//#then - no startup actions run
expect(mockShowConfigErrorsIfAny).not.toHaveBeenCalled()
@@ -152,17 +158,9 @@ describe("createAutoUpdateCheckerHook", () => {
const hook = createAutoUpdateCheckerHook(createPluginInput())
//#when - session.created event is fired twice
hook.event({
event: {
type: "session.created",
},
})
hook.event({
event: {
type: "session.created",
},
})
await new Promise((resolve) => setTimeout(resolve, 50))
runSessionCreatedEvent(hook)
runSessionCreatedEvent(hook)
await flushScheduledWork()
//#then - side effects execute only once
expect(mockShowConfigErrorsIfAny).toHaveBeenCalledTimes(1)
@@ -179,12 +177,8 @@ describe("createAutoUpdateCheckerHook", () => {
const hook = createAutoUpdateCheckerHook(createPluginInput())
//#when - session.created event arrives
hook.event({
event: {
type: "session.created",
},
})
await new Promise((resolve) => setTimeout(resolve, 50))
runSessionCreatedEvent(hook)
await flushScheduledWork()
//#then - local dev toast is shown and background check is skipped
expect(mockShowConfigErrorsIfAny).toHaveBeenCalledTimes(1)
@@ -206,7 +200,7 @@ describe("createAutoUpdateCheckerHook", () => {
type: "session.deleted",
},
})
await new Promise((resolve) => setTimeout(resolve, 50))
await flushScheduledWork()
//#then - no startup actions run
expect(mockShowConfigErrorsIfAny).not.toHaveBeenCalled()
@@ -225,12 +219,8 @@ describe("createAutoUpdateCheckerHook", () => {
})
//#when - session.created event arrives
hook.event({
event: {
type: "session.created",
},
})
await new Promise((resolve) => setTimeout(resolve, 50))
runSessionCreatedEvent(hook)
await flushScheduledWork()
//#then - startup toast includes sisyphus wording
expect(mockShowVersionToast).toHaveBeenCalledTimes(1)