Description
Installation.method() never establishes where the running executable actually came from. It guesses, in two ways, and both are unsound. The visible symptom is that in-app Update now can never succeed on a root-owned npm global prefix, but that is a consequence of the weaker detection layer beneath it.
1. Path substring matching
// packages/opencode/src/installation/index.ts:267-270
if (process.execPath.includes(path.join(".altimate", "bin"))) return "curl" as Method
if (process.execPath.includes(path.join(".opencode", "bin"))) return "curl" as Method
if (process.execPath.includes(path.join(".local", "bin"))) return "curl" as Method
~/.local/bin is a generic user bin directory, not a marker of a standalone install. With npm config set prefix ~/.local — a common way to avoid needing sudo — npm installs to ~/.local/lib/node_modules and links ~/.local/bin/altimate. That install is classified curl, so altimate upgrade runs curl … | bash, writes a standalone binary into the standalone dir, and leaves the npm-managed copy stale and orphaned. Two installs then coexist, and which one runs depends purely on PATH order.
2. "Is it installed anywhere?" instead of "did this binary come from here?"
// packages/opencode/src/installation/index.ts:303
if (output.includes(installedName)) return check.name
The probe loop runs npm list -g --depth=0, pnpm list -g, bun pm ls -g, etc., and returns the first manager whose output mentions the package. That answers whether a package manager has the package — not whether the currently running executable is the one it manages. When more than one install exists, the result is effectively arbitrary, and the upgrade is routed at an install the user is not running.
It is also needlessly expensive: up to seven subprocess spawns on a code path that runs during startup update checks.
Impact
- Upgrades can target the wrong install. The user upgrades, sees no version change, and repeats — because the upgraded copy is not the one on
PATH.
- An npm install can be silently converted into a standalone install via the
.local/bin path above, orphaning the package-manager copy.
- In-app upgrade is permanently non-functional on a root-owned npm prefix (details below), with no actionable error.
Steps to Reproduce
The permissions case is the easiest reproducer:
- Install into a root-owned npm global prefix (i.e.
npm prefix -g resolves somewhere owned by root:wheel, such as /usr/local):
sudo npm install -g @altimateai/altimate-code@<older-version>
- Launch
altimate and wait for the "new version available" prompt.
- Click Update now.
Expected Behavior
The upgrade acts on the install the user is actually running, and either succeeds or fails with a message naming the cause and the remedy — e.g.:
Cannot write to the npm global prefix (<prefix>). Run sudo npm install -g @altimateai/altimate-code@<version>, or switch to a user-owned prefix with npm config set prefix ~/.npm-global.
Actual Behavior
Installation.upgrade() shells out as the current user with no privilege escalation and no writability check:
// packages/opencode/src/installation/index.ts:389
case "npm":
upgradeResult = yield* run(["npm", "install", "-g", `@altimateai/altimate-code@${target}`])
npm fails with EACCES, and the surfaced error is generic:
Upgrade failed for npm (exit code 243).
upgradeFailure() intentionally suppresses package-manager stderr so tokens/env cannot leak (index.ts:172-177), so the underlying EACCES never reaches the user. The background auto-upgrade path fails identically:
[warn] [upgrade] auto-upgrade failed, notifying instead (method=npm, target=0.11.0-beta.4): UpgradeFailedError: Upgrade failed for npm (exit code 243).
Observed repeatedly across separate sessions (three identical entries in ~/.local/share/opencode/log/tui-worker-*.log) and across multiple target versions. The same command run manually with sudo always succeeds, confirming write permission on the global prefix is the only differing factor.
Suggested Fix
Resolve the install from the running binary instead of guessing. The ground truth is already on disk: a package-manager install links its bin entry into the package directory, e.g.
<prefix>/bin/altimate -> ../lib/node_modules/@altimateai/altimate-code/bin/altimate
whereas a standalone install is a plain compiled executable that does not resolve elsewhere. So:
fs.realpathSync(process.execPath).
- If the resolved path lies inside
…/node_modules/@altimateai/altimate-code/, it is a package-manager install — walk up to the prefix and compare it against each manager's reported global prefix (e.g. npm prefix -g) to identify which one owns it.
- If it resolves under the Homebrew prefix (
brew --prefix), it is brew.
- Otherwise treat it as the standalone/curl install.
This is deterministic, stays correct when several installs coexist, removes the .local/bin misclassification, and replaces up to seven subprocess spawns with one realpath plus at most one prefix query. It is a net simplification rather than added machinery.
The permissions fix then falls out of it. Once the real install root is known, pre-check it with fs.accessSync(dir, W_OK) before attempting anything. If it is not writable, do not shell out at all — surface the exact command to run. This needs no EACCES string-matching and leaves the stderr redaction in upgradeFailure() untouched.
Worth applying the same pre-check to the curl method, which writes to the standalone install dir.
Rejected alternative: auto-sudo on permission failure
Not recommended. A TUI cannot host an interactive password prompt safely, sudo npm install -g executes package lifecycle scripts as root, and it would let a network-sourced version check trigger root-level writes. Printing the command for the user to run is the better trade.
Environment
- Install method: npm global (
method=npm) for the reproducer; the detection flaw is method-independent
- Not platform-specific: applies to any root-owned npm global prefix
Description
Installation.method()never establishes where the running executable actually came from. It guesses, in two ways, and both are unsound. The visible symptom is that in-app Update now can never succeed on a root-owned npm global prefix, but that is a consequence of the weaker detection layer beneath it.1. Path substring matching
~/.local/binis a generic user bin directory, not a marker of a standalone install. Withnpm config set prefix ~/.local— a common way to avoid needingsudo— npm installs to~/.local/lib/node_modulesand links~/.local/bin/altimate. That install is classifiedcurl, soaltimate upgraderunscurl … | bash, writes a standalone binary into the standalone dir, and leaves the npm-managed copy stale and orphaned. Two installs then coexist, and which one runs depends purely onPATHorder.2. "Is it installed anywhere?" instead of "did this binary come from here?"
The probe loop runs
npm list -g --depth=0,pnpm list -g,bun pm ls -g, etc., and returns the first manager whose output mentions the package. That answers whether a package manager has the package — not whether the currently running executable is the one it manages. When more than one install exists, the result is effectively arbitrary, and the upgrade is routed at an install the user is not running.It is also needlessly expensive: up to seven subprocess spawns on a code path that runs during startup update checks.
Impact
PATH..local/binpath above, orphaning the package-manager copy.Steps to Reproduce
The permissions case is the easiest reproducer:
npm prefix -gresolves somewhere owned byroot:wheel, such as/usr/local):sudo npm install -g @altimateai/altimate-code@<older-version>altimateand wait for the "new version available" prompt.Expected Behavior
The upgrade acts on the install the user is actually running, and either succeeds or fails with a message naming the cause and the remedy — e.g.:
Actual Behavior
Installation.upgrade()shells out as the current user with no privilege escalation and no writability check:npm fails with
EACCES, and the surfaced error is generic:upgradeFailure()intentionally suppresses package-manager stderr so tokens/env cannot leak (index.ts:172-177), so the underlyingEACCESnever reaches the user. The background auto-upgrade path fails identically:Observed repeatedly across separate sessions (three identical entries in
~/.local/share/opencode/log/tui-worker-*.log) and across multiple target versions. The same command run manually withsudoalways succeeds, confirming write permission on the global prefix is the only differing factor.Suggested Fix
Resolve the install from the running binary instead of guessing. The ground truth is already on disk: a package-manager install links its bin entry into the package directory, e.g.
whereas a standalone install is a plain compiled executable that does not resolve elsewhere. So:
fs.realpathSync(process.execPath).…/node_modules/@altimateai/altimate-code/, it is a package-manager install — walk up to the prefix and compare it against each manager's reported global prefix (e.g.npm prefix -g) to identify which one owns it.brew --prefix), it isbrew.This is deterministic, stays correct when several installs coexist, removes the
.local/binmisclassification, and replaces up to seven subprocess spawns with onerealpathplus at most one prefix query. It is a net simplification rather than added machinery.The permissions fix then falls out of it. Once the real install root is known, pre-check it with
fs.accessSync(dir, W_OK)before attempting anything. If it is not writable, do not shell out at all — surface the exact command to run. This needs noEACCESstring-matching and leaves the stderr redaction inupgradeFailure()untouched.Worth applying the same pre-check to the
curlmethod, which writes to the standalone install dir.Rejected alternative: auto-
sudoon permission failureNot recommended. A TUI cannot host an interactive password prompt safely,
sudo npm install -gexecutes package lifecycle scripts as root, and it would let a network-sourced version check trigger root-level writes. Printing the command for the user to run is the better trade.Environment
method=npm) for the reproducer; the detection flaw is method-independent