Skip to content

watcher correctness + perf: graceful shutdown, mtime-cached scans, single-row UPDATEs, pcr_dir fail-loud#86

Merged
KaluJo merged 7 commits into
mainfrom
fix/watcher-shutdown-perf-correctness
May 18, 2026
Merged

watcher correctness + perf: graceful shutdown, mtime-cached scans, single-row UPDATEs, pcr_dir fail-loud#86
KaluJo merged 7 commits into
mainfrom
fix/watcher-shutdown-perf-correctness

Conversation

@KaluJo
Copy link
Copy Markdown
Collaborator

@KaluJo KaluJo commented May 18, 2026

Summary

Lands the high-impact BUG / PERF / CORRECTNESS items from the recent
CLI audit. Each fix is one commit; commits stand alone so they can
be reverted independently if needed. 132 → 143 tests (every fix
added new coverage).

BUG

  • Improve GitHub integration setup and monorepo session matching #1commands/start.rs graceful shutdown. The Ctrl-C handler
    was installed inside wait_for_shutdown, after PID file
    write and after spawn_all_sources. SIGINT in that window
    killed the process with W_TERMSIG(SIGINT) and leaked the PID
    file. Watcher scan loops were bare loop { sleep; scan() } with
    no shutdown channel either. New crate::shutdown module with
    request_shutdown / is_shutting_down / sleep_unless_shutdown;
    the ctrlc handler is wired at the very top of commands::start::run
    before any setup work; the three long-running scan loops
    (cursor::watcher, session_state_watcher, diff_tracker) now
    cooperatively poll the flag at every iteration boundary.
  • Feature/GitHub integration #2update_draft_response updated multiple rows. The
    unscoped UPDATE drafts WHERE session_id=? AND prompt_text=? was
    overwriting every row with that (session, prompt) pair. In
    practice Claude Code and Cursor both let users re-send identical
    prompt text inside one session ("go", "continue", "yes"), so the
    older row had its response stomped by the newer turn's. Replaced
    with SELECT id ORDER BY captured_at DESC, id DESC LIMIT 1
    UPDATE WHERE id = ? AND don't-shrink-guard. UPDATE ... LIMIT 1
    isn't an option here — rusqlite's bundled SQLite is built
    without SQLITE_ENABLE_UPDATE_DELETE_LIMIT.

PERF

  • chore: add repository metadata and use OIDC for npm releases #3 — Incremental Cursor watcher scan. Full recursive WalkDir
    every 20 s was CPU/disk-heavy at scale and re-processed every
    transcript on every pass. Now: track top-level dir mtime + a
    notify_event_pending flag; on the periodic tick, skip the walk
    entirely when neither signal indicates new activity. When a walk
    does run, per-file mtime cache skips process_session for paths
    whose mtime hasn't moved. Bumped periodic interval from 20 s to
    60 s — notify (with its existing 500 ms debounce) is the primary
    signal; the periodic loop is purely a safety net for notify-misses.
    Pickup latency is unchanged for the notify path (~600 ms
    end-to-end).
  • Rewrite CLI in Go #4gc::orphaned batched git cat-file. Was spawning one
    git cat-file -e <sha> per unpushed commit (O(N) processes per
    GC pass). Now a single git cat-file --batch-check invocation per
    repo: pipe every SHA on stdin, parse <sha> missing /
    <sha> <type> on stdout. The audit suggested git for-each-ref,
    but that walks named refs forward and isn't a SHA-existence test;
    --batch-check is git's purpose-built primitive — documented
    inline. Failure modes are conservative: missing git → empty
    missing-set → no unpushed work gets reclassified as orphan.

