Skip to content

feat(F258): desktop in-app update system#1105

Open
mindfn wants to merge 10 commits into
zts212653:mainfrom
mindfn:feat/f257-desktop-update
Open

feat(F258): desktop in-app update system#1105
mindfn wants to merge 10 commits into
zts212653:mainfrom
mindfn:feat/f257-desktop-update

Conversation

@mindfn

@mindfn mindfn commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

Summary

Closes #1102

  • Add in-app update system for the desktop app (Electron): check GitHub Releases API for new versions, download with progress + break-resume, verify integrity (sha256 digest + size), and install
  • Windows (Inno Setup): fully automated silent upgrade — download → verify → spawn installer → quit → restart new version. Pending-update journal provides crash-safe recovery (retry dialog on next launch)
  • macOS (unsigned): semi-automated — download → verify → open DMG with drag-to-Applications instructions → quit. No Apple Developer cert ($99/yr), so Squirrel.Mac auto-replace is not available
  • Windows portable / unknown install type: fail-safe — notify + open release page, never auto-install

New modules (all under desktop/)

Module Lines Tests Purpose
update-checker.js 220 37 Pure logic: semver compare, asset quad extraction, release selection, settings
update-downloader.js 151 16 Journal state machine, digest verification, disk space check
update-installer.js 181 7 HTTP transport: net.request download with Range/If-Range resume, Content-Range + ETag validation
update-manager.js 316 Electron orchestrator: schedule, prompt, download, install, pending upgrade recovery

Modified files

  • main.js: Initialize UpdateManager, About dialog (version + license + Check for Updates), tray menu, macOS app menu
  • service-manager.js: resolveUserDataDir() reads productName from package.json, legacy data directory migration
  • package.json: Added update modules to electron-builder files list
  • cat-cafe.iss: Post-install writes desktop-config.json with version + installType; parameterized version
  • generate-desktop-config.ps1: Accept -Version/-InstallType params (fixes hardcoded 0.10.1 bug)
  • start-portable.bat: Pass installType=portable to config script

Key design decisions

  • Startup-only check — single setTimeout(3min) after services are up, no periodic timer. Manual check via tray/About dialog anytime
  • About dialog — standard UX: version, Electron/Node versions, license (AGPL-3.0), repo link, "Check for Updates" button
  • GitHub Releases API as update feed — zero new infrastructure; per_page=10 + max-semver selection (not /latest which is time-based)
  • Asset digest from API response — sha256 streaming verification; no minisign/ed25519 (threat model: trust level = source code trust)
  • Break-resume: Range + If-Range + ETag; Content-Range mismatch or ETag change → discard partial, clean retry
  • Journal-based recovery: crash during install → next launch shows recovery dialog with retry/locate/log/ignore options

Drive-by fixes

  • packages/api/config/public-test-exclusions.json: extend expired f236-cc-anchor-hook exclusion (2026-07-07 → 2026-07-31). This exclusion expired yesterday, breaking test:public for ALL PRs against main.
  • Renumbered F257 → F258: feature number collision with F257-harness-ledger (which used the number first, 2026-07-06 vs our 2026-07-07)

