docs · docs/90-meta/INSTALLER.md · synced 2026-09-10

Install and first run

one unsigned Windows installer, what lands where, the first run that fetches the core, SmartScreen

What it is. A Tauri NSIS installer, unsigned (the owner’s decision — docs/70-market/PROCUREMENT_TO_SHIP.md), that installs the GUI (VFlin Antik.exe) and, beside it, the three sidecars: the daemon vflind.exe, the CLI vflin.exe, the MCP server vflin-mcp.exe. The core is not in the installer: the shell downloads it on first run from the package source shipped in the bundle’s install manifest (A87: core.ensure). Every number below was measured on this machine on 2026-09-05; the proof (§5) ran on a clean VFLIN_HOME.

1. Build it from nothing

# prerequisites: Rust (MSVC toolchain), Node 24 + pnpm, the NSIS bundler Tauri fetches on first use
cd ui && pnpm install
# the source the installed product will fetch the core from; a CDN placeholder when unset
export VFLIN_PACKAGE_SOURCE="https://cdn.example/core"        # or file:///D:/vflin-core-pkg for a local proof
pnpm tauri build

tauri build runs the beforeBuildCommandpnpm build && node scripts/build-sidecars.mjs — which builds the web bundle, then builds the three sidecars in release from the root workspace (honouring CARGO_TARGET_DIR when set, else the root target/), copies them to ui/src-tauri/binaries/<name>-<host triple>.exe (Tauri’s naming for bundle.externalBin), and writes ui/src-tauri/resources/install.json ({"packageSource": …}). Both directories are gitignored: the binaries are artifacts, the manifest can carry a machine path. Then the app crate builds (its own standalone workspace under ui/src-tauri, its own target/), and NSIS packs ui/src-tauri/target/release/bundle/nsis/VFlin Antik_0.1.0_x64-setup.exe.

Sizes (2026-09-05, this machine): sidecars vflind.exe 14.2 MB · vflin.exe 2.9 MB · vflin-mcp.exe 2.4 MB (19.5 MB); the installer VFlin Antik_0.1.0_x64-setup.exe 7.4 MB (the 2026-07-26 GUI-only one was 2.8 MB); tauri build rc=0 in 321 s with the web bundle, the sidecar step and the app crate in release. The core is not inside: the first run fetches the 127.7 MB package (A87) from the manifest’s source.

2. What lands where

path
the app + sidecars (per-user install, NSIS currentUser) %LOCALAPPDATA%\VFlin Antik\VFlin Antik.exe, vflind.exe, vflin.exe, vflin-mcp.exe, resources\install.json
the data directory — daemon store, discovery, logs, the installed core ~/.vflin (%USERPROFILE%\.vflin), or $VFLIN_HOME when set
the discovery file every client reads ~/.vflin/local-api.json{port, token, pid, apiVersion}, user-private
the daemon’s journal · the sidecar’s stdout/stderr ~/.vflin/daemon.log · ~/.vflin/logs/vflind.out.log
the core, once installed ~/.vflin/core/<vflin-core-…>/chrome.exe + core-manifest.json; ~/.vflin/core/current.json names it

Why ~/.vflin and not %LOCALAPPDATA%\VFlin. The brief left the root to what the daemon already writes. The daemon, the shell, the CLI (vflin-client) and the MCP server all resolve $VFLIN_HOME, else ~/.vflin; nothing reads %LOCALAPPDATA%\VFlin, and the operator’s live store — 121 profiles — is under ~/.vflin. Changing the default would be a migration of that store, which is a decision, not an installer default. One root, four readers, unchanged. (docs/30-core/CORE_PACKAGE.md said %LOCALAPPDATA% in its first draft; corrected.)