CORRECTNESS

  • Rewrite CLI in Go with incremental prompt bundling workflow #5pcr_dir() returns Result. Previously did
    dirs::home_dir().unwrap_or_else(env::temp_dir), silently writing
    auth + SQLite + watcher state under /tmp whenever $HOME /
    %USERPROFILE% couldn't be resolved (sandboxes, locked-down
    containers). Drafts and login evaporated on reboot. Now returns
    anyhow::Result<PathBuf> with a clear "set HOME and re-run"
    message. Every direct caller updated: CLI entry points surface via
    display::print_error + non-zero exit; library wrappers propagate
    via ? (or .expect() at hard fatal boundaries like the SQLite
    singleton, where the panic message names the missing $HOME
    instead of returning a /tmp path).
  • Rewrite CLI in Go with incremental prompt bundling workflow #6claudecode::process_file state cursor ordering.
    state.set(file_path, lines) ran before the parse. A
    transient parse failure (corrupt JSONL, mid-write truncation,
    schema drift) advanced the cursor without saving anything, so the
    unprocessed lines were lost until a full re-scan — which never
    happens in steady state. Moved state.set to after the
    prompts.is_empty() check.
  • Fix responses and recording prompt response text #7 — Byte-slicing on IDs. Two spots (commands/log.rs::short_sha
    and cursor::session_state_watcher short-id log line) still
    byte-sliced on text we didn't allocate — composer IDs are UUIDs
    in practice but any future non-ASCII tag would have panicked.
    Switched to chars().take(N).collect::<String>(), matching the
    truncate_diff fix from PR vscode: stop dual-watch dupes from chatSessions + transcripts #85.

Branching

