Repository navigation
fix(update): Check for Updates deadline, proxy, self-managed version and re-check - #806
Merged
Merged
Conversation
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Four fixes to Check for Updates in the TUI:
HTTPS_PROXYandNO_PROXY, as the usage and Claude Status requests already do. All three now read anHTTPS_PROXYwith no scheme (proxy.corp:3128) as an HTTP proxy, as curl does.Why (evidence)
Deadline.
fetchLatestNpmVersionrelied onrequest.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:mainconnect ECONNREFUSEDconnect ETIMEDOUTQuitting the TUI during that wait on
mainunder 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_PROXYpointed at a local proxy and direct connections to the registry dropped, onmainthe proxy saw no connection: Node failed at 5 s and Bun hung for 75 s. On this branch the proxy logsCONNECT 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
ccstatuslinewith 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
fetchLatestNpmVersionsets one timer for the whole request. When it fires, it destroys the request and fails the check, the patternusage-fetch.tsandclaude-service-status.tsalready use. The socket timeout is gone because the overall timer always fires first. Tests can pass in their own request function, as they do forfetchStatusPagePath.createProxyAgentfor uppercaseHTTPS_PROXY, skipped whenisExcludedFromProxy('registry.npmjs.org')matches. When the timer fires, it also closes the agent's pending connection to a proxy that never answers CONNECT.createProxyAgentaddshttp://to a value with noscheme://; before,new URL('proxy.corp:3128')tookproxy.corp:as the scheme and the agent connected to localhost:80. AnHTTPS_PROXYthat still isn't a URL fails the check with "HTTPS_PROXY is not a valid URL". The proxy docs now list the update check.getUpdateCheckVersioninApp.tsxchooses the version to compare: the installed version for a pinned install, as before; the version found on PATH when the status line runs the globalccstatuslineand 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.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.isolateHttpsProxyEnvmoves fromclaude-service-status.test.tsinto a sharedproxy-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:
After:
Plain
Before:
After:
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:
After:
Plain
Before:
After:
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:
After:
Plain
Before:
After:
Behind
HTTPS_PROXY, with direct connections dropped (Bun). Before: still checking. After: "Latest: 2.2.33 … An update is available" straight away.Powerline
Before:
After:
Plain
Before:
After:
Testing
mainas of fix(powerline): keep the Custom theme when Powerline is enabled again #793 (a0bedc0): it merges cleanly, and the merge puts back thebeforeEachimport inclaude-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 movedisolateHttpsProxyEnvout.bun run lintis clean, andbun testgives 4160 pass, 0 fail (4147 onmain). The built CLI under Bun 1.4.4 and Node 26.13.0 agrees across runtimes and matchesmainin 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.update-checker.test.ts(7): the timer fires before the connect completes; tunnels throughHTTPS_PROXY, given as a URL or as host:port with no scheme; connects directly whenNO_PROXYlists npmjs.org orHTTPS_PROXYis blank; fails on an invalidHTTPS_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.execFilespy tests that fail the same way onmain(76 tests, 74 pass).main: the built dist under Node 26 and the source under Bun.