Skip to content

fix: resolve symlinked argv1 for Control UI asset detection#14637

Closed
aynorica wants to merge 2 commits intoopenclaw:mainfrom
aynorica:fix/4855-control-ui-assets-npm-global-install
Closed

fix: resolve symlinked argv1 for Control UI asset detection#14637
aynorica wants to merge 2 commits intoopenclaw:mainfrom
aynorica:fix/4855-control-ui-assets-npm-global-install

Conversation

@aynorica
Copy link
Contributor

@aynorica aynorica commented Feb 12, 2026

Summary

Fixes #4855 — Control UI assets not found on npm install -g openclaw when using symlink-based Node version managers (nvm, fnm, n, Homebrew/Linuxbrew).

The root cause is that process.argv[1] preserves the symlink path (e.g. ~/.nvm/versions/node/v22/bin/openclaw) rather than resolving to the real file inside node_modules/openclaw/. The directory walk-up from the symlink directory never finds package.json with name: "openclaw", so asset resolution returns null.

This PR applies fs.realpathSync() to argv1 in candidateDirsFromArgv1() and propagates moduleUrl to the async resolver, covering all resolution paths.

lobster-biscuit

Repro Steps

  1. Install Node via nvm/fnm/n/Homebrew
  2. npm install -g openclaw
  3. openclaw gateway
  4. Access http://127.0.0.1:18789/ → "Missing Control UI assets"

Root Cause

candidateDirsFromArgv1() in src/infra/openclaw-root.ts calls path.resolve(argv1) which does not follow symlinks. For symlink-based version managers:

  • argv1 = ~/.nvm/versions/node/v22/bin/openclaw (symlink)
  • path.dirname(argv1) = the bin/ directory, not the package root
  • Walk-up never finds package.json with name: "openclaw"

Node.js does not resolve symlinks for argv[1] (empirically verified), but fs.realpathSync() does.

