fj/CLAUDE.md
Stephen Way d87a30bb29
docs: CLAUDE.md, CONTRIBUTING.md, CHANGELOG.md, docs/
* CLAUDE.md: project layout, key conventions, where to look first.
  Captures the non-obvious things a future session needs.
* CONTRIBUTING.md: build/test workflow, how to add a subcommand
  (concrete walkthrough), code style.
* CHANGELOG.md: history. 0.1.0 entry covers initial feature set;
  Unreleased captures stability + optimization batch.
* docs/architecture.md: module graph, layering rules, the HTTP funnel,
  pager + SIGINT, repo resolution, test strategy.
* docs/jq.md: --jq syntax cheatsheet (dot paths, brackets, negative
  indices, pipes, what's not supported).
* docs/troubleshooting.md: keychain re-prompts, debug logging, pager
  opt-out, alias precedence, hook bypass, common 401s.
* README.md: links into docs/ and updates binary size to 4 MB.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 12:48:46 -07:00

3.9 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
hooks/pre-push         local CI gate
scripts/               install-hooks.sh, e2e-smoke.sh
docs/                  architecture, jq syntax, troubleshooting

Build / test / hook

  • cargo build --release produces a ~4 MB binary at target/release/fj.
  • cargo test --all runs 60 tests (51 unit + 9 wiremock integration).
  • cargo clippy --all-targets --all-features -- -D warnings is the lint bar. CI (the pre-push hook) treats 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).
  • The simple jq projector lives in cli::api::extract_path. Supports .field, .0, .[0], [-1], and | pipes. See docs/jq.md.
  • All commands respect --host / FJ_HOST, --debug / FJ_DEBUG, --no-pager, FJ_PAGER / PAGER.

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