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>
This commit is contained in:
parent
a6fbf45ba9
commit
d87a30bb29
63
CHANGELOG.md
Normal file
63
CHANGELOG.md
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
# Changelog
|
||||
|
||||
All notable changes will be recorded here. The format follows
|
||||
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Versions follow
|
||||
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- `tokio::signal::ctrl_c()` race in `cli::run` so the pager guard drops
|
||||
cleanly on SIGINT.
|
||||
- 9 wiremock-backed HTTP client integration tests covering retry
|
||||
behavior, header forwarding, pagination, and panic-free error paths.
|
||||
- `Client::for_base_url` test constructor pointing at an arbitrary URL.
|
||||
|
||||
### Changed
|
||||
|
||||
- Trimmed dependencies (no more `indicatif`, `futures-util`,
|
||||
`is-terminal`, `textwrap`, `tempfile`). Dropped reqwest features we
|
||||
don't use (`stream`, `brotli`). Release profile now uses `lto = "fat"`
|
||||
and `panic = "abort"`.
|
||||
- HTTP retry loop builds the request once and clones via
|
||||
`reqwest::Request::try_clone` per attempt (was rebuilding the full
|
||||
RequestBuilder each time).
|
||||
- Binary size: 5.94 MB → 4.15 MB stripped (-30%).
|
||||
|
||||
### Fixed
|
||||
|
||||
- Removed the unsafe `std::env::set_var("FJ_NO_PAGER")` from dispatch.
|
||||
The `--no-pager` flag is now threaded into `pager::maybe_start`.
|
||||
- Replaced the panicking `.expect("token contains invalid header chars")`
|
||||
in `auth_headers` with a typed error that tells the user how to
|
||||
recover.
|
||||
|
||||
## 0.1.0 — 2026-05-13
|
||||
|
||||
Initial release. Multi-host Forgejo CLI with feature parity to `gh`
|
||||
across the surface Forgejo exposes. Commands:
|
||||
|
||||
- `auth`: login, status, logout, list, switch, token, refresh, setup-git
|
||||
- `repo`: list, view, clone, create, fork, sync, edit, rename, archive,
|
||||
unarchive, delete, branches, topics, mirror, mirror-sync
|
||||
- `issue`: list, view, create, edit, close, reopen, comment, develop
|
||||
- `pr`: list, view, create, edit, diff, commits, files, checks, ready,
|
||||
review, status, checkout, merge, close, reopen
|
||||
- `release`: list, view, create, edit, delete, upload, download
|
||||
- `label`, `run`, `secret`, `variable`, `search`, `browse`, `status`,
|
||||
`org`, `ssh-key`, `gpg-key`, `alias`, `config`, `protect`, `hook`,
|
||||
`extension`, `gist`, `api`, `completion`, `man`
|
||||
|
||||
Other highlights:
|
||||
|
||||
- Repo auto-detection from `upstream` / `origin` git remote.
|
||||
- `--web` flag on all list/view subcommands.
|
||||
- `$EDITOR` integration for body inputs.
|
||||
- `fj api` with `-H`, `-X`, `-f`, `-F`, `--paginate`, `--include`,
|
||||
`--silent`, `--jq` (dot-paths, `[N]`/`[-N]`, pipes).
|
||||
- `--debug` / `FJ_DEBUG` request logging.
|
||||
- Tokens in the OS keychain.
|
||||
- Pager via `dup2` redirect to `$FJ_PAGER` / `$PAGER` / `less -FRX`.
|
||||
- Pre-push hook running fmt, clippy `-D warnings`, tests, and release
|
||||
build before any push. Live API smoke gated on `FJ_E2E=1`.
|
||||
83
CLAUDE.md
Normal file
83
CLAUDE.md
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
# 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).
|
||||
77
CONTRIBUTING.md
Normal file
77
CONTRIBUTING.md
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
# Contributing to fj
|
||||
|
||||
## Setup
|
||||
|
||||
```sh
|
||||
git clone ssh://git@rasterhub.com:2222/rasterstate/fj.git
|
||||
cd fj
|
||||
./scripts/install-hooks.sh # symlinks hooks/pre-push into .git/hooks
|
||||
cargo build --release # 4 MB binary at target/release/fj
|
||||
```
|
||||
|
||||
The hook gates every push on fmt, clippy (warnings as errors), the full
|
||||
test suite, and a release build. Set `FJ_E2E=1` to additionally run the
|
||||
live API smoke (requires `fj auth login` already done).
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Branch off `main`.
|
||||
2. Make changes. Keep commits focused.
|
||||
3. `cargo fmt --all` before committing.
|
||||
4. The pre-push hook runs the full bar; if it fails, fix the issue
|
||||
rather than passing `FJ_SKIP_PREPUSH=1` unless you genuinely need
|
||||
to bypass.
|
||||
|
||||
## Testing
|
||||
|
||||
- 60 tests live in the source tree (51 unit + 9 wiremock integration).
|
||||
- Unit tests stay close to the code they cover, in inline `#[cfg(test)]`
|
||||
modules.
|
||||
- HTTP integration tests live in `src/client/integration_tests.rs` and
|
||||
use a wiremock server. Construct test clients via
|
||||
`Client::for_base_url(base, token)`.
|
||||
- Live API tests live in `scripts/e2e-smoke.sh` and exercise read-only
|
||||
operations against the configured Forgejo. Toggle with `FJ_E2E=1`.
|
||||
|
||||
## Adding a subcommand
|
||||
|
||||
Concrete example: adding `fj sticker list`.
|
||||
|
||||
1. `src/api/sticker.rs` — typed API wrappers and request/response
|
||||
structs. Use the `serde_util::deserialize_null_to_default` helper
|
||||
for fields that Forgejo returns as `null` when empty.
|
||||
2. `src/cli/sticker.rs` — clap `Args` / `Subcommand` definitions and
|
||||
the `run` dispatch.
|
||||
3. `src/api/mod.rs` — `pub mod sticker;`
|
||||
4. `src/cli/mod.rs` — `pub mod sticker;`, add to `Command`, add the
|
||||
dispatch arm in `dispatch()`.
|
||||
5. `src/main.rs` — add `"sticker"` to `KNOWN_SUBCOMMANDS` so plugin
|
||||
discovery doesn't shadow it.
|
||||
6. Add inline `#[cfg(test)] mod tests` for any non-trivial pure code.
|
||||
7. If you touched HTTP behavior, add a wiremock test in
|
||||
`src/client/integration_tests.rs`.
|
||||
|
||||
## Code style
|
||||
|
||||
- See `CLAUDE.md` for in-tree conventions.
|
||||
- One small line of comment max per non-obvious thing. Don't narrate
|
||||
what code does, only why.
|
||||
- No `.unwrap()` / `.expect()` in non-test code paths. Use `?` and
|
||||
`anyhow::Error::context` to attach actionable messages.
|
||||
- Prefer `Result` over panics. The one exception is `unreachable!()`
|
||||
for genuinely impossible match arms, but flag it in review.
|
||||
|
||||
## Releasing
|
||||
|
||||
```sh
|
||||
cargo build --release
|
||||
./target/release/fj completion zsh > ~/.zfunc/_fj
|
||||
./target/release/fj man -o ./man
|
||||
```
|
||||
|
||||
No cargo-dist or Homebrew tap yet. Binary lives at `target/release/fj`;
|
||||
symlink into a user-level bin dir.
|
||||
|
||||
## License
|
||||
|
||||
MIT. By contributing you agree to the same license.
|
||||
12
README.md
12
README.md
|
|
@ -112,11 +112,21 @@ fj api /repos/migrate --input '{"clone_addr":"...","repo_name":"x","repo_owner":
|
|||
## Building
|
||||
|
||||
```sh
|
||||
cargo build --release # 5-6 MB stripped binary at target/release/fj
|
||||
cargo build --release # ~4 MB stripped binary at target/release/fj
|
||||
./target/release/fj completion zsh > ~/.zfunc/_fj
|
||||
./target/release/fj man -o ~/man/man1
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
- [`docs/architecture.md`](docs/architecture.md) — module graph, HTTP
|
||||
funnel, pager + SIGINT, repo resolution
|
||||
- [`docs/jq.md`](docs/jq.md) — `fj api --jq` projection syntax
|
||||
- [`docs/troubleshooting.md`](docs/troubleshooting.md) — keychain
|
||||
prompts, 401s, hook bypass, pager opt-out, alias precedence
|
||||
- [`CONTRIBUTING.md`](CONTRIBUTING.md) — build / test / release workflow
|
||||
- [`CHANGELOG.md`](CHANGELOG.md) — release notes
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
|
|
|||
11
docs/README.md
Normal file
11
docs/README.md
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
# `fj` documentation
|
||||
|
||||
- [`architecture.md`](architecture.md) — module graph, layering rules,
|
||||
the HTTP funnel, pager + SIGINT, repo resolution, test strategy.
|
||||
- [`jq.md`](jq.md) — the `fj api --jq` syntax. Dot paths, brackets,
|
||||
negative indices, pipes.
|
||||
- [`troubleshooting.md`](troubleshooting.md) — keychain prompts, hangs,
|
||||
401 errors, `--debug`, pager opt-out, alias precedence, hook bypass.
|
||||
|
||||
For build/test workflow see [`../CONTRIBUTING.md`](../CONTRIBUTING.md).
|
||||
For project conventions see [`../CLAUDE.md`](../CLAUDE.md).
|
||||
138
docs/architecture.md
Normal file
138
docs/architecture.md
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
# Architecture
|
||||
|
||||
`fj` is a thin Rust CLI over Forgejo's `/api/v1`. The whole binary is
|
||||
~10k LOC and shapes itself like `gh`: clap-parsed subcommands, one
|
||||
typed wrapper per API resource, one HTTP client funnel for everything.
|
||||
|
||||
## Module graph
|
||||
|
||||
```
|
||||
┌─────────────┐
|
||||
│ main.rs │ alias expansion + fj-<x> plugin dispatch
|
||||
└──────┬──────┘
|
||||
│
|
||||
┌──────▼──────┐
|
||||
│ cli::run │ Ctrl+C race + pager guard
|
||||
└──────┬──────┘
|
||||
│
|
||||
┌─────────────┼────────────────────┐
|
||||
│ │ │
|
||||
┌────▼─────┐ ┌─────▼─────┐ ┌──────▼──────┐
|
||||
│ cli::auth│ │ cli::repo │ ... │ cli::api │ raw escape hatch
|
||||
└────┬─────┘ └─────┬─────┘ └──────┬──────┘
|
||||
│ │ │
|
||||
│ ┌─────▼─────┐ │
|
||||
│ │ context:: │ -R or git-remote autodetect
|
||||
│ │resolve_repo│
|
||||
│ └─────┬─────┘ │
|
||||
│ │ │
|
||||
│ ┌─────▼─────┐ │
|
||||
└──────►│ api::* │◄─────────────┘
|
||||
│ typed │
|
||||
│ wrappers │
|
||||
└─────┬─────┘
|
||||
│
|
||||
┌─────▼─────┐
|
||||
│ client │ one funnel: auth headers, retry, pagination,
|
||||
│ │ debug logging, custom headers
|
||||
└─────┬─────┘
|
||||
│
|
||||
┌─────▼─────┐
|
||||
│ reqwest │ rustls-tls, JSON, multipart
|
||||
└───────────┘
|
||||
```
|
||||
|
||||
## Layering rules
|
||||
|
||||
- `cli::*` handles flags, argument validation, output formatting. It
|
||||
never makes HTTP calls directly: it calls `api::*`.
|
||||
- `api::*` is one file per Forgejo resource. Each function takes a
|
||||
`&Client` and returns typed `Result<T>`.
|
||||
- `client::*` is the only place that touches `reqwest`. All HTTP goes
|
||||
through `Client::request_with_headers`, which handles auth headers,
|
||||
retries, debug logging, and custom header merging. There is no
|
||||
parallel HTTP path (other than the multipart asset upload in
|
||||
`api::release::upload_asset`, which goes through `client.http()` but
|
||||
still uses `client.token()` for auth).
|
||||
- `config::*` is the on-disk shape of `hosts.toml`. Tokens are NOT
|
||||
stored here. They live in the OS keychain via `auth::*`.
|
||||
- `git::*` shells out to the `git` binary. The only place we call git.
|
||||
|
||||
## Repo resolution
|
||||
|
||||
Every command that operates on a specific repo calls
|
||||
`cli::context::resolve_repo(explicit_repo, explicit_host)`. The
|
||||
algorithm:
|
||||
|
||||
1. If `-R/--repo` was given, parse it as `owner/name`.
|
||||
2. Otherwise call `git remote -v` and pick the first remote that parses
|
||||
as a Forgejo URL. Preference order is `upstream`, then `origin`,
|
||||
then any other.
|
||||
3. The host comes from `--host`, then the detected remote's host, then
|
||||
the configured default host in `hosts.toml`.
|
||||
|
||||
This is the magic that lets `fj pr list` work without any flags inside
|
||||
a clone.
|
||||
|
||||
## HTTP retry
|
||||
|
||||
In `client::request_with_headers`:
|
||||
|
||||
- The request is built once and stored as `reqwest::Request`.
|
||||
- Idempotent methods (GET, HEAD, OPTIONS, PUT, DELETE) get 3 attempts.
|
||||
- POST and PATCH get 1 attempt — they're never retried.
|
||||
- Backoff is `200ms * 2^attempt` between retries.
|
||||
- On 5xx or transport error, we retry. On any other status, we return.
|
||||
|
||||
## Auto-pagination
|
||||
|
||||
`Client::get_all(path, query, total_limit)` follows `Link: rel=next`
|
||||
until either `total_limit` items have accumulated or there are no more
|
||||
pages. List commands switch to this path automatically when the user
|
||||
asks for `--limit > 50` (Forgejo's per-page cap).
|
||||
|
||||
## Pager
|
||||
|
||||
When `cli::run` dispatches a command whose output can run long (list,
|
||||
view, diff, search, status, api), it calls
|
||||
`output::pager::maybe_start(force_disabled)`. That function:
|
||||
|
||||
1. Bails if stdout isn't a TTY or `--no-pager` / `FJ_NO_PAGER` is set.
|
||||
2. Spawns `$FJ_PAGER` / `$PAGER` / `less -FRX`.
|
||||
3. `dup`s our stdout fd (saving it for restore) then `dup2`s the pager
|
||||
child's stdin fd onto stdout (fd 1).
|
||||
4. Returns a `PagerGuard` whose `Drop` impl flushes stdout, restores
|
||||
the saved fd, closes the saved fd, and waits on the child.
|
||||
|
||||
This means the rest of `fj` keeps using `println!`/`print!` unmodified
|
||||
even though it's now writing to the pager.
|
||||
|
||||
## SIGINT
|
||||
|
||||
`cli::run` `tokio::select!`s the command future against
|
||||
`tokio::signal::ctrl_c()`. On SIGINT the command future is dropped,
|
||||
which propagates to the `_pager` binding's `Drop` and tears down the
|
||||
pager cleanly.
|
||||
|
||||
## Aliases and extensions
|
||||
|
||||
`main.rs` runs two passes before clap:
|
||||
|
||||
1. **Alias expansion** via `cli::alias::expand_argv`. Reads
|
||||
`aliases.toml`. If `argv[1]` matches a defined alias, replaces it
|
||||
with the tokenized expansion.
|
||||
2. **Plugin dispatch** via `main::detect_plugin`. If `argv[1]` isn't a
|
||||
known subcommand AND `fj-<name>` is on PATH, exec it.
|
||||
|
||||
Only if neither pass triggers do we hand off to clap.
|
||||
|
||||
## Test strategy
|
||||
|
||||
- **Unit tests** stay inline next to the code they cover. Pure
|
||||
functions only (no I/O).
|
||||
- **Integration tests** for HTTP behavior live in
|
||||
`src/client/integration_tests.rs` and use a wiremock server.
|
||||
Construct test clients via `Client::for_base_url`.
|
||||
- **Live E2E** lives in `scripts/e2e-smoke.sh`, gated on `FJ_E2E=1`
|
||||
in the pre-push hook. It hits the real Forgejo at `rasterhub.com`
|
||||
against `stephen/fj-cli-test`.
|
||||
76
docs/jq.md
Normal file
76
docs/jq.md
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
# `fj api --jq` syntax
|
||||
|
||||
`fj api` ships a small jq-flavored JSON projector. It's not a full jq
|
||||
implementation; it covers the 95% of cases gh users use `--jq` for.
|
||||
|
||||
If you need anything more complex, pipe the raw JSON through real `jq`:
|
||||
|
||||
```sh
|
||||
fj api /repos/foo/bar | jq '.<full expression>'
|
||||
```
|
||||
|
||||
## Stages
|
||||
|
||||
Expressions are split on `|` into stages. Each stage runs against the
|
||||
output of the previous stage.
|
||||
|
||||
```
|
||||
fj api /repos/foo/bar | head
|
||||
# vs
|
||||
fj api /repos/foo/bar -q '.owner | .login'
|
||||
```
|
||||
|
||||
## Path segments
|
||||
|
||||
Within a stage, dot-separated segments traverse the JSON tree. Each
|
||||
segment is one of:
|
||||
|
||||
| Segment | Meaning |
|
||||
| ------------ | --------------------------------------------- |
|
||||
| `.field` | Object field lookup |
|
||||
| `.0` | Array index (positional) |
|
||||
| `.[0]` | Same as `.0`, bracket form |
|
||||
| `.[-1]` | Last element of an array |
|
||||
| `.[-2]` | Second-to-last element |
|
||||
|
||||
Bracket form and dot form are interchangeable: `.data.0.name` and
|
||||
`.data[0].name` mean the same thing.
|
||||
|
||||
## Examples
|
||||
|
||||
Given:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{"id": 1, "owner": {"login": "alice"}},
|
||||
{"id": 2, "owner": {"login": "bob"}}
|
||||
],
|
||||
"total": 2
|
||||
}
|
||||
```
|
||||
|
||||
| Query | Output |
|
||||
| ------------------------------ | ------------------ |
|
||||
| `.total` | `2` |
|
||||
| `.data.0.id` | `1` |
|
||||
| `.data[0].owner.login` | `"alice"` |
|
||||
| `.data[-1].owner.login` | `"bob"` |
|
||||
| `.data | .[0]` | first element |
|
||||
| `.data[0] | .owner | .login` | `"alice"` |
|
||||
|
||||
## Errors
|
||||
|
||||
- A missing field returns a hard error with the field name.
|
||||
- An out-of-range index returns a hard error with the index.
|
||||
- Negative indices that overshoot (e.g. `[-5]` on a 3-element array)
|
||||
also error.
|
||||
|
||||
## Not supported
|
||||
|
||||
The following are intentionally NOT implemented; reach for real jq:
|
||||
|
||||
- Filters like `.[] | select(.state == "open")`
|
||||
- Functions: `map`, `length`, `keys`, etc.
|
||||
- String interpolation
|
||||
- Multiple outputs from a single expression
|
||||
132
docs/troubleshooting.md
Normal file
132
docs/troubleshooting.md
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
# Troubleshooting
|
||||
|
||||
## macOS keeps prompting for keychain access
|
||||
|
||||
After every `cargo build --release` the binary's hash changes, so the
|
||||
macOS keychain treats it as a new application and re-prompts. Click
|
||||
"Always Allow" once and it'll stick until the next rebuild.
|
||||
|
||||
For automated test environments, see
|
||||
`src/client/integration_tests.rs` — those tests use
|
||||
`Client::for_base_url` to bypass the keychain entirely.
|
||||
|
||||
## `fj` hangs on the first command after a rebuild
|
||||
|
||||
Same root cause as above. macOS is prompting for keychain access in a
|
||||
modal dialog you can't see (the binary doesn't have a TTY). Bring the
|
||||
Keychain Access prompt to the foreground and approve it, or run
|
||||
`fj auth status` from a regular terminal first to surface the prompt.
|
||||
|
||||
## `error: HTTP 401 ... authentication failed`
|
||||
|
||||
The stored token is invalid or revoked. Re-authenticate:
|
||||
|
||||
```sh
|
||||
fj auth refresh # re-verify the existing token
|
||||
fj auth refresh --token NEW # replace the stored token
|
||||
fj auth login --host <host> # full re-login (overwrites)
|
||||
```
|
||||
|
||||
## I want to see the raw HTTP requests fj is making
|
||||
|
||||
```sh
|
||||
fj --debug pr list -R foo/bar
|
||||
# or
|
||||
FJ_DEBUG=1 fj pr list -R foo/bar
|
||||
```
|
||||
|
||||
You'll get `→ METHOD URL?query` and `← STATUS url` for every request,
|
||||
plus a 200-character preview of the request body if one was sent.
|
||||
|
||||
## A command is hanging in my CI pipeline
|
||||
|
||||
CI runners often don't have an `$EDITOR`. `fj issue create` and friends
|
||||
will hang waiting for `$EDITOR` to return if you omit `--body`.
|
||||
|
||||
- Pass `--body "text"` explicitly.
|
||||
- Or pass `--body -` to read from stdin.
|
||||
- Set `$VISUAL` or `$EDITOR` to a non-interactive program.
|
||||
|
||||
## The pager doesn't exit / output gets paged when I don't want it
|
||||
|
||||
```sh
|
||||
fj --no-pager repo list # opt out per-command
|
||||
FJ_NO_PAGER=1 fj repo list # opt out in your shell
|
||||
```
|
||||
|
||||
The default pager is `less -FRX`, which exits when output fits one
|
||||
screen. Override with `$FJ_PAGER` or `$PAGER`:
|
||||
|
||||
```sh
|
||||
FJ_PAGER='less -R' fj repo list # don't auto-quit
|
||||
FJ_PAGER=cat fj repo list # disable paging
|
||||
```
|
||||
|
||||
## `error: unexpected argument '-R' found`
|
||||
|
||||
Some `fj repo` subcommands take a positional `repo` arg AND accept `-R`
|
||||
(for symmetry with the rest of the CLI). If you see this on an older
|
||||
build, update to a build after commit `eb716ee`. All commands now accept
|
||||
both forms.
|
||||
|
||||
## `-R` works in some shells, not in others (zsh globbing)
|
||||
|
||||
If `fj api /repos/foo` returns a "no matches found" error before fj
|
||||
even runs, you've hit shell globbing on `/`. Quote the path:
|
||||
|
||||
```sh
|
||||
fj api '/repos/foo/bar'
|
||||
```
|
||||
|
||||
## Aliases aren't expanding
|
||||
|
||||
Aliases are looked up before clap parses, but only the FIRST positional
|
||||
is matched. So:
|
||||
|
||||
```sh
|
||||
fj alias set co "pr checkout"
|
||||
fj co 42 # works: co → pr checkout
|
||||
fj --host other.host co 42 # works: --host is parsed by clap after expansion
|
||||
fj help co # does NOT expand co; "help" is a builtin
|
||||
```
|
||||
|
||||
If your alias name collides with a real subcommand, the real one wins.
|
||||
Aliases never shadow built-in commands.
|
||||
|
||||
## Extension `fj-foo` isn't being found
|
||||
|
||||
`fj` looks for `fj-<name>` on PATH only when `<name>` isn't a known
|
||||
subcommand. The list of known subcommands lives in
|
||||
`src/main.rs::KNOWN_SUBCOMMANDS`. If your plugin name collides with a
|
||||
built-in, rename it.
|
||||
|
||||
## `fj pr ready` returns "pull does not allow editing draft state"
|
||||
|
||||
Forgejo only lets you flip `draft: false` via PATCH if the API user is
|
||||
the PR author or has write permissions. If you're not the author and
|
||||
not a maintainer, this fails. There's no workaround on the client side.
|
||||
|
||||
## The hook hangs on `cargo test`
|
||||
|
||||
Likely a keychain re-prompt for the test binary's new hash. Either
|
||||
approve the prompt or rebuild outside the hook context first, then push:
|
||||
|
||||
```sh
|
||||
cargo test --all # accept any keychain prompts
|
||||
git push # hook reruns; tests fast-cache hit
|
||||
```
|
||||
|
||||
If genuinely stuck:
|
||||
|
||||
```sh
|
||||
FJ_SKIP_PREPUSH=1 git push # bypass once, only when necessary
|
||||
```
|
||||
|
||||
## My commit was rejected because clippy complains about new code
|
||||
|
||||
The hook runs `cargo clippy --all-targets --all-features -- -D warnings`.
|
||||
Every warning is an error. Either fix it or, very rarely and with a
|
||||
comment explaining why, add a targeted `#[allow(clippy::lint_name)]`.
|
||||
|
||||
Don't add `#[allow(dead_code)]` to silence "never used" warnings on
|
||||
real code — that's a signal you have unused code paths to remove.
|
||||
Loading…
Reference in a new issue