diff --git a/docs/superpowers/specs/2026-04-15-installer-download-mode-design.md b/docs/superpowers/specs/2026-04-15-installer-download-mode-design.md new file mode 100644 index 0000000..5b82c9a --- /dev/null +++ b/docs/superpowers/specs/2026-04-15-installer-download-mode-design.md @@ -0,0 +1,274 @@ +# Installer: Download-Mode + Gitea Releases + +Date: 2026-04-15 +Status: Design — awaiting implementation plan + +## Goal + +Turn `ClaudeDo.Installer` into a self-contained tool that any user can run on +any Windows machine to install, update, reconfigure, repair, or uninstall +ClaudeDo. The installer pulls prebuilt binaries from a Gitea release on +`git.kuns.dev` instead of building from source. + +End-user experience: + +1. Download `ClaudeDo.Installer-.exe` from the releases page. +2. Run it. +3. Done — no .NET SDK, no source checkout, no manual steps. + +## Non-Goals + +- Code signing the installer or the app binaries (future concern). +- Cross-platform installs (Windows-only, same as today). +- In-app update notifications (the installer handles updates when run; the app + does not self-update). +- Arbitrary-version selection UI. Installer always targets "latest" release. +- A package-manager listing (winget/Chocolatey/Scoop). Future, separate spec. + +## Current State (2026-04-15) + +The existing installer (`src/ClaudeDo.Installer/`) is a WPF wizard that only +works from inside a source checkout on a machine with the .NET SDK installed: + +- `PublishAppStep` runs `dotnet publish src/ClaudeDo.App/...` +- `PublishWorkerStep` runs `dotnet publish src/ClaudeDo.Worker/...` +- `DeployBinariesStep` copies `bin/Release/.../publish` into the install dir +- Subsequent steps (`WriteConfigStep`, `InitDatabaseStep`, + `CreateShortcutsStep`, `RegisterServiceStep`) are fine to keep. + +The installer also contains a partial "Settings" window +(`Views/SettingsWindow.xaml`, `Views/SettingsViewModel.cs`) — that wiring will +be reused for the Manage-mode branch described below. + +## High-Level Design + +Two pieces, each small: + +**1) A Gitea Actions workflow** that, on every `v*` tag push, builds the App, +Worker, and Installer; packages them; and creates a Gitea Release on the +public repo at `git.kuns.dev/{owner}/ClaudeDo`. + +**2) An installer rewrite** that replaces the three publish/deploy steps with +a single `DownloadAndExtractStep`, detects existing installs via a marker +file, and branches into Install / Update / Manage modes. + +## Release Artifacts + +Each `v*` tag produces a Gitea Release with three assets: + +``` +ClaudeDo--win-x64.zip # contains /app and /worker subdirs +ClaudeDo.Installer-.exe # self-contained installer (no .NET needed) +checksums.txt # SHA256 of the above +``` + +Decisions: + +- **One combined app+worker zip** (not two separate). Reasons: one download, + one extract, guaranteed version-locked pair. +- **Self-contained installer exe** — user does not need .NET installed. +- **App + Worker**: framework-dependent (`--self-contained false`), same as + today. This assumes the target machine has the .NET 8 Desktop Runtime. + *Open decision — see "Decisions to revisit".* +- **Checksums file** — plain text, one line per asset (` `), + verified by installer before extract. + +The "latest installer exe" URL is stable: + +``` +https://git.kuns.dev/{owner}/ClaudeDo/releases/latest/download/ClaudeDo.Installer-.exe +``` + +(Gitea also exposes `/releases/download//` for specific +versions.) + +## Gitea Actions Workflow + +File: `.gitea/workflows/release.yml` + +- **Trigger:** `push` on tags matching `v*` +- **Runner:** Linux container with .NET 8 SDK (`dotnet publish -r win-x64` + works cross-platform). The installer itself requires Windows to run, but + `dotnet publish` can target `win-x64` from Linux. +- **Steps:** + 1. Checkout + 2. Setup .NET 8 SDK + 3. Derive version from tag (`${{ gitea.ref_name }}` without the `v` prefix) + 4. `dotnet publish src/ClaudeDo.App -c Release -r win-x64 --self-contained false /p:Version=$VERSION -o out/app` + 5. `dotnet publish src/ClaudeDo.Worker -c Release -r win-x64 --self-contained false /p:Version=$VERSION -o out/worker` + 6. `dotnet publish src/ClaudeDo.Installer -c Release -r win-x64 --self-contained true /p:Version=$VERSION /p:PublishSingleFile=true -o out/installer` + 7. Zip `out/app` + `out/worker` as `ClaudeDo--win-x64.zip` with + `app/` and `worker/` as top-level dirs + 8. Copy `out/installer/ClaudeDo.Installer.exe` to + `ClaudeDo.Installer-.exe` + 9. Generate `checksums.txt` (`sha256sum` both files) + 10. Create release via Gitea API using the built-in `${{ gitea.token }}` + (this token has repo write scope automatically on Actions runs). Release + name = tag name. Release notes = `git log` summary between previous tag + and this one (nice-to-have). + +The workflow needs **no custom secrets** — `gitea.token` is sufficient for +creating releases on its own repo. + +## Installer Changes + +### New: `install.json` marker file + +Written at the end of every successful install or update to +`{InstallDir}/install.json`: + +```json +{ + "version": "0.2.0", + "installDir": "C:\\Program Files\\ClaudeDo", + "workerDir": "C:\\Program Files\\ClaudeDo\\worker", + "installedAt": "2026-04-15T12:34:56Z" +} +``` + +The installer reads this on startup (from the default install dir, or a +path supplied via CLI arg) to decide which mode to run in. + +### Mode detection (`InstallModeDetector`) + +| `install.json` state | Mode | Wizard flow | +|-------------------------------------------|---------------------|-----------------------------------------------------| +| absent | **Install** | Welcome → Paths → UiSettings → Service → Install | +| present, version < installer's version | **Update** | Welcome (shows old→new) → Install (download + swap) | +| present, version == installer's version | **Manage** | Welcome → choose: Repair · Edit Config · Uninstall | +| present, version > installer's version | **Downgrade warn** | Welcome explains; user can cancel or force Install | + +The installer's own version comes from its assembly (`AssemblyInformational +Version`), set by the workflow's `/p:Version=$VERSION`. + +### New step: `DownloadAndExtractStep` + +Replaces `PublishAppStep`, `PublishWorkerStep`, `DeployBinariesStep`. + +``` +1. GET https://git.kuns.dev/api/v1/repos/{owner}/ClaudeDo/releases/latest + Parse tag_name and asset URLs for: + - ClaudeDo--win-x64.zip + - checksums.txt +2. Download both to %TEMP%\ClaudeDo-install-\ +3. Parse checksums.txt, verify SHA256 of the zip. Fail hard if mismatch. +4. (Update mode only) Stop Worker service via sc.exe stop ClaudeDoWorker. + Wait up to 30s for it to actually stop. If it won't stop, fail. +5. (Update mode only) Delete contents of {InstallDir}/app and + {InstallDir}/worker, but leave the directories and install.json in place. +6. Extract zip: /app -> {InstallDir}/app, /worker -> {InstallDir}/worker. +7. (Update mode only) Start service again via sc.exe start ClaudeDoWorker. +8. Progress is reported via IProgress — the UI already shows it. +``` + +Config files (`~/.todo-app/*.json`) and DB (`~/.todo-app/todo.db`) live +outside `InstallDir` and are never touched by this step — updates are +naturally non-destructive. + +### Update mode — which steps run + +- **Yes:** `DownloadAndExtractStep` +- **No:** `WriteConfigStep` (user already has config — we don't overwrite) +- **No:** `InitDatabaseStep` (DB exists) +- **No:** `CreateShortcutsStep` (already there; Repair can re-run this) +- **Conditional:** `RegisterServiceStep` only if service is not currently + registered (covers someone who unregistered it manually) + +### Manage mode — branches + +- **Repair:** re-download + extract (same as Update), re-create shortcuts, + re-register service. Leaves config/DB alone. +- **Edit Config:** opens Paths / UiSettings / Service pages prefilled from + existing `~/.todo-app/*.json`. Save writes the JSON files and offers + "Restart service" if worker config changed. No download. +- **Uninstall:** stops + unregisters service, removes shortcuts, deletes + `InstallDir`. Asks separately whether to delete `~/.todo-app` (config + DB). + +### Files to add + +``` +src/ClaudeDo.Installer/Core/InstallModeDetector.cs +src/ClaudeDo.Installer/Core/ReleaseClient.cs // Gitea API + downloads +src/ClaudeDo.Installer/Core/ChecksumVerifier.cs +src/ClaudeDo.Installer/Core/InstallManifest.cs // read/write install.json +src/ClaudeDo.Installer/Steps/DownloadAndExtractStep.cs +src/ClaudeDo.Installer/Steps/WriteInstallManifestStep.cs +src/ClaudeDo.Installer/Steps/StopServiceStep.cs // used in Update+Uninstall +src/ClaudeDo.Installer/Steps/StartServiceStep.cs // used in Update+Repair +.gitea/workflows/release.yml +``` + +### Files to remove + +``` +src/ClaudeDo.Installer/Steps/PublishAppStep.cs +src/ClaudeDo.Installer/Steps/PublishWorkerStep.cs +src/ClaudeDo.Installer/Steps/DeployBinariesStep.cs +``` + +### Files to update + +- `Core/InstallerService.cs` — mode-aware step list +- `Core/InstallContext.cs` — add `Version`, `Mode`, `IsFirstInstall` fields +- `Pages/WelcomePage/*` — content + buttons depend on mode +- `Views/WizardViewModel.cs` — route pages based on mode +- `Core/PageResolver.cs` — register new/renamed pages +- `ClaudeDo.Installer.csproj` — add `PublishSingleFile`, `SelfContained` + properties (only active when published) + +## Failure Modes & Recovery + +| Failure | Behavior | +|---------------------------------------|-------------------------------------------------------| +| No network / Gitea unreachable | Step fails with clear message + retry button | +| API returns no releases yet | "No release available — publish a tag first" | +| Checksum mismatch | Step fails, temp files deleted, user prompted retry | +| Zip extraction fails mid-way (update) | InstallDir is left partially empty — user re-runs | +| Service won't stop | Fail before extract; nothing destructive has happened | +| User cancels mid-download | Temp dir cleaned up; install state unchanged | + +For safety, the `DownloadAndExtractStep` always downloads + verifies +**before** it deletes the old binaries. An aborted download cannot leave +an install in a half-deleted state. + +## Security + +- All downloads over HTTPS from a pinned host (`git.kuns.dev`). +- SHA256 verification before extract (protects against partial downloads and + tampered caches on the wire; not a substitute for code signing). +- No tokens shipped in the installer — repo is public. +- Worker service runs under the same account as today (no change). + +## Decisions to Revisit + +1. **Framework-dependent vs self-contained App/Worker.** Framework-dependent + keeps the zip small (~10 MB) but requires the user to install the .NET 8 + Desktop Runtime separately. Self-contained is ~80 MB per process but + zero-dependency. Current plan: framework-dependent, matches today's + behavior. If install friction becomes a problem, flip the flag in the + workflow and installer messaging. + +2. **Release notes content.** Auto-generated `git log` summary vs manual + notes in the tag message vs empty. Start empty; revisit when there are + enough releases to care. + +3. **Signed installer.** Out of scope for v1. Users will see a SmartScreen + warning the first time. Note this in the README. + +4. **Installer distribution page.** A simple `README.md` badge or a pinned + "Latest release" link on the Gitea repo home is enough for v1. + +## Success Criteria + +- On a fresh Windows VM with no source checkout and no .NET SDK: + 1. Download `ClaudeDo.Installer-.exe`. + 2. Run it. + 3. Complete the wizard. + 4. ClaudeDo App launches, Worker service is running, a task can be created + and picked up. +- Running the same installer a second time shows Manage mode. +- Publishing a new tag, then running the installer on the existing install, + performs an update without touching `~/.todo-app/todo.db` or the config + JSONs. +- The entire release pipeline runs on `git.kuns.dev` with no manual steps + beyond `git tag vX.Y.Z && git push --tags`.