Skip to content

Plugins using npm dist-tags (e.g. @dev) never auto-update due to bun lockfile caching #12712

Closed

Description

@ErcinDedeoglu

Problem

When a plugin is configured with an npm dist-tag specifier (e.g. "oc-solomemory@dev"), BunProc.install() resolves the tag to a concrete version on first install and writes it into bun.lock. On subsequent startups, bun sees the specifier "dev" hasn't changed, trusts the lockfile, and skips re-resolving — even though the dist-tag now points to a newer version on npm.

This means plugins using dist-tags like @dev, @beta, @canary, @next etc. are effectively pinned to whatever version was first installed, and never auto-update.

Steps to Reproduce

  1. Configure a plugin with a dist-tag: "plugin": ["my-plugin@dev"]
  2. Start opencode — plugin installs correctly (e.g. 1.0.7-dev.20260208142630.abc1234)
  3. Publish a new version to the same dist-tag (e.g. 1.0.7-dev.20260208144503.def5678)
  4. Restart opencode — plugin stays on the old version

Root Cause

In packages/opencode/src/bun/index.ts (around line 78), BunProc.install() compares the specifier from config against what's in package.json. Since both are "dev", it either skips install entirely or runs bun install which is a no-op due to the lockfile.

Deleting just bun.lock is insufficient — bun also short-circuits when node_modules/<package> already exists. Both bun.lock AND the cached node_modules/<package> directory must be deleted to force re-resolution.

Current Workaround

rm -rf ~/.cache/opencode/node_modules/<plugin-name> ~/.cache/opencode/bun.lock
# Then restart opencode

Suggested Fix

For plugin specifiers that are dist-tags (not semver ranges or exact versions), BunProc.install() should either:

  1. Pass --force to bun for dist-tag specifiers, forcing re-resolution on every startup
  2. Delete the lockfile entry for dist-tag plugins before running bun install
  3. Run bun update <package> instead of bun install when the specifier is a dist-tag

Detection heuristic: if the specifier after @ is not a valid semver range (i.e. doesn't start with a digit, ^, ~, >=, etc.), it's a dist-tag.

Environment

  • opencode: latest (upstream anomalyco/opencode)
  • bun: 1.x
  • OS: Linux (Ubuntu)

Activity

  1. github-actions commented on Feb 8, 2026

    @github-actions
    Contributor

    This issue might be a duplicate of existing issues. Please check:

    Feel free to ignore if none of these address your specific case.

  2. ErcinDedeoglu commented on Feb 8, 2026

    @ErcinDedeoglu
    Author

    Root Cause Found

    The issue is in BunProc.install() at packages/opencode/src/bun/index.ts line 78:

    if (dependencies[pkg] === version && modExists) return mod

    This compares the cached specifier in ~/.cache/opencode/package.json against the config specifier. The install command uses bun add --force --exact:

    const args = ["add", "--force", "--exact", "--cwd", Global.Path.cache, pkg + "@" + version]

    The problem is how bun add --exact writes different dist-tags to package.json:

    Config specifier bun add --exact writes to package.json === "latest" === "dev" Behavior
    my-plugin@latest "3.3.2" (resolved semver) "3.3.2" === "latest" → false — Always re-installs ✅
    my-plugin@dev "dev" (literal tag) — "dev" === "dev" → true Skips forever ❌
    my-plugin@beta "beta" (literal tag) — — Skips forever ❌
    my-plugin@canary "canary" (literal tag) — — Skips forever ❌

    bun add --exact only resolves @latest to a concrete version. All other dist-tags (@dev, @beta, @next, @canary, etc.) are written literally, which means the === check always passes and the plugin never updates.

    @latest accidentally self-heals because the cached value (e.g. "3.3.2") never string-matches "latest", forcing a re-install on every startup.

    Suggested Fix

    For non-latest dist-tags, the equality check needs to resolve the tag before comparing, or alternatively always re-run bun add when the specifier is a dist-tag (not a semver):

    function isDistTag(version: string): boolean {
      // dist-tags are non-semver strings like "dev", "beta", "next", "canary"
      return !/^[\d^~>=<]/.test(version) && version !== "latest"
    }
    
    // In install():
    if (dependencies[pkg] === version && modExists && !isDistTag(version)) return mod

    This would make all dist-tags behave like @latest — always re-resolve on startup.

  3. github-actions commented on Apr 10, 2026

    @github-actions
    Contributor

    To stay organized issues are automatically closed after 90 days of no activity. If the issue is still relevant please open a new one.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions