Skip to content

fix(update): Check for Updates deadline, proxy, self-managed version and re-check - #806

Merged
sirmalloc merged 9 commits into
sirmalloc:mainfrom
eric-engberg:fix/update-check
Nov 6, 2026
Merged

sirmalloc merged 9 commits into
sirmalloc:mainfrom
eric-engberg:fix/update-check

Conversation

@eric-engberg

Copy link
Copy Markdown
Contributor

What

Four fixes to Check for Updates in the TUI:

  • The registry timeout covers the whole request. On a network that drops HTTPS, the check fails after 5 s instead of sitting on "Checking npm registry..." for about 75 s under Bun and Node 18.
  • The registry check honors HTTPS_PROXY and NO_PROXY, as the usage and Claude Status requests already do. All three now read an HTTPS_PROXY with no scheme (proxy.corp:3128) as an HTTP proxy, as curl does.
  • Self-managed global installs compare the global package's own version, not the version of whichever TUI copy is running (npx, bunx, a checkout). A status line that runs a checkout still compares the running TUI's version.
  • The Updates screen checks again after a successful update instead of offering the same install command a second time.

Why (evidence)

Deadline. fetchLatestNpmVersion relied on request.setTimeout(5000). That timer only starts once the socket connects, and Bun's never fires during the connect. With registry.npmjs.org pointed at an address that never answers:

