Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
70cda16
docs: auto-update design spec (electron-updater, manual download/inst…
AhmedFr Jul 22, 2026
25530ec
docs: auto-update implementation plan (11 tasks, TDD)
AhmedFr Jul 22, 2026
c815f4e
feat(updater): shared updater types, IPC channels, electron-updater dep
AhmedFr Jul 22, 2026
60ae81a
feat(updater): dismissedUpdateVersion setting with IPC validation
AhmedFr Jul 22, 2026
5f5e77c
feat(updater): pure logic for update summaries, error kinds, transloc…
AhmedFr Jul 22, 2026
8f52167
feat(updater): UpdaterService state machine over injectable autoUpdater
AhmedFr Jul 22, 2026
f366a93
style(updater): biome format + import order in updater-logic
AhmedFr Jul 22, 2026
c3b80f9
feat(updater): update analytics events and post-install version detec…
AhmedFr Jul 22, 2026
318f4f5
feat(updater): wire UpdaterService through IPC, preload and app startup
AhmedFr Jul 22, 2026
4d07d3a
feat(updater): useUpdater hook syncing updater state to renderers
AhmedFr Jul 22, 2026
4c1a750
feat(updater): Updates tab in Settings with manual download/install flow
AhmedFr Jul 22, 2026
f5c1daa
feat(updater): dismissible update banner in the menu bar panel
AhmedFr Jul 22, 2026
404b6c8
feat(updater): settings-updates deep link from panel banner to Update…
AhmedFr Jul 22, 2026
3166f18
fix(updater): reset settings landing tab on cmd-comma shortcut too
AhmedFr Jul 22, 2026
858da18
fix(updater): dedupe update_available, keep banner through background…
AhmedFr Jul 22, 2026
5433d47
docs: STATUS — auto-update feature branch ready for review
AhmedFr Jul 22, 2026
671ad80
fix(updater): review fixes — instant downloading state, dedupe & polish
AhmedFr Jul 29, 2026
ac0e495
docs: STATUS — PR #46 review fixes landed (671ad80)
AhmedFr Jul 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions STATUS.html

Large diffs are not rendered by default.

1,583 changes: 1,583 additions & 0 deletions docs/superpowers/plans/2026-07-22-auto-update.md

Large diffs are not rendered by default.

95 changes: 95 additions & 0 deletions docs/superpowers/specs/2026-07-22-auto-update-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# Auto-update via electron-updater + GitHub Releases — Design

**Date:** 2026-07-22
**Status:** Approved

## Goal

TidyDisk users update in place instead of downloading a DMG and reinstalling. The flow
is deliberately user-driven: the app checks silently, the user clicks to download and
clicks again to restart and install.

## Context (verified)

- Builds are signed + notarized (`hardenedRuntime`, `notarize: true`) — a hard
requirement for macOS auto-update.
- `package.json` already has `publish: { provider: "github", releaseType: "draft" }`,
and published releases (e.g. v1.1.0) already contain `latest-mac.yml`, the ZIP, and
blockmaps — everything electron-updater consumes. Repo is public.
- The release process does not change: `pnpm release` + upload + publish the draft.
Publishing the GitHub release remains the "go live" switch.
- Users on versions before this feature ships still need one manual reinstall.

## Approach

`electron-updater` with `autoDownload = false`, wrapped in a single main-process
service exposing a state machine over IPC. Rejected alternatives: built-in
`autoUpdater` + update.electronjs.org (no release notes/size before download, poor
manual-flow support); hand-rolled GitHub API + DMG download (reinvents checksum and
atomic install; recreates the reinstall UX).

## 1. Main process: `UpdaterService`

New folder `src/main/updater/`:

- `updater-service.ts` wraps electron-updater with `autoDownload = false`,
`autoInstallOnAppQuit = true` (a download click expresses intent; quitting after
download applies the update even without the install click).
- State machine, every transition broadcast over IPC:

```ts
type UpdaterState =
| { phase: 'idle'; currentVersion: string }
| { phase: 'checking' }
| { phase: 'available'; info: UpdateSummary } // version, date, sizeBytes, notes
| { phase: 'downloading'; info: UpdateSummary; percent: number }
| { phase: 'downloaded'; info: UpdateSummary }
| { phase: 'error'; message: string; kind: 'network' | 'translocation' | 'unknown' }
```

- Public methods: `check()`, `download()`, `quitAndInstall()`, `getState()`.
- Auto-check: a few seconds after app ready (never delays startup), then every
6 hours (same pattern as license revalidation). Disabled in dev / unpackaged builds.
- `UpdateSummary.notes` = GitHub release body; empty body → no changelog shown.

## 2. IPC + shared types + settings

- Invokable channels: `updaterCheck`, `updaterDownload`, `updaterInstall`.
- Broadcast: `onUpdaterState`.
- `UpdaterState`/`UpdateSummary` types in `src/shared/`.
- New persisted setting `dismissedUpdateVersion: string | null` in `SettingsStore`.
Banner shows iff `available.version !== dismissedUpdateVersion`; a newer version
naturally re-shows the banner.

## 3. Panel banner (tray dropdown only)

New component folder `src/renderer/src/components/UpdateBanner/`:

- Renders when phase is `available` / `downloading` / `downloaded` and not dismissed.
- Compact single row: "TidyDisk v1.2.0 available · 4.2 MB · Jul 20" + ✕.
- ✕ only in the `available` phase; `downloading` shows percent, `downloaded` shows
"Ready to install".
- Clicking the body opens the Launcher on the Settings view, deep-linked/scrolled to
the Updates section.
- No banner in the Launcher window.

## 4. Settings "Updates" section

New component folder `UpdateSettings/` (pattern: `ScanLocationsSettings`), new section
in `SettingsView`:

- Always: current version, "Check for updates" button, last-checked time.
- When update available: version, release date, download size, rendered release notes
(markdown, restrained styling), and one action button that morphs:
**Download update → progress → Restart and install**.
- Errors inline: network → "Try again"; translocation → "Move TidyDisk to
Applications to enable updates".

## 5. Analytics, tests, release

- PostHog: `update_available`, `update_download_clicked`, `update_installed` (fired on
next launch via persisted last-seen-version comparison), all with version props.
- Tests: vitest unit tests for state-machine transitions and banner-dismissal logic.
Real download/install can't run in CI (needs signed app) → manual test checklist in

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use “requires a signed app.”

Line 93’s “needs signed app” wording is ungrammatical.

🧰 Tools
🪛 LanguageTool

[style] ~93-~93: The double modal “needs signed” is nonstandard (only accepted in certain dialects). Consider “to be signed”.
Context: ...download/install can't run in CI (needs signed app) → manual test checklist in the P...

(NEEDS_FIXED)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/superpowers/specs/2026-07-22-auto-update-design.md` at line 93, Update
the wording in the manual test checklist description to say “requires a signed
app” instead of “needs signed app,” preserving the surrounding meaning and
formatting.

Source: Linters/SAST tools

the PR.
- Branch `feat/auto-update`, PR into `main`.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"main": "out/main/index.js",
"author": "Ahmed Abouelleil",
"license": "MIT",
"repository": "github:AhmedFr/clean-my-node-modules",
"scripts": {
"dev": "electron-vite dev",
"dev:clean": "sh scripts/dev-clean.sh",
Expand Down Expand Up @@ -43,6 +44,7 @@
"vitest": "^2.1.8"
},
"dependencies": {
"electron-updater": "^6.8.9",
"posthog-node": "^5.39.3",
"react": "^18.3.1",
"react-dom": "^18.3.1",
Expand Down
70 changes: 58 additions & 12 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions src/main/analytics/analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ export const ANALYTICS_EVENTS = [
'clean_performed',
'analytics_disabled',
'share_card_copied',
'update_available',
'update_download_clicked',
'update_installed',
] as const
export type AnalyticsEvent = (typeof ANALYTICS_EVENTS)[number]

Expand Down
2 changes: 2 additions & 0 deletions src/main/app-context.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { LicenseStore } from './license/license-store'
import type { PackageStore } from './packages/package-store'
import type { ProjectStore } from './projects/project-store'
import type { SettingsStore } from './settings/settings-store'
import type { UpdaterService } from './updater/updater-service'
import type { LauncherWindow } from './windows/launcher-window'
import type { PanelWindow } from './windows/panel-window'

Expand All @@ -11,6 +12,7 @@ export interface AppContext {
packages: PackageStore
settings: SettingsStore
license: LicenseStore
updater: UpdaterService
analytics: Analytics
panel: PanelWindow
launcher: LauncherWindow
Expand Down
Loading
Loading