feat(launchers/helper-go): Linux + Windows support#104
Conversation
Cross-platform install for the agent-wiki helper. macOS chain stays
as-is; Linux + Windows added via build-tag-suffixed install_<os>.go
modules + per-OS build scripts + per-OS backend routes.
- internal/install: install_common.go (pure render helpers, testable
from any platform) + install_{darwin,linux,windows}.go selected via
filename suffix. macOS uses osacompile + LaunchServices (unchanged).
Linux writes a freedesktop .desktop file + runs xdg-mime. Windows
writes HKCU\Software\Classes\agentwiki via reg add (no extra deps).
- scripts/build-linux.sh + build-windows.sh: cross-compile + package
each as a tarball/zip containing the binary + install.sh/install.bat
that drops it in ~/.local/bin or %LOCALAPPDATA% and runs the
install subcommand.
- Windows is unsigned for v1 — SmartScreen warning is documented in
the README + FE install copy. Authenticode is a follow-up.
- CI workflow refactored from 1 job to 3 parallel jobs (darwin keeps
the AWS-OIDC + signing chain; linux + windows run on ubuntu-latest
with the Go cross-compiler, no secrets). workflow_call now outputs
darwin-/linux-/windows-artifact-name.
- docker-build-push + nightly-build pull all 3 artifacts into
backend/static/installers/ and verify each file before docker build.
- Backend: /installer/mac, /installer/linux?arch=amd64|arm64,
/installer/windows. /installer/app stays as a back-compat mac alias.
- FE InstallHelperPane: UA-based platform detect → per-OS download
button + install copy; unknown platform shows all 3.
freedesktop spec §6.5 says field codes (e.g. %u) inside quoted args have unspecified behavior. Revert codex's well-meaning quote-add and note the constraint in the helper's doc.
Greptile SummaryExtends the Go launcher to macOS/Linux/Windows by adding build-tag-selected
Confidence Score: 4/5The cross-platform wiring is structurally sound; the one defect worth fixing before merge is in the Windows install path. The Windows Install() function silently drops a UserHomeDir error rather than returning it, meaning the .agentwiki config directory may never be created on affected machines and the launcher will fail later with no clear indication of the root cause. The Linux counterpart handles this correctly. Everything else — the CI workflow split, the new backend routes, the frontend UA detection, the build scripts, and the test coverage — looks correct. packages/agentwiki-launcher-go/internal/install/install_windows.go — the UserHomeDir error handling; frontend/src/components/agents/InstallHelperPane.tsx — minor button variant inconsistency in the unknown-platform fallback. Important Files Changed
Sequence DiagramsequenceDiagram
participant FE as InstallHelperPane
participant UA as navigator.userAgent
participant BE as Backend /installer/…
participant FS as static/installers/
FE->>UA: detectPlatform()
UA-->>FE: "mac" | "linux" | "windows" | "unknown"
alt mac
FE->>BE: GET /installer/mac
BE->>FS: AgentWikiLauncher.zip
FS-->>BE: file stream
BE-->>FE: application/zip
else linux
FE->>BE: "GET /installer/linux?arch=amd64"
BE->>FS: agentwiki-launcher-linux-amd64.tar.gz
FS-->>BE: file stream
BE-->>FE: application/gzip
else windows
FE->>BE: GET /installer/windows
BE->>FS: agentwiki-launcher-windows-amd64.zip
FS-->>BE: file stream
BE-->>FE: application/zip
end
Note over FE: User runs install.sh / install.bat / drags .app
FE->>FE: launcher install subcommand
Note over FE: agentwiki:// URL scheme registered
Prompt To Fix All With AIFix the following 2 code review issues. Work through them one at a time, proposing concise fixes.
---
### Issue 1 of 2
packages/agentwiki-launcher-go/internal/install/install_windows.go:33-37
Silent error swallow on `UserHomeDir` — the outer `if` condition eats any failure, so if the home directory can't be resolved the `.agentwiki` config dir is silently skipped. The launcher will later fail to write its endpoint config with no indication of why. The Linux counterpart returns the error unconditionally; Windows should do the same.
```suggestion
home, err := os.UserHomeDir()
if err != nil {
return err
}
if err := os.MkdirAll(filepath.Join(home, ".agentwiki"), 0o755); err != nil {
return err
}
```
### Issue 2 of 2
frontend/src/components/agents/InstallHelperPane.tsx:131-142
In the `unknown`-platform fallback block, only the macOS button carries `variant="action"`. The Linux and Windows buttons render with the default (secondary) style, making all three look like different levels of priority on SSR/first-paint where the platform hasn't been resolved yet. They should share the same variant.
```suggestion
<Button
size="md"
variant="action"
onClick={() => download("/api/installer/linux?arch=amd64")}
>
Download for Linux (amd64)
</Button>
<Button
size="md"
variant="action"
onClick={() => download("/api/installer/windows")}
>
Download for Windows
</Button>
```
Reviews (1): Last reviewed commit: "fix(launchers/helper-go): unquote launch..." | Re-trigger Greptile |
| if home, err := os.UserHomeDir(); err == nil && home != "" { | ||
| if err := os.MkdirAll(filepath.Join(home, ".agentwiki"), 0o755); err != nil { | ||
| return err | ||
| } | ||
| } |
There was a problem hiding this comment.
Silent error swallow on
UserHomeDir — the outer if condition eats any failure, so if the home directory can't be resolved the .agentwiki config dir is silently skipped. The launcher will later fail to write its endpoint config with no indication of why. The Linux counterpart returns the error unconditionally; Windows should do the same.
| if home, err := os.UserHomeDir(); err == nil && home != "" { | |
| if err := os.MkdirAll(filepath.Join(home, ".agentwiki"), 0o755); err != nil { | |
| return err | |
| } | |
| } | |
| home, err := os.UserHomeDir() | |
| if err != nil { | |
| return err | |
| } | |
| if err := os.MkdirAll(filepath.Join(home, ".agentwiki"), 0o755); err != nil { | |
| return err | |
| } |
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/agentwiki-launcher-go/internal/install/install_windows.go
Line: 33-37
Comment:
Silent error swallow on `UserHomeDir` — the outer `if` condition eats any failure, so if the home directory can't be resolved the `.agentwiki` config dir is silently skipped. The launcher will later fail to write its endpoint config with no indication of why. The Linux counterpart returns the error unconditionally; Windows should do the same.
```suggestion
home, err := os.UserHomeDir()
if err != nil {
return err
}
if err := os.MkdirAll(filepath.Join(home, ".agentwiki"), 0o755); err != nil {
return err
}
```
How can I resolve this? If you propose a fix, please make it concise.| <Button | ||
| size="md" | ||
| onClick={() => download("/api/installer/linux?arch=amd64")} | ||
| > | ||
| Download for Linux (amd64) | ||
| </Button> | ||
| <Button | ||
| size="md" | ||
| onClick={() => download("/api/installer/windows")} | ||
| > | ||
| Download for Windows | ||
| </Button> |
There was a problem hiding this comment.
In the
unknown-platform fallback block, only the macOS button carries variant="action". The Linux and Windows buttons render with the default (secondary) style, making all three look like different levels of priority on SSR/first-paint where the platform hasn't been resolved yet. They should share the same variant.
| <Button | |
| size="md" | |
| onClick={() => download("/api/installer/linux?arch=amd64")} | |
| > | |
| Download for Linux (amd64) | |
| </Button> | |
| <Button | |
| size="md" | |
| onClick={() => download("/api/installer/windows")} | |
| > | |
| Download for Windows | |
| </Button> | |
| <Button | |
| size="md" | |
| variant="action" | |
| onClick={() => download("/api/installer/linux?arch=amd64")} | |
| > | |
| Download for Linux (amd64) | |
| </Button> | |
| <Button | |
| size="md" | |
| variant="action" | |
| onClick={() => download("/api/installer/windows")} | |
| > | |
| Download for Windows | |
| </Button> |
Prompt To Fix With AI
This is a comment left during a code review.
Path: frontend/src/components/agents/InstallHelperPane.tsx
Line: 131-142
Comment:
In the `unknown`-platform fallback block, only the macOS button carries `variant="action"`. The Linux and Windows buttons render with the default (secondary) style, making all three look like different levels of priority on SSR/first-paint where the platform hasn't been resolved yet. They should share the same variant.
```suggestion
<Button
size="md"
variant="action"
onClick={() => download("/api/installer/linux?arch=amd64")}
>
Download for Linux (amd64)
</Button>
<Button
size="md"
variant="action"
onClick={() => download("/api/installer/windows")}
>
Download for Windows
</Button>
```
How can I resolve this? If you propose a fix, please make it concise.… variant in unknown-platform fallback Greptile #104: - P1 install_windows.go — match Linux: fail loudly on UserHomeDir error - P2 InstallHelperPane.tsx — Linux/Windows fallback buttons need variant="action" to match macOS
Tightens the cross-platform packaging from "zip + installer script" to single-file downloads where possible, and ports the rest of the dispatch flow (Pin/Switch dialogs, terminal spawn) so the new packages actually work end-to-end on Linux + Windows — not just install. Packaging - Windows: single .exe linked with -H windowsgui (no console flash on every agentwiki:// dispatch). Drops install.bat + zip wrapper. User downloads .exe, double-clicks, SmartScreen → "More info" → "Run anyway", a MessageBox confirms install succeeded. - Linux: AppImage (amd64) is the default — chmod +x, double-click, done. install_linux.go picks up $APPIMAGE so the .desktop entry written to ~/.local/share/applications points at the AppImage file, not the ephemeral FUSE mount. Tarball stays as fallback for arm64 and distros that prefer manual install. Dispatch flow (the install was the easy part) - internal/dialog package: ConfirmPin / ConfirmSwitch with per-OS build-tag impls — osascript (mac), zenity/kdialog (linux), mshta MsgBox (windows). Pure helpers (escapeAppleScript, vbQuote) in dialog_common.go so tests run on every OS. - internal/terminal package: terminal_<os>.go per build tag. Linux picks $TERMINAL / x-terminal-emulator / gnome-terminal / konsole / xfce4-terminal / mate-terminal / tilix / xterm in that order, falls back to headless bash if none found. Windows writes a .bat wrapper and opens `cmd /c start cmd /k wrapper.bat` so the window stays alive for the CLI session. - Windows error visibility: -H windowsgui detaches stderr from any console; main.go now tees errors to ~/.agentwiki/stub.log so silent GUI failures are diagnosable. Routes + FE - /installer/linux accepts format=appimage (default) | tar.gz with arch=amd64|arm64. - /installer/windows now serves the .exe (application/octet-stream). - FE InstallHelperPane: AppImage download link with chmod+x guidance; .exe link with SmartScreen guidance. CI - linux job installs appimagetool (--appimage-extract trick since GH runners have no FUSE), builds tarballs + AppImage, uploads both. - windows job uploads .exe (no zip). - docker-build-push + nightly verify the 5 expected files.
AppImage still requires `chmod +x` once before first run — annoying for mac-parity UX. Add .deb (Debian / Ubuntu / Mint) + .rpm (Fedora / RHEL / openSUSE) so double-click goes through Software Center / GNOME Software / KDE Discover and the URL handler is registered via postinst. Zero terminal, zero chmod. - scripts/build-deb.sh — dpkg-deb pack with /usr/bin + /usr/share/applications layout, postinst+postrm refresh desktop database. xdg-utils dep. - scripts/build-rpm.sh — rpmbuild with matching layout + %post/%postun. Requires Linux (macOS brew rpm refuses x86_64 cross-build). - Makefile: release-deb + release-rpm targets. - CI: apt-get install rpm, run both scripts, upload artifacts alongside AppImage + tarballs. - docker-build-push + nightly: verify .deb + .rpm files present too. - backend installer.py: /installer/linux?format=deb (NEW default), format=rpm, format=appimage, format=tar.gz. Versioned filenames resolved via glob at request time. - FE InstallHelperPane: default Linux download = .deb; instructions list the rpm / appimage / tar.gz fallbacks.
rpmbuild on Fedora / RHEL inserts a dist tag (.el9, .fc40) between the release number and arch, producing filenames like agentwiki-launcher-0.9.0-1.el9.x86_64.rpm. The previous glob `agentwiki-launcher-*-1.x86_64.rpm` required the literal sequence `1.x86_64`, missing every dist-tagged build. CI rpmbuild on ubuntu happens to omit the dist tag, which is why local + GH-artifact paths worked but the rpm-picks-newest-version test (which simulates real distro outputs) failed. Glob is now `agentwiki-launcher-*-1*.x86_64.rpm` — matches both plain and dist-tagged releases. Verified locally: matched: agentwiki-launcher-0.10.0-1.fc40.x86_64.rpm
Description
Cross-platform install for the launcher. macOS chain unchanged; Linux + Windows added via filename-suffixed build-tag modules + per-OS build scripts + per-OS backend routes.
internal/install/install_common.go— pureRenderLinuxDesktopFile/RenderWindowsRegistryCommands(testable from any platform)install_linux.go— writes~/.local/share/applications/agentwiki-launcher.desktop, runsxdg-mime+update-desktop-databaseinstall_windows.go—reg add HKCU\Software\Classes\agentwikiviareg.exe(no extra Go deps)install_darwin.go(renamed) — same behavior, func renamed toInstallfor cross-OS dispatchscripts/build-linux.sh— amd64+arm64 tarballs (binary + install.sh →~/.local/bin)scripts/build-windows.sh— amd64 zip (.exe+ install.bat →%LOCALAPPDATA%). Unsigned for v1; SmartScreen warning documented. Authenticode is a follow-up.darwin-/linux-/windows-artifact-name.docker-build-push.yml+nightly-build.yml: pull all 3, verify each before docker build/installer/{mac,linux,windows};/installer/appkept as mac aliasInstallHelperPane: UA platform detect → per-OS download + install copyCodex review pass 1 caught
.desktopExecquoting + no-fstring rule violations + xdg-mime stderr fallback. Fixed in same commit.How Has This Been Tested?