Off main rather than stacked on fix/vscode-dual-watch-dedup
recent PRs (#80#85) are all single feature branches against
main, none stacked. Conflict-free either way; these files don't
overlap with the dedup work.

Test plan

  • cargo fmt --all clean.
  • cargo check --workspace --all-targets clean.
  • cargo clippy --workspace --all-targets -- -D warnings clean.
  • cargo test --workspace143 passed across 13 binaries; 0 failures (was 128 on this main baseline; the new tests are the 11+ added across the 7 fixes — most notably the SIGINT integration test, the mtime-cache unit tests, the update_draft_response multi-row regression test, the claudecode state-ordering test, the gc --batch-check test, and the pcr_dir error-branch test).
  • pcr start smoke test on a real ~/.cursor/projects/: watcher
    starts, captures prompts, Ctrl-C produces exit code 0 and removes
    the PID file (covered by pcr-cli/tests/start_graceful_shutdown.rs).

Deviations from the audit description

  • Task 4 used git cat-file --batch-check rather than the audit's
    literal git for-each-ref refs/heads/ suggestion — for-each-ref
    walks named refs, which isn't the right primitive for "does this
    exact SHA exist". Documented inline in the new helper. Same
    one-subprocess-per-repo cost shape the audit asked for.
  • Task 5 keeps FileState::new returning Self (with a fail-fast
    .expect) rather than Result<Self>. The strict constraint not
    to touch vscode/watcher.rs (recent dedup work) means propagating
    Result through FileState::new would have either bled into
    that file or required a Result-shaped facade around it. The
    fail-fast panic message is unambiguous and the audit's correctness
    intent (no silent /tmp fallback) is fully preserved.

Not in scope

  • Claude Code watcher's rx.recv() main loop and 250 ms debounce
    pump don't yet observe crate::shutdown. Those threads die when
    the process exits (which is fine — they have no in-flight DB
    writes when blocking on notify), but wiring them through the
    shared flag would tighten the shutdown story further. Leaving
    for a follow-up so this PR stays focused on the audit's
    explicit items.
  • VS Code watcher untouched per the user's constraint.

Made with Cursor

KaluJo and others added 7 commits May 18, 2026 02:56
Mirrors the truncate-on-char-boundary fix in `util::text` and the
recent diff-truncate fix. Two spots still byte-sliced on text we
didn't allocate ourselves:

- `commands/log.rs::short_sha` (`sha[..7]`) — composer / commit
  shas are hex in practice but `short_sha` is also called on the
  `manual-*` sentinels, and any future non-ASCII tag would have
  panicked.
- `sources/cursor/session_state_watcher.rs` short-id log line
  (`&row.composer_id[..len.min(8)]`) — composer ids are UUIDs but
  the byte-slice would have paniced if Cursor ever shipped a
  Unicode session identifier.

Both now use `chars().take(N).collect::<String>()`, same shape as
the existing `truncate` helper. No public behavior change for
ASCII inputs (length still 7 / 8).

Co-authored-by: Cursor <cursoragent@cursor.com>
`process_file` used to `state.set(file_path, line_count)` BEFORE
calling `parse_claude_code_session`. If the parser returned an
empty session — corrupt JSONL, mid-write truncation, schema drift,
anything — the cursor still moved forward, so on the next scan
none of those unprocessed lines would ever be retried (the watcher
short-circuits on `line_count <= prev_count`). A full rescan never
happens in steady state, so the loss was permanent.

Move `state.set` to *after* `session.prompts.is_empty()` so the
cursor only advances when parse produced something usable.

Tests:

- New `claudecode_state_ordering` integration test asserts that a
  three-line corrupt-JSONL file leaves the cursor at 0, then
  rewriting the same path with a valid user/assistant turn moves
  the cursor forward. A second call on identical bytes is
  idempotent (no regression).

Co-authored-by: Cursor <cursoragent@cursor.com>
`UPDATE drafts SET response_text = ? WHERE session_id = ? AND
prompt_text = ?` overwrote every row with that (session, prompt)
pair. Claude Code and Cursor both let users re-send identical
prompt text inside a single session ("go", "continue", "yes"),
and we already keep those as distinct rows — the v2 content hash
folds `captured_at` in so they survive the `content_hash UNIQUE`
constraint. Both rows were getting the second turn's response
text written onto them, which produced wrong transcripts on the
older row.

Switch to a select-then-update shape so we always target a
single id:

1. `SELECT id ... ORDER BY captured_at DESC, id DESC LIMIT 1` —
   the watcher only ever enriches "the newest draft for this
   (session, prompt) pair", so anchor on that.
2. `UPDATE ... WHERE id = ? AND (response_text IS NULL OR
   LENGTH < LENGTH(?))` — the don't-shrink guard moves to the
   UPDATE so a stale "newer reply" call doesn't get fall back
   to an older row that happens to qualify.

`LIMIT 1` in the UPDATE itself isn't an option here: rusqlite's
bundled SQLite isn't built with `SQLITE_ENABLE_UPDATE_DELETE_LIMIT`
(verified — `UPDATE ... LIMIT 1` errors with a syntax error).

Tests:

- New `drafts_update_response_scoped` integration test creates
  two drafts with identical (session_id, prompt_text) and
  asserts only the newer row receives the response across three
  successive `update_draft_response` calls (initial, idempotent,
  extended). The older row is verified untouched at every step.

Co-authored-by: Cursor <cursoragent@cursor.com>
`pcr start` previously had two correctness gaps on SIGINT:

1. The Ctrl-C handler was installed inside `wait_for_shutdown`,
   AFTER PID file write and AFTER `spawn_all_sources`. If SIGINT
   landed in that window, the default signal handler killed the
   process with `W_TERMSIG(SIGINT)` (exit status 130) and the PID
   file leaked behind.
2. Watcher scan loops (`cursor::watcher::start`,
   `session_state_watcher::run_blocking`, `diff_tracker::run_blocking`)
   were bare `loop { sleep(N s); poll() }` with no shutdown channel,
   so Ctrl-C tore them down mid-scan rather than letting them
   unwind at an iteration boundary.

Both fixes share a single process-wide flag in the new
`crate::shutdown` module:

- `request_shutdown()` is wired to the `ctrlc` handler installed
  at the very top of `commands::start::run` — before PID file
  write, before watcher spawn — so the window where the default
  SIGINT handler could kill us is gone.
- `is_shutting_down()` is cheap; `sleep_unless_shutdown(d)` slices
  long sleeps into 200 ms chunks so each watcher reacts within
  ~one chunk of Ctrl-C instead of waiting up to its full tick.
- The three scan loops above now `while sleep_unless_shutdown(d) {
  ... }` so they exit cleanly. PID file cleanup runs through the
  existing `PidFileGuard::drop` (already idempotent via
  `let _ = remove_file`).

Tests:

- `crate::shutdown::tests` covers the early-exit and
  full-completion paths of `sleep_unless_shutdown` with a 50 ms
  background thread that flips the flag.
- New `pcr-cli/tests/start_graceful_shutdown.rs` (Unix only)
  spawns the real `pcr` binary, waits for the PID file, sends
  SIGINT via `libc::kill(2)`, and asserts (a) the process exits
  with status 0, (b) the PID file is gone. Without the fix this
  test fails with `W_TERMSIG(SIGINT)` and a leaked PID file.

Co-authored-by: Cursor <cursoragent@cursor.com>
The periodic safety-net loop ran a full recursive WalkDir over
`~/.cursor/projects/` every 20 s and re-processed every transcript
on every pass — relying on the in-process `seen` dedup to no-op
already-saved bubbles. At scale (hundreds of long-lived projects
× thousands of bubbles each) this dominates CPU and disk seek
budget for a watcher that's supposed to be invisible.

Three changes:

1. **Fast-path skip on quiet polls.** Track the top-level dir
   mtime + a `notify_event_pending` flag set by the fsnotify
   loop. The next periodic tick checks both: when neither
   indicates new activity, return None immediately and don't
   walk anything. New project subdirs change the top-level
   mtime; in-session bubble writes are caught by the notify
   flag (they don't bump the parent's mtime).

2. **Per-file mtime cache.** When a walk does run, snapshot
   each transcript's mtime and skip `process_session` for any
   path whose mtime equals the previously-cached value.
   Already-processed bubbles were no-ops via dedup anyway, but
   the open + JSON-fetch + DB-lookup cost is what the cache
   actually saves.

3. **Bump periodic interval from 20 s to 60 s.** Notify is the
   primary signal (sub-second after debounce); the periodic
   loop is purely a safety net for notify-misses, so 60 s
   matches the audit's "treat as safety-net every 60-120 s"
   guidance without changing pickup latency for the notify
   path (still ~600 ms end-to-end via the existing 500 ms
   debounce + 100 ms checker tick).

Initial-scan path passes `force = true` to bypass the fast-path
skip — the caches start empty so the first walk has to populate
them regardless.

Tests:

- `collect_pending_initial_walk_returns_all_transcripts` — force
  walk surfaces every transcript and populates the cache.
- `collect_pending_fast_path_skips_when_nothing_changed` — second
  poll with no signals returns None (no walk).
- `collect_pending_walks_when_notify_event_signalled` — notify
  flag forces a walk even when the dir mtime is stale; flag is
  consumed.
- `collect_pending_returns_only_files_whose_mtime_moved` —
  poke one transcript's mtime, assert exactly that path is
  pending (other transcript is filtered out).
- `collect_pending_force_runs_walk_even_without_signals` —
  matches the start() initial-scan invariant.

Co-authored-by: Cursor <cursoragent@cursor.com>
\`gc_orphaned\` shelled out \`git cat-file -e <sha>\` once per
unpushed prompt_commit row to test whether the commit still
exists. With N unpushed commits per project this was N git
processes per GC pass. At scale (hundreds of stale unpushed
commits across long-lived projects) the fork/exec overhead
dominates the actual GC work.

Replace the loop with a single \`git cat-file --batch-check\`
invocation: pipe every SHA on stdin, parse \`<sha> missing\` /
\`<sha> <type>\` lines on stdout. One process per repo,
regardless of N. Same orphan-detection semantics — a SHA is
classified orphan iff git can't resolve the object (rebased
away, branch deleted, repo re-cloned).

The audit suggested \`git for-each-ref refs/heads/\`, but
for-each-ref walks named refs forward (and its
committerdate-by-default output isn't a SHA-existence test);
\`cat-file --batch-check\` is the purpose-built primitive for
"does this exact object exist". Documented inline.

Failure modes preserved conservatively: if git is missing /
the repo is broken / the subprocess can't be spawned, return
an empty missing set so the caller doesn't reclassify
unpushed work as orphan because the user's git binary is
mis-installed.

Tests:

- \`batch_check_marks_only_unknown_shas_as_missing\` builds a
  one-commit repo with a TempDir, queries both the real HEAD
  sha and a 40-zeros sha, asserts only the bogus one shows up
  in the missing set. Skips gracefully if \`git --version\`
  isn't on PATH.
- \`batch_check_returns_empty_when_input_is_empty\` — no work,
  no subprocess.
- \`batch_check_returns_empty_on_git_failure\` — a non-git
  directory must NOT cause every queried SHA to be reported
  missing (would wrongly GC unpushed work).

Co-authored-by: Cursor <cursoragent@cursor.com>
…/tmp

\`config::pcr_dir()\` previously did
\`dirs::home_dir().unwrap_or_else(env::temp_dir).join(".pcr-dev")\`.
The silent fallback meant that on any machine where \`$HOME\` /
\`%USERPROFILE%\` couldn't be resolved (sandboxes, locked-down
containers, cron-like environments) the CLI quietly wrote the
SQLite store, auth.json, watcher PID, and per-source state
files under \`/tmp/.pcr-dev/\` — where they evaporated on
reboot. The user's login disappeared and every watcher
re-emitted every prompt on next start because its state cursor
was gone.

Change the signature to \`anyhow::Result<PathBuf>\` and update
each caller:

- \`commands/start.rs::pid_file_path\` propagates Result; the
  CLI entry point surfaces the error via \`display::print_error\`
  and returns \`GenericError\` before spawning any watchers.
- \`commands/hook.rs::run\` treats Err as "no live watcher to
  signal" and exits 0 — that's the only branch a hook should
  ever take.
- \`auth.rs\`: \`auth_file_path\` returns Result. \`load\` falls
  through to None (best-effort by design). \`save\` propagates
  via \`?\`. \`clear\` no-ops since there's nothing to clear if
  the path can't be resolved.
- \`projects.rs::file_path\` returns Result. \`load\` (hot path
  called from every watcher tick) degrades to an empty list,
  matching its existing best-effort behaviour on a missing
  projects.json. \`save\` propagates via \`?\`.
- \`sources/cursor/diff_tracker.rs::state_path\` returns
  \`Option<PathBuf>\` — load_state and save_state are
  soft-state operations and already swallow IO errors; making
  them Option lets them skip persistence cleanly when \`$HOME\`
  is unavailable.
- \`store/db.rs::db_path\` returns Result; \`open()\` keeps its
  panic-on-failure semantics (the SQLite singleton is built
  with \`OnceLock\` and already \`.expect\`s on schema errors)
  but the panic message now explicitly names the missing
  \$HOME instead of returning a \`/tmp\` path.
- \`sources/shared/state.rs::FileState::new\` keeps its
  \`-> Self\` signature so the constraint not to touch the
  recent vscode/watcher dedup work doesn't bleed into this
  diff. Instead it \`.expect\`s with the same clear "refusing
  to put state under /tmp" message — failing fast at watcher
  construction is strictly better than silent fallback.

Tests:

- \`config::tests::pcr_dir_returns_err_when_home_is_unresolvable\`
  drives the error branch by removing HOME + USERPROFILE,
  calls \`pcr_dir()\`, and asserts the message mentions
  \`\$HOME\` so users can self-diagnose. Skips gracefully on
  Unix platforms where \`dirs::home_dir()\` falls through to
  \`getpwuid_r\` (the regression is still gone, the test just
  can't observe it from there). Env is restored before any
  assertion can panic so the rest of the test binary stays
  sane.

Diff: 177 lines added / 34 removed across 8 files. Per the
audit's expectation, this is a >100-line correctness fix worth
the scale.

Co-authored-by: Cursor <cursoragent@cursor.com>
@KaluJo KaluJo merged commit 8f837f4 into main May 18, 2026
2 checks passed
@KaluJo KaluJo deleted the fix/watcher-shutdown-perf-correctness branch May 18, 2026 14:49
KaluJo added a commit that referenced this pull request May 18, 2026
Adds a best-effort background update check that prints a soft
"X is available — run: brew upgrade pcr" notice at the end of every
interactive `pcr` command when a newer `pcr-dev` ships on npm.

Modeled on the `update-notifier` npm package and `cargo`'s own
behaviour:

- Background thread fires off a 3-second-timeout GET against
  https://registry.npmjs.org/pcr-dev/latest at the *start* of the
  command, runs concurrently with the command itself, and writes the
  result to `~/.pcr-dev/update-check.json` regardless of whether the
  foreground command has exited. The thread is intentionally not
  joined — network failures, captive portals, slow DNS never delay
  the user's primary signal.
- At the *end* of the command, the cached file is read and a one-line
  notice is printed to stderr if (a) the cached version is greater
  than `CARGO_PKG_VERSION` under naive `major.minor.patch` semver,
  and (b) we haven't shown the notice in the last hour (so back-to-
  back `pcr log; pcr show` doesn't double-print).
- Suggested upgrade command is install-method-aware: inspects
  `current_exe()` for `/Cellar/`, `/opt/homebrew/`, or `/node_modules/`
  and prints `brew upgrade pcr`, `npm i -g pcr-dev@latest`, or a
  generic `https://pcr.dev/install` link respectively.
- Hard skips: `--json` output, the hidden `hook` + `mcp` subcommands
  (they're stdio JSON-RPC / Stop-hook channels), `CI=*` env, and
  `PCR_NO_UPDATE_CHECK=1` for users who want to opt out.

6 new unit tests cover semver comparison (including prerelease
suffixes), forward-compatible cache deserialisation (older payloads
without `last_notice_unix` decode cleanly), and the quiet-subcommand
skip list. Workspace tests: 134 passed (was 128 on this main baseline).

Also adds CHANGELOG.md (none previously existed in this repo) seeded
with an Unreleased section that catalogues this feature plus the
in-flight fixes from PRs #85 (vscode dual-watch dedup) and #86
(watcher correctness + perf) so they have a single grep-able home
when those PRs merge.

Independent of PR #85 / #86 — touches `lib.rs`, `entry.rs`, and a
new file. Safe to merge in any order.

Made with [Cursor](https://cursor.com)

Co-authored-by: Cursor <cursoragent@cursor.com>
KaluJo added a commit that referenced this pull request May 18, 2026
PR #86 changed `config::pcr_dir()` to return `Result<PathBuf>` so
that auth + SQLite + watcher state never silently fall back to
`/tmp` when neither `$HOME` nor `%USERPROFILE%` resolves. The
update-notifier is best-effort, so it absorbs the Err the same way
it absorbs every other failure: collapse to `None` and silently
skip the cache operation. The foreground command never sees the
error.

Cache helper signatures:
  cache_path() -> PathBuf            // before
  cache_path() -> Option<PathBuf>    // after

load_cache + save_cache add a `let-else` guard at the top. Tests
unchanged — all 6 update_check::tests still pass, workspace 153
passing / 0 failing.

Co-authored-by: Cursor <cursoragent@cursor.com>
KaluJo added a commit that referenced this pull request May 18, 2026
…md (#87)

* feat: "newer version available" notice on pcr runs + start CHANGELOG.md

Adds a best-effort background update check that prints a soft
"X is available — run: brew upgrade pcr" notice at the end of every
interactive `pcr` command when a newer `pcr-dev` ships on npm.

Modeled on the `update-notifier` npm package and `cargo`'s own
behaviour:

- Background thread fires off a 3-second-timeout GET against
  https://registry.npmjs.org/pcr-dev/latest at the *start* of the
  command, runs concurrently with the command itself, and writes the
  result to `~/.pcr-dev/update-check.json` regardless of whether the
  foreground command has exited. The thread is intentionally not
  joined — network failures, captive portals, slow DNS never delay
  the user's primary signal.
- At the *end* of the command, the cached file is read and a one-line
  notice is printed to stderr if (a) the cached version is greater
  than `CARGO_PKG_VERSION` under naive `major.minor.patch` semver,
  and (b) we haven't shown the notice in the last hour (so back-to-
  back `pcr log; pcr show` doesn't double-print).
- Suggested upgrade command is install-method-aware: inspects
  `current_exe()` for `/Cellar/`, `/opt/homebrew/`, or `/node_modules/`
  and prints `brew upgrade pcr`, `npm i -g pcr-dev@latest`, or a
  generic `https://pcr.dev/install` link respectively.
- Hard skips: `--json` output, the hidden `hook` + `mcp` subcommands
  (they're stdio JSON-RPC / Stop-hook channels), `CI=*` env, and
  `PCR_NO_UPDATE_CHECK=1` for users who want to opt out.

6 new unit tests cover semver comparison (including prerelease
suffixes), forward-compatible cache deserialisation (older payloads
without `last_notice_unix` decode cleanly), and the quiet-subcommand
skip list. Workspace tests: 134 passed (was 128 on this main baseline).

Also adds CHANGELOG.md (none previously existed in this repo) seeded
with an Unreleased section that catalogues this feature plus the
in-flight fixes from PRs #85 (vscode dual-watch dedup) and #86
(watcher correctness + perf) so they have a single grep-able home
when those PRs merge.

Independent of PR #85 / #86 — touches `lib.rs`, `entry.rs`, and a
new file. Safe to merge in any order.

Made with [Cursor](https://cursor.com)

Co-authored-by: Cursor <cursoragent@cursor.com>

* update_check: handle pcr_dir() -> Result<PathBuf> from #86

PR #86 changed `config::pcr_dir()` to return `Result<PathBuf>` so
that auth + SQLite + watcher state never silently fall back to
`/tmp` when neither `$HOME` nor `%USERPROFILE%` resolves. The
update-notifier is best-effort, so it absorbs the Err the same way
it absorbs every other failure: collapse to `None` and silently
skip the cache operation. The foreground command never sees the
error.

Cache helper signatures:
  cache_path() -> PathBuf            // before
  cache_path() -> Option<PathBuf>    // after

load_cache + save_cache add a `let-else` guard at the top. Tests
unchanged — all 6 update_check::tests still pass, workspace 153
passing / 0 failing.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
@KaluJo KaluJo mentioned this pull request May 18, 2026
KaluJo added a commit that referenced this pull request May 18, 2026
Bumps the workspace to 0.3.0 — the 0.x minor (rather than 0.2.10
patch) is motivated by the breaking signature change to
`pcr_core::config::pcr_dir()` from #86 (returns `Result<PathBuf>`
instead of `PathBuf`). The CLI surface (`pcr <cmd>` flags / exit
codes / output format) is unchanged.

Version touchpoints:

  * `Cargo.toml` workspace.package.version → 0.3.0
  * `crates/pcr-napi/package.json` version + all 4 optionalDependencies
  * `crates/pcr-napi/npm/{darwin-arm64,darwin-x64,linux-x64-gnu,
    win32-x64-msvc}/package.json` versions
  * `README.md` TUI mock version stamp
  * `CHANGELOG.md` `[Unreleased]` promoted to `[0.3.0] — 2026-05-18`
    with full release notes catalogued by PR (#85, #86, #87, #88)
    and grouped Added / Changed / Fixed / Tests.

Workspace verification:

  * `cargo fmt --all --check` clean
  * `cargo clippy --workspace --all-targets -- -D warnings` clean
  * `cargo test --workspace` — 153 passing, 0 failing (was 128 on
    the v0.2.9 baseline; +25 across the 4 merged PRs)
  * `cargo build -p pcr-cli --release` → `pcr 0.3.0 (rust)`

After this lands on `main`, the release commit is tagged `v0.3.0`
locally and pushed; that triggers the release workflow which
publishes npm + builds binaries + dispatches the homebrew formula
update.

Co-authored-by: Cursor <cursoragent@cursor.com>
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