chore!: rewrite mirrorstack-cli in Rust (login + whoami)#8
Merged
Conversation
Replaces the entire Go codebase with a clap-based Rust implementation. Same surface area (login + app module init), same OAuth contract against api-platform. Why: prefer Rust for the CLI's distribution story (smaller statically linked binary, polished release tooling) and per-platform OS handler registration story for the upcoming custom-scheme work (#7). Crates: - clap (derive) for arg parsing — no more hand-rolled switch dispatch - reqwest (blocking, rustls) for HTTP — avoids OpenSSL on macOS - serde + serde_json — token response decoding - sha2 + base64 + rand — PKCE generation - dirs — cross-platform config_dir - tempfile — atomic credentials write - open — best-effort browser launcher - thiserror + anyhow — typed library errors + application errors Modules: - src/auth/ OAuth 2.0 + PKCE client (port of internal/auth) - src/credentials/ Atomic 0600 token persistence (port of internal/credentials) - src/browser.rs Cross-platform URL opener (port of internal/browser) - src/scaffold/ Module scaffold + validation + templates (port of scaffold/) - src/commands/ Subcommand modules (login, app) Tests: - 14 cargo tests cover PKCE generation, AuthorizeURL shape, token exchange happy + 4 typed sentinels (invalid_grant / invalid_request / invalid_client / unsupported_grant_type) + 5xx ErrServerError + unknown OAuth code, credentials roundtrip + 0600 mode + ErrNotFound, scaffold name validation + to_title transformations. Release binary is 3.2MB stripped (was ~6MB for the Go version). Closes #5. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Local-dev convenience: copy .env.example to .env and the CLI picks up MIRRORSTACK_API_URL / MIRRORSTACK_WEB_URL without prefixing every invocation. Process env still wins when both are set, so per-call overrides keep working. - dotenvy::dotenv().ok() at the top of main, before clap parses args - .env.example checked in with the localhost defaults - .env added to .gitignore (real values stay local) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Prints the authenticated user identity. Reads access_token from the saved credentials file, calls GET /v1/auth/me with Authorization: Bearer, and renders email + name + slug + id. Friendly errors: - "not signed in. Run mirrorstack login..." when no credentials file - "session expired. Run mirrorstack login..." on 401/403 New module src/api.rs hosts authenticated API calls; whoami is the first caller. Future commands (logout, apps list, etc.) will reuse the same module. Auto-refresh on token expiry is a follow-up — for v1 the user re-runs login. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Findings from the multi-agent review applied: - /token and /me responses now read through Read::take(64KB) so a hostile endpoint can't OOM the CLI with a giant body. Success path uses reqwest's resp.json::<T>() helper; the error path bounds the read explicitly. - DEFAULT_API_BASE, DEFAULT_WEB_BASE, ENV_API_URL, ENV_WEB_URL hoisted to commands/mod.rs. login + whoami now share one definition. - ApiError::BuildClient (which discarded the source error) replaced by the existing #[from] reqwest::Error path. Reqwest builder failures (e.g. TLS config) keep their cause. - scaffold::run_init no longer Path::exists()-pre-checks before fs::create_dir; just calls create_dir and matches AlreadyExists. Sidesteps the TOCTOU race between check and write. Skipped with reason: - Shared error base for auth+api: only 2 modules; premature - Removing url dep: tests and AuthorizeURL builder use it directly, reqwest's re-export version pinning is fragile - Lazy iterator over scaffold templates: 18 entries; not worth it - AuthError::Other vs Unexpected collapse: mild overlap, not worth churn - authorize_url unwrap_or_else fallback: defensive, will revisit if it ever masks a bug Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Trim PR #8 to its essential scope. The scaffold command is tracked in #9 and lands in a follow-up PR — the validation regex and the 18 file templates are the chunky parts and aren't load-bearing for the auth pipeline this PR is about. Removes: - src/commands/app.rs (the App subcommand wrapper) - src/scaffold/{mod,templates,validate}.rs (~380 lines) - App variant + dispatch arm in src/commands/mod.rs - mod scaffold; in src/main.rs - "app module init" reference in README The dropped code can be lifted from the prior commits in this branch when #9 lands. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three jobs on every push to main and every PR:
- lint: cargo fmt --check + cargo clippy --all-targets with
-D warnings (RUSTFLAGS) so any warning fails CI
- test: cargo test --all-features --no-fail-fast on Linux
- build: cargo build --release --locked across
ubuntu-latest / macos-latest / windows-latest, fail-fast disabled
so we see all platform issues at once
Swatinem/rust-cache@v2 caches ~/.cargo/{registry,git} + target/
between runs to keep CI cycle time reasonable. CARGO_INCREMENTAL=0
because incremental compilation is wasted in a fresh-checkout CI run.
Source touched only by `cargo fmt --all` (no behavior changes); the
workflow is the substantive add.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #5. Replaces #6 (closed) — same OAuth contract, Rust implementation. Module scaffolding (`app module init`) is tracked separately at #9 and lands in a follow-up PR.
What changed
The entire Go codebase is replaced with a Rust workspace using:
Surface area
Module layout
```
src/
├── main.rs entry, dotenvy loader, ExitCode dispatch
├── commands/
│ ├── mod.rs clap Cli root + shared DEFAULT_API_BASE constants
│ ├── login.rs login command
│ └── whoami.rs whoami command
├── auth/
│ ├── mod.rs PKCE, AuthorizeURL, token exchange, typed errors
│ └── tests.rs
├── api.rs authenticated API calls (GET /v1/auth/me)
├── credentials/
│ ├── mod.rs atomic 0600 write to <config_dir>/mirrorstack/cli/credentials.json
│ └── tests.rs
└── browser.rs cross-platform URL opener
```
Security
Tests
`cargo test` — 11 pass:
Binary size
Local testing
```bash
cargo test
cp .env.example .env # MIRRORSTACK_API_URL=http://localhost:8081, MIRRORSTACK_WEB_URL=http://localhost:3000
cargo run -- login
cargo run -- whoami
```
Out of scope
app module initscaffold to Rust #9🤖 Generated with Claude Code