Skip to content

feat(core): enable Node.js runtime alongside Bun#16825

Closed
thdxr wants to merge 31 commits intodevfrom
refactor/lsp-core-improvements
Closed

feat(core): enable Node.js runtime alongside Bun#16825
thdxr wants to merge 31 commits intodevfrom
refactor/lsp-core-improvements

Conversation

@thdxr
Copy link
Member

@thdxr thdxr commented Mar 10, 2026

Summary

Refactors and improvements to the LSP server and core components.

Changes

  • LSP server updates
  • Configuration improvements
  • Provider and plugin enhancements
  • Core utility improvements

@greptile-apps
Copy link

greptile-apps bot commented Mar 10, 2026

Greptile Summary

This PR replaces the ad-hoc Bun-based package installation (BunProc.install) with a new Npm module backed by @npmcli/arborist, and refactors the LSP server to resolve language-server binaries via Npm.which() rather than manually locating pre-downloaded JS files. The overall direction is cleaner and more portable, but the new Npm module introduces several issues that need attention before merging.

Key changes:

  • New packages/opencode/src/npm/index.ts module using @npmcli/arborist for package installation and binary resolution
  • LSPServer now calls Npm.which(<pkg>) instead of spawning bun run <js-path>; significantly reduces per-language boilerplate
  • BunProc.install() removed; Plugin, Provider, and Config now delegate to Npm.add() / Npm.install()
  • Global.Path.bin relocated from the XDG data dir to the XDG cache dir (cleared on version bump, forcing re-installation)
  • util/which.ts now always appends Global.Path.bin to PATH, removing the need for callers to pass it manually

Issues found:

  • Infinite recursion in Npm.which: if a package installs but exposes no .bin entries, the function recurses forever
  • Wrong binary selected: Npm.which returns files[0] (first alphabetically), which can be the wrong binary for packages that expose multiple executables (e.g. @astrojs/language-server)
  • console.log debug statement left in Npm.install
  • .gitignore unconditionally overwritten in Config init — any user customisations are silently lost on each startup
  • Potential "undefined" prefix in PATH in util/which.ts when all PATH env vars are absent

Confidence Score: 2/5

  • Not safe to merge — the new Npm module contains an infinite recursion bug and incorrect binary selection that will silently break LSP servers for several languages.
  • The core logic of Npm.which() has two correctness bugs: infinite recursion when a package has no bin entries, and returning the wrong binary for packages that expose multiple executables. These directly affect LSP server startup for Vue, Astro, and any other multi-binary package. The debug console.log and unconditional .gitignore overwrite are lower severity but still warrant fixing before shipping.
  • packages/opencode/src/npm/index.ts requires the most attention — the which function's infinite recursion and wrong-binary-selection bugs are the highest risk items.

Important Files Changed

Filename Overview
packages/opencode/src/npm/index.ts New module replacing BunProc for package management via @npmcli/arborist. Contains three critical issues: a debug console.log, infinite recursion risk in which when a package has no bin entries, and incorrect binary selection for multi-binary packages.
packages/opencode/src/lsp/server.ts LSP server refactored to use Npm.which() for binary resolution instead of manually managing Bun-installed JS files. Simplifies installation logic significantly. Correctness depends on Npm.which() returning the right binary for each package.
packages/opencode/src/config/config.ts Switched from BunProc to Npm.install for dependency installation in config directories. The .gitignore is now unconditionally overwritten on every init, breaking user customisations.
packages/opencode/src/util/which.ts Refactored to always append Global.Path.bin to PATH instead of requiring callers to pass it. The PATH base could be undefined in edge cases, producing an invalid "undefined:/path" string.
packages/opencode/src/global/index.ts Moved Global.Path.bin from the XDG data directory to the XDG cache directory. This is intentional — the cache directory is cleared on version bumps, which forces re-installation of managed binaries.
packages/opencode/src/bun/index.ts Removed the BunProc.install() and related package management code; installation responsibility now lives in Npm module. Remaining code is clean.
packages/opencode/src/plugin/index.ts Switched from BunProc.install to Npm.add for plugin installation. The version-parsing logic (lastIndexOf "@") was removed; Npm.add accepts the full package specifier string directly, which is simpler.
packages/opencode/src/provider/provider.ts One-line change replacing BunProc.install with Npm.add for provider package installation. Clean and consistent with the rest of the refactor.
packages/opencode/package.json Added @npmcli/arborist and its type definitions as dependencies to support the new Npm module.
.opencode/plans/1771135996918-mighty-wizard.md Planning document for a future "Project References" feature. No production code impact; committed as part of the opencode session artifacts.

Sequence Diagram

