Skip to content

M5 (2/3): Botloft sets itself up, and an NSIS installer - #11

Merged
httpsphl merged 3 commits into
mainfrom
feat/m5-installer
Sep 28, 2026
Merged

httpsphl merged 3 commits into
mainfrom
feat/m5-installer

Conversation

@httpsphl

Copy link
Copy Markdown
Owner

Second M5 slice. It sits on top of #10 (the first commit here is #10's). Once #10 merges with a merge commit, this diff shrinks to the two app commits.

Setup without questions or jargon

  • The app sets itself up. When nothing runs, it calls daemon_install, which runs the bundled botloftd service install from M5 (1/3): start at logon as a scheduled task, keep the computer awake #10. When the running daemon is older than the app, it updates it the same way. Each check installs or updates at most once, so a failure shows up instead of looping. daemon_start is removed; daemon_restart backs a "Restart Botloft in the background" button for when a connection doesn't come up.
  • The UI no longer says "daemon", "port" or "scheduled task". It talks about "Botloft" and running "in the background", and technical text (errors, paths, ports) is folded under Details. Welcome says Botloft runs in the background and starts with Windows. If Claude Code is missing, the welcome screen and the "Bots can't start" banner explain it and link to https://code.claude.com/docs/en/setup. Bot state hints are plainer too ("Usage limit", "Working on something").
  • botloftd now prints errors on one line (what failed: why), and the app shows that line under Details.

Installer

  • pnpm bundle builds the release sidecar (scripts/sidecar.mjs) and runs tauri build --config src-tauri/tauri.bundle.conf.json. That file holds externalBin and the per-user NSIS config. It stays out of tauri.conf.json because tauri-build copies externalBin in dev and during clippy as well: that would require the release sidecar for every build and would overwrite target\debug\botloftd.exe.
  • Per-user install to %LOCALAPPDATA%\Botloft (Tauri's default), with no admin prompt. That is also the data folder. File names don't collide, and the uninstaller only deletes what it installed, so the data survives an uninstall (spec 5).
  • hooks.nsh: a real uninstall runs botloftd service uninstall. An update also runs the old uninstaller, but with /UPDATE; the hook skips that case, the daemon keeps running, and the app updates it the next time it opens.
  • The exe is Botloft.exe (mainBinaryName), and its manifest adds longPathAware.

Tested

  • pnpm check: 48 tests pass. Clippy and the Rust tests pass.
  • Built the installer (4.6 MiB), then with a scratch install folder and a scratch data folder (BOTLOFT_HOME):
    • silent install: Botloft.exe, botloftd.exe and uninstall.exe are in place, and the Add/Remove Programs entry is registered;
    • launching the installed app with no clicks: the task is registered and the daemon runs from the data folder's bin;
    • installer run again with /UPDATE: the daemon keeps running with the same PID;
    • silent uninstall: the hook stops the daemon and removes the task and the binary; the install folder and the Add/Remove entry are gone, no shortcuts are left, and the data is kept.
  • Preview: first run goes straight to the welcome screen.

Manual test (clean machine)

  1. Run pnpm bundle, copy target\release\bundle\nsis\Botloft_0.1.0_x64-setup.exe to a machine that has Claude Code signed in, and run it. The finish page opens Botloft, which should land on the welcome screen without asking anything.
  2. Create a crew and a bot, then reboot. After logging in, the bot comes back by itself and answers in its chat (open the app to check).
  3. Uninstall from Settings > Apps: the task is gone (Get-ScheduledTask Botloft finds nothing) and %LOCALAPPDATA%\Botloft keeps the data.

The installer is not code-signed yet, so SmartScreen will warn on first run.

…r awake (M5)

`botloftd service install|status|restart|uninstall` registers a per-user
scheduled task through the Task Scheduler COM API (schtasks output is
localized). A logon trigger starts the daemon at logon; a time trigger
that repeats every minute with IgnoreNew brings it back after a crash.
Tested on Windows 11 25H2: RestartOnFailure does not restart a process
that exits with an error, and a logon trigger's repetition only begins
at the next logon, so neither can be the watchdog.

`service install` copies the running binary to <home>\bin and the task
runs that copy, so the app's installer can replace its own files while
the daemon runs. A busy exe is renamed aside instead of overwritten.
One task per data folder, so a dev BOTLOFT_HOME never replaces the real
installation; `--home` overrides BOTLOFT_HOME for every subcommand.

The daemon's manifest asks for long paths and no console
(consoleAllocationPolicy), embedded as a resource because the linker's
manifest tool warns about the new element. Older Windows gets FreeConsole.
Fatal start-up errors now reach the log file, since nobody sees the
task's stderr.

Keep-awake holds a power request while any bot is busy. Unlike
SetThreadExecutionState it belongs to a handle, not a thread, which fits
Tokio's worker pool. The spec is updated to match.
The app no longer asks the owner to start anything. When nothing runs,
it installs the daemon it ships (`daemon_install`, which runs the
sidecar's `botloftd service install`); when the running daemon is older
than the app, it updates it the same way. Each check installs or updates
at most once, so a failure shows instead of looping. `daemon_start` is
gone; `daemon_restart` backs a "Restart Botloft in the background"
button while a connection does not come up.

The owner never has to know that a daemon, a port or a scheduled task
exists: the interface says "Botloft" and "in the background", and keeps
technical text (errors, paths, ports) folded under "Details". A missing
Claude Code links to its official setup page from the welcome screen and
the "Bots can't start" banner.

botloftd now prints errors on one line (`what failed: why`), which the
app shows as is.
`pnpm bundle` builds botloftd for release, copies it where Tauri looks
for the sidecar and runs `tauri build` with tauri.bundle.conf.json, which
turns on `externalBin` and the per-user NSIS installer. It stays out of
tauri.conf.json because tauri-build copies `externalBin` in dev and in
clippy too, which would demand the release sidecar on every build and
overwrite target\debug\botloftd.exe.

The installer never touches the running daemon: the daemon runs from its
own copy in the data folder, so the sidecar file is never in use. Its
uninstall hook runs `botloftd service uninstall` on a real uninstall and
does nothing when an update runs the old uninstaller with /UPDATE; the
app updates the daemon when it next opens. Bots and data stay.

The app's exe is now Botloft.exe and its manifest adds longPathAware to
Tauri's default. The README no longer describes the ConPTY runtime.
@httpsphl
httpsphl merged commit 8affb40 into main Sep 28, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant