This was generated by AI during triage.
Why
OpenCode 1.0.39 rewrites or deletes the global config package-lock.json whenever TUI startup resolves @opencode-ai/plugin from an existing local or bundled copy. The subsequent dirty check sees no lockfile, concludes declared dependencies are missing, and runs Arborist reify again. Users therefore observe npm install on every OpenCode launch.
Reproduction
Using an isolated XDG_CONFIG_HOME containing opencode/package.json, a valid npm lock and installed @opencode-ai/plugin:
- Start
opencode . under a PTY and exit after initialization.
- First run rewrites the lockfile with byte-identical content (mtime changes).
- Second run reaches a state where
package-lock.json is deleted while the reinstall is running.
The headless opencode serve path does not reproduce because it does not wait for the TUI dependency bootstrap.
Root cause
packages/core/src/npm.ts:220-222 removes package-lock.json whenever at least one requested dependency was satisfied locally/bundled (remaining.length !== requested.length). The same call then performs the normal dirty check at lines 245-273. Because the lock was just deleted, every dependency declared by package.json appears unlocked and reify() runs.
This behavior was introduced by b5a1c36494 (fix(core): bundle plugin SDK and degrade references for offline startup). Its two tests currently assert that the lockfile is absent, but do not invoke Npm.install twice or assert that reification was skipped.
Scope
- Preserve a valid npm lock when the requested plugin SDK is already available locally or from the bundled SDK.
- Skip registry/reify work when local/bundled resolution plus the existing dependency state is clean.
- Keep registry fallback working when the plugin SDK is genuinely unavailable or another dependency is missing.
- Replace the lock-deletion pins with deterministic repeated-install regression tests for both local and bundled plugin paths.
- Add an isolated TUI startup regression that runs twice and asserts the config lockfile remains byte- and mtime-stable after dependency initialization.
Acceptance
- Two consecutive
Npm.install calls with an existing local plugin and valid lock do not delete/rewrite the lock and do not invoke Arborist reify.
- The same guarantee holds when the bundled SDK is copied into place.
- Missing
node_modules and genuine lock/package drift still invoke reify and produce a valid lock.
- Two isolated 1.0.39-equivalent TUI initializations leave
package-lock.json present with unchanged bytes and mtime after the first stable run.
- Focused core npm/config/TUI tests, affected package typechecks, and lint pass with no new warnings.
Non-goals
- Changing package managers, lockfile formats, plugin SDK version policy, or the local > bundled > registry precedence.
Why
OpenCode 1.0.39 rewrites or deletes the global config
package-lock.jsonwhenever TUI startup resolves@opencode-ai/pluginfrom an existing local or bundled copy. The subsequent dirty check sees no lockfile, concludes declared dependencies are missing, and runs Arboristreifyagain. Users therefore observenpm installon every OpenCode launch.Reproduction
Using an isolated
XDG_CONFIG_HOMEcontainingopencode/package.json, a valid npm lock and installed@opencode-ai/plugin:opencode .under a PTY and exit after initialization.package-lock.jsonis deleted while the reinstall is running.The headless
opencode servepath does not reproduce because it does not wait for the TUI dependency bootstrap.Root cause
packages/core/src/npm.ts:220-222removespackage-lock.jsonwhenever at least one requested dependency was satisfied locally/bundled (remaining.length !== requested.length). The same call then performs the normal dirty check at lines 245-273. Because the lock was just deleted, every dependency declared bypackage.jsonappears unlocked andreify()runs.This behavior was introduced by
b5a1c36494(fix(core): bundle plugin SDK and degrade references for offline startup). Its two tests currently assert that the lockfile is absent, but do not invokeNpm.installtwice or assert that reification was skipped.Scope
Acceptance
Npm.installcalls with an existing local plugin and valid lock do not delete/rewrite the lock and do not invoke Arborist reify.node_modulesand genuine lock/package drift still invoke reify and produce a valid lock.package-lock.jsonpresent with unchanged bytes and mtime after the first stable run.Non-goals