Skip to content

Update nudges, and five bug fixes from a full-codebase review - #7

Merged
NotAProgrammer187 merged 6 commits into
mainfrom
feat/update-nudges-and-store-fixes
Aug 12, 2026
Merged

NotAProgrammer187 merged 6 commits into
mainfrom
feat/update-nudges-and-store-fixes

Conversation

@NotAProgrammer187

Copy link
Copy Markdown
Owner

Feature: update nudges

Neither ccswitch nor the claude binary it launches announced new versions. A detached helper now refreshes a small cache at most once a day — latest ccswitch release, latest Claude Code on the npm registry, and the installed claude --version — and the moments you're back at a prompt (Claude Code exiting, the picker closing, ccswitch version) print one line per stale tool with the command that updates it:

ccswitch: Claude Code v2.1.228 is available (you have v2.0.0) — run: claude update

No launch ever waits on the network (nudges read only the cache, the refresh is a separate process), nothing installs itself, and CCSWITCH_NO_UPDATE_CHECK=1 turns it all off. Verified end-to-end against the real endpoints.

Bug fixes (from a two-agent review of the whole codebase, each finding verified against the code before fixing)

  • Sync/defaults could truncate an account's config. Any read error on an existing .claude.json/settings.json was treated as "file is empty", so the merge rewrote the file with only the merged keys — destroying the identity block the code explicitly promises to preserve. Read errors now abort the write.
  • A corrupt state.json was silently wiped. A truncated state file parsed to empty and the next run/use saved that emptiness, losing every directory link. It's now moved aside to state.json.corrupt with a warning, and saves are write-then-rename so truncation can't happen mid-save.
  • Ctrl-C meant for Claude Code killed ccswitch. The terminal delivers the interrupt to the whole foreground group; claude traps it, its wrapper died. cmdRun now ignores it for the duration of the child.
  • npm's claude.cmd failed to launch from a spaced path with quoted args ('C:\Users\Ryan' is not recognized…). Reproduced empirically; fixed with the cmd /s /c "..." idiom and verified against spaced paths, no args, and quoted args.
  • Case-collision holes in the profile store. Linux could create both work and Work (ambiguous everywhere, since lookup is case-insensitive), Windows wrongly rejected case-only renames, and rename/delete keyed last-used state by the caller's spelling. All aligned with how Find resolves.
  • Smaller: stale watch-panel replies no longer make the footer claim "updated" while rows are still checking (fetch generations), and --every's error now says what it means.

New regression tests cover the store fixes and the version-compare/nudge logic. go vet, go test, and builds for windows/linux/darwin all pass.

Known limitation (not fixed here): concurrent ccswitch invocations can race on state.json (load-modify-save with no lock); the window is milliseconds and the fix needs file locking, so it's left documented.

Also adds .gitattributes enforcing LF (CRLF in install.sh would break curl | sh).

readFileOrEmpty returned nil on any read error, not just a missing
file, so a target .claude.json or settings.json that exists but
momentarily can't be read (AV scan, ACL mishap) was treated as empty —
and the merge then rewrote the file with only the merged keys,
destroying the account's identity block or hand-edited settings. The
renamed readFileIfPresent treats only ErrNotExist as empty and
surfaces every other error, which now aborts the item instead of
writing.
Three profile-store fixes:

- A state.json that fails to parse was silently discarded, and the
  next TouchProfile overwrote it — permanently losing every directory
  link. It is now moved aside to state.json.corrupt with a warning,
  and saves go through write-then-rename so a crash mid-save can't
  truncate the file in the first place.

- Create and Rename checked for collisions with exact-case os.Stat
  while Find resolves case-insensitively, so Linux could hold both
  'work' and 'Work' — ambiguous everywhere a name is looked up — and
  Windows wrongly rejected a case-only rename. Both now collide the
  way Find resolves, and case-only renames of the same profile are
  allowed.

- Rename and Delete keyed last-used state by the caller's spelling of
  the name rather than the resolved profile name, dropping recency on
  a case-insensitive rename and leaving stale entries behind.
Routing the npm claude.cmd through exec.Command("cmd.exe", "/c", bin,
args...) let Go quote each argument CommandLineToArgvW-style, but
cmd.exe parses by its own rules: with a quoted path plus one quoted
argument it strips the first and last quote on the line and tries to
run the path's first word. Reproduced with a batch file under a
spaced path — 'ccswitch run work -- -p "two words"' failed to launch.
The /s /c "..." idiom hands cmd.exe the whole payload in one outer
pair of quotes it strips verbatim; verified against spaced paths,
no args, and quoted args. The batch routing now lives in one
claudeCommand helper shared by every claude invocation.
A refresh while a previous batch's replies were still in flight let
the stale replies decrement the new batch's pending counter, so the
footer said 'updated' while rows still showed 'checking'. Fetches now
carry a generation stamp; stale replies still fill rows in but no
longer count the new batch down.

The --every guard also said 'capped at 15s minimum' while actually
rejecting — it now says what it does ('must be at least 15s'), and a
bare '--every=' errors like a missing value instead of being silently
ignored.
Neither tool announced updates: ccswitch upgrade is manual, and the
claude binary's own updater can lag or be disabled. A detached helper
(the hidden update-check command) now refreshes a small cache at most
once a day — latest ccswitch release, latest Claude Code on the npm
registry, and the installed claude --version — and the moments you're
back at a prompt print one line per stale tool with the command that
updates it. No launch ever waits on the network: nudges read only the
cache. CCSWITCH_NO_UPDATE_CHECK=1 silences the whole feature.

cmdRun also now ignores Ctrl-C while claude runs: the terminal
delivers the interrupt to the whole foreground group, and claude
traps it to cancel a response — its wrapper dying instead dropped a
shell prompt onto a live TUI and lost the exit-code passthrough.
install.sh is piped straight to sh from raw.githubusercontent.com — a
CRLF commit would break it with '\r: command not found'. The index is
already all-LF; .gitattributes now keeps it that way, with PowerShell
scripts as the one CRLF exception.
@NotAProgrammer187
NotAProgrammer187 merged commit 63748d5 into main Aug 12, 2026
2 checks passed
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