# ClaudeDo.Installer WPF GUI installer, updater, and configuration tool for ClaudeDo. Not WiX/NSIS — the app is its own installer. Note: this is the one project where `System.Windows` is correct (WPF, not Avalonia). ## Project Facts - `true`, `WinExe`, `net8.0-windows` - `true` — allows Linux CI to cross-compile - Single-file framework-dependent publish: `dotnet publish -r win-x64 -p:PublishSingleFile=true` (needs .NET 8 Desktop Runtime) - Entry point: `App.xaml` / `App.xaml.cs` (no `Program.cs`) - References: `ClaudeDo.Data`, `ClaudeDo.Releases`, `ClaudeDo.Localization` - Manifests: `app.manifest` (requireAdministrator, Release) / `app.debug.manifest` (asInvoker, Debug) - No CLI args — mode is detected from `install.json` + the Gitea API ## Startup Sequence (`App.OnStartup`) 1. Load locale 2. Detect mode — `InstallModeDetector` reads `install.json` + Gitea API 3. Open `WizardWindow` (FreshInstall / Update) or `SettingsWindow` (Config) The installer does **not** self-update. Each release ships a stable-named `ClaudeDo.Installer.exe` asset (permanent URL `…/releases/latest/download/ClaudeDo.Installer.exe`); the binary only changes when the user downloads a fresh copy. The in-app "Update" button relaunches the on-disk installer to run the *app* update. App-update detection is unaffected: `WriteInstallManifestStep` records `ctx.InstalledVersion` (the release tag from `DownloadAndExtractStep`), which `InstallModeDetector` compares against the latest tag. ## Modes (`Core/InstallerMode.cs`) | Mode | Condition | Window | |---|---|---| | `FreshInstall` | No `install.json` | Full wizard: Welcome → **SystemCheck** → Paths → Service → UiSettings → Install | | `Update` | `install.json` present + newer release available | Wizard — Welcome + Install pages only (SystemCheck **not** shown) | | `Config` | Current version, or Gitea API unreachable | `SettingsWindow` (settings / repair / uninstall) | ## Install Pipelines Each step implements `IInstallStep`; `InstallerService` runs them sequentially, stops on failure. **FreshInstall:** `DownloadAndExtractStep` → `WriteConfigStep` → `InitDatabaseStep` → `RegisterMcpStep` (optional) → `RegisterAutostartStep` → `CreateShortcutsStep` → `WriteUninstallRegistryStep` → `WriteInstallManifestStep` → `StartWorkerStep` **Update:** `StopWorkerStep` → `DownloadAndExtractStep` → `RegisterAutostartStep` → `RegisterMcpStep` → `StartWorkerStep` → `WriteInstallManifestStep` → `WriteUninstallRegistryStep` **Repair** (via `SettingsViewModel`): `StopWorkerStep` → `DownloadAndExtractStep` → `RegisterAutostartStep` → `StartWorkerStep` **Uninstall** (`UninstallRunner`): Stop worker → remove legacy task/service → delete HKLM uninstall key + shortcuts → delete install dir (cmd.exe trampoline if uninstaller exe is inside it) → optionally delete `~/.todo-app` ## Folder Layout ``` Installer/ Steps/ — one class per action (see pipeline lists above) Core/ — InstallContext, InstallerMode, InstallModeDetector, InstallManifest(+Store), ConfigModels, InstallerService, UninstallRunner, PageResolver, AutostartShortcut, ShortcutFactory, ProcessRunner, DarkTitleBar Interfaces/ — IInstallStep + StepResult/StepStatus/StepProgress, IInstallerPage Checks/ — environment preflight checks, see "Environment Checks" below Pages/ — WelcomePage, SystemCheckPage, PathsPage, ServicePage, UiSettingsPage, InstallPage (each: ViewModel + View.xaml) Views/ — WizardWindow(+WizardViewModel), SettingsWindow(+SettingsViewModel) ``` ## Key Step Behaviors **`RegisterMcpStep`** — registers the external MCP endpoint with the Claude CLI: ``` claude mcp remove --scope user claudedo claude mcp add --transport http --scope user claudedo http://127.0.0.1:{ExternalMcpPort}/mcp ``` Non-fatal if `claude` CLI is missing or too old (prints the manual command). Server name: `claudedo`. **`RegisterAutostartStep`** — creates a per-user Startup-folder shortcut `ClaudeDo Worker.lnk` (`Environment.SpecialFolder.Startup`); `AutostartShortcut.Install` skips the rewrite (and reports it) when the shortcut already points at the current worker exe, so update/repair runs don't touch it needlessly. Also migrates away from legacy mechanisms, unconditionally on every run (no cached "already migrated" flag — see the comment in the step): - Deletes legacy Windows service: `sc.exe stop/delete ClaudeDoWorker` - Deletes legacy scheduled task: `schtasks /Delete /TN ClaudeDoWorker` No new service or scheduled task is created. Rationale: the worker must run in the user's interactive session so Claude CLI auth works. **`DownloadAndExtractStep`** — fetches `checksums.txt` first and only touches the install dir after the zip verifies. The zip is cached in `%TEMP%\ClaudeDo-download-cache` (ctor takes an override for tests) and reused on a retry when its SHA-256 still matches, so a failed attempt doesn't cost another full download. Cache is dropped after a successful install, a bad download is deleted immediately, other versions are pruned. `app\`/`worker\` are stashed to `*.bak` before extraction and restored if it fails. ### Gotcha: the installer must never run from inside the install dir `App.OnStartup` sets `Environment.CurrentDirectory` to `%TEMP%`, and the UI passes an explicit `WorkingDirectory` when it relaunches us. A process's current directory is locked by Windows: inheriting the app's CWD (`\app`, from the Start Menu shortcut's "start in") made the installer block its own `app` → `app.bak` rename, so every update failed with "Could not replace the existing files" — unaffected by retries or a reboot. Keep both guards. ## `InstallContext` Defaults | Property | Default | |---|---| | `InstallDirectory` | `C:\Program Files\ClaudeDo` | | `DbPath` | `~/.todo-app/todo.db` | | `LogRoot` | `~/.todo-app/logs` | | `SandboxRoot` | `~/.todo-app/sandbox` | | `WorktreeRootStrategy` | `sibling` | | `SignalRPort` | `47821` | | `ExternalMcpPort` | `47822` | | `QueueBackstopIntervalMs` | `30000` | | `ClaudeBin` | `claude` | | `AutoStart` | `true` | | `SignalRUrl` | `http://127.0.0.1:47821/hub` | ## Files Written by Install | Path | Content | |---|---| | `~/.todo-app/worker.config.json` | Worker config | | `~/.todo-app/ui.config.json` | UI config | | `~/.todo-app/todo.db` | SQLite DB (EF migrations) | | `\install.json` | Install manifest | | `\app\` | UI binaries | | `\worker\` | Worker binaries | | `\uninstaller\ClaudeDo.Installer.exe` | Uninstaller copy | | `HKLM\Software\Microsoft\Windows\CurrentVersion\Uninstall\ClaudeDo` | Uninstall registry key | | Start Menu shortcut | `ClaudeDo.lnk` | | Desktop shortcut (optional) | `ClaudeDo.lnk` | | `%APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup\ClaudeDo Worker.lnk` | Worker autostart | The Apps & Features uninstall string and "Rerun Installer" both point at `\uninstaller\ClaudeDo.Installer.exe` with no `/uninstall` flag — Config mode is detected from `install.json`. ## Environment Checks `Checks/` holds one `IEnvironmentCheck` per concern, run in parallel by `EnvironmentCheckService.RunAllAsync`: | Check | Severity | What it verifies | |---|---|---| | `GitCheck` | Error | `git` resolvable (via `ExecutableResolver`) and runs | | `WriteAccessCheck` | Error | install dir + `%APPDATA%` (or first existing parent) are writable | | `ClaudeCliCheck` | Error | `claude` resolvable on PATH, including npm `.cmd`/`.bat`/`.ps1` shims | | `ClaudeVersionCheck` | Error | resolved `claude --version` ≥ `ClaudeVersionCheck.MinimumVersion` (currently `2.1.220`) | | `ClaudeAuthCheck` | Error | `claude auth status --json` reports `loggedIn: true` (never sends a prompt) | | `GitIdentityCheck` | Warning | `git config user.name`/`user.email` are set | | `PortCheck` | Warning | `SignalRPort`/`ExternalMcpPort` are free, or already owned by a running `ClaudeDo.Worker` | | `PermissionModeAutoCheck` | Warning | CLI's `--help` still lists `auto` as a `--permission-mode` choice | Each check returns a `CheckResult` with `CheckStatus` (`Ok` / `Failed` / `Unknown`). A check that throws is caught by `EnvironmentCheckService` and turned into `Unknown`, never a crash. **Gating rule:** `EnvironmentCheckReport.HasBlockingError` is true only when a check with `Severity == Error` has `Status == Failed`. Warnings never block, and `Unknown` never blocks regardless of severity (an indeterminate result — e.g. the CLI not found, so version/auth/auto-mode can't be checked — must not strand the user; the underlying `Error`-severity check for the CLI itself, `ClaudeCliCheck`, is what blocks in that case). The check-row rendering and the check-run logic (busy state, summary text, Recheck command) live in one place — `Checks/CheckListViewModel.cs` + `Checks/CheckListView.xaml` — composed by every page that hosts a check list, not duplicated per page. `SystemCheckPage` (`Pages/SystemCheckPage/`) hosts the check list in the **FreshInstall** wizard only, registered via `PageResolver` at `Order = 1` (directly after `WelcomePage`); `WizardViewModel` filters it back out in `Update` mode along with Paths/Service/UiSettings. Checks run automatically on page entry (`LoadAsync`, guarded against double-entry). "Next" is disabled via `IInstallerPage.BlocksNavigation` (`IsRunning || HasBlockingError`) — `WizardViewModel.CanGoNext` subscribes to `PropertyChanged` on the current page so a live recheck can flip it back. A "Recheck" button re-runs `EnvironmentCheckService.RunAllAsync` (disabled while already running). **"Claude Help Me" button** (`Core/ClaudeHelpLauncher.cs`) — a second button below the shared check-list footer, enabled only when `claude-cli` is `Ok` and `claude-auth` is not `Failed` (`Unknown` stays enabled — an indeterminate login state shouldn't block the one feature that could help diagnose it). `BuildReportAsync` renders all check results (Id/Severity/Status/Message table, plus the full `Detail` of any `Failed` check) and system info (OS, `dotnet --list-runtimes`, resolved `git`/`claude` messages, planned install dir/ports) into `%TEMP%\claudedo-setup-diagnose.md` — English and hardcoded (an AI assistant reads it, not the user) and deliberately excludes credentials/tokens/env-var dumps. `LaunchTerminal` then opens `wt.exe -d %TEMP% cmd.exe /k ` (or `cmd.exe /k ` if `wt.exe` isn't resolvable) via the injectable `IProcessLauncher`, pointing the initial prompt at that report file. Errors from either step surface as `ClaudeHelpError` on the page, never an exception. It reads the last report via `CheckListViewModel.LastReport`, so it re-evaluates on every check run. `DiagnosePage` (`Pages/DiagnosePage/`) hosts the same `CheckListView` in `SettingsWindow` (Config mode only, `ShowInSettings = true` / `ShowInWizard = false`, `Order = 5` — after UiSettings). Nothing here blocks navigation and checks do **not** auto-run on load — only on a "Recheck" click. Unlike the wizard, its `InstallContext` is built from the **installed** configuration (`InstallerWorkerConfig.Load()` for `ClaudeBin`/`SignalRPort`, the shared `InstallContext` for `InstallDirectory`/`ExternalMcpPort`), refreshed on every `LoadAsync()` — not the wizard-default `InstallContext` the DI container hands out, which is only populated once a page's `ApplyAsync` (i.e. Save) runs. `DiagnosePage` intentionally has **no** "Claude Help Me" button — that one is wizard-only.