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)

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)

# 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."
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:

"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:

# 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:

# 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:

{
  "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:

# 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:

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:

# 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.