3. What the first run does

  1. The shell starts the daemon (daemon_ensure, a Tauri command in Rust): it asks the discovery file’s daemon for system/health — a stale file whose writer is gone answers nothing and does not count — and when nothing answers it spawns vflind.exe from beside the app, detached (own process group, no console window, VFLIN_HOME passed through, stdout/stderr to ~/.vflin/logs/vflind.out.log), then waits up to 20 s for health before the UI reads discovery. Two shells racing to the same conclusion start at most one daemon: the daemon refuses to start on a store another daemon already serves (it asks that daemon for health with the token in the file, logs “another vflind already serves this store”, exits 0). That guard sits before the startup sweeps on purpose — those sweeps reconcile every RUNNING profile to DIRTY on the assumption that no daemon runs, so a second instance would have declared the first one’s open browsers stopped.
  2. The shell installs the core (core_bootstrap): reads resources/install.json, calls core.ensure {source}, polls core.status every 500 ms and emits a Tauri event core-progress with the job (stage, bytesDone, bytesTotal, state) on every change; returns the final job. On a machine that already has the core this is a no-op in milliseconds (noop:true). The daemon persists the source that worked as packageSource in ~/.vflin/core.json after a successful install — so a later vflin core ensure needs no argument, and the installer never writes a daemon file. The first-run screen that renders the event is design’s (G40/next); the method and the event are here.
  3. Closing the window does not stop the daemon. The operator’s browsers live in it, and an orderly daemon exit closes and seals every one of them (A86). Stopping is explicit: daemon_stop_all in the shell → system.shutdown in the Local API (vflin system shutdown on the CLI, vflin_system_shutdown for an agent) — the goodbye path, with the answer naming how many cores it closes. A detached daemon has no console to receive Ctrl-C on, which is why this is an API method and not a signal.
  4. After a reboot the daemon is not running (no autostart is registered — see §6); the next start of the app starts it again, in seconds, and the core is already there. No reboot is required after installing.

4. What the user sees at install (unsigned)

Windows SmartScreen: “Windows protected your PC” with no publisher name. Click More info → Run anyway. This is the known cost of shipping without a code-signing certificate (PROCUREMENT_TO_SHIP.md §3: a certificate does not remove the warning immediately either; reputation does). Some antivirus products flag unsigned installers on first sight; the sha256 of the installer is published beside it so a download can be verified by hand.

5. Proof — a clean store, the installer, the first run

Measured 2026-09-05 with a88_proof.py: the installer run silently (/S), the INSTALLED app launched with VFLIN_HOME pointing at an empty directory of mine (the owner’s ~/.vflin untouched), the install manifest’s source = the A87 package (file:///D:/vflin-a87-pkg-xz).

step result
installer /S rc=0, 4.0 s, VFlin Antik.exe 11.0 MB, vflind.exe 14.2 MB, vflin.exe 2.9 MB, vflin-mcp.exe 2.4 MB
app launched → daemon up by itself yes, 3.8 s, daemon pid 60892
the daemon is the INSTALLED sidecar %USERPROFILE%\AppData\Local\VFlin Antik\vflind.exe
core.ensure driven by the app from install.json installed=True, activeSource=package, job done in 10.1 s
installed chrome.dll sha256 == A87’s d2996cbb…2346 same
packageSource persisted by the daemon file:///D:/vflin-a87-pkg-xz
first profile started (headed) from the INSTALLED core start 200 in 6.7 s; executable under ~/core/vflin-core-…
vflin system shutdown accepted; daemon gone in 24.8 s; shutdown: closed 1 of 1 core(s) gracefully, 1 sealed to LOCAL_CACHED, in 24.0s; chrome left 0

No reboot was needed at any point; the app started the daemon itself, the daemon installed the core itself, and the first profile ran from the installed core with the same chrome.dll hash A87 measured in three places.

6. What this does not do (on purpose, or not yet)

  • No autostart. The daemon starts when the app starts. A logon-time start (Task Scheduler / Run key) would make browsers survive the app being closed AND the user logging back in — but it is a persistence entry an installer writes into the user’s session, and that is its own decision (ALPHA_PLAN row 2 lists it; not taken here).
  • No signature, no updater. ALPHA_PLAN rows 10 and procurement.
  • local-unlock.json is not in the bundle — it is the developer’s local factor and stays a file the operator creates; check-no-secrets runs on the tree.
  • The CLI and MCP server are installed but not on PATH: %LOCALAPPDATA%\VFlin Antik\vflin.exe is the full path; an MCP client’s mcpServers.command points at …\vflin-mcp.exe (MCP_SKILL_PACK.md). Adding the directory to the user’s PATH is a one-line NSIS hook, not written here.