From 0f417031b90c4e10523466f14b130335381db5c3 Mon Sep 17 00:00:00 2001 From: Oscar-Williams Date: Sat, 7 Nov 2026 04:28:18 +0800 Subject: [PATCH 1/4] fix(auto-update-checker): resolve the configured npm registry and skip the check when disabled --- .../hooks/auto-update-checker/index.test.ts | 3 + .../src/hooks/auto-update-checker/index.ts | 1 + .../resolve-npm-registry.test.ts | 123 ++++++++++++++++++ .../resolve-npm-registry.ts | 69 ++++++++++ packages/plugin/src/index.ts | 9 +- 5 files changed, 204 insertions(+), 1 deletion(-) create mode 100644 packages/plugin/src/hooks/auto-update-checker/resolve-npm-registry.test.ts create mode 100644 packages/plugin/src/hooks/auto-update-checker/resolve-npm-registry.ts diff --git a/packages/plugin/src/hooks/auto-update-checker/index.test.ts b/packages/plugin/src/hooks/auto-update-checker/index.test.ts index 1dd0626ee8..1ed9544777 100644 --- a/packages/plugin/src/hooks/auto-update-checker/index.test.ts +++ b/packages/plugin/src/hooks/auto-update-checker/index.test.ts @@ -187,6 +187,9 @@ describe("auto-update-checker/index", () => { // Even after waiting longer than initDelayMs would be, no check fires await new Promise((r) => setTimeout(r, 50)); expect(showToast).not.toHaveBeenCalled(); + // Disabling the checker means no npm request at all (#854): the + // registry lookup is the request. + expect(checkerMocks.getLatestVersion).not.toHaveBeenCalled(); }); test("on-disk timestamp dedupes concurrent plugin instances", async () => { diff --git a/packages/plugin/src/hooks/auto-update-checker/index.ts b/packages/plugin/src/hooks/auto-update-checker/index.ts index 67e4b04113..88551b1ac0 100644 --- a/packages/plugin/src/hooks/auto-update-checker/index.ts +++ b/packages/plugin/src/hooks/auto-update-checker/index.ts @@ -428,4 +428,5 @@ function showToast( ctx.client.tui.showToast({ body: { title, message, variant, duration } }).catch(() => {}); } +export { resolveNpmRegistry } from "./resolve-npm-registry"; export type { AutoUpdateCheckerOptions } from "./types"; diff --git a/packages/plugin/src/hooks/auto-update-checker/resolve-npm-registry.test.ts b/packages/plugin/src/hooks/auto-update-checker/resolve-npm-registry.test.ts new file mode 100644 index 0000000000..600cfc2d96 --- /dev/null +++ b/packages/plugin/src/hooks/auto-update-checker/resolve-npm-registry.test.ts @@ -0,0 +1,123 @@ +import { afterEach, describe, expect, test } from "bun:test"; +import { mkdirSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +import { resolveNpmRegistry } from "./resolve-npm-registry"; + +const REGISTRY_KEY = "npm_config_registry"; +const originalRegistry = process.env[REGISTRY_KEY]; +const originalHome = process.env.HOME; +const originalUserProfile = process.env.USERPROFILE; + +const scratchRoots: string[] = []; + +function makeScratchDir(): string { + const dir = join(tmpdir(), `mc-registry-test-${Date.now()}-${Math.random().toString(36).slice(2)}`); + mkdirSync(dir, { recursive: true }); + scratchRoots.push(dir); + return dir; +} + +function clearRegistryEnv(): void { + delete process.env[REGISTRY_KEY]; +} + +describe("resolveNpmRegistry", () => { + afterEach(() => { + clearRegistryEnv(); + if (originalRegistry !== undefined) process.env[REGISTRY_KEY] = originalRegistry; + if (originalHome !== undefined) process.env.HOME = originalHome; + if (originalUserProfile !== undefined) process.env.USERPROFILE = originalUserProfile; + for (const dir of scratchRoots.splice(0)) { + rmSync(dir, { recursive: true, force: true }); + } + }); + + test("env var wins over everything", () => { + const project = makeScratchDir(); + writeFileSync(join(project, ".npmrc"), "registry = https://mirror.example.test/npm/\n"); + process.env[REGISTRY_KEY] = "https://env.example.test/npm/"; + + expect(resolveNpmRegistry(project)).toBe("https://env.example.test/npm"); + }); + + test("project .npmrc wins over user .npmrc", () => { + clearRegistryEnv(); + const project = makeScratchDir(); + const home = makeScratchDir(); + process.env.HOME = home; + process.env.USERPROFILE = home; + writeFileSync(join(project, ".npmrc"), "registry=https://project.example.test/npm/\n"); + writeFileSync(join(home, ".npmrc"), "registry=https://user.example.test/npm/\n"); + + expect(resolveNpmRegistry(project)).toBe("https://project.example.test/npm"); + }); + + test("user .npmrc is used when the project has no .npmrc", () => { + clearRegistryEnv(); + const project = makeScratchDir(); + const home = makeScratchDir(); + process.env.HOME = home; + process.env.USERPROFILE = home; + writeFileSync(join(home, ".npmrc"), "registry=https://user.example.test/npm/\n"); + + expect(resolveNpmRegistry(project)).toBe("https://user.example.test/npm"); + }); + + test("falls back to the public default when nothing is configured", () => { + clearRegistryEnv(); + const project = makeScratchDir(); + const home = makeScratchDir(); + process.env.HOME = home; + process.env.USERPROFILE = home; + + expect(resolveNpmRegistry(project)).toBe("https://registry.npmjs.org"); + }); + + test("trailing slashes are trimmed", () => { + clearRegistryEnv(); + process.env[REGISTRY_KEY] = "https://env.example.test/npm///"; + + expect(resolveNpmRegistry(null)).toBe("https://env.example.test/npm"); + }); + + test("comment lines and scoped-registry keys are ignored", () => { + clearRegistryEnv(); + process.env.HOME = ""; + process.env.USERPROFILE = ""; + const project = makeScratchDir(); + writeFileSync( + join(project, ".npmrc"), + [ + "# operator comment", + "; another comment", + "@cortexkit:registry=https://scoped.example.test/npm/", + "", + ].join("\n"), + ); + + expect(resolveNpmRegistry(project)).toBe("https://registry.npmjs.org"); + }); + + test("a malformed .npmrc fails open to the default", () => { + clearRegistryEnv(); + process.env.HOME = ""; + process.env.USERPROFILE = ""; + const project = makeScratchDir(); + // A directory named .npmrc makes readFileSync throw. + mkdirSync(join(project, ".npmrc"), { recursive: true }); + + expect(resolveNpmRegistry(project)).toBe("https://registry.npmjs.org"); + }); + + test("a null project directory skips the project file", () => { + clearRegistryEnv(); + const home = makeScratchDir(); + process.env.HOME = home; + process.env.USERPROFILE = home; + writeFileSync(join(home, ".npmrc"), "registry=https://user.example.test/npm/\n"); + + expect(resolveNpmRegistry(null)).toBe("https://user.example.test/npm"); + }); +}); diff --git a/packages/plugin/src/hooks/auto-update-checker/resolve-npm-registry.ts b/packages/plugin/src/hooks/auto-update-checker/resolve-npm-registry.ts new file mode 100644 index 0000000000..5aa25d8565 --- /dev/null +++ b/packages/plugin/src/hooks/auto-update-checker/resolve-npm-registry.ts @@ -0,0 +1,69 @@ +import { existsSync, readFileSync } from "node:fs"; +import { join } from "node:path"; + +import { NPM_REGISTRY_URL } from "./constants"; + +/** + * Resolves the npm registry the update check should query. + * + * The checker used to hardcode registry.npmjs.org, which broke behind + * registries an operator configures for npm/Bun (mirrors, private proxies): + * installs already honor the configured registry because the spawned package + * manager reads the .npmrc chain itself, only the check bypassed it. + * + * Resolution order mirrors npm's own precedence: + * 1. `npm_config_registry` in the environment (npm and bun both export it + * to child processes when a registry is configured). + * 2. `registry = ` in the project `.npmrc` (the OpenCode project + * directory). + * 3. `registry = ` in the user `.npmrc`. + * 4. The public default. + * + * Never throws: an unreadable or malformed .npmrc fails open to the next + * source, so a broken config cannot take the update check down. + */ +export function resolveNpmRegistry(projectDir: string | null | undefined): string { + const fromEnv = process.env.npm_config_registry?.trim(); + if (fromEnv) return normalizeRegistryUrl(fromEnv); + + for (const npmrcPath of npmrcCandidates(projectDir)) { + const fromNpmrc = readRegistryFromNpmrc(npmrcPath); + if (fromNpmrc) return normalizeRegistryUrl(fromNpmrc); + } + + return NPM_REGISTRY_URL; +} + +function npmrcCandidates(projectDir: string | null | undefined): string[] { + const candidates: string[] = []; + if (projectDir) candidates.push(join(projectDir, ".npmrc")); + if (process.env.HOME) candidates.push(join(process.env.HOME, ".npmrc")); + if (process.env.USERPROFILE) candidates.push(join(process.env.USERPROFILE, ".npmrc")); + return candidates; +} + +/** + * Extracts the `registry` value from an .npmrc-style file. Only the + * top-level `registry` key matters here; scoped-registry keys + * (`@scope:registry`) configure publication targets, not the package + * metadata source for a specific package name. + */ +function readRegistryFromNpmrc(npmrcPath: string): string | null { + if (!existsSync(npmrcPath)) return null; + try { + const lines = readFileSync(npmrcPath, "utf-8").split(/\r?\n/); + for (const rawLine of lines) { + const line = rawLine.trim(); + if (!line || line.startsWith(";") || line.startsWith("#")) continue; + const match = /^registry\s*=\s*(\S+)\s*$/.exec(line); + if (match?.[1]) return match[1]; + } + } catch { + return null; + } + return null; +} + +function normalizeRegistryUrl(url: string): string { + return url.replace(/\/+$/, ""); +} diff --git a/packages/plugin/src/index.ts b/packages/plugin/src/index.ts index 783525558a..c9204ea120 100644 --- a/packages/plugin/src/index.ts +++ b/packages/plugin/src/index.ts @@ -38,7 +38,7 @@ import { } from "./features/magic-context/storage-db"; import { recordToolDefinition } from "./features/magic-context/tool-definition-tokens"; import { runDeferredV22Backfill } from "./features/magic-context/v22-deferred-backfill"; -import { createAutoUpdateCheckerHook } from "./hooks/auto-update-checker"; +import { createAutoUpdateCheckerHook, resolveNpmRegistry } from "./hooks/auto-update-checker"; import { COMPARTMENT_AGENT_SYSTEM_PROMPT, COMPARTMENT_STRUCTURAL_SYSTEM_PROMPT, @@ -881,7 +881,14 @@ const server: Plugin = async (ctx) => { }, }, autoUpdateChecker: createAutoUpdateCheckerHook(ctx, { + // auto_update: false skips the checker entirely — no npm + // request, matching the host updater's disable behavior. + enabled: pluginConfig.auto_update !== false, autoUpdate: pluginConfig.auto_update !== false, + // Installs honor the operator's configured registry (the + // spawned package manager reads the .npmrc chain); the check + // resolves the same registry instead of hardcoding npmjs. + npmRegistryUrl: resolveNpmRegistry(ctx.directory), signal: autoUpdateAbort.signal, // Multi-project plugin reloads coordinate via this on-disk // timestamp so npm gets hit at most once per check window From f29ddf650d0cd261f5dc8fa1fae2248f8955fc25 Mon Sep 17 00:00:00 2001 From: Oscar Williams Date: Sun, 8 Nov 2026 17:12:41 +0800 Subject: [PATCH 2/4] fix(auto-update-checker): narrow to skipping the check when auto_update is false Drop the registry resolution (covered by #855) and keep only the disable path: auto_update: false now disables the checker so no npm request is made. Adds a regression test asserting no registry lookup when disabled. --- .../hooks/auto-update-checker/index.test.ts | 39 +++++- .../src/hooks/auto-update-checker/index.ts | 13 +- .../resolve-npm-registry.test.ts | 123 ------------------ .../resolve-npm-registry.ts | 69 ---------- packages/plugin/src/index.ts | 15 +-- 5 files changed, 55 insertions(+), 204 deletions(-) delete mode 100644 packages/plugin/src/hooks/auto-update-checker/resolve-npm-registry.test.ts delete mode 100644 packages/plugin/src/hooks/auto-update-checker/resolve-npm-registry.ts diff --git a/packages/plugin/src/hooks/auto-update-checker/index.test.ts b/packages/plugin/src/hooks/auto-update-checker/index.test.ts index 1ed9544777..724c89ddfe 100644 --- a/packages/plugin/src/hooks/auto-update-checker/index.test.ts +++ b/packages/plugin/src/hooks/auto-update-checker/index.test.ts @@ -187,11 +187,46 @@ describe("auto-update-checker/index", () => { // Even after waiting longer than initDelayMs would be, no check fires await new Promise((r) => setTimeout(r, 50)); expect(showToast).not.toHaveBeenCalled(); - // Disabling the checker means no npm request at all (#854): the - // registry lookup is the request. expect(checkerMocks.getLatestVersion).not.toHaveBeenCalled(); }); + test("auto_update: false makes no npm request (#854)", async () => { + checkerMocks.findPluginEntry.mockImplementation(() => ({ + entry: "@cortexkit/opencode-magic-context", + pinnedVersion: null, + isPinned: false, + configPath: "/config/opencode.jsonc", + })); + checkerMocks.getCachedVersion.mockImplementation(() => "0.15.5"); + checkerMocks.getLatestVersion.mockImplementation(async () => "0.15.6"); + const { autoUpdateCheckerOptionsFromConfig, createAutoUpdateCheckerHook } = + await freshIndexImport(); + const { ctx, showToast } = createCtx(); + + createAutoUpdateCheckerHook(ctx as Parameters[0], { + ...autoUpdateCheckerOptionsFromConfig(false), + showStartupToast: false, + initDelayMs: 0, + storageDir: makeTempStorageDir(), + }); + + await new Promise((r) => setTimeout(r, 50)); + expect(checkerMocks.getLatestVersion).not.toHaveBeenCalled(); + expect(showToast).not.toHaveBeenCalled(); + }); + + test("auto_update unset or true keeps the check enabled", async () => { + const { autoUpdateCheckerOptionsFromConfig } = await freshIndexImport(); + expect(autoUpdateCheckerOptionsFromConfig(undefined)).toEqual({ + enabled: true, + autoUpdate: true, + }); + expect(autoUpdateCheckerOptionsFromConfig(true)).toEqual({ + enabled: true, + autoUpdate: true, + }); + }); + test("on-disk timestamp dedupes concurrent plugin instances", async () => { checkerMocks.getLocalDevVersion.mockImplementation(() => "0.15.6-dev"); const { createAutoUpdateCheckerHook } = await freshIndexImport(); diff --git a/packages/plugin/src/hooks/auto-update-checker/index.ts b/packages/plugin/src/hooks/auto-update-checker/index.ts index 88551b1ac0..1aba6854e4 100644 --- a/packages/plugin/src/hooks/auto-update-checker/index.ts +++ b/packages/plugin/src/hooks/auto-update-checker/index.ts @@ -74,6 +74,18 @@ function warn(message: string): void { * that pass synthetic events keep working — the hook itself never * triggers a check now. */ +/** + * Maps the plugin's `auto_update` setting to checker options. `false` turns + * the checker off entirely so no npm request is made, matching the host + * updater's disable behavior. + */ +export function autoUpdateCheckerOptionsFromConfig( + autoUpdateSetting: boolean | undefined, +): Pick { + const enabled = autoUpdateSetting !== false; + return { enabled, autoUpdate: enabled }; +} + export function createAutoUpdateCheckerHook( ctx: PluginInput, options: AutoUpdateCheckerOptions = {}, @@ -428,5 +440,4 @@ function showToast( ctx.client.tui.showToast({ body: { title, message, variant, duration } }).catch(() => {}); } -export { resolveNpmRegistry } from "./resolve-npm-registry"; export type { AutoUpdateCheckerOptions } from "./types"; diff --git a/packages/plugin/src/hooks/auto-update-checker/resolve-npm-registry.test.ts b/packages/plugin/src/hooks/auto-update-checker/resolve-npm-registry.test.ts deleted file mode 100644 index 600cfc2d96..0000000000 --- a/packages/plugin/src/hooks/auto-update-checker/resolve-npm-registry.test.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { afterEach, describe, expect, test } from "bun:test"; -import { mkdirSync, rmSync, writeFileSync } from "node:fs"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; - -import { resolveNpmRegistry } from "./resolve-npm-registry"; - -const REGISTRY_KEY = "npm_config_registry"; -const originalRegistry = process.env[REGISTRY_KEY]; -const originalHome = process.env.HOME; -const originalUserProfile = process.env.USERPROFILE; - -const scratchRoots: string[] = []; - -function makeScratchDir(): string { - const dir = join(tmpdir(), `mc-registry-test-${Date.now()}-${Math.random().toString(36).slice(2)}`); - mkdirSync(dir, { recursive: true }); - scratchRoots.push(dir); - return dir; -} - -function clearRegistryEnv(): void { - delete process.env[REGISTRY_KEY]; -} - -describe("resolveNpmRegistry", () => { - afterEach(() => { - clearRegistryEnv(); - if (originalRegistry !== undefined) process.env[REGISTRY_KEY] = originalRegistry; - if (originalHome !== undefined) process.env.HOME = originalHome; - if (originalUserProfile !== undefined) process.env.USERPROFILE = originalUserProfile; - for (const dir of scratchRoots.splice(0)) { - rmSync(dir, { recursive: true, force: true }); - } - }); - - test("env var wins over everything", () => { - const project = makeScratchDir(); - writeFileSync(join(project, ".npmrc"), "registry = https://mirror.example.test/npm/\n"); - process.env[REGISTRY_KEY] = "https://env.example.test/npm/"; - - expect(resolveNpmRegistry(project)).toBe("https://env.example.test/npm"); - }); - - test("project .npmrc wins over user .npmrc", () => { - clearRegistryEnv(); - const project = makeScratchDir(); - const home = makeScratchDir(); - process.env.HOME = home; - process.env.USERPROFILE = home; - writeFileSync(join(project, ".npmrc"), "registry=https://project.example.test/npm/\n"); - writeFileSync(join(home, ".npmrc"), "registry=https://user.example.test/npm/\n"); - - expect(resolveNpmRegistry(project)).toBe("https://project.example.test/npm"); - }); - - test("user .npmrc is used when the project has no .npmrc", () => { - clearRegistryEnv(); - const project = makeScratchDir(); - const home = makeScratchDir(); - process.env.HOME = home; - process.env.USERPROFILE = home; - writeFileSync(join(home, ".npmrc"), "registry=https://user.example.test/npm/\n"); - - expect(resolveNpmRegistry(project)).toBe("https://user.example.test/npm"); - }); - - test("falls back to the public default when nothing is configured", () => { - clearRegistryEnv(); - const project = makeScratchDir(); - const home = makeScratchDir(); - process.env.HOME = home; - process.env.USERPROFILE = home; - - expect(resolveNpmRegistry(project)).toBe("https://registry.npmjs.org"); - }); - - test("trailing slashes are trimmed", () => { - clearRegistryEnv(); - process.env[REGISTRY_KEY] = "https://env.example.test/npm///"; - - expect(resolveNpmRegistry(null)).toBe("https://env.example.test/npm"); - }); - - test("comment lines and scoped-registry keys are ignored", () => { - clearRegistryEnv(); - process.env.HOME = ""; - process.env.USERPROFILE = ""; - const project = makeScratchDir(); - writeFileSync( - join(project, ".npmrc"), - [ - "# operator comment", - "; another comment", - "@cortexkit:registry=https://scoped.example.test/npm/", - "", - ].join("\n"), - ); - - expect(resolveNpmRegistry(project)).toBe("https://registry.npmjs.org"); - }); - - test("a malformed .npmrc fails open to the default", () => { - clearRegistryEnv(); - process.env.HOME = ""; - process.env.USERPROFILE = ""; - const project = makeScratchDir(); - // A directory named .npmrc makes readFileSync throw. - mkdirSync(join(project, ".npmrc"), { recursive: true }); - - expect(resolveNpmRegistry(project)).toBe("https://registry.npmjs.org"); - }); - - test("a null project directory skips the project file", () => { - clearRegistryEnv(); - const home = makeScratchDir(); - process.env.HOME = home; - process.env.USERPROFILE = home; - writeFileSync(join(home, ".npmrc"), "registry=https://user.example.test/npm/\n"); - - expect(resolveNpmRegistry(null)).toBe("https://user.example.test/npm"); - }); -}); diff --git a/packages/plugin/src/hooks/auto-update-checker/resolve-npm-registry.ts b/packages/plugin/src/hooks/auto-update-checker/resolve-npm-registry.ts deleted file mode 100644 index 5aa25d8565..0000000000 --- a/packages/plugin/src/hooks/auto-update-checker/resolve-npm-registry.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { existsSync, readFileSync } from "node:fs"; -import { join } from "node:path"; - -import { NPM_REGISTRY_URL } from "./constants"; - -/** - * Resolves the npm registry the update check should query. - * - * The checker used to hardcode registry.npmjs.org, which broke behind - * registries an operator configures for npm/Bun (mirrors, private proxies): - * installs already honor the configured registry because the spawned package - * manager reads the .npmrc chain itself, only the check bypassed it. - * - * Resolution order mirrors npm's own precedence: - * 1. `npm_config_registry` in the environment (npm and bun both export it - * to child processes when a registry is configured). - * 2. `registry = ` in the project `.npmrc` (the OpenCode project - * directory). - * 3. `registry = ` in the user `.npmrc`. - * 4. The public default. - * - * Never throws: an unreadable or malformed .npmrc fails open to the next - * source, so a broken config cannot take the update check down. - */ -export function resolveNpmRegistry(projectDir: string | null | undefined): string { - const fromEnv = process.env.npm_config_registry?.trim(); - if (fromEnv) return normalizeRegistryUrl(fromEnv); - - for (const npmrcPath of npmrcCandidates(projectDir)) { - const fromNpmrc = readRegistryFromNpmrc(npmrcPath); - if (fromNpmrc) return normalizeRegistryUrl(fromNpmrc); - } - - return NPM_REGISTRY_URL; -} - -function npmrcCandidates(projectDir: string | null | undefined): string[] { - const candidates: string[] = []; - if (projectDir) candidates.push(join(projectDir, ".npmrc")); - if (process.env.HOME) candidates.push(join(process.env.HOME, ".npmrc")); - if (process.env.USERPROFILE) candidates.push(join(process.env.USERPROFILE, ".npmrc")); - return candidates; -} - -/** - * Extracts the `registry` value from an .npmrc-style file. Only the - * top-level `registry` key matters here; scoped-registry keys - * (`@scope:registry`) configure publication targets, not the package - * metadata source for a specific package name. - */ -function readRegistryFromNpmrc(npmrcPath: string): string | null { - if (!existsSync(npmrcPath)) return null; - try { - const lines = readFileSync(npmrcPath, "utf-8").split(/\r?\n/); - for (const rawLine of lines) { - const line = rawLine.trim(); - if (!line || line.startsWith(";") || line.startsWith("#")) continue; - const match = /^registry\s*=\s*(\S+)\s*$/.exec(line); - if (match?.[1]) return match[1]; - } - } catch { - return null; - } - return null; -} - -function normalizeRegistryUrl(url: string): string { - return url.replace(/\/+$/, ""); -} diff --git a/packages/plugin/src/index.ts b/packages/plugin/src/index.ts index c9204ea120..eb4c573366 100644 --- a/packages/plugin/src/index.ts +++ b/packages/plugin/src/index.ts @@ -38,7 +38,10 @@ import { } from "./features/magic-context/storage-db"; import { recordToolDefinition } from "./features/magic-context/tool-definition-tokens"; import { runDeferredV22Backfill } from "./features/magic-context/v22-deferred-backfill"; -import { createAutoUpdateCheckerHook, resolveNpmRegistry } from "./hooks/auto-update-checker"; +import { + autoUpdateCheckerOptionsFromConfig, + createAutoUpdateCheckerHook, +} from "./hooks/auto-update-checker"; import { COMPARTMENT_AGENT_SYSTEM_PROMPT, COMPARTMENT_STRUCTURAL_SYSTEM_PROMPT, @@ -881,14 +884,8 @@ const server: Plugin = async (ctx) => { }, }, autoUpdateChecker: createAutoUpdateCheckerHook(ctx, { - // auto_update: false skips the checker entirely — no npm - // request, matching the host updater's disable behavior. - enabled: pluginConfig.auto_update !== false, - autoUpdate: pluginConfig.auto_update !== false, - // Installs honor the operator's configured registry (the - // spawned package manager reads the .npmrc chain); the check - // resolves the same registry instead of hardcoding npmjs. - npmRegistryUrl: resolveNpmRegistry(ctx.directory), + // auto_update: false skips the checker entirely: no npm request. + ...autoUpdateCheckerOptionsFromConfig(pluginConfig.auto_update), signal: autoUpdateAbort.signal, // Multi-project plugin reloads coordinate via this on-disk // timestamp so npm gets hit at most once per check window From a8b6b250dd7501e46be6d5415aabb73636922645 Mon Sep 17 00:00:00 2001 From: Oscar Williams Date: Sun, 8 Nov 2026 17:12:58 +0800 Subject: [PATCH 3/4] refactor(auto-update-checker): keep the hook docblock attached to its function --- .../src/hooks/auto-update-checker/index.ts | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/packages/plugin/src/hooks/auto-update-checker/index.ts b/packages/plugin/src/hooks/auto-update-checker/index.ts index 1aba6854e4..1943657b6e 100644 --- a/packages/plugin/src/hooks/auto-update-checker/index.ts +++ b/packages/plugin/src/hooks/auto-update-checker/index.ts @@ -46,6 +46,18 @@ function warn(message: string): void { log(`WARN: ${message}`); } +/** + * Maps the plugin's `auto_update` setting to checker options. `false` turns + * the checker off entirely so no npm request is made, matching the host + * updater's disable behavior. + */ +export function autoUpdateCheckerOptionsFromConfig( + autoUpdateSetting: boolean | undefined, +): Pick { + const enabled = autoUpdateSetting !== false; + return { enabled, autoUpdate: enabled }; +} + /** * Auto-update checker. * @@ -74,18 +86,6 @@ function warn(message: string): void { * that pass synthetic events keep working — the hook itself never * triggers a check now. */ -/** - * Maps the plugin's `auto_update` setting to checker options. `false` turns - * the checker off entirely so no npm request is made, matching the host - * updater's disable behavior. - */ -export function autoUpdateCheckerOptionsFromConfig( - autoUpdateSetting: boolean | undefined, -): Pick { - const enabled = autoUpdateSetting !== false; - return { enabled, autoUpdate: enabled }; -} - export function createAutoUpdateCheckerHook( ctx: PluginInput, options: AutoUpdateCheckerOptions = {}, From fea8a8aacf20d12b27076c5089577ab95eceed4c Mon Sep 17 00:00:00 2001 From: Oscar <319719771+Oscar-Williams@users.noreply.github.com> Date: Mon, 9 Nov 2026 15:34:54 +0000 Subject: [PATCH 4/4] e2e: re-pin the OpenCode 2 index.ts hash after auto_update:false Co-authored-by: Oscar --- packages/e2e-tests/src/opencode2-runner/sha256-pins.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/e2e-tests/src/opencode2-runner/sha256-pins.json b/packages/e2e-tests/src/opencode2-runner/sha256-pins.json index 149d198424..75f8a0cdc5 100644 --- a/packages/e2e-tests/src/opencode2-runner/sha256-pins.json +++ b/packages/e2e-tests/src/opencode2-runner/sha256-pins.json @@ -3,7 +3,7 @@ "packages/e2e-tests/mutations/goldens-dg-1.json": "3460cfe3cfa31ef3a4576235ac24fba0b36d9372804fd941a9c7ab6d239b52af", "packages/e2e-tests/mutations/goldens-dg-2.json": "f8b7e04765f428b159fa21306fa6e70b7918a92309537b67af1a6cc6a5b69eda", "packages/e2e-tests/mutations/goldens-dg-3.json": "155c8ec37fb0be2d906a0000e2671d5e473106ae70b02b0cfb0d1ee8e28f7c41", - "packages/plugin/src/index.ts": "bbe50e7bcd9bd7d8ea12dfbbcc1ef6d08e7be045d45e19c4cb2cd2cfcc031211", + "packages/plugin/src/index.ts": "68ef7e0fb67c5d90270646b513105a1fc116e9646871c9bd9ca92f1974ae5555", "packages/plugin/src/v2/server.ts": "4f0054e8a5fe8628f84e309ecf306b3cde146882e69386117b1c522a3cc0c876", "packages/e2e-tests/src/opencode2-runner/host-rows.json": "1208c136df23a54de1fef24f3ea5c3155f16ed1c28fa3d7e5864aec4cdfbcc82", "packages/e2e-tests/src/opencode2-runner/payload-v1.json": "992fedba515632bccec56dab34a36f96d192a90ee06f1c33d68020bf7bb25091",