← Commit history

hd -> hydrogen prose; rename docs/adom-hydrogen.md

Kyle Bergstedt ·16483f30e9 ·2mo ago ·parent fe3195a
1 file changed +452−452
skills/adom-desktop-installer/SKILL.md+452−452
@@ -1,452 +1,452 @@----
-name: adom-desktop-installer
-description: How to invoke the Adom Desktop NSIS installer from a parent installer (especially Hydrogen Desktop's setup steps) — silent install, /NOLAUNCH switch for HD-managed bundling, live progress via the structured log file at %TEMP%\adom-desktop-install.log. Use when bundling AD inside another Windows installer, writing an install supervisor / setup-step UI, detecting AD's installed version, or upgrading an existing AD install programmatically.
----
-
-# Adom Desktop installer integration
-
-Adom Desktop (AD) ships as a single NSIS installer:
-
-```
-Adom Desktop_<version>_x64-setup.exe   (~4.8 MB on v1.8.45)
-```
-
-It installs to `%LOCALAPPDATA%\Adom Desktop\` — no UAC prompt, no admin needed. Tauri-2 bundled NSIS with our customisations in `src-tauri/installer-hooks.nsh`.
-
-## Command-line surface
-
-| Flag | Meaning |
-|---|---|
-| (none) | Installs silently, auto-launches `adom-desktop.exe` after install |
-| `/S` | Standard NSIS silent flag — same behavior since the top-level `SilentInstall silent` directive already suppresses UI |
-| `/NOLAUNCH` | **v1.8.45+** — install silently, do NOT auto-launch after install. Caller is responsible for spawning AD with the desired flags |
-| `/D=<path>` | Install destination override. Default is `$LOCALAPPDATA\Adom Desktop`. Rarely needed |
-| `<uninstall.exe> /S` | Silent uninstall. The uninstaller lives at `%LOCALAPPDATA%\Adom Desktop\uninstall.exe` |
-
-**UI behavior**: zero UI in all cases. `SilentInstall silent` is set at the top of the NSIS script, so even `setup.exe` invoked from Explorer runs without a window. No Welcome page, no progress bar, no Finish page.
-
-**Auto-launch**: by default the installer calls `ExecShell "" "$INSTDIR\adom-desktop.exe"` at the end of POSTINSTALL. Pass `/NOLAUNCH` to suppress this. Used by HD when bundling AD because HD wants to spawn AD with `--embedded --start-hidden --relay-url ... --session-token ...` itself, not let AD launch standalone first and get respawned via single-instance.
-
-## Live progress log
-
-The installer writes structured progress to `%TEMP%\adom-desktop-install.log` (Windows expands `%TEMP%` to the user's temp dir, typically `C:\Users\<name>\AppData\Local\Temp`). Each line is `[STAGE] message\r\n`:
-
-```
-[START] adom-desktop installer 1.8.45 starting
-[PREINSTALL] Cleaning legacy desktop shortcuts
-[PREINSTALL] Done. Tauri will now copy bundle files.
-[POSTINSTALL] Bundle files copied. Cleaning Tauri's auto-created desktop shortcut.
-[POSTINSTALL] /NOLAUNCH passed — skipping auto-launch (caller will spawn)
-[DONE] adom-desktop installer complete (no-launch mode)
-```
-
-**`[START]`** opens with a truncated write (fresh log per install). **`[DONE]`** is always the final line — readers tailing the file can use that as the completion sentinel without needing to wait for the process exit.
-
-Stage tags currently emitted:
-- `[START]` — once, at the top
-- `[PREINSTALL]` — Tauri's beforeBuildCommand hook ran, files about to be copied
-- `[POSTINSTALL]` — files copied; Tauri's standard shortcuts created and our hook is cleaning the unwanted ones
-- `[DONE]` — final line, installer about to exit
-
-The uninstaller writes the same log with `[PREUNINSTALL]` / `[POSTUNINSTALL]` stages.
-
-## Recipe: HD's setup-step UI tails the log
-
-Spawn the installer with `/NOLAUNCH`, poll the log file on a 200 ms timer, surface each new line to HD's UI. When `[DONE]` shows up OR the process exits, the install is complete.
-
-### Rust (HD's installer driver)
-
-```rust
-use std::{env, fs, path::PathBuf, process::Command, time::Duration};
-use std::os::windows::process::CommandExt;
-use tokio::time::sleep;
-
-const CREATE_NO_WINDOW: u32 = 0x08000000;
-
-async fn install_adom_desktop<F: FnMut(String)>(
-    installer_path: &str,
-    mut on_line: F,
-) -> Result<(), String> {
-    let log_path: PathBuf = [&env::var("TEMP").map_err(|e| e.to_string())?, "adom-desktop-install.log"]
-        .iter()
-        .collect();
-    // Pre-clear so we don't read stale lines from a previous install.
-    let _ = fs::remove_file(&log_path);
-
-    let mut child = Command::new(installer_path)
-        .args(["/S", "/NOLAUNCH"])
-        .creation_flags(CREATE_NO_WINDOW)
-        .spawn()
-        .map_err(|e| format!("spawn installer: {e}"))?;
-
-    let mut last_pos = 0usize;
-    loop {
-        // Tail any new content
-        if let Ok(content) = fs::read_to_string(&log_path) {
-            if content.len() > last_pos {
-                let slice = &content[last_pos..];
-                if let Some(last_nl) = slice.rfind('\n') {
-                    let consumed = &slice[..=last_nl];
-                    for line in consumed.lines() {
-                        on_line(line.to_string());
-                        if line.starts_with("[DONE]") {
-                            // Don't return yet — wait for process exit
-                            // so we surface the actual exit code, but
-                            // we know progress streaming is finished.
-                        }
-                    }
-                    last_pos += consumed.len();
-                }
-            }
-        }
-        // Check if installer has exited
-        if let Some(status) = child.try_wait().map_err(|e| e.to_string())? {
-            return if status.success() {
-                Ok(())
-            } else {
-                Err(format!("installer exited with {}", status))
-            };
-        }
-        sleep(Duration::from_millis(200)).await;
-    }
-}
-
-// Caller:
-// install_adom_desktop(r"C:\path\to\Adom Desktop_1.8.45_x64-setup.exe", |line| {
-//     hd_setup_step.append_live_output(&line);
-// }).await?;
-```
-
-### Bash (debugging / scripting)
-
-```bash
-# Pre-clear stale log
-rm -f "$TEMP/adom-desktop-install.log" 2>/dev/null
-
-# Spawn installer in background, tail the log
-"./Adom Desktop_1.8.45_x64-setup.exe" /S /NOLAUNCH &
-INSTALLER_PID=$!
-
-# Tail until process exits OR [DONE] appears
-while kill -0 $INSTALLER_PID 2>/dev/null; do
-  [ -f "$TEMP/adom-desktop-install.log" ] && tail -F "$TEMP/adom-desktop-install.log" &
-  sleep 0.2
-done
-wait
-echo "Installer exited"
-```
-
-## Recipe: detect if AD is already installed (and at what version)
-
-The signed NSIS installer (`installMode: "currentUser"`) writes AD's uninstall info to `HKCU` — no admin needed. **⚠ The key name is the product name `Adom Desktop` (WITH a space), NOT `AdomDesktop`** (verified live 2026-07-03). Two history notes so you don't chase the wrong key:
-- The NSIS uninstall key = the product name → `…\Uninstall\Adom Desktop` (space).
-- AD's OWN launch-time code used to ALSO write an `AdomDesktop` (no-space) key to **HKLM** — that write was **DELETED in v1.9.68** (the no-UAC fix). So `AdomDesktop` no longer exists anywhere; read `Adom Desktop`.
-
-```
-HKCU\Software\Microsoft\Windows\CurrentVersion\Uninstall\Adom Desktop
-  DisplayName       = "Adom Desktop"
-  DisplayVersion    = "1.9.74"
-  InstallLocation   = "C:\Users\<name>\AppData\Local\Adom Desktop"
-  UninstallString   = "C:\Users\<name>\AppData\Local\Adom Desktop\uninstall.exe"
-  Publisher         = "Adom Industries, Inc."
-```
-
-```rust
-use winreg::enums::HKEY_CURRENT_USER;
-use winreg::RegKey;
-
-fn adom_desktop_installed_version() -> Option<String> {
-    let hkcu = RegKey::predef(HKEY_CURRENT_USER);
-    // NOTE the SPACE — the NSIS uninstall key is the product name "Adom Desktop".
-    let key = hkcu
-        .open_subkey(r"Software\Microsoft\Windows\CurrentVersion\Uninstall\Adom Desktop")
-        .ok()?;
-    key.get_value("DisplayVersion").ok()
-}
-```
-
-Compare with `semver::Version::parse` to decide whether to skip / upgrade / leave-alone.
-
-## Recipe: HD's setup-step ladder for embedding AD
-
-The full pattern HD's setup-step UI runs when embedding AD:
-
-```
-1. Check installed version          → registry read above
-2. Decide:
-     no install         → install bundled AD
-     older than bundled → install bundled AD (upgrade)
-     same or newer      → skip install entirely
-3. Run installer (silent + /NOLAUNCH)   → tail log, surface to UI
-4. Write embedded marker file       → %LOCALAPPDATA%\Adom Desktop\embedded.json
-5. Delete AD's user-facing shortcuts → Start Menu, Desktop, Startup folder
-                                       (so user can't double-launch standalone)
-6. Spawn AD with embedded flags     → adom-desktop.exe --embedded
-                                          --start-hidden
-                                          --relay-url ws://127.0.0.1:8765
-                                          --relay-name hydrogen-desktop
-                                          --session-token <hd's token>
-7. Poll AD's direct API for liveness → GET http://127.0.0.1:47200/health
-```
-
-Steps 4 and 5 are also covered by AD's installer if you pass `--embedded` at AD's first launch (the embedded-mode detection writes the marker and AD's autostart-upgrade routine handles its own shortcuts). Doing them in HD's installer is belt-and-suspenders.
-
-## Bundle-resource pattern
-
-HD's `tauri.conf.json` should include the AD installer as a bundle resource so it ships inside HD's NSIS installer:
-
-```json
-"bundle": {
-  "resources": {
-    "resources/Adom Desktop_1.8.45_x64-setup.exe": "resources/"
-  }
-}
-```
-
-At runtime HD finds it under its install dir at `$INSTDIR\resources\Adom Desktop_<ver>_x64-setup.exe` — pass that path to the install function above.
-
-## Versioning + upgrade strategy
-
-| Scenario | What HD's installer should do |
-|---|---|
-| No AD installed | Install bundled version |
-| AD older than bundled | Install bundled version (upgrade) |
-| AD same as bundled | Skip install step; still spawn with embedded flags |
-| AD newer than bundled | Skip install step; spawn with embedded flags. Don't downgrade — embedded mode works for any AD ≥ v1.8.42 |
-
-The bundled AD installer is the minimum supported version for the HD release. HD can spawn newer ADs without modification.
-
-## Exit codes
-
-NSIS returns:
-- `0` — install succeeded
-- `2` — user cancelled (won't happen with `SilentInstall silent` since there's no UI to cancel)
-- Other — install error (rare with our hooks; typical cause is disk full or `%LOCALAPPDATA%` not writable)
-
-Treat anything non-zero as a hard failure and surface the log file's last 20 lines to the user.
-
-## Where the installer lives
-
-**v1.8.115+** — the end-user binary home is the **V2 wiki blob store** (the
-GitHub repo is private; GH release URLs 404 for anonymous downloads). Two
-discovery surfaces, both anonymous:
-
-**Preferred for parent installers (HD setup): the packages API.** Resolve
-the latest registered version and its assets in one or two GETs — note
-`?org=adom` is REQUIRED (the slug exists in multiple orgs; bare URLs 400):
-
-```
-https://wiki.adom.inc/api/v1/packages/adom-desktop/dist-tags?org=adom
-  → {"dist_tags":{"latest":"X.Y.Z"}}
-https://wiki.adom.inc/api/v1/packages/adom-desktop/manifest?org=adom
-  → { version, ..., assets: { "windows-x64": { filename, url, sha256, size } } }
-```
-
-The `assets` map is server-derived from the page's blobs (sha256 computed
-from the actual bytes — integrity-checkable). `assets[...].url` is
-server-relative (`/blob/app/adom-desktop/<file>`); prefix the git-wiki host.
-
-**Fallback / updater path: version.json**, V2-first with V1 fallback:
-
-```
-https://wiki.adom.inc/api/v1/pages/adom-desktop/files/version.json
-https://wiki.adom.inc/api/v1/pages/adom-desktop/files/version.json
-```
-
-The V2 manifest's `windows.installer_url` / `cli.<plat>.binary_url` point at
-the V2 files endpoint; V1's point at V1 static copies. **Follow the
-manifest's URLs — do NOT construct binary URLs by hand.**
-
-Network quirk: HEAD on V2 blob/file URLs may return 404 while GET works
-(git-wiki #40 — fix in flight). Probe with a Range GET (`Range: bytes=0-0`,
-expect 206), never bare HEAD. Asset filenames are dot-form everywhere
-(`Adom.Desktop_<ver>_windows_x64-setup.exe`); V1 dot-normalized since v1.8.72.
-
-The wiki page's `metadata.install.windows_installer` field points at the
-canonical filename for the current published version. HD's release build
-should fetch the latest via the V2-first pattern:
-
-```bash
-# Try V2 first, fall back to V1.
-fetch_manifest() {
-  curl -fL --max-time 15 \
-    "https://wiki.adom.inc/api/v1/pages/adom-desktop/files/version.json" \
-    2>/dev/null \
-  || curl -fL --max-time 15 \
-    "https://wiki.adom.inc/api/v1/pages/adom-desktop/files/version.json"
-}
-
-MANIFEST=$(fetch_manifest)
-INSTALLER_URL=$(echo "$MANIFEST" | jq -r .windows.installer_url)
-INSTALLER_SHA=$(echo "$MANIFEST" | jq -r .windows.installer_sha256)
-# Each manifest carries a self-consistent installer_url that points at the
-# SAME backend the manifest was served from — V2 manifest → V2 installer URL,
-# V1 manifest → V1 installer URL. No URL mixing across backends.
-
-curl -fL "$INSTALLER_URL" -o resources/ad-setup.exe
-
-# Verify SHA256 before bundling — defends against partial fetches + tampering
-DOWNLOAD_SHA=$(sha256sum resources/ad-setup.exe | awk '{print $1}')
-[ "$DOWNLOAD_SHA" = "$INSTALLER_SHA" ] || { echo "SHA mismatch"; exit 1; }
-```
-
-The desktop AD's built-in auto-updater (`src-tauri/src/updater.rs`) walks an
-ordered `MANIFEST_URLS` list — V2 first, V1 fallback — and uses whichever
-responds first. Once V2 has been the source of truth for a release cycle
-without incident, V1 will be retired from the pipeline.
-
-`version.json` also carries the SHA256 hash of the installer so HD's build
-pipeline can verify the download before bundling.
-
-## Asset naming convention (v1.8.113+)
-
-The Adom Desktop wiki page hosts TWO distinct types of artifacts per release:
-
-1. **Full AD GUI installer** — the actual desktop app (Tauri window, bundled
-   bridges, relauncher). One per OS / architecture.
-2. **CLI companion binary** — a separate executable that talks to AD over
-   the relay. Used by HD's WSL2 container, gallia builds, sibling-app
-   installers, cron jobs. Does NOT contain AD itself.
-
-Both ship together at the same version (single `python scripts/bump-version.py
-patch` bumps both, single `cargo tauri build` + `cargo build --release` in
-`cli/` produces both, single `scripts/release-publish.sh` uploads both).
-
-### Filename pattern
-
-| Artifact | Wiki asset name pattern | Example (v1.8.113) |
-|---|---|---|
-| Full AD GUI — Windows | `Adom.Desktop_<ver>_windows_x64-setup.exe` | `Adom.Desktop_1.8.113_windows_x64-setup.exe` |
-| Full AD GUI — macOS | `Adom.Desktop_<ver>_macos_<arch>.dmg` | `Adom.Desktop_1.8.113_macos_aarch64.dmg` (future) |
-| Full AD GUI — Linux | `Adom.Desktop_<ver>_linux_<arch>.AppImage` | `Adom.Desktop_1.8.113_linux_x86_64.AppImage` (future) |
-| CLI companion — Linux | `adom-desktop-cli_<ver>_linux_<arch>` | `adom-desktop-cli_1.8.113_linux_x86_64` |
-| CLI companion — macOS | `adom-desktop-cli_<ver>_macos_<arch>` | `adom-desktop-cli_1.8.113_macos_aarch64` (future) |
-| CLI companion — Windows | `adom-desktop-cli_<ver>_windows_<arch>.exe` | `adom-desktop-cli_1.8.113_windows_x64.exe` (future) |
-
-The `_cli_` token in the middle is the only signal you need to know "this is
-the companion, not the app." `linux_x86_64` always means Linux x86_64,
-regardless of whether the file is a GUI installer or a CLI binary.
-
-### After install, the binary name is `adom-desktop`
-
-The wiki asset name disambiguates at FETCH time. At INSTALL time the binary
-is renamed to `adom-desktop` so the command users type doesn't change:
-
-```bash
-# Linux example — install the CLI companion
-curl -fL "https://wiki.adom.inc/api/v1/pages/adom-desktop/files/adom-desktop-cli_1.8.113_linux_x86_64" \
-  -o /tmp/ad
-sudo install -m 755 /tmp/ad /usr/local/bin/adom-desktop
-adom-desktop --version
-# → adom-desktop 1.8.113 (<sha>, built <ts>)
-```
-
-### Version.json `.cli` slot
-
-The auto-update manifest (v1.8.113+) carries a typed `cli` sibling section
-for the CLI binaries:
-
-```json
-{
-  "manifest_version": 1,
-  "version": "1.8.113",
-  "windows": { "installer_url": ".../Adom.Desktop_1.8.113_windows_x64-setup.exe", ... },
-  "cli": {
-    "linux_x86_64": {
-      "binary_url":      ".../adom-desktop-cli_1.8.113_linux_x86_64",
-      "binary_filename": "adom-desktop-cli_1.8.113_linux_x86_64",
-      "binary_sha256":   "...",
-      "binary_size":     8012408
-    }
-  },
-  "changelog_url": "...",
-  "minimum_required": "1.3.30"
-}
-```
-
-Pre-v1.8.113 readers ignore `.cli` entirely (additive schema; no
-`manifest_version` bump required). The desktop AD's own auto-updater only
-reads `.windows / .macos / .linux` for its GUI installer URL — it doesn't
-touch `.cli`.
-
-For HD / gallia / Docker-side fetchers, prefer the typed lookup:
-
-```bash
-# Best: read the URL out of the manifest (so the filename can evolve)
-MANIFEST=$(fetch_manifest)
-LINUX_CLI_URL=$(echo "$MANIFEST" | jq -r '.cli.linux_x86_64.binary_url')
-LINUX_CLI_SHA=$(echo "$MANIFEST" | jq -r '.cli.linux_x86_64.binary_sha256')
-curl -fL "$LINUX_CLI_URL" -o /tmp/ad
-[ "$(sha256sum /tmp/ad | awk '{print $1}')" = "$LINUX_CLI_SHA" ] || exit 1
-sudo install -m 755 /tmp/ad /usr/local/bin/adom-desktop
-```
-
-### Windows CLI is shipped with the NSIS (v1.8.114+)
-
-Installing AD on Windows now drops the CLI companion next to the GUI exe
-and adds the install dir to the user's PATH so PowerShell / Claude Desktop /
-cmd find it without configuration:
-
-```
-%LOCALAPPDATA%\Adom Desktop\adom-desktop.exe        ← GUI (Windows subsystem)
-%LOCALAPPDATA%\Adom Desktop\adom-desktop-cli.exe    ← CLI companion (Console subsystem)
-%LOCALAPPDATA%\Adom Desktop\ad-relauncher.exe       ← visible self-restart helper
-```
-
-NSIS's POSTINSTALL hook prepends `%LOCALAPPDATA%\Adom Desktop` to
-`HKCU\Environment\Path` (no UAC — user-level only) and broadcasts
-`WM_SETTINGCHANGE` so freshly-opened shells pick up the change. Already-
-open shells need a re-launch (OS limitation).
-
-After install, in a fresh PowerShell:
-
-```powershell
-adom-desktop-cli --version
-# adom-desktop 1.8.114 (<sha>, built <ts>)
-adom-desktop-cli status
-adom-desktop-cli screenshot_window '{"title-contains":"VS Code"}'
-```
-
-The CLI auto-discovers the local AD via direct-API probe at
-`127.0.0.1:47200` — no relay needed when AD is on the same box. ~5ms RTT
-for compatible verbs; verbs in AD's `cliRequired` list still flow through
-a relay if one's running (HD's WSL2 distro typically owns the relay).
-
-**Relay self-detect (v1.8.114+):** `adom-desktop-cli serve` checks for
-another adom-desktop relay on `127.0.0.1:8766/health` before binding. If
-one already answers, it prints a friendly note and exits cleanly without
-starting a second relay (which would just sit idle — AD's WS client only
-attaches to the first one it finds). Pass `--force-bind` to override.
-
-PREUNINSTALL strips the PATH entry back out and rebroadcasts so an
-uninstall of AD also gets PATH cleanup.
-
-### Legacy alias migration window
-
-Releases prior to v1.8.113 published the Linux CLI as `adom-desktop-linux`
-(no version, no architecture). HD's WSL2 setup and a few cron-based
-installers have that literal filename baked in.
-
-To keep them working through the rename, v1.8.113 publishes BOTH names:
-
-| Name | Status | Wiki URL (V2 / V1) |
-|---|---|---|
-| `adom-desktop-cli_<ver>_linux_x86_64` | **canonical, v1.8.113+** | V2 / V1 |
-| `adom-desktop-linux` | **deprecated alias** — byte-identical copy | V2 / V1 |
-
-Both files are byte-identical with the same SHA256. The legacy alias stays
-through **v1.8.116**, then disappears. Migrate by then. Recommended pattern:
-
-```bash
-# Migration period (v1.8.113 → v1.8.115): try new name, fall back to alias
-LINUX_CLI_URL_NEW=$(echo "$MANIFEST" | jq -r '.cli.linux_x86_64.binary_url // empty')
-if [ -n "$LINUX_CLI_URL_NEW" ]; then
-  curl -fL "$LINUX_CLI_URL_NEW" -o /tmp/ad
-else
-  # Falling back — likely talking to a pre-v1.8.113 manifest
-  curl -fL "https://wiki.adom.inc/api/v1/pages/adom-desktop/files/adom-desktop-linux" \
-    -o /tmp/ad
-fi
-```
-
-Post-v1.8.116, the fallback branch dies; only the canonical name will exist.
+---+name: adom-desktop-installer+description: How to invoke the Adom Desktop NSIS installer from a parent installer (especially Hydrogen Desktop's setup steps) — silent install, /NOLAUNCH switch for HD-managed bundling, live progress via the structured log file at %TEMP%\adom-desktop-install.log. Use when bundling AD inside another Windows installer, writing an install supervisor / setup-step UI, detecting AD's installed version, or upgrading an existing AD install programmatically.+---++# Adom Desktop installer integration++Adom Desktop (AD) ships as a single NSIS installer:++```+Adom Desktop_<version>_x64-setup.exe   (~4.8 MB on v1.8.45)+```++It installs to `%LOCALAPPDATA%\Adom Desktop\` — no UAC prompt, no admin needed. Tauri-2 bundled NSIS with our customisations in `src-tauri/installer-hooks.nsh`.++## Command-line surface++| Flag | Meaning |+|---|---|+| (none) | Installs silently, auto-launches `adom-desktop.exe` after install |+| `/S` | Standard NSIS silent flag — same behavior since the top-level `SilentInstall silent` directive already suppresses UI |+| `/NOLAUNCH` | **v1.8.45+** — install silently, do NOT auto-launch after install. Caller is responsible for spawning AD with the desired flags |+| `/D=<path>` | Install destination override. Default is `$LOCALAPPDATA\Adom Desktop`. Rarely needed |+| `<uninstall.exe> /S` | Silent uninstall. The uninstaller lives at `%LOCALAPPDATA%\Adom Desktop\uninstall.exe` |++**UI behavior**: zero UI in all cases. `SilentInstall silent` is set at the top of the NSIS script, so even `setup.exe` invoked from Explorer runs without a window. No Welcome page, no progress bar, no Finish page.++**Auto-launch**: by default the installer calls `ExecShell "" "$INSTDIR\adom-desktop.exe"` at the end of POSTINSTALL. Pass `/NOLAUNCH` to suppress this. Used by Hydrogen when bundling AD because Hydrogen wants to spawn AD with `--embedded --start-hidden --relay-url ... --session-token ...` itself, not let AD launch standalone first and get respawned via single-instance.++## Live progress log++The installer writes structured progress to `%TEMP%\adom-desktop-install.log` (Windows expands `%TEMP%` to the user's temp dir, typically `C:\Users\<name>\AppData\Local\Temp`). Each line is `[STAGE] message\r\n`:++```+[START] adom-desktop installer 1.8.45 starting+[PREINSTALL] Cleaning legacy desktop shortcuts+[PREINSTALL] Done. Tauri will now copy bundle files.+[POSTINSTALL] Bundle files copied. Cleaning Tauri's auto-created desktop shortcut.+[POSTINSTALL] /NOLAUNCH passed — skipping auto-launch (caller will spawn)+[DONE] adom-desktop installer complete (no-launch mode)+```++**`[START]`** opens with a truncated write (fresh log per install). **`[DONE]`** is always the final line — readers tailing the file can use that as the completion sentinel without needing to wait for the process exit.++Stage tags currently emitted:+- `[START]` — once, at the top+- `[PREINSTALL]` — Tauri's beforeBuildCommand hook ran, files about to be copied+- `[POSTINSTALL]` — files copied; Tauri's standard shortcuts created and our hook is cleaning the unwanted ones+- `[DONE]` — final line, installer about to exit++The uninstaller writes the same log with `[PREUNINSTALL]` / `[POSTUNINSTALL]` stages.++## Recipe: Hydrogen's setup-step UI tails the log++Spawn the installer with `/NOLAUNCH`, poll the log file on a 200 ms timer, surface each new line to Hydrogen's UI. When `[DONE]` shows up OR the process exits, the install is complete.++### Rust (Hydrogen's installer driver)++```rust+use std::{env, fs, path::PathBuf, process::Command, time::Duration};+use std::os::windows::process::CommandExt;+use tokio::time::sleep;++const CREATE_NO_WINDOW: u32 = 0x08000000;++async fn install_adom_desktop<F: FnMut(String)>(+    installer_path: &str,+    mut on_line: F,+) -> Result<(), String> {+    let log_path: PathBuf = [&env::var("TEMP").map_err(|e| e.to_string())?, "adom-desktop-install.log"]+        .iter()+        .collect();+    // Pre-clear so we don't read stale lines from a previous install.+    let _ = fs::remove_file(&log_path);++    let mut child = Command::new(installer_path)+        .args(["/S", "/NOLAUNCH"])+        .creation_flags(CREATE_NO_WINDOW)+        .spawn()+        .map_err(|e| format!("spawn installer: {e}"))?;++    let mut last_pos = 0usize;+    loop {+        // Tail any new content+        if let Ok(content) = fs::read_to_string(&log_path) {+            if content.len() > last_pos {+                let slice = &content[last_pos..];+                if let Some(last_nl) = slice.rfind('\n') {+                    let consumed = &slice[..=last_nl];+                    for line in consumed.lines() {+                        on_line(line.to_string());+                        if line.starts_with("[DONE]") {+                            // Don't return yet — wait for process exit+                            // so we surface the actual exit code, but+                            // we know progress streaming is finished.+                        }+                    }+                    last_pos += consumed.len();+                }+            }+        }+        // Check if installer has exited+        if let Some(status) = child.try_wait().map_err(|e| e.to_string())? {+            return if status.success() {+                Ok(())+            } else {+                Err(format!("installer exited with {}", status))+            };+        }+        sleep(Duration::from_millis(200)).await;+    }+}++// Caller:+// install_adom_desktop(r"C:\path\to\Adom Desktop_1.8.45_x64-setup.exe", |line| {+//     hd_setup_step.append_live_output(&line);+// }).await?;+```++### Bash (debugging / scripting)++```bash+# Pre-clear stale log+rm -f "$TEMP/adom-desktop-install.log" 2>/dev/null++# Spawn installer in background, tail the log+"./Adom Desktop_1.8.45_x64-setup.exe" /S /NOLAUNCH &+INSTALLER_PID=$!++# Tail until process exits OR [DONE] appears+while kill -0 $INSTALLER_PID 2>/dev/null; do+  [ -f "$TEMP/adom-desktop-install.log" ] && tail -F "$TEMP/adom-desktop-install.log" &+  sleep 0.2+done+wait+echo "Installer exited"+```++## Recipe: detect if AD is already installed (and at what version)++The signed NSIS installer (`installMode: "currentUser"`) writes AD's uninstall info to `HKCU` — no admin needed. **⚠ The key name is the product name `Adom Desktop` (WITH a space), NOT `AdomDesktop`** (verified live 2026-07-03). Two history notes so you don't chase the wrong key:+- The NSIS uninstall key = the product name → `…\Uninstall\Adom Desktop` (space).+- AD's OWN launch-time code used to ALSO write an `AdomDesktop` (no-space) key to **HKLM** — that write was **DELETED in v1.9.68** (the no-UAC fix). So `AdomDesktop` no longer exists anywhere; read `Adom Desktop`.++```+HKCU\Software\Microsoft\Windows\CurrentVersion\Uninstall\Adom Desktop+  DisplayName       = "Adom Desktop"+  DisplayVersion    = "1.9.74"+  InstallLocation   = "C:\Users\<name>\AppData\Local\Adom Desktop"+  UninstallString   = "C:\Users\<name>\AppData\Local\Adom Desktop\uninstall.exe"+  Publisher         = "Adom Industries, Inc."+```++```rust+use winreg::enums::HKEY_CURRENT_USER;+use winreg::RegKey;++fn adom_desktop_installed_version() -> Option<String> {+    let hkcu = RegKey::predef(HKEY_CURRENT_USER);+    // NOTE the SPACE — the NSIS uninstall key is the product name "Adom Desktop".+    let key = hkcu+        .open_subkey(r"Software\Microsoft\Windows\CurrentVersion\Uninstall\Adom Desktop")+        .ok()?;+    key.get_value("DisplayVersion").ok()+}+```++Compare with `semver::Version::parse` to decide whether to skip / upgrade / leave-alone.++## Recipe: Hydrogen's setup-step ladder for embedding AD++The full pattern Hydrogen's setup-step UI runs when embedding AD:++```+1. Check installed version          → registry read above+2. Decide:+     no install         → install bundled AD+     older than bundled → install bundled AD (upgrade)+     same or newer      → skip install entirely+3. Run installer (silent + /NOLAUNCH)   → tail log, surface to UI+4. Write embedded marker file       → %LOCALAPPDATA%\Adom Desktop\embedded.json+5. Delete AD's user-facing shortcuts → Start Menu, Desktop, Startup folder+                                       (so user can't double-launch standalone)+6. Spawn AD with embedded flags     → adom-desktop.exe --embedded+                                          --start-hidden+                                          --relay-url ws://127.0.0.1:8765+                                          --relay-name hydrogen-desktop+                                          --session-token <hydrogen's token>+7. Poll AD's direct API for liveness → GET http://127.0.0.1:47200/health+```++Steps 4 and 5 are also covered by AD's installer if you pass `--embedded` at AD's first launch (the embedded-mode detection writes the marker and AD's autostart-upgrade routine handles its own shortcuts). Doing them in Hydrogen's installer is belt-and-suspenders.++## Bundle-resource pattern++Hydrogen's `tauri.conf.json` should include the AD installer as a bundle resource so it ships inside Hydrogen's NSIS installer:++```json+"bundle": {+  "resources": {+    "resources/Adom Desktop_1.8.45_x64-setup.exe": "resources/"+  }+}+```++At runtime Hydrogen finds it under its install dir at `$INSTDIR\resources\Adom Desktop_<ver>_x64-setup.exe` — pass that path to the install function above.++## Versioning + upgrade strategy++| Scenario | What Hydrogen's installer should do |+|---|---|+| No AD installed | Install bundled version |+| AD older than bundled | Install bundled version (upgrade) |+| AD same as bundled | Skip install step; still spawn with embedded flags |+| AD newer than bundled | Skip install step; spawn with embedded flags. Don't downgrade — embedded mode works for any AD ≥ v1.8.42 |++The bundled AD installer is the minimum supported version for the Hydrogen release. Hydrogen can spawn newer ADs without modification.++## Exit codes++NSIS returns:+- `0` — install succeeded+- `2` — user cancelled (won't happen with `SilentInstall silent` since there's no UI to cancel)+- Other — install error (rare with our hooks; typical cause is disk full or `%LOCALAPPDATA%` not writable)++Treat anything non-zero as a hard failure and surface the log file's last 20 lines to the user.++## Where the installer lives++**v1.8.115+** — the end-user binary home is the **V2 wiki blob store** (the+GitHub repo is private; GH release URLs 404 for anonymous downloads). Two+discovery surfaces, both anonymous:++**Preferred for parent installers (Hydrogen setup): the packages API.** Resolve+the latest registered version and its assets in one or two GETs — note+`?org=adom` is REQUIRED (the slug exists in multiple orgs; bare URLs 400):++```+https://wiki.adom.inc/api/v1/packages/adom-desktop/dist-tags?org=adom+  → {"dist_tags":{"latest":"X.Y.Z"}}+https://wiki.adom.inc/api/v1/packages/adom-desktop/manifest?org=adom+  → { version, ..., assets: { "windows-x64": { filename, url, sha256, size } } }+```++The `assets` map is server-derived from the page's blobs (sha256 computed+from the actual bytes — integrity-checkable). `assets[...].url` is+server-relative (`/blob/app/adom-desktop/<file>`); prefix the git-wiki host.++**Fallback / updater path: version.json**, V2-first with V1 fallback:++```+https://wiki.adom.inc/api/v1/pages/adom-desktop/files/version.json+https://wiki.adom.inc/api/v1/pages/adom-desktop/files/version.json+```++The V2 manifest's `windows.installer_url` / `cli.<plat>.binary_url` point at+the V2 files endpoint; V1's point at V1 static copies. **Follow the+manifest's URLs — do NOT construct binary URLs by hand.**++Network quirk: HEAD on V2 blob/file URLs may return 404 while GET works+(git-wiki #40 — fix in flight). Probe with a Range GET (`Range: bytes=0-0`,+expect 206), never bare HEAD. Asset filenames are dot-form everywhere+(`Adom.Desktop_<ver>_windows_x64-setup.exe`); V1 dot-normalized since v1.8.72.++The wiki page's `metadata.install.windows_installer` field points at the+canonical filename for the current published version. Hydrogen's release build+should fetch the latest via the V2-first pattern:++```bash+# Try V2 first, fall back to V1.+fetch_manifest() {+  curl -fL --max-time 15 \+    "https://wiki.adom.inc/api/v1/pages/adom-desktop/files/version.json" \+    2>/dev/null \+  || curl -fL --max-time 15 \+    "https://wiki.adom.inc/api/v1/pages/adom-desktop/files/version.json"+}++MANIFEST=$(fetch_manifest)+INSTALLER_URL=$(echo "$MANIFEST" | jq -r .windows.installer_url)+INSTALLER_SHA=$(echo "$MANIFEST" | jq -r .windows.installer_sha256)+# Each manifest carries a self-consistent installer_url that points at the+# SAME backend the manifest was served from — V2 manifest → V2 installer URL,+# V1 manifest → V1 installer URL. No URL mixing across backends.++curl -fL "$INSTALLER_URL" -o resources/ad-setup.exe++# Verify SHA256 before bundling — defends against partial fetches + tampering+DOWNLOAD_SHA=$(sha256sum resources/ad-setup.exe | awk '{print $1}')+[ "$DOWNLOAD_SHA" = "$INSTALLER_SHA" ] || { echo "SHA mismatch"; exit 1; }+```++The desktop AD's built-in auto-updater (`src-tauri/src/updater.rs`) walks an+ordered `MANIFEST_URLS` list — V2 first, V1 fallback — and uses whichever+responds first. Once V2 has been the source of truth for a release cycle+without incident, V1 will be retired from the pipeline.++`version.json` also carries the SHA256 hash of the installer so Hydrogen's build+pipeline can verify the download before bundling.++## Asset naming convention (v1.8.113+)++The Adom Desktop wiki page hosts TWO distinct types of artifacts per release:++1. **Full AD GUI installer** — the actual desktop app (Tauri window, bundled+   bridges, relauncher). One per OS / architecture.+2. **CLI companion binary** — a separate executable that talks to AD over+   the relay. Used by Hydrogen's WSL2 container, gallia builds, sibling-app+   installers, cron jobs. Does NOT contain AD itself.++Both ship together at the same version (single `python scripts/bump-version.py+patch` bumps both, single `cargo tauri build` + `cargo build --release` in+`cli/` produces both, single `scripts/release-publish.sh` uploads both).++### Filename pattern++| Artifact | Wiki asset name pattern | Example (v1.8.113) |+|---|---|---|+| Full AD GUI — Windows | `Adom.Desktop_<ver>_windows_x64-setup.exe` | `Adom.Desktop_1.8.113_windows_x64-setup.exe` |+| Full AD GUI — macOS | `Adom.Desktop_<ver>_macos_<arch>.dmg` | `Adom.Desktop_1.8.113_macos_aarch64.dmg` (future) |+| Full AD GUI — Linux | `Adom.Desktop_<ver>_linux_<arch>.AppImage` | `Adom.Desktop_1.8.113_linux_x86_64.AppImage` (future) |+| CLI companion — Linux | `adom-desktop-cli_<ver>_linux_<arch>` | `adom-desktop-cli_1.8.113_linux_x86_64` |+| CLI companion — macOS | `adom-desktop-cli_<ver>_macos_<arch>` | `adom-desktop-cli_1.8.113_macos_aarch64` (future) |+| CLI companion — Windows | `adom-desktop-cli_<ver>_windows_<arch>.exe` | `adom-desktop-cli_1.8.113_windows_x64.exe` (future) |++The `_cli_` token in the middle is the only signal you need to know "this is+the companion, not the app." `linux_x86_64` always means Linux x86_64,+regardless of whether the file is a GUI installer or a CLI binary.++### After install, the binary name is `adom-desktop`++The wiki asset name disambiguates at FETCH time. At INSTALL time the binary+is renamed to `adom-desktop` so the command users type doesn't change:++```bash+# Linux example — install the CLI companion+curl -fL "https://wiki.adom.inc/api/v1/pages/adom-desktop/files/adom-desktop-cli_1.8.113_linux_x86_64" \+  -o /tmp/ad+sudo install -m 755 /tmp/ad /usr/local/bin/adom-desktop+adom-desktop --version+# → adom-desktop 1.8.113 (<sha>, built <ts>)+```++### Version.json `.cli` slot++The auto-update manifest (v1.8.113+) carries a typed `cli` sibling section+for the CLI binaries:++```json+{+  "manifest_version": 1,+  "version": "1.8.113",+  "windows": { "installer_url": ".../Adom.Desktop_1.8.113_windows_x64-setup.exe", ... },+  "cli": {+    "linux_x86_64": {+      "binary_url":      ".../adom-desktop-cli_1.8.113_linux_x86_64",+      "binary_filename": "adom-desktop-cli_1.8.113_linux_x86_64",+      "binary_sha256":   "...",+      "binary_size":     8012408+    }+  },+  "changelog_url": "...",+  "minimum_required": "1.3.30"+}+```++Pre-v1.8.113 readers ignore `.cli` entirely (additive schema; no+`manifest_version` bump required). The desktop AD's own auto-updater only+reads `.windows / .macos / .linux` for its GUI installer URL — it doesn't+touch `.cli`.++For Hydrogen / gallia / Docker-side fetchers, prefer the typed lookup:++```bash+# Best: read the URL out of the manifest (so the filename can evolve)+MANIFEST=$(fetch_manifest)+LINUX_CLI_URL=$(echo "$MANIFEST" | jq -r '.cli.linux_x86_64.binary_url')+LINUX_CLI_SHA=$(echo "$MANIFEST" | jq -r '.cli.linux_x86_64.binary_sha256')+curl -fL "$LINUX_CLI_URL" -o /tmp/ad+[ "$(sha256sum /tmp/ad | awk '{print $1}')" = "$LINUX_CLI_SHA" ] || exit 1+sudo install -m 755 /tmp/ad /usr/local/bin/adom-desktop+```++### Windows CLI is shipped with the NSIS (v1.8.114+)++Installing AD on Windows now drops the CLI companion next to the GUI exe+and adds the install dir to the user's PATH so PowerShell / Claude Desktop /+cmd find it without configuration:++```+%LOCALAPPDATA%\Adom Desktop\adom-desktop.exe        ← GUI (Windows subsystem)+%LOCALAPPDATA%\Adom Desktop\adom-desktop-cli.exe    ← CLI companion (Console subsystem)+%LOCALAPPDATA%\Adom Desktop\ad-relauncher.exe       ← visible self-restart helper+```++NSIS's POSTINSTALL hook prepends `%LOCALAPPDATA%\Adom Desktop` to+`HKCU\Environment\Path` (no UAC — user-level only) and broadcasts+`WM_SETTINGCHANGE` so freshly-opened shells pick up the change. Already-+open shells need a re-launch (OS limitation).++After install, in a fresh PowerShell:++```powershell+adom-desktop-cli --version+# adom-desktop 1.8.114 (<sha>, built <ts>)+adom-desktop-cli status+adom-desktop-cli screenshot_window '{"title-contains":"VS Code"}'+```++The CLI auto-discovers the local AD via direct-API probe at+`127.0.0.1:47200` — no relay needed when AD is on the same box. ~5ms RTT+for compatible verbs; verbs in AD's `cliRequired` list still flow through+a relay if one's running (Hydrogen's WSL2 distro typically owns the relay).++**Relay self-detect (v1.8.114+):** `adom-desktop-cli serve` checks for+another adom-desktop relay on `127.0.0.1:8766/health` before binding. If+one already answers, it prints a friendly note and exits cleanly without+starting a second relay (which would just sit idle — AD's WS client only+attaches to the first one it finds). Pass `--force-bind` to override.++PREUNINSTALL strips the PATH entry back out and rebroadcasts so an+uninstall of AD also gets PATH cleanup.++### Legacy alias migration window++Releases prior to v1.8.113 published the Linux CLI as `adom-desktop-linux`+(no version, no architecture). Hydrogen's WSL2 setup and a few cron-based+installers have that literal filename baked in.++To keep them working through the rename, v1.8.113 publishes BOTH names:++| Name | Status | Wiki URL (V2 / V1) |+|---|---|---|+| `adom-desktop-cli_<ver>_linux_x86_64` | **canonical, v1.8.113+** | V2 / V1 |+| `adom-desktop-linux` | **deprecated alias** — byte-identical copy | V2 / V1 |++Both files are byte-identical with the same SHA256. The legacy alias stays+through **v1.8.116**, then disappears. Migrate by then. Recommended pattern:++```bash+# Migration period (v1.8.113 → v1.8.115): try new name, fall back to alias+LINUX_CLI_URL_NEW=$(echo "$MANIFEST" | jq -r '.cli.linux_x86_64.binary_url // empty')+if [ -n "$LINUX_CLI_URL_NEW" ]; then+  curl -fL "$LINUX_CLI_URL_NEW" -o /tmp/ad+else+  # Falling back — likely talking to a pre-v1.8.113 manifest+  curl -fL "https://wiki.adom.inc/api/v1/pages/adom-desktop/files/adom-desktop-linux" \+    -o /tmp/ad+fi+```++Post-v1.8.116, the fallback branch dies; only the canonical name will exist.