feat: keep installed CLI up to date with background self-upgrade#47
Merged
Conversation
Adds an `upgrade` subcommand and a once-per-day background version check
wired into the root command's PersistentPreRun. When a newer release is
found on GitHub:
- prints a one-line notice to stderr
- by default spawns a detached `promptconduit upgrade` that downloads
the os/arch archive, verifies SHA256 against checksums.txt, and
atomically swaps the running binary (kernel keeps the open inode on
Linux/macOS; Windows renames .old aside and cleans up next run)
- is skipped for the `hook` subcommand to keep per-event latency low
- is skipped on dev builds and when running under a non-writable
install path (e.g. Homebrew), with a hint to use `brew upgrade`
Users can opt out via `promptconduit config set --disable-auto-update=true`
or `PROMPTCONDUIT_AUTO_UPDATE=0`. The check result is cached at
`~/.config/promptconduit/update.json` so subsequent commands within 24h
re-use the answer without hitting the GitHub API.
The `updater` package is stdlib-only — no new module dependencies.
https://claude.ai/code/session_019CWBC2E8pQuShejfsKYrvp
…ock + integration tests Round of DX polish on the auto-update flow: - Notification now includes the release-notes URL so users can see what's in the upgrade without leaving their terminal. Drops the misleading "run `promptconduit version` to confirm" hint — the background swap is async so the very next foreground command is what actually shows the new version. - One-time success notice: when the running binary's version doesn't match the cache's recorded current_version, we just got swapped, so print `upgraded vX → vY` plus the release URL and rewrite the cache so the notice doesn't repeat. - File lock at `~/.config/promptconduit/upgrade.lock` (O_CREATE|O_EXCL) so two concurrent `promptconduit X` invocations don't both spawn an upgrade subprocess and race on the binary swap. Stale locks (>10m, longer than DownloadTimeout) are reclaimed automatically. - Integration tests with httptest.Server: end-to-end download + checksum verify + atomic swap, plus lock acquire/release/stale-claim coverage. https://claude.ai/code/session_019CWBC2E8pQuShejfsKYrvp
The CLI grew two pairs of "old vs new" send paths during the migration to
the raw-envelope format. Nothing calls the legacy variants anymore, so
delete them:
- /v1/prompts/ingest-multipart (PromptMetadata, PromptContextMetadata,
GitMetadata, SerializedAttachment, SendPromptWithAttachments[Async],
SendPromptDirect, sendPromptAsync*/Blocking, and the
`hook --send-prompt` subcommand + sendPromptFromStdin handler).
Prompt+attachment capture goes through SendEnvelopeWithAttachments
on /v1/events/raw instead.
- /v1/transcripts/sync structured path (SyncTranscript, TranscriptSync-
Request, TranscriptConversation, TranscriptMessage, and the
`buildSyncRequest` helper). All sync calls go through
SyncTranscriptRaw (/v1/transcripts/sync/raw) or the chunked upload,
which both send raw JSONL for server-side categorization.
- Stale hook.go helpers (isPromptEvent, getPromptText,
buildPromptMetadata) that only fed those dead paths.
Breaking-change OK per maintainer — the legacy server endpoints can be
retired now that the CLI no longer hits them.
https://claude.ai/code/session_019CWBC2E8pQuShejfsKYrvp
The post-upgrade success notice fired whenever the running binary's
version differed from the cache, including downgrades. After e.g. `brew
install --version v0.3.1` against a cache that recorded v0.3.2, the
"upgraded v0.3.2 → v0.3.1" line is misleading. Gate on a real forward
step via a new `CheckResult.DetectUpgrade` helper.
Test coverage additions:
- DetectUpgrade table test (forward, equal, downgrade, dev/unparseable,
nil/empty cache).
- CheckLatest end-to-end via httptest.Server (releaseAPIURL is now a
var so tests can point it at the local mock): happy path returns
newer, equal version returns not-newer, non-200 surfaces an error.
- LoadCache corrupt-JSON guard, and confirmation that ShouldCheck
still returns true (so the next invocation re-checks instead of
wedging on a corrupt cache).
- cmd/root_test.go covers `skipUpdateCheckFor` for hook/upgrade/nested
subcommands and the PROMPTCONDUIT_AUTO_UPDATE_CHILD env override.
https://claude.ai/code/session_019CWBC2E8pQuShejfsKYrvp
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.
Summary
promptconduit upgradesubcommand (--check,--force) that downloads the matching os/arch release archive, verifies SHA256 againstchecksums.txt, and atomically replaces the running binary.internal/updaterpackage (stdlib-only — no new module deps) that handles version checks, asset selection, archive extraction (tar.gz and zip), and platform-aware self-replacement (kernel-friendly overwrite on Linux/macOS;.old-aside trick on Windows, cleaned up on next launch).rootCmd.PersistentPreRun. Once every 24h on top-level commands, if a newer release is found it prints a one-line notice with the release-notes URL to stderr and (unless the user opted out) spawns a detachedpromptconduit upgradesubprocess so the swap happens in the background — the foreground command is never blocked beyond a 5s timeout, and the running process keeps its inode and finishes unaffected.upgraded vX → vYnotice the first run after a self-replace, gated on a real forward step so a downgrade (e.g.brew install --versionolder build) is silent rather than mislabelled as an upgrade.~/.config/promptconduit/upgrade.lockso concurrent invocations don't race on the swap; stale locks (>10m) are reclaimed automatically.hooksubcommand (must stay fast — it runs per event), theupgradesubcommand (checks itself), dev builds (Version=="dev"), and when the install dir is not writable (Homebrew users seebrew upgrade promptconduitinstead).promptconduit config set --disable-auto-update=trueorPROMPTCONDUIT_AUTO_UPDATE=0. Check result is cached at~/.config/promptconduit/update.json.config showreports auto-update state;config set --disable-auto-update=<bool>toggles it.Legacy cleanup (in this PR)
Separate commit removes dead send paths that no longer have any caller:
/v1/prompts/ingest-multipartchain (SendPromptWith*,SendPromptDirect,hook --send-prompt,PromptMetadata/GitMetadata/SerializedAttachment, and stale hook helpersisPromptEvent/getPromptText/buildPromptMetadata)./v1/transcripts/syncpath (SyncTranscript,TranscriptSyncRequest/Conversation/Message,buildSyncRequest).All sends now go through the raw-envelope endpoint and the raw-JSONL transcript sync. Net −479 lines in that commit.
Test plan
go build ./...,go vet ./...,go test ./...all clean.internal/updater: semver compare (incl. pre-release/build suffix stripping and unparseable inputs),ShouldCheckTTL + version-mismatch invalidation, asset/checksum selection, the "no checksums.txt = refuse" guard,LoadCachecorrupt-JSON guard, andDetectUpgrade(forward/equal/downgrade/dev/nil cases).httptest.Server:CheckLatesthappy path / equal version / non-200; end-to-end download → checksum verify → atomic swap; checksum-mismatch rejection; lock acquire / release / stale-claim.cmd/root_test.gocoversskipUpdateCheckForfor hook/upgrade/nested subcommands and thePROMPTCONDUIT_AUTO_UPDATE_CHILDenv override.config showreportsAuto-update: enabled/disabled,config set --disable-auto-updateround-trips,hookskips the check, post-upgrade notice fires on a simulated upgrade and is silent on a simulated downgrade..oldcleanup works after a real swap. (replaceBinaryWindows branch is covered byCleanupOldBinarysemantics but the actual swap isn't exercised on a Windows runner.)https://claude.ai/code/session_019CWBC2E8pQuShejfsKYrvp