Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
30b6bfc
fix: use new injection assets
zerebos Jul 30, 2026
ac98528
fix: embed assets
zerebos Jul 30, 2026
b3676e4
feat: add options to different operations
zerebos Jul 30, 2026
00b89ae
fix: better error handling
zerebos Jul 30, 2026
8bc0da4
feat: switch to tracking resources dir
zerebos Jul 30, 2026
1b01758
fix: use resources path everywhere
zerebos Jul 30, 2026
c9825bc
feat: add more testing
zerebos Jul 30, 2026
4b9b529
feat: more tests before refactoring
zerebos Jul 30, 2026
8148b1f
refactor: simplify the path resolution
zerebos Jul 30, 2026
cf7b35b
feat: rework injection to match app.asar
zerebos Jul 30, 2026
3549c30
chore: update related tests
zerebos Jul 30, 2026
ae23d8b
feat: update paths for macos + linux + flatpak
zerebos Jul 30, 2026
56b6f94
fix: remove references to corepath
zerebos Jul 31, 2026
845219b
fix: resolve injected folders
zerebos Jul 31, 2026
1830764
fix: reinject should always work
zerebos Jul 31, 2026
af10aa8
fix: add additional sanity checks
zerebos Jul 31, 2026
f2b8df2
fix: add protection for empty resources dir
zerebos Jul 31, 2026
f1d8f0c
chore: update injection js
zerebos Jul 31, 2026
1e1336a
fix: more edge cases
zerebos Jul 31, 2026
a480b9c
feat: kill discord before injection
zerebos Jul 31, 2026
900ad1d
fix: yet more sanity checking
zerebos Aug 1, 2026
812d894
fix: add note for wsl and kill timeout
zerebos Aug 1, 2026
f65b892
fix: more protection edge cases
zerebos Aug 1, 2026
f88bb24
chore: more protection for snap installs
zerebos Aug 1, 2026
2f7bfef
fix: edge case handling
zerebos Aug 1, 2026
bf87423
fix: cleanly resolve flaky enumeration
zerebos Aug 1, 2026
14baf96
feat: support downloading rolling release
zerebos Aug 2, 2026
b0bea8c
feat: add dev build options
zerebos Aug 2, 2026
8fcfb53
fix: run gofmt
zerebos Aug 3, 2026
b074497
fix: make dev flag install only
zerebos Aug 3, 2026
96778e3
fix: fix version folder sorting
zerebos Aug 3, 2026
62d94c5
Merge pull request #11 from BetterDiscord/feat/bd-canary
zerebos Aug 4, 2026
1d2c425
fix: path should be labelled as resources
zerebos Aug 4, 2026
4028ca3
feat: additional rollback protection
zerebos Aug 4, 2026
0113edf
feat: introduce AGENTS file
zerebos Aug 4, 2026
7cd3d95
chore: updating contributing guide
zerebos Aug 4, 2026
387b6c0
chore: update readme to clarify snap
zerebos Aug 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 106 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# AGENT OPERATING NOTES

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.)

## 1. Project layout recap

A pure-Go Cobra CLI (`bdcli`) for installing, updating, and managing BetterDiscord. It is the command-line sibling of the BetterDiscord GUI installer — the two repos share domain concepts (release channels, Discord discovery, asar download, injection) but no code; do not assume installer file paths or APIs exist here.