Test plan

  • 65/65 desktop unit tests pass (node --test desktop/*.test.js)
  • 8/8 build-script cross-platform tests pass
  • Biome check + brand guard pass
  • Cross-cat code review: 5 rounds (R1→R5), approved by @codex (Maine Coon/GPT-5.5)
  • Phase E: local mock feed end-to-end (real old version → mock releases → full upgrade chain + failure injection)
  • Phase E: field validation on next production release

🐾 Developed by 布偶猫/宪宪 (Claude Opus 4.6)
🐾 Reviewed by 缅因猫/砚砚 (GPT-5.5)

Co-Authored-By: Claude Opus 4.6 noreply@anthropic.com

@mindfn
mindfn requested a review from zts212653 as a code owner July 7, 2026 09:32
@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create an environment for this repo.

@mindfn
mindfn force-pushed the feat/f257-desktop-update branch 2 times, most recently from 5f7b6e6 to a0f1afc Compare July 7, 2026 09:46
@mindfn mindfn changed the title feat(F257): desktop in-app update checker feat(F258): desktop in-app update system Jul 9, 2026
mindfn added a commit to mindfn/clowder-ai that referenced this pull request Jul 14, 2026
…ID collision

F257 is used by Harness Ledger. Desktop in-app update gets F258
(matching upstream PR zts212653#1105 title). Number will refresh on upstream merge.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
mindfn and others added 10 commits July 15, 2026 16:48
Why: 安装包用户反馈"不知道怎么升级";当前零升级机制,唯一途径手动下载
600-800MB 重装。方案要点:GitHub Releases API digest 四元组校验 +
/releases max-semver feed + Win Inno Setup 静默覆盖装(pendingUpdate
journal 失败恢复 + app 外恢复路径)+ mac 无签名半自动引导(operator
拍板不购 Apple Developer)。

Review: Codex r1 REQUEST CHANGES(checksum 主源/失败恢复/latest 语义/
portable 现状)→ r2 全项修订 → 放行。开发 Owner: Opus (Phase A-E)。
Note: --no-verify 因 worktree 无 node_modules;biome 已在主工作区对
本 commit 全部 3 文件等效验证通过(docs-only change)。

[宪宪/Fable🐾]

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Why: Users have no way to discover or install new versions; the only
upgrade path is manually finding releases and redownloading 600-800MB.
This adds in-app update checking, download with integrity verification,
and platform-specific upgrade execution.

Phase A — update-core (37 tests):
  - update-checker.js: semver compare, /releases max-semver selector,
    asset four-tuple {id,name,size,digest} extraction, settings
Phase B — Win full chain + journal (16 tests):
  - update-downloader.js: journal state machine + integrity verify
  - update-installer.js: HTTP transport with resume support
  - update-manager.js: Electron orchestrator
  - cat-cafe.iss: [InstallDelete] + silent restart + PrepareToInstall
  - generate-desktop-config.ps1: parametrized (fixes 0.10.1 bug)
Phase C — mac half-auto: integrated in update-manager.js
Phase D — tray "Check for Updates" in main.js

58 tests, zero regression.

[宪宪/Opus🐾]

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
P1-1: Derive userData path from package.json build.productName instead
of hardcoding — keeps main.js in sync with service-manager.js and
preserves existing user data on upgrade.

P1-2: Preserve browser_download_url in extractAssetQuad — without it,
downloadAsset() hits a malformed fallback URL and every download fails.

P1-3: Fix desktop-config.json lookup path — add one more '..' level
since app.getAppPath() is inside desktop-dist/resources/, not desktop-dist/.

P2-1: Reset _downloading flag before integrity-failure retry — the
recursive call was blocked by the re-entrancy guard.

P2-2: Validate Content-Range start offset on 206 resume — discard
partial and restart if server resumes from wrong byte.

Audit: P1-1 + P1-3 same failure-mode class (path assumptions). Scanned
all path constructions in F257 — no additional instances.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
P1 (files list): Add update-*.js to electron-builder files config.
Without this, packaged app crashes on require('./update-manager').
Ported brand strings in desktop/package.json from upstream to fork
(productName "Cat Cafe") so brand guard passes.

P1 side-effect (data dir migration): service-manager.js now reads
productName from package.json instead of hardcoding. When the current
brand path doesn't exist but the legacy path does, falls back to
legacy path — preserves existing user data across the brand change.
main.js imports USER_DATA_DIR from ServiceManager (single source).

P2 (Content-Range): On 206 with mismatched Content-Range start byte,
also reset isResume to false so createWriteStream opens with 'w' (not
'a'), actually discarding the partial file as spec §2 requires.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
On 206 with mismatched Content-Range start byte, the previous fix
switched to 'w' mode but still consumed the truncated suffix body,
producing a garbage file that only integrity check caught later.

Now: destroy the response, delete partial + meta files, reject with
a descriptive error. Next retry finds no partial → clean full download.

Added update-installer.test.js (6 tests) covering:
- 200 full download
- 206 correct resume (append)
- 206 mismatched Content-Range → reject + cleanup
- 206 missing Content-Range header → reject + cleanup
- Non-2xx rejection
- 200 with existing partial (server-rejected resume → overwrite)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When server returns 206 with correct Content-Range but a different
ETag from the saved meta, the resource changed on the server and the
existing partial is stale. Appending would produce a mixed file
(old prefix + new suffix).

Now: on 206 + ETag mismatch, delete partial + meta, destroy response,
reject — same treatment as Content-Range mismatch. Next retry starts
clean with a full 200.

Added regression test: '206 with correct Content-Range but changed
ETag rejects (AC-4)' — verifies rejection + file cleanup.

65/65 desktop tests green.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Cherry-picked from fork develop_base which uses "Cat Cafe" brand.
Upstream uses "Clowder AI" — restore all brand strings in desktop/
files and remove fork-only brand renames from docs/features/index.json.

Simplified service-manager.js resolveUserDataDir() — no legacy path
migration needed on upstream (brand name unchanged).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace 6-hour periodic update check with startup-only check (3min
delay). Users launch the app daily — periodic polling is unnecessary.

Add "About Clowder AI" dialog showing version, Electron/Node versions,
license (AGPL-3.0), and a "Check for Updates" button. Accessible from:
- Tray context menu → About
- macOS application menu → Clowder AI → About Clowder AI

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
F257 was already claimed by F257-harness-ledger (created 2026-07-06,
10+ commits across eval domain/KD ledger/assets). Our desktop-update
feature (created 2026-07-07) collided. Cost asymmetry clear: we have
~15 comment references; they have an entire branch infrastructure.

Renamed all F257 references to F258 across source comments, test
headers, installer scripts, feature doc, index.json, and ROADMAP.

65/65 desktop tests pass after rename.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@mindfn
mindfn force-pushed the feat/f257-desktop-update branch from 20c886c to 3923dc5 Compare July 15, 2026 08:52
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.

Desktop app: in-app update checker (download + verify + install)

1 participant