fj/CLAUDE.md
Stephen Way e60a810c69
Some checks are pending
ci / check (push) Waiting to run
docs: sync README + CLAUDE.md + architecture.md with recent additions
* 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>
2026-05-13 14:57:07 -07:00

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 --release produces a ~4 MB binary at target/release/fj.
  • cargo test --all runs 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 warnings is the lint bar. Both the pre-push hook and .forgejo/workflows/ci.yml treat warnings as errors.
  • ./scripts/install-hooks.sh symlinks hooks/pre-push into .git/hooks.
  • FJ_E2E=1 enables the live API smoke against stephen/fj-cli-test.
  • FJ_SKIP_PREPUSH=1 bypasses the hook for genuine emergencies only.

Key conventions

  • Every repo-scoped subcommand accepts -R/--repo AND auto-detects from the git remote (upstream then origin). The resolver is cli::context::resolve_repo.
  • Bodies (--body) support: an inline string, - for stdin, or omit to open $EDITOR. See cli::editor.
  • Lists with --limit > 50 transparently follow Link: rel=next via Client::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 api lives in cli::api::extract_path. Supports .field, .0, .[0], [-1], and | pipes. See docs/jq.md.
  • --json-fields foo,bar is a GLOBAL flag that applies gh-style projection to ANY --json output. Lives in output::json_filter::project, set from cli::run, read by output::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::imp is 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 test builds 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 in src/client/integration_tests.rs, which bypass the keychain via Client::for_base_url.
  • fj auth setup-git interpolates the hostname into a shell-evaluated credential helper string. The host is validated against a strict DNS pattern (validate_hostname in cli/auth.rs) before interpolation. Don't bypass that validation if you refactor.
  • fj auth token and auth status --show-token refuse to write a plaintext token to a TTY by default (use --force).
  • The repo URL in Cargo.toml is https://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 into cli/mod.rs (Command enum, dispatch) and main.rs (KNOWN_SUBCOMMANDS). Typed API wrapper in api/<x>.rs.
  • Adding a new Forgejo endpoint: api/<resource>.rs is the right home.
  • Touching HTTP behavior: client/mod.rs::request_with_headers is 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.rs rather than tests/ (this is a binary crate, no [lib] target, so integration tests live inline).