Some checks are pending
ci / check (push) Waiting to run
* README: new commands (milestone, search code, watch/star, edit-comment, request-review) and the `--json-fields` / `--no-pager` globals. * CLAUDE.md: layout includes json_filter and .forgejo/workflows; test count updated to 75; HTTP retry, JSON projection, pager-on-Unix conventions documented; setup-git hostname-validation invariant called out; auth token TTY guard noted. * docs/architecture.md: HTTP retry section now covers 429 / Retry-After and Request::try_clone; new JSON projection section explains the global --json-fields flow; test strategy mentions remote CI and ignored env-mutating tests. * docs/README.md: link to SECURITY.md and CHANGELOG.md. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 KiB
5 KiB
fj — project notes for Claude
fj is a Forgejo CLI, in the spirit of GitHub's gh. Multi-host, tokens
in the OS keychain, ~10k LOC Rust with full feature parity to gh for
the surface Forgejo exposes.
Layout
src/
main.rs entry: alias expansion, fj-<name> plugin dispatch
cli/ clap subcommand tree
mod.rs top-level Cli + dispatch + Ctrl+C race + pager
context.rs RepoFlag + resolve_repo (`-R` or git-remote detect)
editor.rs $EDITOR + body resolution helpers
web.rs cross-platform `--web` opener
<subcommand>.rs one file per command group (auth, repo, pr, …)
api/ typed wrappers around /api/v1
mod.rs split_repo + shared serde helpers
<resource>.rs one file per resource (issue, pull, release, …)
client/ HTTP client
mod.rs Client, retry-on-5xx, debug logging, headers
pagination.rs Page<T> + Link header parser
error.rs ApiError
integration_tests wiremock-based tests
config/ hosts.toml shape + load/save
auth/ OS keychain wrapper
git/ git invocation + remote URL parser
output/ tables, colors, relative_time, state_pill, pager,
json_filter (--json-fields projection)
.forgejo/workflows/ CI mirror of the pre-push hook
hooks/pre-push local CI gate
scripts/ install-hooks.sh, e2e-smoke.sh
docs/ architecture, jq, gh-to-fj, faq, troubleshooting
Build / test / hook
cargo build --releaseproduces a ~4 MB binary attarget/release/fj.cargo test --allruns 75 tests (65 unit + 10 wiremock integration). 2 tests are#[ignore](env-mutating, racy with cargo's harness).cargo clippy --all-targets --all-features -- -D warningsis the lint bar. Both the pre-push hook and.forgejo/workflows/ci.ymltreat warnings as errors../scripts/install-hooks.shsymlinkshooks/pre-pushinto.git/hooks.FJ_E2E=1enables the live API smoke againststephen/fj-cli-test.FJ_SKIP_PREPUSH=1bypasses the hook for genuine emergencies only.
Key conventions
- Every repo-scoped subcommand accepts
-R/--repoAND auto-detects from the git remote (upstreamthenorigin). The resolver iscli::context::resolve_repo. - Bodies (
--body) support: an inline string,-for stdin, or omit to open$EDITOR. Seecli::editor. - Lists with
--limit > 50transparently followLink: rel=nextviaClient::get_all. Per-page cap is the Forgejo default (50). - HTTP retry: GET/HEAD/OPTIONS/PUT/DELETE get 3 attempts with 200/400/800
ms exponential backoff. 429 honors
Retry-After(capped 30 s). POST and PATCH are never retried. - The simple jq projector for
fj apilives incli::api::extract_path. Supports.field,.0,.[0],[-1], and|pipes. Seedocs/jq.md. --json-fields foo,baris a GLOBAL flag that applies gh-style projection to ANY--jsonoutput. Lives inoutput::json_filter::project, set fromcli::run, read byoutput::print_json.- All commands respect
--host/FJ_HOST,--debug/FJ_DEBUG,--no-pager,FJ_PAGER/PAGER,--json-fields. - The pager is Unix-only.
output::pager::impis gated#[cfg(unix)]with a no-op stub for other platforms.
Things to remember
- Tokens are stored in the OS keychain (service
fj, key = hostname). Never serialized to disk. Re-prompts after rebuilds are expected on macOS because the binary hash changes; click "Always Allow." cargo testbuilds a separate test binary with a different hash, so it gets its own keychain prompt. Tests that need keychain access are the wiremock-backed ones insrc/client/integration_tests.rs, which bypass the keychain viaClient::for_base_url.fj auth setup-gitinterpolates the hostname into a shell-evaluated credential helper string. The host is validated against a strict DNS pattern (validate_hostnameincli/auth.rs) before interpolation. Don't bypass that validation if you refactor.fj auth tokenandauth status --show-tokenrefuse to write a plaintext token to a TTY by default (use--force).- The repo URL in
Cargo.tomlishttps://rasterhub.com/rasterstate/fj. - Commit author is
Stephen Way <stephen@rasterstate.com>. The user has a CLAUDE.md preference: no em-dashes, terse responses, no preamble. - MSRV is 1.82 (we use
Option::is_none_or).
Where to look first
- Adding a new subcommand: copy an existing
cli/<x>.rs(e.g.label). Wire intocli/mod.rs(Commandenum,dispatch) andmain.rs(KNOWN_SUBCOMMANDS). Typed API wrapper inapi/<x>.rs. - Adding a new Forgejo endpoint:
api/<resource>.rsis the right home. - Touching HTTP behavior:
client/mod.rs::request_with_headersis the one spot all requests funnel through. Don't add a parallel code path. - Test a new API surface: add to
src/client/integration_tests.rsrather thantests/(this is a binary crate, no[lib]target, so integration tests live inline).