Behavior Changes

  • candidateDirsFromArgv1() now also tries fs.realpathSync(argv1) and adds the resolved directory as a candidate alongside the original. If realpathSync throws (path doesn't exist), the original candidates are preserved — no change in behavior for non-symlink installs.
  • resolveControlUiDistIndexPath() now accepts an optional moduleUrl parameter (backward-compatible — still accepts a bare string) and forwards it to resolveOpenClawPackageRoot(), matching the sync resolver's existing pattern.
  • resolveControlUiDistIndexHealth() forwards moduleUrl when available.

No behavior change for standard npm global installs, local installs, or packaged app installs — these paths are unaffected since realpathSync returns the same path when no symlinks exist.

Codebase and GitHub Search

Tests

3 new test cases added to src/infra/control-ui-assets.test.ts:

Test What it validates
resolves package root when argv1 is a symlink resolveOpenClawPackageRoot() follows symlinks via realpathSync
resolves control-ui root when argv1 is a symlink resolveControlUiRootSync() finds dist/control-ui/ through symlinked argv1
resolves dist index path when argv1 is a symlink (async) resolveControlUiDistIndexPath() returns correct index.html through symlinked argv1

All 17 tests pass (14 existing + 3 new). Zero regressions.

Symlink tests use fs.symlinkSync with relative targets, matching real nvm/fnm behavior. Tests skip gracefully if symlink creation fails (Windows CI without elevated privileges).

Manual Testing

Prerequisites
  • Linux x64, Node v25.6.0
Steps
  1. Created simulated nvm layout: bin/openclaw → symlink → lib/node_modules/openclaw/openclaw.mjs
  2. Verified process.argv[1] returns the symlink path (not resolved)
  3. Confirmed fs.realpathSync(argv1) resolves to the real package path
  4. Ran npx vitest run src/infra/control-ui-assets.test.ts — 17/17 pass
  5. Ran npx tsgo --noEmit — clean
  6. Ran npx oxlint + npx oxfmt --check on changed files — clean

Evidence

 ✓ src/infra/control-ui-assets.test.ts (17 tests) 36ms

 Test Files  1 passed (1)
      Tests  17 passed (17)
$ npx oxlint src/infra/openclaw-root.ts src/infra/control-ui-assets.ts src/infra/control-ui-assets.test.ts
Found 0 warnings and 0 errors.

$ npx oxfmt --check src/infra/openclaw-root.ts src/infra/control-ui-assets.ts src/infra/control-ui-assets.test.ts
All matched files use the correct format.

Files changed (3):

File Change
src/infra/openclaw-root.ts Add realpathSync fallback in candidateDirsFromArgv1() (+12 lines)
src/infra/control-ui-assets.ts Accept moduleUrl in async resolver, forward to resolveOpenClawPackageRoot (+10 lines, -3 lines)
src/infra/control-ui-assets.test.ts 3 new symlink resolution test cases (+62 lines)

Sign-Off

  • Models used: Claude Opus 4.6 (AI-assisted, human-reviewed)
  • Submitter effort: Researched across Windows + Linux, simulated 8 version manager layouts, empirically verified Node.js argv[1] symlink behavior, implemented targeted fix with tests
  • Agent notes: Fix follows existing codebase pattern (realpathSync already used for execPath in same file). Backward-compatible — bare string still accepted by async resolver. The realpathSync call is a single syscall with negligible perf impact.

Greptile Overview

Greptile Summary

This PR fixes Control UI asset discovery for global installs where the CLI entrypoint (process.argv[1]) is a symlink (common with nvm/fnm/n/Homebrew). The core change is in src/infra/openclaw-root.ts, where candidateDirsFromArgv1() now adds a fs.realpathSync()-resolved directory as an additional candidate so the package-root walk can reach the real node_modules/openclaw location.

To ensure the async Control UI resolver benefits from the same root-discovery inputs as the sync path, resolveControlUiDistIndexPath() now accepts either a string argv1 or an { argv1, moduleUrl } options object and forwards moduleUrl into resolveOpenClawPackageRoot(). Tests in src/infra/control-ui-assets.test.ts add coverage for resolving through a symlinked argv1, matching the reported global-install scenario.

Confidence Score: 5/5

  • This PR is safe to merge with minimal risk.
  • Changes are localized to path discovery helpers, maintain backward compatibility for existing call patterns, and are covered by new tests that exercise the symlinked-argv1 regression scenario. Repo-wide search found no other call sites impacted by the function signature broadening.
  • No files require special attention

(2/5) Greptile learns from your feedback when you react with thumbs up/down!

Copilot AI review requested due to automatic review settings February 12, 2026 12:40
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Fixes Control UI asset detection for global installs when process.argv[1] points at a symlinked shim (common with nvm/fnm/n/Homebrew setups), ensuring the resolver can still locate the openclaw package root and dist/control-ui/index.html.

Changes:

  • Resolve symlinks in candidateDirsFromArgv1() via realpathSync() to add the real entrypoint directory as a package-root candidate.
  • Extend the async Control UI index resolver to accept/propagate moduleUrl (parity with existing sync resolution patterns).
  • Add Vitest coverage for symlinked argv1 scenarios (package root, control-ui root, async index path).

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

File Description
src/infra/openclaw-root.ts Adds realpathSync-based candidate directory to support symlinked argv1 resolution.
src/infra/control-ui-assets.ts Threads optional moduleUrl through async path resolution to improve robustness.
src/infra/control-ui-assets.test.ts Adds tests validating symlinked argv1 resolution for root/index detection.

@aynorica aynorica force-pushed the fix/4855-control-ui-assets-npm-global-install branch from 40c484a to a25137b Compare February 12, 2026 12:54
@gumadeiras
Copy link
Member

Landed on main.

Thanks @aynorica!

@gumadeiras gumadeiras closed this Feb 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Control UI assets not found on npm global install (resolveControlUiDistIndexPath fails)

2 participants

Comments