sequenceDiagram
    participant LSP as LSPServer / Plugin / Provider
    participant Npm as Npm module
    participant Arb as @npmcli/arborist
    participant FS as Filesystem (.bin)

    LSP->>Npm: which(pkg)
    Npm->>FS: readdir(cache/packages/<pkg>/node_modules/.bin)
    alt .bin exists and non-empty
        FS-->>Npm: [files]
        Npm-->>LSP: path to files[0]
    else .bin empty or missing
        FS-->>Npm: []
        Npm->>Npm: add(pkg)
        Npm->>Arb: new Arborist({ path: cache/packages/<pkg> })
        Arb->>Arb: loadVirtual()
        alt already installed (virtual tree exists)
            Arb-->>Npm: tree with edgesOut
            Npm-->>LSP: first.path (skips reify)
        else not installed
            Arb->>Arb: reify({ add: [pkg] })
            Arb-->>Npm: result tree
            Npm-->>Npm: return first.path
        end
        Npm->>Npm: which(pkg) [recursive call]
        Npm->>FS: readdir(...)
        FS-->>Npm: [files] (or [] → infinite recursion risk)
        Npm-->>LSP: path to files[0]
    end
Loading

Last reviewed commit: 64b5e70

Comment on lines +75 to +83
export async function which(pkg: string) {
const dir = path.join(directory(pkg), "node_modules", ".bin")
const files = await readdir(dir).catch(() => [])
if (!files.length) {
await add(pkg)
return which(pkg)
}
return path.join(dir, files[0])
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unbounded infinite recursion when package has no bin entries

which calls add(pkg), then unconditionally recurses via return which(pkg). If the package installs successfully but exposes zero bin entries (no bin field in package.json, or binLinks fails to create symlinks), readdir returns [] again and the function recurses forever, eventually crashing with a stack overflow.

A base-case guard is needed after the install attempt:

Suggested change
export async function which(pkg: string) {
const dir = path.join(directory(pkg), "node_modules", ".bin")
const files = await readdir(dir).catch(() => [])
if (!files.length) {
await add(pkg)
return which(pkg)
}
return path.join(dir, files[0])
}
export async function which(pkg: string) {
const dir = path.join(directory(pkg), "node_modules", ".bin")
const files = await readdir(dir).catch(() => [])
if (!files.length) {
await add(pkg)
const filesAfterInstall = await readdir(dir).catch(() => [])
if (!filesAfterInstall.length) throw new InstallFailedError({ pkg })
return path.join(dir, filesAfterInstall[0])
}
return path.join(dir, files[0])
}

await add(pkg)
return which(pkg)
}
return path.join(dir, files[0])
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

which picks first alphabetical binary — wrong for multi-binary packages

readdir returns directory entries in filesystem order (alphabetical on most systems). Packages like @astrojs/language-server (exposes astro-ls) or @vue/language-server (exposes vue-language-server) may also install helper binaries. Taking files[0] silently picks whichever name sorts first, which is not guaranteed to be the main entrypoint binary.

For example, if @astrojs/language-server installs astro-check and astro-ls into .bin, files[0] would be astro-check, not astro-ls.

Consider accepting an explicit binary name as a parameter, or looking up the bin field from the installed package's package.json.

thdxr and others added 4 commits March 9, 2026 21:39
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Formatters now determine their executable location when enabled rather than
using hardcoded paths. This ensures formatters work correctly regardless
of how the tool was installed or where executables are located on the system.
@thdxr thdxr force-pushed the refactor/lsp-core-improvements branch 2 times, most recently from 59a7d8d to 7478e07 Compare March 10, 2026 04:34
thdxr and others added 4 commits March 10, 2026 11:40
Enable running opencode on Node.js by adding platform-specific database adapters and replacing Bun-specific shell execution with cross-platform Process utility.
@thdxr thdxr changed the title refactor: lsp server and core improvements feat(core): enable Node.js runtime alongside Bun Mar 10, 2026
thdxr added 4 commits March 10, 2026 12:14
Plugins no longer receive shell access or server URL to prevent unauthorized
execution and limit plugin sandbox surface area.
…erver

- Enables graceful server shutdown for workspace management
- Removes unsupported serverUrl getter that threw errors in plugin context
- Removed debug console.log when dependency installation fails so users see clean warning messages instead of raw error dumps
- Fixed database connection cleanup to prevent resource leaks between sessions
- Added support for loading custom tools from both .opencode/tool (singular) and .opencode/tools (plural) directories, matching common naming conventions
@thdxr thdxr closed this Mar 10, 2026
@thdxr thdxr deleted the refactor/lsp-core-improvements branch March 10, 2026 17:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants