# Contributing Thanks for helping out. Issues, ideas, and pull requests are all welcome. ## Scope proton-cli mirrors what the Proton **web clients** let a user do. If an action isn't possible in the official web UI, it doesn't belong here, even when an API endpoint for it exists. Web-client parity beats API completeness. ## Getting set up The repository uses [devbox](https://www.jetify.com/devbox) and [direnv](https://direnv.net/) to pin the toolchain: ```bash git clone https://github.com/roman-16/proton-cli.git cd proton-cli direnv allow # or: devbox shell ``` Without devbox you need Go 1.26 or newer, plus `actionlint`, `charm-freeze`, `golangci-lint`, `goreleaser`, `just`, `nixfmt`, `protoc`, `protoc-gen-go` and `shellcheck` for the tasks below. ## Everyday commands `just --list` is the full set. The ones you'll reach for: ```bash go build ./cmd/proton # quick build (no CAPTCHA helper) just build # release-shaped binary, embeds the webview helper just run -- mail messages list just lint # format, regenerate, and check everything; run before every commit just test-fast # unit, golden and conformance tests just flake # build the nix package, after a dependency bump just snapshot # every release artifact, without publishing just demo # regenerate the README demo images just update # move every dependency and tool to the latest version ``` `just lint` has to pass with no findings, and has to leave the tree clean. It formats Go and Nix, regenerates the command reference, and checks the workflows, the release configuration, the shell scripts and the Go, so a stale generated file fails the same way a lint finding does. CI runs the same recipe. ## README demo images The terminal panel in the README is a recording of a real session, rendered with [freeze](https://github.com/charmbracelet/freeze). It records as `primary`, the same account the integration tests use, so it needs no credentials of its own: ```bash just demo ``` [`scripts/terminal-demo/README.md`](scripts/terminal-demo/README.md) explains the pieces and the rules that keep the recording honest. ## Tests `just test-fast` runs the unit, golden and conformance tests. No credentials, no network, seconds to finish, safe to run any time: ```bash just test-fast ``` The suite under `tests/` is different: those are **integration tests against the live Proton API**. They run on the primary and secondary accounts, create and delete real data in them, and take several minutes. ```bash export PROTON_CLI_TEST_PRIMARY_USER=primary@proton.me # never your own account export PROTON_CLI_TEST_PRIMARY_PASSWORD=... export PROTON_CLI_TEST_SECONDARY_USER=secondary@proton.me export PROTON_CLI_TEST_SECONDARY_PASSWORD=... just test-one TestMailSendAndRead # a single integration test just test # every test that runs on the two free accounts just test-all # those, then the ones needing a paid plan ``` `just login` and `just seed` sign the accounts in and fill them, for working with them by hand. Never point any of this at an account you care about. Credentials can go in a local `.env` file (see `.env.example`), which the devbox shell loads automatically. Unit test files are named after the file they test (`size.go` → `size_test.go`). The integration tests are grouped by feature area instead. ## Project layout | Path | Contents | | --- | --- | | `cmd/proton/` | Entry point | | `internal/cli/` | Cobra command tree, flags, exit codes | | `internal/service/` | Per-product logic (mail, drive, calendar, contacts, pass) | | `internal/proton/` | API client, request plumbing, error types | | `internal/crypto/`, `internal/account/` | Key handling, SRP login, sessions | | `internal/render/`, `internal/view/` | Output formatting: tables, JSON, YAML, progress | | `internal/hv/` | Human-verification webview helper | | `cmd/proton-hv/` | The webview helper binary | | `tests/` | Live-API integration tests | | `scripts/` | OpenAPI generator, installers, release helpers, README demo | | `assets/` | Logo and the generated README demo images | | `docs/` | User documentation | | `CHANGELOG.md` | What each version changed for the people using it, and the thing that releases it | ## Working with Proton's API Proton's web client is the reference for endpoints, payload shapes, and crypto flows: ```bash cd /tmp && git clone --depth 1 https://github.com/ProtonMail/WebClients.git ``` `openapi.yaml` in the repository root is generated from that source and covers roughly 740 endpoints. Regenerate it with: ```bash just openapi ``` A weekly workflow does the same thing and commits when upstream changes. See [`scripts/README.md`](scripts/README.md). ## Pull requests - Keep the change focused, and match the surrounding style. - Run `just lint` and `just test-fast`. - Add or adjust integration tests when you touch behaviour that they cover. - Update `docs/` and, when it's user-facing, the README. - Leave [`CHANGELOG.md`](CHANGELOG.md) alone; its entries are written when a release is cut, from the commits that went into it. Write the commit message so the entry can be written from it. - Commit messages follow [Conventional Commits](https://www.conventionalcommits.org/) (`feat:`, `fix:`, `docs:`, `build:`, …). Release notes are not generated from them: what a release says is written by hand in `CHANGELOG.md`, because a commit documents a step in the source and an entry documents a difference someone can feel. ## Releases [`CHANGELOG.md`](CHANGELOG.md) is the release button. Add a version section to it in [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) form and merge it to `main`; that is the whole of it. The version, the tag and the release notes all come from the one file the change was reviewed in, so shipping is a decision made once, in a diff, rather than a version typed into a form afterwards. The section is written when the release is cut, from the commits since the last tag, so there is no `[Unreleased]` heading accumulating between releases and a merge that is not a release leaves the file untouched. That puts the whole of a release in one reviewable diff, and it puts the burden on commit messages, which is where the reasoning is while it is still fresh. The **Release** workflow runs when CI passes on `main`, reads the newest version section, and stops there when a release for it is already published - which is what nearly every merge does, in seconds. Otherwise it builds the CAPTCHA helper on a native runner per platform, tags, and hands the section to GoReleaser as the release notes. The tag is pushed last on purpose: it is fetched by users, resolved by `go install`, and the version GoReleaser derives, so nothing that outlives a failed run happens until everything that can fail has passed. GoReleaser then builds every target, publishes the GitHub release, and updates the APT repository, AUR, Homebrew tap, winget, and npm. Because the file decides and not the run, a release that fails partway through is finished by re-running the workflow: an existing tag is reused and its own commit released rather than whatever `main` has become, and a release already published is left alone. The file is held to its format by `just test-fast`, which is also what keeps the button safe: versions move one step at a time, so after 2.2.3 the file may say 2.2.4, 2.3.0 or 3.0.0 and nothing else, and a `[YANKED]` section is never republished. `just notes` prints the version and the notes the current file would publish. `just snapshot` runs the same GoReleaser pipeline locally without publishing, so a packaging mistake surfaces before the tag rather than after it. It builds the helper for your platform and stands in placeholders for the other four, since those need native runners: the artifacts in `dist/` prove the packaging, not the helper bytes, and the foreign binaries in there are not runnable. ## Security Please don't file security issues publicly. [`SECURITY.md`](SECURITY.md) has the private reporting channels.