- `cmd/` — Cobra commands, one file per command (`install`, `uninstall`, `update`, `info`, `discover`, `plugins`, `themes`, `store`, `version`, `completion`). `root.go` wires the global `--silent` flag, the `BDCLI_SILENT` env fallback, and version info.
- `internal/betterdiscord/` — BetterDiscord core: data-folder setup, asar download (website → GitHub fallback), install/repair, addon management, and the store client.
- `internal/discord/` — Discord install discovery and injection. Per-OS path logic lives in `paths_windows.go` / `paths_darwin.go` / `paths_linux.go` with shared logic in `paths_common.go`. Injection assets (`app_index.js`, `app_package.json`) are embedded from `internal/discord/assets/` via `//go:embed`.
- `internal/models/` — channels (`iota` constants: Stable, Canary, PTB), GitHub release shapes, options, store models.
- `internal/output/` — the single gateway for user-facing output (`Printf`, `Println`, `Blank`, `NewTableWriter`, `SetWriters`). See §4.
- `internal/utils/` — download, path, and string helpers.
- `internal/wsl/` — WSL detection and Windows-home path mapping (lazy `sync.Once`, no `init()` cost outside WSL).
- `main.go` — entry point; `-ldflags -X main.version/commit/date` feed `cmd.SetVersionInfo`. A version of `"dev"` (the default) marks a debug build (`cmd.IsDebugBuild`).
- `scripts/completions.sh` — regenerates `completions/` (bash/zsh/fish); runs automatically in the GoReleaser `before` hook.
- `Taskfile.yml` — dev/build/test/lint/release shortcuts. `.goreleaser.yaml` — release build + publishing. `.golangci.yml` — lint config (`errcheck` excludes the `fmt.Fprint*` family).
- `package.json` — npm distribution wrapper (`@betterdiscord/cli` via `@go-task/go-npm`); it downloads release binaries, contains no JS logic. The version stays `0.0.0` in-repo — CI stamps it during release. There is no frontend and no `node_modules` toolchain to install for development.
- Generated / gitignored, never hand-edit or commit: `dist/`, `debug/`, `completions/`, `notes/`, `.task/`.

## 2. Build, dev & test commands

