# FlintTrade Desktop FlintTrade is a self-hosted web app first. The backend serves the terminal and API from one origin, and a normal browser remains the primary supported way to run it. The desktop package is a small Electron 45 shell around that same local application. > **Installer availability:** the current source baseline is `v0.0.1`. Previous > Tauri and PyInstaller assets do not satisfy the Electron source-bootstrap contract. The public > [download surface](https://flinttrade.vercel.app/download) distinguishes the > source-built web-app install from Electron-shell installers. > It withholds Electron commands and download buttons unless one release > contains the complete asset set (four installers plus `SHA256SUMS.txt`). Use the > [source web-app setup](setup/QUICKSTART.md) or build the Electron shell locally > whenever that complete set is unavailable. ## Delivery model An Electron release contains exactly four shell installers plus one checksum file: | Platform | Canonical asset | |---|---| | macOS 13+, Apple Silicon and Intel | `FlintTrade--mac-universal.dmg` | | Windows x64 | `FlintTrade--win-x64.exe` | | Linux x64 | `FlintTrade--linux-x64.AppImage` | | Linux ARM64 | `FlintTrade--linux-arm64.AppImage` | | All platforms | `SHA256SUMS.txt` | Electron 45 requires macOS 13 (Ventura) or later; macOS 12 (Monterey) is no longer supported by the desktop shell. The shell bundles Node 24.22.2. See the [Electron 45 release notes](https://www.electronjs.org/blog/electron-45-0). The macOS, Windows, Linux and tray assets are generated from the same canonical FlintTrade angular `F` and green flint-spark mark used by the terminal and public site. ![FlintTrade app icon in the locally built macOS DMG](screenshots/13-desktop-dmg-icon.png) The installer contains Electron, the local splash, bootstrap resources and licence notices. It does not contain a frozen Python backend, a prebuilt terminal, or a separately downloaded runtime payload. The shell builds an inspectable source checkout on first launch. The shell and application runtime therefore have separate update lifecycles: - **Source/runtime update:** stage and build a sibling source checkout, health prove it against an isolated temporary workspace, drain the current backend, atomically promote the candidate, and retain one last-known-good checkout for rollback. - **Electron shell update:** select a newer release on the current stable or prerelease channel only when its canonical platform installer and `SHA256SUMS.txt` are present, cryptographically verify the installer's GitHub Sigstore provenance against the exact public repository, tag and release workflow, and hand its attested name and digest to the packaged install script before the shell exits. The script re-hashes the downloaded bytes against that app-provided digest, so replacing the asset after attestation verification cannot change what is installed. The app launches each shell installer from a unique owner-private directory under the shell profile. Failed, cancelled, pre-launch-failed and successful stages are all deliberately retained for forensic review. There is currently no automatic stage purge. Neither the app nor an exited installer treats a re-derived pathname as authority for recursive deletion after the original filesystem identity has been relinquished. Path replacements are therefore always preserved rather than treated as cleanup targets. There is no payload feed, rolling updater release, renderer-owned manifest, or generic native-command bridge. ## First launch The Electron shell owns machine-level bootstrap and lifecycle work. On first launch it: 1. acquires the application singleton and provisions the owner-only `master_password` file in the platform workspace; 2. resolves the managed source at `~/.flinttrade/src/FlintTrade` and tools at `~/.flinttrade/tools` (under the current user's home on every platform); 3. acquires the official source with system Git, or uses the official HTTPS archive fallback when Git is unavailable; 4. provisions checksum-verified pinned `uv` and Node 22 distributions into the managed tools directory regardless of system Node availability; `uv` then provisions Python 3.14 and Corepack activates the repository-pinned pnpm 10.34.5; 5. runs the frozen Python and JavaScript installs, then builds the terminal; 6. promotes the completed candidate only after all build steps pass; 7. starts `packaging/desktop_backend.py` from the managed virtual environment and waits for both `FLINTTRADE_BACKEND_READY port=` and a successful loopback `/api/v1/ping` before opening the main window. Bootstrap progress is available as pushed events and a polled snapshot. The splash keeps a visible heartbeat during long work, redacts durable logs, and offers bounded retry or cancellation after failure. A partially built candidate never replaces the active source. The application source and user data are separate: | Platform | User workspace | |---|---| | macOS | `~/Library/Application Support/flinttrade/` | | Windows | `%APPDATA%\flinttrade\` | | Linux | `~/.flinttrade/` | `FLINTTRADE_WORKSPACE_DIR` and `FLINTTRADE_HOME` remain explicit advanced overrides. Source promotion, rollback, shell update and ordinary uninstall do not mutate the workspace. ## Desktop lifecycle - Closing the main window hides it to the system tray; it does not stop the backend. - The tray exposes Show and Quit, and a tray click toggles visibility. - `CommandOrControl+Shift+F` toggles the window globally. - macOS dock activation restores a hidden window. - Backend lines in the form `FLINTTRADE_NOTIFY\t\t<body>` become native notifications after strict parsing. - Explicit quit starts the backend's bounded asynchronous drain. Electron requests graceful shutdown first and then closes the owned process containment if proof does not arrive. - A crashed backend replaces the remote terminal with the local recovery surface instead of leaving a stale page. The main window loads only the backend's selected `127.0.0.1` origin. Electron uses context isolation, renderer sandboxing and no Node integration. The preload exposes named `window.flintDesktop` methods only; every IPC handler validates the sender frame, and navigation, downloads, webviews, permissions and unexpected child windows are denied. ## Install after an Electron release is available The public [download surface](https://flinttrade.vercel.app/download) distinguishes the source-built web-app install from Electron-shell installers. It exposes the supported one-command Electron paths only when one release contains all four canonical installers plus `SHA256SUMS.txt`: ```bash # macOS or Linux curl -fsSL https://flinttrade.vercel.app/install.sh | bash ``` ```powershell # Windows 10/11 irm https://flinttrade.vercel.app/install.ps1 | iex ``` The scripts enumerate official GitHub releases, require an internally consistent semantic-version channel, verify that the current platform's exact canonical asset and `SHA256SUMS.txt` are both present, and verify the downloaded installer before changing the installed shell. An in-app update additionally requires GitHub's Sigstore artifact attestation; a direct first install remains usable with the release checksum because no trusted FlintTrade app exists yet to perform that provenance check. `--ref <tag>` selects an exact release and fails closed when that release is incomplete. `--no-launch` skips the final launch. `--build-from-source` / `-BuildFromSource` is a separate contributor path that packages only the shell from a trusted checkout. That path also compiles the packaged native filesystem boundary: macOS requires the system `/usr/bin/clang` and Node's installed N-API headers, Linux requires `/usr/bin/cc` plus those headers, and Windows requires the x64 .NET Framework `csc.exe`. The installer checks these prerequisites before changing a checkout. The install scripts record exact shell identity in owner-private local state. Updates and ordinary uninstalls refuse unknown same-name applications, links, reparse aliases, changed executables or integration files that are not proved by that receipt. ### If the download site returns an error Use this whenever an install command above fails. The hosted URLs are only a convenience redirect to the scripts in this repository, and a deployment without an immutable source commit answers `503` for all four routes rather than redirecting. Fetch the same installer straight from GitHub: ```bash # macOS or Linux curl -fsSL https://raw.githubusercontent.com/navaneeshnagarajan/FlintTrade/main/scripts/install/flinttrade-install.sh | bash ``` ```powershell # Windows irm https://raw.githubusercontent.com/navaneeshnagarajan/FlintTrade/main/scripts/install/flinttrade-install.ps1 | iex ``` Or run the same file from a clone — see [`scripts/install/`](../scripts/install/): ```bash # macOS or Linux bash scripts/install/flinttrade-install.sh ``` ```powershell # Windows powershell -ExecutionPolicy Bypass -File scripts\install\flinttrade-install.ps1 ``` Every flag documented above (`--ref`, `--no-launch`, `--build-from-source`, and their PowerShell spellings) works identically on this path — but note that `curl … | bash` and `irm … | iex` give the script no argument vector. To pass a flag, use `bash -s --` (for example `curl -fsSL <url> | bash -s -- --ref v1.2.3`), the scriptblock form on Windows (`& ([scriptblock]::Create((irm <url>))) -Ref v1.2.3`), or run the file from a clone as shown above. ### macOS boundary Local `make desktop-package` and `pack:mac` builds always force an **ad-hoc seal**: `codesign --verify --deep --strict` proves bundle integrity, but the app has no Developer ID trust and is not notarised. A manually downloaded local build can therefore be blocked by Gatekeeper and may require the Privacy & Security override. Do not describe an ad-hoc seal as Apple signing. Only the release workflow can use distribution signing and notarisation. When all distribution-signing secrets are present, CI requires the complete notarisation trio too; without those complete sets it produces an ad-hoc-sealed DMG, and partial configuration fails the release instead of publishing an ambiguously signed build. ### Windows boundary The Windows asset is an Electron-builder NSIS x64 installer and installs per user. Windows 11 on ARM uses x64 emulation. Electron includes Chromium, so the shell does not download or depend on the retired WebView2-based desktop runtime. Authenticode signing is not currently configured. Source apply and rollback use a packaged native filesystem helper rather than claiming a Node directory-flush guarantee. The helper binds every managed directory to its Windows volume serial and 128-bit file ID, rejects reparse or aliased paths, and commits same-volume journal and promotion renames through exact no-delete-share handles plus a native parent-directory flush. Journal replacement and removal bind the target to its expected file ID and SHA-256 digest, deny compatible writers while that evidence is authenticated, and require the reserved `.previous` name to be absent. A normal commit may use that name transiently for the already-pinned target, but deletes it through the same handle only after verifying the replacement. Before inspecting or mutating the logical journal, the helper reconciles only an exact canonical transaction receipt whose recorded target, temporary replacement, prior identity and replacement identity match the files still present. This lets a crash after `.previous` was published finish or roll back deterministically. A `.previous` without that complete receipt-bound evidence, an invalid or changed fixed `.transaction` receipt, or an ambiguous target/previous/temporary state is preserved and blocks mutation instead of being adopted as cleanup authority. Windows source cleanup pins the managed parent, renames the exact identity-bound root to its deterministic quarantine and flushes the parent before reclamation. The packaged helper then snapshots every direct child by native file ID, reopens that exact identity without following reparse points, and deletes only handles whose canonical parent remains inside the pinned quarantine. A reparse entry itself may be removed, but its external target is never traversed. Late, changed, locked, over-budget or ambiguous evidence leaves the exact quarantine in place rather than widening deletion authority. The managed target is absent while retained evidence is tracked in `.flinttrade-source-cleanup.json` or `.flinttrade-preserved-source-quarantines.json`. Startup and the next source apply retry entries in the source-cleanup inventory; a successful native reclamation prunes its row, so ordinary failed candidates and staging roots do not accumulate until the metadata cap denies future updates. Preserved last-known-good promotion evidence remains manual-forensics evidence: recovery only proves an already removed quarantine absent before pruning that inventory. Each inventory remains independently capped at 64 entries as an integrity bound. Evidence which stays locked or changes identity may still require the documented manual archive-and-remove procedure. A changed identity or ambiguous target/quarantine arrangement is never adopted as deletion authority. The helper executable is SHA-256-bound into the Electron main bundle at build time. The Windows Job supervisor opens and hashes that exact ordinary file, denies write/delete sharing, and retains the handle through `CreateProcess` so the verified helper cannot be replaced between authentication and execution. ### Linux boundary Linux publishes separate x64 and ARM64 AppImages. The install script uses a per-user destination, desktop entry and command wrapper without `sudo`. If FUSE 2 is unavailable, it attempts an installer-owned AppImage extraction and runs the extracted `AppRun` instead. ## Uninstall After an Electron release is installed with the one-command installer, ordinary uninstall removes only the identity-proved shell and the integration files named by that installer's receipt: ```bash # macOS or Linux curl -fsSL https://flinttrade.vercel.app/uninstall.sh | bash ``` ```powershell # Windows irm https://flinttrade.vercel.app/uninstall.ps1 | iex ``` The workspace, Electron profile, managed source and tools, and legacy desktop data are retained so a reinstall can recover them. The uninstaller refuses to delete a same-name path without the exact receipt and executable/integration identity. A manual macOS Finder copy from the DMG deliberately has no installer receipt. Quit FlintTrade and move that `FlintTrade.app` to Trash in Finder; the workspace, Electron profile, managed source and tools remain available for reinstall. Once the unreceipted app is gone, the purge command below can still remove recognised FlintTrade data. The script will not claim an unreceipted same-name application as its deletion authority. `--purge` on macOS/Linux, or `-Purge` on Windows, additionally requests deletion of recognised FlintTrade data. Purge is irreversible, requires typed or explicit scripted confirmation, stays within the current user's home and fails closed on links, reparse points, foreign ownership or an unproved custom workspace: ```bash # macOS or Linux curl -fsSL https://flinttrade.vercel.app/uninstall.sh | bash -s -- --purge ``` ```powershell # Windows & ([scriptblock]::Create((irm https://flinttrade.vercel.app/uninstall.ps1))) -Purge ``` ### If the download site returns an error The hosted URLs are only a convenience redirect to the scripts in this repository. If `https://flinttrade.vercel.app/uninstall.sh` or `https://flinttrade.vercel.app/uninstall.ps1` returns an error, fetch the same uninstaller straight from GitHub: ```bash # macOS or Linux curl -fsSL https://raw.githubusercontent.com/navaneeshnagarajan/FlintTrade/main/scripts/install/flinttrade-uninstall.sh | bash ``` ```powershell # Windows irm https://raw.githubusercontent.com/navaneeshnagarajan/FlintTrade/main/scripts/install/flinttrade-uninstall.ps1 | iex ``` Or run the same uninstaller directly from a clone (or from the managed source checkout at `~/.flinttrade/src/FlintTrade`): ```bash # macOS or Linux bash scripts/install/flinttrade-uninstall.sh ``` ```powershell # Windows powershell -ExecutionPolicy Bypass -File scripts\install\flinttrade-uninstall.ps1 ``` Both accept the same `--purge` / `-Purge` flag. These are the same files the site serves — see [`scripts/install/`](../scripts/install/). ## Build and verify locally Shell development requires Git and Node 22.23.4 or newer. Use the repository's pinned pnpm 10.34.5; Rust is needed only for the optional `core/ticks` package, not for Electron packaging. These lines run unchanged in bash, zsh and Windows PowerShell: ```bash pnpm install --frozen-lockfile python scripts/ft.py desktop-test # Electron TypeScript + Vitest python scripts/ft.py desktop-build # verify resources, test, and bundle main/preload python scripts/ft.py desktop-package # package and verify this host's installer ``` On POSIX, `make desktop-test`, `make desktop-build` and `make desktop-package` are aliases for the same three targets. Installer output lands in `packages/apps/desktop/release/electron/`. Package-specific commands are also available: ```bash pnpm --filter @flinttrade/desktop typecheck pnpm --filter @flinttrade/desktop test:electron pnpm --filter @flinttrade/desktop bundle pnpm --filter @flinttrade/desktop pack:mac pnpm --filter @flinttrade/desktop pack:win pnpm --filter @flinttrade/desktop pack:linux:x64 pnpm --filter @flinttrade/desktop pack:linux:arm64 ``` Only the command matching the host platform is expected to work locally. A Mac package does not prove Windows or Linux runtime behaviour. The local macOS command always uses the ad-hoc seal; Apple distribution signing and notarisation are release-CI-only paths. ## Release CI `.github/workflows/desktop-release.yml` is the release authority. Release Please dispatches it from the immutable version tag and expected commit SHA, or a maintainer can dispatch it without a tag for build-only artefacts. The matrix runs: - macOS universal on `macos-14`; - Windows x64 on `windows-latest`; - Linux x64 on `ubuntu-22.04`; - Linux ARM64 on `ubuntu-22.04-arm`. The two Linux legs stay on 22.04 deliberately, and are the one place in CI exempt from the supported-runner floor in `flint.toml`. They compile the native addon that ships inside the installer, so the runner's glibc becomes the minimum glibc of the published binary — raising the runner would raise the floor for every user. The jobs that compile nothing (`validate`, `publish`, `electron-desktop-tests`) track the supported floor instead. Each job installs the frozen JavaScript workspace, verifies the bootstrap tool manifest, bundles Electron, builds the installer and verifies the packaged security contract. Publication refuses an existing non-empty release, creates `SHA256SUMS.txt`, attests each asset's provenance, and rechecks the exact five-asset set. The in-app updater fetches attestations by exact installer digest and requires the `desktop-release.yml` `workflow_dispatch` identity at the selected tag, GitHub's public Fulcio issuer, and valid Rekor and certificate transparency proofs before installer handoff. `test.yml` runs the Electron TypeScript, Vitest, bundle and hardened Linux directory-package gate on normal code changes. The weekly/manual `nightly-cross-platform.yml` packages Electron on macOS, Windows and Linux. Windows and Linux runtime semantics remain CI/contributor-owned evidence; a single local Mac run is not cross-platform proof. ## Relevant source | Path | Role | |---|---| | [`packages/apps/desktop/electron/`](../packages/apps/desktop/electron/) | Electron main/preload, bootstrap, updates, lifecycle and tests | | [`packages/apps/desktop/splash/`](../packages/apps/desktop/splash/) | Local bootstrap/recovery surface | | [`packages/apps/desktop/resources/bootstrap/`](../packages/apps/desktop/resources/bootstrap/) | Packaged bootstrap entrypoints and verified tool manifest | | [`packaging/desktop_backend.py`](../packaging/desktop_backend.py) | Source guardian and backend process owner | | [`scripts/install/`](../scripts/install/) | Identity-bound shell install and uninstall scripts | | [`.github/workflows/desktop-release.yml`](../.github/workflows/desktop-release.yml) | Four-installer build and publication | ## Troubleshooting - **The download page does not show Electron install commands or downloads.** This is expected unless one release contains all four canonical installers plus `SHA256SUMS.txt`. Use the distinct source-built web-app setup or build the Electron shell locally when the complete set is unavailable. - **First launch remains on the splash.** Read the redacted bootstrap log in the workspace's `logs/` directory. Retry resumes through a new attempt; a failed candidate does not replace the active source. FlintTrade durably caps first-run allocation at three retained attempts (the initial attempt plus two retries) and stops before allocating another candidate, download, snapshot or extraction tree. The app never pathname-deletes this forensic evidence; use the explicit uninstall purge flow if all three attempts fail. - **A source update is refused.** The updater rejects dirty, foreign or changed active checkouts. Keep personal source work outside the managed active path. On Windows, close programs holding files in the managed checkout and retry; FlintTrade preserves the journal instead of deleting around a lock. A filesystem which refuses the native parent flush is unsupported and the update remains fail-closed. - **Windows reports too many preserved source-operation lease quarantines.** These are `.flinttrade-bootstrap-operation.lock.stale-*` directories left by interrupted bootstrap or update ownership, not either source-cleanup inventory. FlintTrade revalidates them but does not pathname-delete them on Windows, and blocks after 64 are retained. Stop FlintTrade, archive the exact directories for diagnosis, then remove only confirmed stale quarantine directories; never remove the active `.flinttrade-bootstrap-operation.lock`. - **macOS reports an unidentified developer.** The current boundary is an ad-hoc seal, not Developer ID signing or notarisation. Use the documented Privacy & Security override for a locally built package. - **Linux cannot mount the AppImage.** Use the one-command installer after a release is available; it attempts the FUSE-less extracted layout. - **Port 5100 is already in use.** The desktop guardian starts the managed backend with `--port 0`, so a contributor backend on 5100 does not conflict.