Skip to content

fix(update-notifier): cache npm-list install-method check + cheap short-circuits#755

Open
999cleo wants to merge 1 commit into
campfirein:mainfrom
999cleo:fix/cache-npm-install-method-check
Open

fix(update-notifier): cache npm-list install-method check + cheap short-circuits#755
999cleo wants to merge 1 commit into
campfirein:mainfrom
999cleo:fix/cache-npm-install-method-check

Conversation

@999cleo
Copy link
Copy Markdown

@999cleo 999cleo commented Jun 2, 2026

Cache the npm list -g install-method check so it doesn't run on every CLI invocation

What this fixes

The update-notifier and block-command-update-npm init hooks both call isNpmGlobalInstall(execSync), which shells out to npm list -g byterover-cli --depth=0 on every single brv invocation. That subprocess walks the entire global node_modules tree and is the single largest source of CLI startup latency.

Measured on Termux/Android arm64 (the slowest environment, but the same class of overhead exists everywhere):

$ time brv --version
byterover-cli/3.16.1 android-arm64 node-v24.14.1
real  0m11.7s

Tracing the init hooks with timestamped log lines shows ~11.2s of the 11.7s total goes into the init hook chain, dominated by the npm list -g subprocess (~5.2s on its own here, measured independently). On a typical Linux desktop with SSD the same npm call is 200-500ms, so the real-world impact for most users is closer to a 1-2s slowdown per invocation, but it's still wasted work because:

  1. The install method virtually never changes between invocations of the same install.
  2. block-command-update-npm only acts when commandId === 'update', but pays the cost on every other command too.
  3. update-notifier also has no path that can do anything in a non-TTY context (the interactive confirm prompt can't run), but it still pays the cost before checking isTTY.

Patch summary

1. On-disk cache for the install-method check

New helpers in src/oclif/hooks/init/update-notifier.ts:

  • getInstallMethodCachePath() — XDG-style path at ~/.config/byterover-cli/install-method.json.
  • readInstallMethodCache(cliPath, ...) — returns boolean | undefined. Returns undefined (cache miss) when the file is missing, malformed, expired beyond TTL, or for a different CLI install path.
  • writeInstallMethodCache(cliPath, isNpmGlobal, ...) — best-effort write, silently no-ops on I/O failure so a broken cache never breaks a user's CLI run.
  • isNpmGlobalInstallCached(cliPath, execSync, ...) — read-through cache wrapper around the original isNpmGlobalInstall.

Cache TTL: 7 days. Cache key: install path (so nvm version switches, prefix changes, etc. invalidate the cache automatically on the next run).

2. Cheap short-circuits first in the update-notifier hook

The hook now checks isTTY and BRV_SKIP_UPDATE_CHECK before doing any subprocess or filesystem work. Previously the npm check ran first and the TTY check was deep inside handleUpdateNotification, so non-interactive callers (piped output, CI, scripts, daemons) paid the full cost just to no-op.

New env var BRV_SKIP_UPDATE_CHECK is an explicit escape hatch for CI / scripted invocations that want zero startup overhead.

3. block-command-update-npm only runs for the update command

The hook now bails immediately when opts.id !== 'update'. Previously it called isNpmGlobalInstall on every invocation just to maybe-block one specific command. This is a pure removal of wasted work.

4. Tests

Existing tests pass unchanged. New tests added in test/hooks/init/update-notifier.test.ts:

  • shouldRunUpdateCheck (new short-circuits) — 4 tests covering BRV_SKIP_UPDATE_CHECK, isTTY=false, isTTY omitted (back-compat), BRV_ENV=development interacting with isTTY=true.
  • install-method cache — 12 tests covering cache hit, cache miss, expired TTL, mismatched cliPath, malformed JSON, missing file, false-value preservation, parent-dir creation, write failure safety, and the wrapper behaviour skipping live calls on hit.

Backward compatibility

  • isNpmGlobalInstall (uncached) is preserved with identical signature and behaviour. Any external/internal callers that depend on it continue to work.
  • handleBlockCommandUpdateNpm and handleUpdateNotification signatures are unchanged.
  • shouldRunUpdateCheck adds an optional isTTY arg. Existing callers without it default to the legacy behaviour (no TTY-based short-circuit), so back-compat is preserved.

Measured impact

After patching, on the same Termux/arm64 box:

brv --version (before): 11,712ms
brv --version (after) :  1,028ms     # 11.4x faster on the cold call
brv --version (after, cached): ~810ms # 14.4x faster on warm cache

On a Linux desktop (extrapolating from the npm-list timing): expect brv --version to drop from ~1.5-2s to ~300-500ms on a warm cache.

What this does NOT do

  • Doesn't touch the daemon, the agent process, or any of the synthesis/LLM paths.
  • Doesn't change how the npm check works when it does run — same command, same parsing, same return type.
  • Doesn't disable update notifications. Interactive TTY users still get the prompt on the same 1-hour interval the existing code already uses.
  • Doesn't add any new runtime dependencies. Pure standard library (node:fs, node:os, node:path).

Manual test plan

npm run typecheck                                  # passes
npx mocha --forbid-only test/hooks/init/update-notifier.test.ts   # all green
npx mocha --forbid-only test/hooks/init/block-command-update-npm.test.ts   # all green (no changes to test file needed)

# Cold install (no cache file)
rm -rf ~/.config/byterover-cli
time brv --version   # should run npm list once

# Warm install (cache file populated)
time brv --version   # should NOT run npm list, much faster

# Cache is invalidated when install moves
mv ~/.config/byterover-cli/install-method.json /tmp/cached
# edit timestamp in /tmp/cached to be 8 days old, copy back
time brv --version   # should re-run npm list

…rt-circuits

The `update-notifier` and `block-command-update-npm` init hooks both ran
`npm list -g byterover-cli --depth=0` on every CLI invocation. That
subprocess walks the entire global node_modules tree (200-500ms on a fast
desktop, 3-5s on slower environments like CI cold caches or Termux), and it
ran even when the hook would no-op anyway because:

  * stdout is not a TTY (piped output, CI, scripts, daemons all have no
    interactive prompt path);
  * the command being invoked is not `update` (so
    block-command-update-npm has nothing to block);
  * the install method has not changed since the last invocation.

This commit:

  1. Adds an on-disk cache for the npm-list result at
     ~/.config/byterover-cli/install-method.json. Cache key is the CLI's
     install path, TTL is 7 days, malformed/missing/expired cache files
     fall through to the live check transparently. Pure-stdlib (node:fs,
     node:os, node:path), no new runtime deps.

  2. Short-circuits the update-notifier hook on isTTY=false and on a new
     BRV_SKIP_UPDATE_CHECK env var BEFORE doing any subprocess or
     filesystem work, so non-interactive callers pay no startup cost.

  3. Gates block-command-update-npm on opts.id === 'update' so every
     other command path skips the npm check entirely.

The original `isNpmGlobalInstall(execSync)` export is preserved
unchanged for back-compat; the new cached wrapper is exposed as
`isNpmGlobalInstallCached(cliPath, execSync, ...)` and is what the
hooks use.

Measured on Termux/arm64 (worst case for npm-list subprocess cost):

  before: brv --version  ~11,700ms
  after:  brv --version  ~810ms          (14x faster)

Typical Linux desktop with SSD: expect roughly ~1500ms -> ~400ms.

Tests:

  * 16 new tests in test/hooks/init/update-notifier.test.ts covering:
    cache hit / miss / expired TTL / mismatched cliPath / malformed JSON /
    missing file / explicit-false preservation / parent-dir creation /
    write-failure safety / shouldRunUpdateCheck short-circuits.
  * Existing tests in test/hooks/init/block-command-update-npm.test.ts
    pass unchanged.
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.

1 participant