Runtime main this branch
Bun 1.4.2 75,012 ms, then connect ECONNREFUSED 5,006 ms, "timed out after 5000ms"
Node 18.20.8 75,022 ms, then connect ETIMEDOUT 5,024 ms
Node 26.10.0 5,018 ms (its global agent's own 5 s socket timeout) 5,019 ms

Quitting the TUI during that wait on main under Bun cleared the screen, but the process kept running for another 71 s. On this branch it exits once the check gives up at its 5 s deadline, so at most 5 s after the check started.

Proxy. The request had no agent, and neither Node nor Bun reads proxy variables by default. With HTTPS_PROXY pointed at a local proxy and direct connections to the registry dropped, on main the proxy saw no connection: Node failed at 5 s and Bun hung for 75 s. On this branch the proxy logs CONNECT registry.npmjs.org:443, and the check answers in 17–25 ms under Bun 1.4.2, Node 18 and Node 26.

Self-managed version. Take a status line of ccstatusline with no pinned metadata, an npm global at 2.2.20 and a TUI at 2.2.32. Manage Installation showed "Active PATH match: npm global 2.2.20", but Check for Updates said "Current: 2.2.32 … ccstatusline is up to date." In the reverse case (global already at the latest release, TUI older), it offered to reinstall the version already installed.

Stale result. After "Run npm install -g ccstatusline@X" succeeded, the Updates screen still showed the result from before the update: the old Current version and the same command, which reinstalled X if picked again. The same happened after the PATH-warning "Update Complete" notice.

How

  • fetchLatestNpmVersion sets one timer for the whole request. When it fires, it destroys the request and fails the check, the pattern usage-fetch.ts and claude-service-status.ts already use. The socket timeout is gone because the overall timer always fires first. Tests can pass in their own request function, as they do for fetchStatusPagePath.
  • It also uses those files' proxy helpers: createProxyAgent for uppercase HTTPS_PROXY, skipped when isExcludedFromProxy('registry.npmjs.org') matches. When the timer fires, it also closes the agent's pending connection to a proxy that never answers CONNECT. createProxyAgent adds http:// to a value with no scheme://; before, new URL('proxy.corp:3128') took proxy.corp: as the scheme and the agent connected to localhost:80. An HTTPS_PROXY that still isn't a URL fails the check with "HTTPS_PROXY is not a valid URL". The proxy docs now list the update check.
  • getUpdateCheckVersion in App.tsx chooses the version to compare: the installed version for a pinned install, as before; the version found on PATH when the status line runs the global ccstatusline and that version is known; otherwise the running TUI's version. A status line that runs a checkout also counts as self-managed, but Claude Code runs the checkout, not the global on PATH, so it keeps the running TUI's version. The offered command already used that global install's package manager.
  • After a successful update, the screen runs a fresh check from an effect, once the new installation metadata has reached settings (calling it straight from the update callback would read the settings from before the update). This is one extra registry request, and it shows the truth even when PATH still resolves an old copy.
  • isolateHttpsProxyEnv moves from claude-service-status.test.ts into a shared proxy-test-helpers.ts, used by both suites.

Under npx this relies on #749, which made the PATH lookup skip npx's cached copy (it has no version); under bunx and other launches it works on its own.

Demo

All demos use a stub npm and fake bins in a scratch HOME, plus a preloaded fake registry. For the timeout and proxy demos, registry.npmjs.org's DNS is pointed at an address that never answers, and a local proxy with a throwaway CA serves the registry. Nothing is installed or fetched.

Self-managed install: Check for Updates. The TUI runs from a checkout at 2.2.32, the npm global is at 2.2.20, and the latest release is 2.2.32. Before: "Current: 2.2.32 … up to date". After: "Current: 2.2.20 … An update is available".

Powerline

Before:

Self-managed update check before the fix, Powerline

After:

Self-managed update check after the fix, Powerline

Plain

Before:

Self-managed update check before the fix, Plain

After:

Self-managed update check after the fix, Plain

After running the offered update. A pinned global at 2.2.32 is updated to 2.2.33. Before: the "✓ Global package updated" flash over the stale "An update is available" and the same command. After: a fresh check, "Current: 2.2.33 … up to date".

Powerline

Before:

Updates screen after an update before the fix, Powerline

After:

Updates screen after an update after the fix, Powerline

Plain

Before:

Updates screen after an update before the fix, Plain

After:

Updates screen after an update after the fix, Plain

A network that drops HTTPS to the registry (Bun). Before: still "Checking npm registry..." when the GIF ends (it takes about 75 s). After: it fails at 5 s.

Powerline

Before:

Registry check on a dropped network before the fix, Powerline

After:

Registry check on a dropped network after the fix, Powerline

Plain

Before:

Registry check on a dropped network before the fix, Plain

After:

Registry check on a dropped network after the fix, Plain

Behind HTTPS_PROXY, with direct connections dropped (Bun). Before: still checking. After: "Latest: 2.2.33 … An update is available" straight away.

Powerline

Before:

Registry check behind a proxy before the fix, Powerline

After:

Registry check behind a proxy after the fix, Powerline

Plain

Before:

Registry check behind a proxy before the fix, Plain

After:

Registry check behind a proxy after the fix, Plain

Testing

  • On main as of fix(powerline): keep the Custom theme when Powerline is enabled again #793 (a0bedc0): it merges cleanly, and the merge puts back the beforeEach import in claude-service-status.test.ts, which [low] perf(claude-status): hold a lock while the status page fetch is in flight #635 started using after this branch moved isolateHttpsProxyEnv out. bun run lint is clean, and bun test gives 4160 pass, 0 fail (4147 on main). The built CLI under Bun 1.4.4 and Node 26.13.0 agrees across runtimes and matches main in plain and Powerline mode, and the TUI opens and exits cleanly under both. It adds no JavaScript or Node API newer than Node 14, the build's target.
  • 13 new tests:
    • update-checker.test.ts (7): the timer fires before the connect completes; tunnels through HTTPS_PROXY, given as a URL or as host:port with no scheme; connects directly when NO_PROXY lists npmjs.org or HTTPS_PROXY is blank; fails on an invalid HTTPS_PROXY; with a real local proxy that never answers, gives up at the deadline and closes the connection.
    • App.test.ts (6): a self-managed install older than, and at, the latest release; a status line that runs a checkout, at and behind the latest release, with a different global on PATH; a fresh check after an update, with and without the PATH warning.
  • Each new test failed before its fix. Removing the proxy-connection close makes the stalled-proxy test fail. The new Ink tests passed 20 out of 20 runs in a loop.
  • Under Node (vitest) on the 3 changed test files: 89 tests, 87 pass. The 2 failures are existing execFile spy tests that fail the same way on main (76 tests, 74 pass).
  • Both TUI flows driven in tmux on this branch and on main: the built dist under Node 26 and the source under Bun.

eric-engberg and others added 9 commits October 16, 2026 00:43
Check for Updates is meant to give up on the npm registry after five
seconds, but its only timer was the request's socket timeout. That timer
starts once the socket has connected, and Bun's doesn't fire while it
connects at all. On a network that silently drops outbound HTTPS (a
firewall that expects a proxy, a captive portal), the Updates screen sat
on "Checking npm registry..." until the OS gave up on the connection:
about 75 seconds on macOS under Bun, and the same on Node 18 and older,
whose global agent has no default timeout. Node 19+ only stopped at five
seconds because its global agent's own socket timeout happens to match.

The check now arms one timer for the whole request, as the usage and
status-page requests already do. When it fires, it destroys the request
and fails the check with "npm registry request timed out after 5000ms".
The socket timeout is gone, since the deadline always fires first. The
request function can be passed in, so tests can stand in for the
network.
The usage and Claude Status requests go through the proxy named by
HTTPS_PROXY unless NO_PROXY excludes the host, but Check for Updates
called https.request with no agent, and neither Node nor Bun reads the
proxy variables on its own. On a network where only the proxy gets out,
every other network widget worked while the Updates screen failed with
"npm registry request timed out after 5000ms" (and under Bun waited
about 75 seconds for the OS to give up first).

The registry request now uses the same helpers: createProxyAgent for
HTTPS_PROXY, skipped when isExcludedFromProxy matches registry.npmjs.org.
At the deadline it also closes the agent's connection to a proxy that
never answers CONNECT, which would otherwise keep the process alive. A
HTTPS_PROXY that isn't a URL fails the check with a message that says
so. The proxy docs now list the update check, and the test helper that
clears the proxy variables is shared by both suites that need it.
With Claude Code's status line set to the global `ccstatusline` command
and no pinned metadata, Check for Updates compared the latest release
with the version of the TUI that was running. The README starts the TUI
with npx or bunx, so that is often a different copy from the global
package Claude Code runs. Manage Installation showed "Active PATH match:
npm global 2.2.20", yet Check for Updates said "Current: 2.2.32 ...
ccstatusline is up to date" and offered nothing, and a global already
at the latest release got an offer to reinstall it whenever the TUI was
older.

Pinned installs already use the version found on PATH. Self-managed
installs now do too when that version is known, and fall back to the
running TUI's version otherwise. The offered command already targeted
that global package's manager, so the version shown now matches what it
would update.
After "Run npm install -g ccstatusline@X" succeeded, the TUI flashed
"Global package updated" (or showed the Update Complete notice) and went
back to the Updates screen, which still held the result from before the
update: the old Current version, "An update is available." and the same
install command, which reinstalled X if picked again. Only Check again
showed that the install was up to date.

A successful update now runs the check again. It runs from an effect
once the new installation metadata has reached `settings`: calling the
check from the update's own callback would read the settings from
before the update, so a self-managed install would still show as one,
and a pinned install whose PATH version can't be read would compare
the old version. Both the flash path and the PATH-warning notice path
land on the fresh result.
… PATH's

A status line that runs a local checkout (any command running
ccstatusline.ts) is classified as self-managed, the same as the bare global
`ccstatusline` command. Check for Updates then compared the latest release
with the version of whatever global `ccstatusline` sits first on PATH, a copy
Claude Code never runs for that status line. A checkout at the latest release
with an old global on PATH was told an update is available and offered
`npm install -g`; a checkout behind the latest release read as up to date
when the global was current.

The PATH version now applies only when the status line command is the global
`ccstatusline` (with or without --config). Every other self-managed command
compares this TUI's own version again, as before the self-managed change.
A proxy given as host:port, such as `proxy.corp:3128` (a form curl
accepts as http://), went to HttpsProxyAgent as it was. `new URL()`
parses it without complaint but takes `proxy.corp:` as the scheme and
leaves the host empty, so the agent connected to localhost:80. Check
for Updates, which now goes through HTTPS_PROXY, then failed with only
"Unable to query npm registry", where it used to connect directly and
work wherever direct connections were allowed. The invalid-URL check
missed this form, since nothing threw.

createProxyAgent now adds `http://` when the value has no `scheme://`,
as curl does, so the request goes through the named proxy. Values that
still aren't URLs, like one with spaces, fail as before. The usage and
Claude Status requests build their agents with the same helper, so they
read the variable the same way.
# Conflicts:
#	README.md
#	docs/DEVELOPMENT.md
#	docs/USAGE.md
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants