fix(daemon): resolve Homebrew Cellar path to stable symlink for gateway install#32185
Conversation
Greptile SummaryThis PR fixes a real-world breakage where Key concerns:
Confidence Score: 3/5
Last reviewed commit: 0597772 |
src/daemon/runtime-paths.ts
Outdated
| const cellarMatch = nodePath.match(/^(.+?)\/Cellar\/[^/]+\/[^/]+\/bin\/node$/); | ||
| if (!cellarMatch) { | ||
| return nodePath; | ||
| } | ||
| const stablePath = `${cellarMatch[1]}/bin/node`; |
There was a problem hiding this comment.
Incorrect stable path for versioned Homebrew formulas (node@22, node@20, etc.)
The regex captures the prefix before /Cellar/ and always resolves to <prefix>/bin/node. For the default node formula this is correct (/opt/homebrew/bin/node), but for versioned formulas like node@22 the Homebrew-managed stable path is the keg path — e.g. /opt/homebrew/opt/node@22/bin/node — not /opt/homebrew/bin/node.
Problematic scenario: A user runs openclaw gateway install with node@22 as their active runtime (e.g. process.execPath = /opt/homebrew/Cellar/node@22/22.12.0/bin/node) while the default node formula (v24+) is also installed and linked at /opt/homebrew/bin/node. In this case:
- The regex matches and
stablePathbecomes/opt/homebrew/bin/node. fs.accesssucceeds because the default Node binary exists there.- The function returns
/opt/homebrew/bin/node(pointing to Node 24+) instead of the user's intended Node 22 binary. - The plist ends up referencing the wrong Node binary entirely.
A more robust approach would capture the formula name and resolve to the keg path for versioned formulas, or restrict the match to only the unversioned node formula:
// Option A – restrict to the unversioned `node` formula only, letting versioned
// formulas fall back to their original Cellar paths (safe, no false substitution):
const cellarMatch = nodePath.match(/^(.+?)\/Cellar\/node\/[^/]+\/bin\/node$/);
// Option B – resolve versioned formulas to their keg path instead:
const cellarMatch = nodePath.match(/^(.+?)\/Cellar\/(node(?:@\d+)?)\/[^/]+\/bin\/node$/);
if (!cellarMatch) return nodePath;
const [, prefix, formula] = cellarMatch;
const stablePath = formula === "node"
? `${prefix}/bin/node`
: `${prefix}/opt/${formula}/bin/node`;Prompt To Fix With AI
This is a comment left during a code review.
Path: src/daemon/runtime-paths.ts
Line: 163-167
Comment:
**Incorrect stable path for versioned Homebrew formulas (`node@22`, `node@20`, etc.)**
The regex captures the prefix before `/Cellar/` and always resolves to `<prefix>/bin/node`. For the default `node` formula this is correct (`/opt/homebrew/bin/node`), but for versioned formulas like `node@22` the Homebrew-managed stable path is the keg path — e.g. `/opt/homebrew/opt/node@22/bin/node` — not `/opt/homebrew/bin/node`.
**Problematic scenario:** A user runs `openclaw gateway install` with `node@22` as their active runtime (e.g. `process.execPath` = `/opt/homebrew/Cellar/node@22/22.12.0/bin/node`) while the default `node` formula (v24+) is also installed and linked at `/opt/homebrew/bin/node`. In this case:
1. The regex matches and `stablePath` becomes `/opt/homebrew/bin/node`.
2. `fs.access` succeeds because the *default* Node binary exists there.
3. The function returns `/opt/homebrew/bin/node` (pointing to Node 24+) instead of the user's intended Node 22 binary.
4. The plist ends up referencing the wrong Node binary entirely.
A more robust approach would capture the formula name and resolve to the keg path for versioned formulas, or restrict the match to only the unversioned `node` formula:
```typescript
// Option A – restrict to the unversioned `node` formula only, letting versioned
// formulas fall back to their original Cellar paths (safe, no false substitution):
const cellarMatch = nodePath.match(/^(.+?)\/Cellar\/node\/[^/]+\/bin\/node$/);
// Option B – resolve versioned formulas to their keg path instead:
const cellarMatch = nodePath.match(/^(.+?)\/Cellar\/(node(?:@\d+)?)\/[^/]+\/bin\/node$/);
if (!cellarMatch) return nodePath;
const [, prefix, formula] = cellarMatch;
const stablePath = formula === "node"
? `${prefix}/bin/node`
: `${prefix}/opt/${formula}/bin/node`;
```
How can I resolve this? If you propose a fix, please make it concise.…ay install When `openclaw gateway install` runs under Homebrew Node, `process.execPath` resolves to the versioned Cellar path (e.g. /opt/homebrew/Cellar/node/25.7.0/bin/node). This path breaks when Homebrew upgrades Node, silently killing the gateway daemon. Resolve Cellar paths to the stable Homebrew symlink (/opt/homebrew/bin/node) which Homebrew updates automatically during upgrades. Closes openclaw#32182 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…olution Address review feedback: versioned Homebrew formulas (node@22, node@20) use keg-only paths where the stable symlink is at <prefix>/opt/<formula>/bin/node, not <prefix>/bin/node. Updated resolveStableNodePath to: 1. Try <prefix>/opt/<formula>/bin/node first (works for both default + versioned) 2. Fall back to <prefix>/bin/node for the default "node" formula 3. Return the original Cellar path if neither stable path exists Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
b9d617c to
9810508
Compare
|
Landed via temp rebase onto main.
Thanks @scoootscooob! |
|
Correction with final SHAs: Thanks @scoootscooob! |
Summary
openclaw gateway installwritesprocess.execPathinto the LaunchAgent plist. Under Homebrew Node, this resolves to the versioned Cellar path (e.g./opt/homebrew/Cellar/node/25.7.0/bin/node) which breaks when Homebrew upgrades Node.resolveStableNodePath()that detects Cellar paths and resolves them to the stable Homebrew symlink (/opt/homebrew/bin/node) which Homebrew updates automatically during upgrades./opt/homebrew/Cellar/...) and Intel Mac (/usr/local/Cellar/...) paths. Falls back gracefully to the original path if the symlink doesn't exist.Closes #32182
Test plan
resolveStableNodePathunit tests (5 cases): Cellar → symlink, Intel Mac Cellar → symlink, missing symlink fallback, non-Cellar passthrough, system path passthroughresolvePreferredNodePathresolves Cellar execPath to stable symlink🤖 Generated with Claude Code