Prereqs: Go (matches `go.mod`), plus optionally [Task](https://taskfile.dev/), golangci-lint, and GoReleaser. No CGO, no platform build tags for local dev.

- `go run main.go <command>` or `task run -- <args>` — run the CLI locally (`task run` injects dev version ldflags).
- `task build` — local binary at `dist/bdcli`.
- `task test` (or `go test ./...`) — all tests.
- `task check` — `go fix` + `go fmt` + `go vet` + `golangci-lint` + tests, in one shot.
- `task ci` — what CI actually runs: deps + fix + fmt + vet + coverage + build. The golangci-lint GitHub action runs separately in `ci.yml`.
- `task build:all` / `task release:snapshot` — GoReleaser snapshot cross-builds (all OS/arch from one machine).

Before you claim a change is validated, run at minimum `gofmt`, `go vet ./...`, and `go test ./...`; prefer `task check` when golangci-lint is available.

## 3. Running isolated tests

- Narrow with the `-run` regex, e.g. `go test ./internal/models -run TestDiscordChannel` or `go test ./internal/discord -run TestInject`.
- Many tests gate on `runtime.GOOS` with `t.Skipf` when the OS doesn't match — run the subset aligned with your OS; these are guards, not cross-platform stubs. A green run on Linux does not prove the Windows/macOS paths.
- Network-dependent code is tested against `httptest` servers: endpoint URLs are declared as package-level `var`s (see `internal/betterdiscord/download.go`) precisely so tests can repoint them. Keep new endpoints in that pattern; never write a test that hits the real network.
- Coverage/profiling artifacts go to `debug/` (`task coverage:html`, `task bench:cpu`); that directory is gitignored.

## 4. Style rules

1. **Imports**
- Standard library first, blank line, then everything else (external deps and `github.com/betterdiscord/cli/internal/...` share one alphabetized block in existing files). Match the surrounding file; `gofmt` is canonical.
2. **Formatting & naming**
- Always run `gofmt`; tabs are canonical indentation.
- Exported identifiers are PascalCase with doc comments; private helpers stay lowercase. Channel constants are grouped `iota` blocks.
- Comments in this codebase explain *why* (see `internal/discord/injection.go`) — keep that bar for non-obvious logic, especially anything transactional or platform-specific.
3. **Output protocol** — the CLI equivalent of the installer's event protocol:
- All user-facing output goes through `internal/output`, never `fmt.Print*` to stdout directly. This is what makes `--silent` / `BDCLI_SILENT` (which swap in `io.Discard`) and output-capturing tests work.
- Status lines are emoji-prefixed: `✅` success, `❌` failure, `🔁` retry/fallback. Follow-up detail lines are indented with three spaces (`output.Printf(" %s\n", err.Error())`).
- Tabular output uses `output.NewTableWriter()`; versions are normalized with `output.FormatVersion`.
4. **Error handling**
- Prefer early returns over nested conditionals.
- On failure: print the human-readable `❌` message via `output`, then `return` the error up to the Cobra `RunE` — `cmd.Execute()` prints it to stderr and exits 1. Don't both print and re-wrap the same message at every level.
- Swallow errors only when a fallback genuinely handles them (e.g. website → GitHub download fallback), and always log the fallback so users can see what happened.
5. **Platform code**
- OS-specific logic belongs in `paths_*.go`-style files or explicit `runtime.GOOS` switches, with WSL handled through `internal/wsl`. Don't sprinkle ad-hoc OS conditionals through command code.
6. **Injection safety**
- The `app.asar` shadow injection is transactional: the original asar is preserved as `betterdiscord.app.asar` and any failure after the rename rolls back. Writability is probed (`probeWritable`) before any destructive step, and Snap installs are rejected up-front by design. Preserve all three properties when touching install/uninstall/repair flows.

## 5. Release & CI context

Releases are **tag-driven via GoReleaser**, all from a single Ubuntu runner (unlike the installer's per-OS matrix — everything here cross-compiles with `CGO_ENABLED=0`).

- **Trigger.** Pushing a `vX.Y.Z` tag runs `.github/workflows/release.yml`: `task ci`, then GoReleaser (linux/windows/darwin × amd64/arm64), then an npm publish. The `nightly` tag is ignored by GoReleaser; prereleases are auto-detected (`prerelease: auto`).
- **Version.** Injected via `-ldflags "-X main.version={{ .Version }}"` plus commit/date. CI also runs `npm version <tag>` before publishing, which is why `package.json` stays at `0.0.0` in the repo.
- **Artifacts.** `bdcli_<version>_<os>_<arch>.tar.gz` (`.zip` on Windows) containing the binary, README, LICENSE, and shell completions, plus `bdcli_checksums.txt`. Completions are generated by `scripts/completions.sh` in the `before` hook.
- **Publishing.** Homebrew cask pushed directly to `BetterDiscord/homebrew-tap` `main`; winget manifest PR'd from the `betterdiscord/winget-pkgs` fork to `microsoft/winget-pkgs` — both need `GH_PAT` (`GITHUB_TOKEN` can't push cross-repo). npm publish uses OIDC provenance (`id-token: write`), so there is no npm token secret.
- **CI on PRs/push** (`ci.yml`): `task ci` + the golangci-lint action, Linux only. Cross-platform correctness relies on the OS-gated tests and review — flag platform-specific risk in PRs since CI won't catch it.
- Keep `Taskfile.yml`, `.goreleaser.yaml`, and `release.yml` in sync when changing build flags or artifact names; the npm wrapper's `goBinary.url` template in `package.json` must keep matching GoReleaser's archive naming.

## 6. Documentation & collaborator expectations

- `README.md` is the user-facing reference (command table, compatibility matrix, FAQ). If you add/rename a command or flag, update the README command reference and help-output snippet in the same PR.
- `CONTRIBUTING.md` covers commit style (present-tense imperative, 72-char subject), the Go styleguide summary, and the AI-assisted-contributions policy (see also §10). If it conflicts with these notes or with actual code, escalate via an issue rather than silently picking one.
- Keep AGENTS focused on machine-readable operating guidance; long-form docs belong in the README.

## 7. Cursor / Copilot / assistant config

- `CLAUDE.md` is a symlink to this file, so Claude Code reads the same guidance.
- There are no `.cursor/rules/`, `.cursorrules`, or `.github/copilot-instructions.md` files; default assistant settings apply.
- If future agents add such rules, append them to this section so every agent reads the new constraints first.

## 8. Security, secrets, and environment

- **Never commit credentials, tokens, or `.env` files.** Release publishing relies solely on GitHub Actions secrets: `GITHUB_TOKEN` (release create/upload) and `GH_PAT` (homebrew tap push, winget cross-repo PR). npm uses OIDC provenance — no npm token exists. No secret is needed to build locally.
- **Supply chain.** The `betterdiscord.asar` is fetched over TLS from the official site with a GitHub-release fallback; there is no checksum verification of that asar yet — treat it as a known gap, not something to silently remove or quietly "fix" without discussion.
- **Dev builds must not fall back.** The `canary` development asar is GitHub-only; a failure there must never fall back to stable (a developer silently receiving stable is a near-undetectable footgun — this is documented in `download.go`; preserve it).
- **Snap is unsupported on purpose.** The read-only squashfs mount can't host the injection; the code rejects Snap installs with an actionable message before touching a running client. Don't add speculative Snap support.
- Keep tracked config (`go.mod`, `package.json`, `.goreleaser.yaml`) clean; do not add untracked credential copies to the tree.

## 9. Help & escalation

- If you find conflicting conventions between AGENTS, `CONTRIBUTING.md`, and actual code, escalate by opening an issue so the maintainers can harmonize them.
- Use the BetterDiscord Discord (linked from the README) when platform-integration questions exceed local knowledge.
- Document any new tooling (e.g., a new Task target or script) within this file so future agents immediately know how to operate the repo.

## 10. AI self-disclosure

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.
1 change: 1 addition & 0 deletions CLAUDE.md
26 changes: 20 additions & 6 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ The following is a set of guidelines for contributing to BetterDiscord CLI. Thes
* [Your First Code Contribution](#your-first-code-contribution)
* [Pull Requests](#pull-requests)

[AI-assisted contributions](#ai-assisted-contributions)

[Styleguides](#styleguides)
* [Git Commit Messages](#git-commit-messages)
* [Go Styleguide](#go-styleguide)
Expand Down Expand Up @@ -44,7 +46,7 @@ The repository is organized into:
│ ├── output/ // Output formatting
│ ├── utils/ // Shared utilities
│ └── wsl/ // WSL support
├── completions/ // Shell completion scripts (bash, fish, zsh)
├── completions/ // Generated shell completions (bash, fish, zsh; gitignored)
├── main.go // CLI entry point
├── go.mod / go.sum // Dependency management
└── README.md // Project documentation
Expand All @@ -68,9 +70,11 @@ Common commands:

The project uses [Taskfile](https://taskfile.dev/) for common tasks:

* `task build` - compile binaries for all platforms.
* `task test` - run tests and coverage.
* `task install` - install the CLI locally (from source).
* `task run -- <args>` - run the CLI locally with dev version info.
* `task build` - build a binary for your current platform into `dist/`.
* `task build:all` - build binaries for all platforms (requires GoReleaser).
* `task test` - run all tests (use `task coverage` for coverage).
* `task check` - format, vet, lint, and test in one shot.

See `Taskfile.yml` for the full list of available tasks.

Expand Down Expand Up @@ -116,6 +120,15 @@ Please follow these steps to have your contribution considered by the maintainer

While the prerequisites above must be satisfied prior to having your pull request reviewed, the reviewer(s) may ask you to complete additional design work, tests, or other changes before your pull request can be ultimately accepted.

## AI-assisted contributions

AI tools are fine to use. Submitting output with little or no personal review is not.

- **Review the diff yourself.** If you can't explain why each changed line is correct, the PR isn't ready.
- **Run the required checks locally.** Don't rely on CI to catch problems you could catch before pushing.
- **Disclose AI involvement in your PR description.** If an AI wrote a meaningful portion of the code, say so and briefly describe what you reviewed. This isn't meant as a penalty, it's useful context for the reviewer.
- **If you used an autonomous agent with minimal personal review of the output, say that explicitly.** PRs that appear to be unreviewed agent output and don't disclose this will be closed without detailed feedback.

## Styleguides

### Git Commit Messages
Expand All @@ -129,11 +142,12 @@ While the prerequisites above must be satisfied prior to having your pull reques
### Go Styleguide

* Run `gofmt` on any Go files you touch.
* Keep standard library imports first, then a blank line, then external deps, then local packages.
* Keep standard library imports first, then a blank line, then all remaining imports (external and internal packages share one alphabetized block).
* Prefer early returns for error handling.
* Route all user-facing output through `internal/output` (never `fmt.Print*` to stdout directly) so `--silent` and output-capturing tests keep working.
* Use clear, descriptive names for functions and variables.
* Add comments to exported functions and types.
* Write tests for new functionality (see existing `*_test.go` files for patterns).
* Write tests for new functionality (see existing `*_test.go` files for patterns). Tests must not hit the real network, point package-level endpoint vars at `httptest` servers instead.

## Additional Notes

Expand Down
56 changes: 39 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ This repository contains the source code for the BetterDiscord CLI. It is a nati

- Easy installation and uninstallation of BetterDiscord
- Support for multiple Discord channels (Stable, PTB, Canary)
- Automatically stops and restarts Discord during install/uninstall
- Optional BetterDiscord development builds via `--dev`
- Discover Discord installs and suggested paths
- Manage plugins and themes (list, install, update, remove)
- Browse and search the BetterDiscord store
Expand Down Expand Up @@ -162,6 +164,10 @@ bdcli uninstall --path /path/to/Discord
bdcli uninstall --all
bdcli uninstall --full

# Install the BetterDiscord development build (install-only; update tracks stable)
bdcli install --dev
BDCLI_DEV_BUILD=1 bdcli install

# Check-only updates
bdcli update --check
bdcli plugins update <name|id> --check
Expand Down Expand Up @@ -189,25 +195,25 @@ BDCLI_SILENT=1 bdcli update
A cross-platform CLI for installing, updating, and managing BetterDiscord.

Usage:
bdcli [flags]
bdcli [command]
bdcli [flags]
bdcli [command]

Available Commands:
completion Generate shell completions
discover Discover Discord installations and related data
help Help about any command
info Displays information about BetterDiscord installation
install Installs BetterDiscord to your Discord
plugins Manage BetterDiscord plugins
store Browse and search the BetterDiscord store
themes Manage BetterDiscord themes
uninstall Uninstalls BetterDiscord from your Discord
update Update BetterDiscord to the latest version
version Print the version number
completion Generate shell completions
discover Discover Discord installations and related data
help Help about any command
info Displays information about BetterDiscord installation
install Installs BetterDiscord to your Discord
plugins Manage BetterDiscord plugins
store Browse and search the BetterDiscord store
themes Manage BetterDiscord themes
uninstall Uninstalls BetterDiscord from your Discord
update Update BetterDiscord to the latest version
version Print the version number

Flags:
--silent Suppress non-error output
-h, --help help for bdcli
-h, --help help for bdcli
--silent Suppress non-error output

Use "bdcli [command] --help" for more information about a command.
```
Expand All @@ -222,7 +228,21 @@ Yes. Flatpak Discord installs are supported.

### Why is Snap Discord unsupported on Linux?

Discord Snap packaging/runtime changes prevent the CLI from supporting Snap installs.
Upstream Snap packaging changes mount Discord in a read-only filesystem, so the CLI cannot write the BetterDiscord injection into it. Native and Flatpak installs remain supported, and the CLI detects Snap installs and rejects them with a clear error instead of leaving a half-modified install.

### Do I need to close Discord before installing or uninstalling?

No. The CLI stops Discord automatically before modifying it and restarts it afterward if it was running. The exception is `bdcli update`, which only replaces the BetterDiscord asar, you need to restart Discord manually for the update to take effect.

### How do I install the BetterDiscord development build?

Use the `--dev` flag (or set `BDCLI_DEV_BUILD=1`):

```bash
bdcli install --dev
```

The development build is BetterDiscord's rolling `canary` pre-release on GitHub (unrelated to the Discord Canary channel). It is not versioned, so `bdcli update` always tracks the stable release. Rerun `bdcli install --dev` to get the newest development build.

### How can I use the global BetterDiscord folder with Flatpak?

Expand Down Expand Up @@ -329,7 +349,9 @@ Release outline:
│ ├── betterdiscord/ # BetterDiscord installation logic
│ ├── discord/ # Discord path resolution and injection
│ ├── models/ # Data models
│ └── utils/ # Utility functions
│ ├── output/ # Output formatting
│ ├── utils/ # Utility functions
│ └── wsl/ # WSL detection and path mapping
├── main.go # Entry point
├── Taskfile.yml # Task automation
└── .goreleaser.yaml # Release configuration
Expand Down
2 changes: 1 addition & 1 deletion cmd/discover.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ var discoverInstallsCmd = &cobra.Command{
if inst.IsInjected() {
bdStatus = "yes"
}
fmt.Fprintf(tw, "%s\t%s\t%s\t%s\t%s\n", ch.Name(), inst.Version, typeLabel, bdStatus, inst.CorePath)
fmt.Fprintf(tw, "%s\t%s\t%s\t%s\t%s\n", ch.Name(), inst.Version, typeLabel, bdStatus, inst.ResourcesPath)
}
}

Expand Down
Loading