These notes are curated for downstream agents. Treat them as binding guidance: each section funnels a different expectation for builds, style, testing and compliance with upstream tooling. (CLAUDE.md is a symlink to this file.)
A Wails app: Go backend + Svelte (SvelteKit) frontend, packaged as a single native binary.
frontend/— Bun + SvelteKit UI written in Svelte 5 (runes). Built by Vite intofrontend/buildand embedded into Go via//go:embed all:frontend/buildinmain.go. Wails TS bindings are generated intofrontend/src/lib/wailsjs.api/— the Wails-boundController: install/repair/uninstall actions, native dialogs, and the GUI logging bridge.betterdiscord/— download plus install/repair/uninstall of the BetterDiscord asar.discord/— Discord install discovery and injection; per-OS path logic lives inpaths_*.go.types/,utils/,wsl/— shared Go types, backend helpers, and WSL detection/path mapping.main.go/app.go— Wails entry point, window options, and the startup update check.build/— product icons plus plist/manifest/packaging templates (scaffolded/generated; not usually hand-edited).scripts/— release helpers:build-frontend.sh,build-appimage.sh,render-plist.sh, and thewinresGo generator.wails.json— Wails config + frontend hooks (frontend:install=bun install,frontend:build=bun run build). Keep it synchronized with the commands you run locally.Taskfile.yml— dev/build/check/release shortcuts..goreleaser.yaml— release build + publishing config.
Prereqs: Go (matches go.mod), Bun, the Wails CLI, and optionally Task + GoReleaser. On Linux, wails dev/wails build require -tags webkit2_41 (the Task shortcuts add this automatically).
Follow this checklist before you claim a build is validated.
cd frontend && bun install- Bootstraps the renderer toolchain. Required before
wails dev/buildand before running any Bun scripts. Thefrontend:installhook inwails.jsonruns this duringwails build.
- Bootstraps the renderer toolchain. Required before
task dev(orwails dev)- Launches the Go server, compiles the renderer on demand, and proxies events between them. Closest analog to the installed app; use it for interactive testing (dialogs, runtime events, system prompts).
task build(orwails build)- Produces a local native binary and triggers the
frontend:buildhook (bun run build) so the embed picks up the latest UI. Note: official release artifacts come from GoReleaser (see §6), notwails build.
- Produces a local native binary and triggers the
bun run dev– Vite dev server only (renderer, no Go bindings).bun run build– rebuild the renderer bundle intofrontend/buildwhen changing UI/TS/CSS without touching Go.bun run check–svelte-kit sync+svelte-check(Svelte/TypeScript static checks).bun run lint– ESLint using the shared config infrontend/eslint.config.js.bun run test– Vitest unit tests.
go test ./...(ortask test) – backend tests.go vet ./...(ortask vet) – static analysis.gofmt -w .– required formatting for the.gofiles you touch.task check– runs frontend check + lint,go vet, andgo testin one shot. It does not run the frontend Vitest suite; runbun run testfor that.
- Backend: narrow with the
-runregex, e.g.go test ./types -run TestDiscordChannel_Name(swap the suite name as needed). - Many backend tests gate on
runtime.GOOSwitht.Skipfwhen the OS doesn't match — run the subset aligned with your OS; these are guards, not cross-platform stubs. - The frontend has real Vitest unit tests (co-located
*.test.ts, e.g.Checkbox.test.ts,handlers.test.ts). Run all withbun run test, or a single file withbunx vitest run src/lib/utils/handlers.test.ts.bun run checkcovers types separately.
- Imports
- Standard library imports first (e.g.,
context,embed,log). - Blank line, then external dependencies (Wails, pkg/browser), then another blank line.
- Local packages (
installer/api,installer/types,installer/utils, …) go last.
- Standard library imports first (e.g.,
- Formatting
- Always run
gofmt. Tabs are canonical Go indentation. - Tables (like the ones in
channel_test.go) are aligned for readability.
- Always run
- Types & naming
- Exported structs and interfaces follow PascalCase (e.g.,
App,Controller). - Private helpers stay lowercase.
- Channel constants are grouped and defined with
iota(Stable,Canary,PTB).
- Exported structs and interfaces follow PascalCase (e.g.,
- Error handling
- Prefer early returns instead of nested conditionals.
- When something fails (e.g.,
install.InstallBD()), emit the failure event (runtime.EventsEmit(action.ctx, "failure")) andreturnimmediately. - Swallow errors only when a retry loop is not viable; always log or emit events so the renderer can surface the failure.
- Context & runtime
- Store the Wails
context.Contextin the struct once (SetContext). Reuse the stored value for dialogs, events, etc. - Use the context for runtime calls (
runtime.MessageDialog,runtime.OpenDirectoryDialog). Guard all runtime returns and treat empty strings as cancellations.
- Store the Wails
- Logging/event protocol
Controller.Writemirrorsfmtoutput to the UI by emitting alogevent; the default logger is wired to it inmain.go.- Emit only the semantic events both sides agree on:
log,success,failure,reset, andnavigate(which carries an{action}payload). Do not invent new event names without updating the Svelte listeners.
- Assets & versions
- UI assets live in
frontend/build. Any Svelte change needs abun run buildso the Go embed rule picks it up. - The version string is injected via
-ldflags "-X main.version=…"(see §6). Compare against GitHub release data withutils.CompareVersionsto decide if an update is needed.
- UI assets live in
- Dialogs
- Wrap each dialog in a helper that returns string results (
BrowseForDiscord,ConfirmAction,ShowNotice). If the call errors or returns an empty string, propagate an empty response and let the renderer treat it as cancel.
- Wrap each dialog in a helper that returns string results (
- Prefer
$lib/aliases for shared assets, stores, components (see+page.svelteor+layout.svelte). Import generated Wails bindings via the@apialias. - Local constants (e.g.,
const installPaths = ...) live near top-level script scope for clarity. - Keep third-party imports grouped (Svelte, Wails runtime, helpers) and sorted within each category.
- Always use
<script lang="ts">. - Default to
constunless a binding reassigns; reactive state uses$state/$derivedrunes (this is Svelte 5 — no legacywritable/derivedstores). - Introspect derived state with expressions like
$derived(window.navigator.platform.startsWith("Mac"))(see+layout.svelte). - Inline helper functions (
log,succeed,reset) should be declared near the state definitions.
- Use the shared
<Page>,<TextDisplay>,<ProgressBar>,Titlebar, andFootercomponents to keep layout consistent. - Where the UI depends on state (e.g.,
currentAction), render the SVG logic inline inside named snippet helpers ({#snippet icon()}) instead of spreading props. listenFor/unlistenForlifecycles should be paired inonMount/onDestroyblocks to avoid leaking event listeners. Register listeners before kicking off backend actions so early events aren't missed.
- Keep component styles scoped with
<style>blocks. Avoid global selectors. - Reference CSS custom properties (
--bg2,--bg2-alt) for colors; these live infrontend/src/lib/styles/theme.css. - Use textures/gradients and layered pseudo-elements (see
.installer-body::afterin+layout.svelte) to maintain the polished aesthetic. - Avoid brittle absolute positioning; prefer flex layouts and
contain: strictfor window surfaces.
- Store logs and progress inside
$statewrappers, and mutate them vialogs.push(...)or direct assignments. - Expose
activeflags for async work, so the page can wirecanGoNext={!active}. - Treat
listenForpayloads as trusted strings but still guard against empty events (e.g., callmessage.trim()before pushing into logs).
- Use
typealiases for simple shapes. Keep them near the components that consume them. - Keep
frontend/package.jsonandbun.lockupdated in parallel when dependencies change. - For generated bindings, prefer the Wails TS typing output in
frontend/src/lib/wailsjs, imported through the@apialias to keep runtime definitions stable.
Releases are tag-driven via GoReleaser, not a bespoke pipeline.
- Trigger. Pushing a
vX.Y.Ztag runs.github/workflows/release.yml, which invokes GoReleaser (.goreleaser.yaml) across a linux/windows/macOS matrix.max-parallel: 1+mode: keep-existingmean the first leg creates the GitHub release and the others add to it; each leg setsRELEASE_OSso GoReleaser builds only that OS. - Prereleases. A tag like
v2.0.0-alpha.1is detected as a prerelease (prerelease: auto); winget/homebrew publishing is skipped (skip_upload: auto). - Version. Injected via
-ldflags "-X main.version={{ .Tag }}". CI also overrideswails.jsoninfo.productVersionso Windows file metadata matches the tag. Amain.versionthat doesn't start withvis treated as a dev build and skips the update check (app.go). - Artifacts.
BetterDiscord-Installer-Windows.exe,-Mac.zip(universal.app),-Linux.zip, plus-Linux.AppImage.zip(built byscripts/build-appimage.sh, attached viarelease.extra_files) and per-OSchecksums_<os>.txt. - Publishing. The Homebrew cask and winget manifest are pushed to
BetterDiscord/homebrew-tapand thebetterdiscord/winget-pkgsfork usingGH_PAT(falls back toGITHUB_TOKEN, which can't push cross-repo). - CI on PRs/push (
ci.yml): frontend check/lint/test/build,go test,goreleaser check, and a linux release snapshot. The Windows/macOS release legs are only exercised by the manualsnapshot.yml(workflow_dispatch) — run it before tagging. - Windows resources. GoReleaser runs plain
go build, which (unlikewails build) doesn't embed the icon/manifest/version info;scripts/winresgenerates the.sysoin a pre-build hook.
- Reference
README.mdfor high-level orientation andCONTRIBUTING.mdfor style-guide summaries and the AI-assisted-contributions policy. - Emulate the verbose variable names and whole-module imports suggested in
CONTRIBUTING.md(e.g., avoid destructuring core Node modules if the repo avoids it). - When documenting new behavior, add sections to
README.md(or a dedicateddocs/file); keep AGENTS focused on machine-readable guidance.
CLAUDE.mdis a symlink to this file, so Claude Code reads the same guidance.- There are no
.cursor/rules/or.cursorrulesfiles and no.github/copilot-instructions.md; default assistant settings apply. - If future agents add cursor/Copilot rules, append them to this section so every agent reads the new constraints first.
- Never commit credentials, tokens, or
.envfiles. Release publishing relies solely on GitHub Actions secrets:GITHUB_TOKEN(create/upload the release) andGH_PAT(open cross-repo PRs to the Homebrew tap and the winget-pkgs fork). No secret is needed to build locally. - The app is currently unsigned/unnotarized — there is no code-signing secret. Windows SmartScreen and macOS Gatekeeper warnings are expected and documented in the README FAQ; do not add signing config unless the certs/secrets actually exist.
- Supply chain.
scripts/build-appimage.shpinsappimagetoolto a version and verifies its SHA-256 before use. The downloadedbetterdiscord.asaris fetched over TLS from the official site (GitHub release as a fallback) — there is no checksum verification of that asar yet; treat it as a known gap, not something to silently remove. - Auto-update is non-silent.
app.gochecks the latest GitHub release and, if newer, asks the user and opens the download in a browser — it never downloads or executes an update itself. Preserve that confirmation flow. - Keep tracked config (
wails.json,frontend/package.json,go.mod) clean; do not add untracked credential copies to the tree.
- If you find conflicting conventions between AGENTS and
CONTRIBUTING.md, escalate by opening an issue so the maintainers can harmonize the documents. - Use the Wails community (https://wails.io/support) or BetterDiscord Discord (linked from README) when runtime integration questions exceed local knowledge.
- Document any new tooling (e.g., a new CLI helper) within this file so future agents immediately know how to operate the repo.
If you are an autonomous agent submitting a PR where the human operator did not personally review the output before submission, say so explicitly in the PR description. For example:
This PR was generated by [agent/tool]. The human operator [reviewed the diff and ran the checks locally / did not review the output before submission].
This is a transparency expectation, not a penalty against AI-assisted work. It helps calibrate review effort. PRs that appear to be unreviewed agent output without this disclosure will be closed without detailed feedback.