docs: switch demo recording to vhs (declarative + reproducible)
Some checks are pending
ci / check (push) Waiting to run
Some checks are pending
ci / check (push) Waiting to run
* scripts/demo.tape: vhs tape file driving a ~30s session: --version, repo view (auto-detect), issue list, pr list, api + jq, selective JSON, help. Catppuccin Mocha, 1100x720. * scripts/record-demo.sh: thin wrapper with preflight checks (vhs + fj installed and authenticated, run from a clone). Outputs assets/demo.gif and assets/demo.mp4. * Removed scripts/_demo-session.sh (the asciinema sketch). vhs is strictly better for scripted demos: deterministic timing, direct GIF/MP4 output, no upload step. * README: replaced the asciinema TODO with the GIF embed (will resolve once assets/demo.gif is recorded and committed). To record: brew install vhs cargo build --release && ln -sf \$PWD/target/release/fj ~/.local/bin/fj fj auth login --host rasterhub.com ./scripts/record-demo.sh Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
d58bd312f9
commit
65420678d9
14
README.md
14
README.md
|
|
@ -9,12 +9,18 @@
|
||||||
Multi-host from day one. Tokens are stored in your OS keychain.
|
Multi-host from day one. Tokens are stored in your OS keychain.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<img src="assets/demo.gif" alt="fj demo" width="800">
|
||||||
|
</p>
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
TODO: replace the line below with an asciinema embed once the v0.1.0
|
The GIF above is generated from scripts/demo.tape via charmbracelet/vhs.
|
||||||
release is tagged. Run `./scripts/record-demo.sh` then
|
To refresh it after a feature change:
|
||||||
`asciinema upload dist/demo.cast` and paste the returned URL.
|
brew install vhs
|
||||||
|
./scripts/record-demo.sh
|
||||||
|
git commit -am 'docs: refresh demo recording'
|
||||||
-->
|
-->
|
||||||
<!-- [](https://asciinema.org/a/REPLACE) -->
|
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
|
|
|
||||||
2
dist/v0.1.0/SHA256SUMS
vendored
Normal file
2
dist/v0.1.0/SHA256SUMS
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
eb5fd0206a5194179f6d1aecee7fe4a4a5434e1f44ed75a525f7f41d18670061 fj-v0.1.0-darwin-aarch64.tar.gz
|
||||||
|
d7102095f051f4af7af1d9dbf34678df85142fdfece71dcab0cc111aeb942c2c fj-v0.1.0-darwin-x86_64.tar.gz
|
||||||
BIN
dist/v0.1.0/fj-v0.1.0-darwin-aarch64.tar.gz
vendored
Normal file
BIN
dist/v0.1.0/fj-v0.1.0-darwin-aarch64.tar.gz
vendored
Normal file
Binary file not shown.
121
dist/v0.1.0/fj-v0.1.0-darwin-aarch64/CHANGELOG.md
vendored
Normal file
121
dist/v0.1.0/fj-v0.1.0-darwin-aarch64/CHANGELOG.md
vendored
Normal file
|
|
@ -0,0 +1,121 @@
|
||||||
|
# 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 (distribution + open-source ready)
|
||||||
|
|
||||||
|
- `LICENSE` (MIT) at the repo root. Cargo.toml had always declared MIT;
|
||||||
|
the file was just missing.
|
||||||
|
- `.forgejo/workflows/release.yml`: on `v*` tags, builds for
|
||||||
|
`darwin-aarch64`, `darwin-x86_64`, `linux-x86_64`; uploads tarballs,
|
||||||
|
`SHA256SUMS`, and a pre-rendered `fj.rb` to the Forgejo release.
|
||||||
|
- Homebrew formula template at `dist/homebrew/fj.rb.tmpl` and
|
||||||
|
`scripts/render-homebrew-formula.sh` to fill SHA256s post-release.
|
||||||
|
Publishes into the existing `rasterandstate/homebrew-tap`.
|
||||||
|
- `.forgejo/issue_template/{bug,feature,api-gap}.md` and
|
||||||
|
`.forgejo/pull_request_template.md` to keep triage cheap.
|
||||||
|
- `scripts/record-demo.sh` + `scripts/_demo-session.sh` to record an
|
||||||
|
asciinema demo; README has a placeholder embed for the v0.1.0 cast.
|
||||||
|
- `docs/compatibility.md`: tested Forgejo versions, caveats for older
|
||||||
|
Gitea, and Forgejo-only endpoints.
|
||||||
|
- One-shot version probe during `fj auth login` that warns when the
|
||||||
|
server reports a pre-7.x version (with tests for the parser).
|
||||||
|
|
||||||
|
### Added (agent-focused Forgejo gaps)
|
||||||
|
|
||||||
|
- `fj issue edit-comment` / `delete-comment`. Lets an agent (or you)
|
||||||
|
fix or remove a wrong comment after the fact.
|
||||||
|
- `fj search code "..."` (and `-R owner/name` to scope to one repo).
|
||||||
|
Powered by Forgejo's `/repos/search/code` endpoint.
|
||||||
|
- `fj pr request-review N user1 user2,user3` and
|
||||||
|
`fj pr unrequest-review N user1`. Distinct from `pr review`, which
|
||||||
|
submits your own review.
|
||||||
|
- `fj repo watch` / `unwatch` / `star` / `unstar` / `starred`.
|
||||||
|
- `fj milestone {list,view,create,edit,close,reopen,delete,assign}`.
|
||||||
|
Includes `fj milestone assign N --milestone ID|none` to attach an
|
||||||
|
issue or PR to a milestone.
|
||||||
|
|
||||||
|
### Added (UX + stability)
|
||||||
|
|
||||||
|
- `--json-fields field1,field2` global flag. gh-style projection on top
|
||||||
|
of any `--json` output, with dotted-path support
|
||||||
|
(`--json-fields owner.login,id`).
|
||||||
|
- 429 / Retry-After honored in the retry loop with a 30 s cap. Wiremock
|
||||||
|
test added.
|
||||||
|
- `did you mean` suggestions on typo'd subcommands via clap's
|
||||||
|
`suggestions` feature.
|
||||||
|
- `fj auth token` and `fj auth status --show-token` now refuse to write
|
||||||
|
to a TTY (use `--force` to override). Avoids accidental shoulder-
|
||||||
|
surfing or capture in shell history.
|
||||||
|
- `tokio::signal::ctrl_c()` race in `cli::run` so the pager guard drops
|
||||||
|
cleanly on SIGINT.
|
||||||
|
- 10 wiremock-backed HTTP client integration tests covering retry
|
||||||
|
behavior (5xx, 429), header forwarding, pagination, and panic-free
|
||||||
|
error paths.
|
||||||
|
- `Client::for_base_url` test constructor pointing at an arbitrary URL.
|
||||||
|
- `.forgejo/workflows/ci.yml` runs the same gate as the pre-push hook
|
||||||
|
on every push and PR.
|
||||||
|
|
||||||
|
### Added (docs)
|
||||||
|
|
||||||
|
- `SECURITY.md` covering threat model, known sharp edges, and reporting.
|
||||||
|
- `docs/gh-to-fj.md` — complete command-by-command mapping.
|
||||||
|
- `docs/faq.md` — common questions about tokens, hosts, debug,
|
||||||
|
scripting, plugins.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Trimmed dependencies (no more `indicatif`, `futures-util`,
|
||||||
|
`is-terminal`, `textwrap`, `tempfile` in prod). Dropped reqwest
|
||||||
|
features we don't use (`stream`, `brotli`). Release profile uses
|
||||||
|
`lto = "fat"` and `panic = "abort"`.
|
||||||
|
- HTTP retry loop builds the request once and clones via
|
||||||
|
`reqwest::Request::try_clone` per attempt.
|
||||||
|
- Binary size: 5.94 MB → 4.15 MB stripped (-30%).
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **Shell injection** in `fj auth setup-git`. The hostname now must
|
||||||
|
match a strict DNS pattern before being interpolated into the
|
||||||
|
credential-helper string, and we call `git config` directly with
|
||||||
|
separate args instead of going through `sh -c`.
|
||||||
|
- **Pager won't compile on Windows**. The libc-based `dup2` redirect
|
||||||
|
now lives behind `#[cfg(unix)]`; non-Unix gets a no-op stub that
|
||||||
|
returns `None` from `maybe_start`.
|
||||||
|
- Removed the unsafe `std::env::set_var("FJ_NO_PAGER")` from dispatch.
|
||||||
|
`--no-pager` is now threaded into `pager::maybe_start(force_disabled)`.
|
||||||
|
- Replaced the panicking `.expect("token contains invalid header chars")`
|
||||||
|
in `auth_headers` with a typed error.
|
||||||
|
|
||||||
|
## 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`.
|
||||||
21
dist/v0.1.0/fj-v0.1.0-darwin-aarch64/LICENSE
vendored
Normal file
21
dist/v0.1.0/fj-v0.1.0-darwin-aarch64/LICENSE
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 Stephen Way
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
149
dist/v0.1.0/fj-v0.1.0-darwin-aarch64/README.md
vendored
Normal file
149
dist/v0.1.0/fj-v0.1.0-darwin-aarch64/README.md
vendored
Normal file
|
|
@ -0,0 +1,149 @@
|
||||||
|
# fj
|
||||||
|
|
||||||
|
A command-line tool for [Forgejo](https://forgejo.org) instances, in the spirit of GitHub's `gh`.
|
||||||
|
|
||||||
|
Multi-host from day one. Tokens are stored in your OS keychain.
|
||||||
|
|
||||||
|
<!--
|
||||||
|
TODO: replace the line below with an asciinema embed once the v0.1.0
|
||||||
|
release is tagged. Run `./scripts/record-demo.sh` then
|
||||||
|
`asciinema upload dist/demo.cast` and paste the returned URL.
|
||||||
|
-->
|
||||||
|
<!-- [](https://asciinema.org/a/REPLACE) -->
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Homebrew (macOS + Linux):
|
||||||
|
brew tap rasterandstate/tap
|
||||||
|
brew install fj
|
||||||
|
|
||||||
|
# From source:
|
||||||
|
cargo install --path .
|
||||||
|
# or
|
||||||
|
cargo build --release && cp target/release/fj ~/.local/bin/fj
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quick start
|
||||||
|
|
||||||
|
```sh
|
||||||
|
fj auth login # add a host and token
|
||||||
|
fj auth status # show signed-in hosts
|
||||||
|
fj repo list # repos you own
|
||||||
|
fj repo view owner/name # repo overview
|
||||||
|
fj issue list -R owner/name # issues
|
||||||
|
fj pr list -R owner/name --state open # pull requests
|
||||||
|
fj api /repos/search -X GET -f q=foo # raw API escape hatch
|
||||||
|
```
|
||||||
|
|
||||||
|
`-R/--repo` is optional inside a git clone. fj detects the upstream from
|
||||||
|
`upstream`, then `origin`, then any other remote.
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
| Group | Common operations |
|
||||||
|
| ---------- | --------------------------------------------------------------------------------- |
|
||||||
|
| `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, watch, unwatch, star, unstar, starred |
|
||||||
|
| `issue` | list, view, create, edit, close, reopen, comment, edit-comment, delete-comment, develop |
|
||||||
|
| `pr` | list, view, create, edit, diff, commits, files, checks, ready, review, request-review, unrequest-review, status, checkout, merge, close, reopen |
|
||||||
|
| `release` | list, view, create, edit, delete, upload, download |
|
||||||
|
| `label` | list, create, edit, delete |
|
||||||
|
| `milestone`| list, view, create, edit, close, reopen, delete, assign |
|
||||||
|
| `run` | list, view, rerun, cancel (Forgejo Actions workflow runs) |
|
||||||
|
| `secret` | list, set, delete (Actions secrets) |
|
||||||
|
| `variable` | list, set, delete (Actions variables) |
|
||||||
|
| `search` | repos, issues, prs, users, code |
|
||||||
|
| `browse` | open the current repo (or a path within it) in your browser |
|
||||||
|
| `status` | notifications inbox + `--mark-read` |
|
||||||
|
| `org` | list, view, teams |
|
||||||
|
| `ssh-key` | list, add, delete |
|
||||||
|
| `gpg-key` | list, add, delete |
|
||||||
|
| `alias` | list, set, delete |
|
||||||
|
| `config` | get, set, list, path |
|
||||||
|
| `protect` | list, view, set, delete branch protection rules |
|
||||||
|
| `hook` | list, create, delete, test webhooks |
|
||||||
|
| `gist` | list, create (thin wrapper over `gist-*` repos) |
|
||||||
|
| `extension`| list, run discovered `fj-<name>` plugins on PATH |
|
||||||
|
| `api` | raw HTTP with `-X`, `-f`, `-F`, `-H`, `-q` (jq-ish), `--paginate`, `--include` |
|
||||||
|
| `completion`| Print shell completions (bash, zsh, fish, powershell, elvish) |
|
||||||
|
| `man` | Generate man pages into a directory |
|
||||||
|
|
||||||
|
## Global flags
|
||||||
|
|
||||||
|
- `--host <hostname>` (or `FJ_HOST`): pick the host explicitly.
|
||||||
|
- `--debug` (or `FJ_DEBUG=1`): log every HTTP request to stderr.
|
||||||
|
- `--no-pager` (or `FJ_NO_PAGER=1`): skip the pager.
|
||||||
|
- `--json-fields foo,bar`: gh-style projection on top of any `--json`
|
||||||
|
output. Dotted paths supported (`--json-fields owner.login,id`).
|
||||||
|
- `--web` on most list/view subcommands: open the relevant page in
|
||||||
|
your default browser.
|
||||||
|
|
||||||
|
## Config
|
||||||
|
|
||||||
|
- Hosts and the current host live in `$XDG_CONFIG_HOME/fj/hosts.toml`
|
||||||
|
(`~/Library/Application Support/fj/hosts.toml` on macOS).
|
||||||
|
- Aliases in `aliases.toml`. Preferences in `config.toml`.
|
||||||
|
- Tokens live in the OS keychain under service `fj`, keyed by hostname.
|
||||||
|
|
||||||
|
## Aliases
|
||||||
|
|
||||||
|
```sh
|
||||||
|
fj alias set co "pr checkout"
|
||||||
|
fj co 42 # equivalent to: fj pr checkout 42
|
||||||
|
```
|
||||||
|
|
||||||
|
## Extensions
|
||||||
|
|
||||||
|
Any executable on `$PATH` named `fj-<name>` is invokable as `fj <name> [args]`.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
fj extension list # show discovered plugins
|
||||||
|
fj my-plugin some-arg # invokes `fj-my-plugin some-arg`
|
||||||
|
```
|
||||||
|
|
||||||
|
## Plugin-style usage of `fj api`
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Auto-paginate a list endpoint into a single JSON array:
|
||||||
|
fj api /repos/search -f q=fj --paginate -q .
|
||||||
|
|
||||||
|
# Pass a custom request header:
|
||||||
|
fj api /user -H "X-Trace-Id: foo"
|
||||||
|
|
||||||
|
# Show response headers along with the body:
|
||||||
|
fj api /version --include
|
||||||
|
|
||||||
|
# Send a literal JSON body:
|
||||||
|
fj api /repos/migrate --input '{"clone_addr":"...","repo_name":"x","repo_owner":"y"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Hooks
|
||||||
|
|
||||||
|
- `hooks/pre-push` runs `cargo fmt --check`, `cargo clippy -D warnings`,
|
||||||
|
`cargo test`, and a release build before any push.
|
||||||
|
- With `FJ_E2E=1`, the hook also runs `scripts/e2e-smoke.sh` against the
|
||||||
|
configured host.
|
||||||
|
- Install via `./scripts/install-hooks.sh`.
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
```sh
|
||||||
|
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
|
||||||
BIN
dist/v0.1.0/fj-v0.1.0-darwin-aarch64/fj
vendored
Executable file
BIN
dist/v0.1.0/fj-v0.1.0-darwin-aarch64/fj
vendored
Executable file
Binary file not shown.
BIN
dist/v0.1.0/fj-v0.1.0-darwin-x86_64.tar.gz
vendored
Normal file
BIN
dist/v0.1.0/fj-v0.1.0-darwin-x86_64.tar.gz
vendored
Normal file
Binary file not shown.
121
dist/v0.1.0/fj-v0.1.0-darwin-x86_64/CHANGELOG.md
vendored
Normal file
121
dist/v0.1.0/fj-v0.1.0-darwin-x86_64/CHANGELOG.md
vendored
Normal file
|
|
@ -0,0 +1,121 @@
|
||||||
|
# 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 (distribution + open-source ready)
|
||||||
|
|
||||||
|
- `LICENSE` (MIT) at the repo root. Cargo.toml had always declared MIT;
|
||||||
|
the file was just missing.
|
||||||
|
- `.forgejo/workflows/release.yml`: on `v*` tags, builds for
|
||||||
|
`darwin-aarch64`, `darwin-x86_64`, `linux-x86_64`; uploads tarballs,
|
||||||
|
`SHA256SUMS`, and a pre-rendered `fj.rb` to the Forgejo release.
|
||||||
|
- Homebrew formula template at `dist/homebrew/fj.rb.tmpl` and
|
||||||
|
`scripts/render-homebrew-formula.sh` to fill SHA256s post-release.
|
||||||
|
Publishes into the existing `rasterandstate/homebrew-tap`.
|
||||||
|
- `.forgejo/issue_template/{bug,feature,api-gap}.md` and
|
||||||
|
`.forgejo/pull_request_template.md` to keep triage cheap.
|
||||||
|
- `scripts/record-demo.sh` + `scripts/_demo-session.sh` to record an
|
||||||
|
asciinema demo; README has a placeholder embed for the v0.1.0 cast.
|
||||||
|
- `docs/compatibility.md`: tested Forgejo versions, caveats for older
|
||||||
|
Gitea, and Forgejo-only endpoints.
|
||||||
|
- One-shot version probe during `fj auth login` that warns when the
|
||||||
|
server reports a pre-7.x version (with tests for the parser).
|
||||||
|
|
||||||
|
### Added (agent-focused Forgejo gaps)
|
||||||
|
|
||||||
|
- `fj issue edit-comment` / `delete-comment`. Lets an agent (or you)
|
||||||
|
fix or remove a wrong comment after the fact.
|
||||||
|
- `fj search code "..."` (and `-R owner/name` to scope to one repo).
|
||||||
|
Powered by Forgejo's `/repos/search/code` endpoint.
|
||||||
|
- `fj pr request-review N user1 user2,user3` and
|
||||||
|
`fj pr unrequest-review N user1`. Distinct from `pr review`, which
|
||||||
|
submits your own review.
|
||||||
|
- `fj repo watch` / `unwatch` / `star` / `unstar` / `starred`.
|
||||||
|
- `fj milestone {list,view,create,edit,close,reopen,delete,assign}`.
|
||||||
|
Includes `fj milestone assign N --milestone ID|none` to attach an
|
||||||
|
issue or PR to a milestone.
|
||||||
|
|
||||||
|
### Added (UX + stability)
|
||||||
|
|
||||||
|
- `--json-fields field1,field2` global flag. gh-style projection on top
|
||||||
|
of any `--json` output, with dotted-path support
|
||||||
|
(`--json-fields owner.login,id`).
|
||||||
|
- 429 / Retry-After honored in the retry loop with a 30 s cap. Wiremock
|
||||||
|
test added.
|
||||||
|
- `did you mean` suggestions on typo'd subcommands via clap's
|
||||||
|
`suggestions` feature.
|
||||||
|
- `fj auth token` and `fj auth status --show-token` now refuse to write
|
||||||
|
to a TTY (use `--force` to override). Avoids accidental shoulder-
|
||||||
|
surfing or capture in shell history.
|
||||||
|
- `tokio::signal::ctrl_c()` race in `cli::run` so the pager guard drops
|
||||||
|
cleanly on SIGINT.
|
||||||
|
- 10 wiremock-backed HTTP client integration tests covering retry
|
||||||
|
behavior (5xx, 429), header forwarding, pagination, and panic-free
|
||||||
|
error paths.
|
||||||
|
- `Client::for_base_url` test constructor pointing at an arbitrary URL.
|
||||||
|
- `.forgejo/workflows/ci.yml` runs the same gate as the pre-push hook
|
||||||
|
on every push and PR.
|
||||||
|
|
||||||
|
### Added (docs)
|
||||||
|
|
||||||
|
- `SECURITY.md` covering threat model, known sharp edges, and reporting.
|
||||||
|
- `docs/gh-to-fj.md` — complete command-by-command mapping.
|
||||||
|
- `docs/faq.md` — common questions about tokens, hosts, debug,
|
||||||
|
scripting, plugins.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Trimmed dependencies (no more `indicatif`, `futures-util`,
|
||||||
|
`is-terminal`, `textwrap`, `tempfile` in prod). Dropped reqwest
|
||||||
|
features we don't use (`stream`, `brotli`). Release profile uses
|
||||||
|
`lto = "fat"` and `panic = "abort"`.
|
||||||
|
- HTTP retry loop builds the request once and clones via
|
||||||
|
`reqwest::Request::try_clone` per attempt.
|
||||||
|
- Binary size: 5.94 MB → 4.15 MB stripped (-30%).
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **Shell injection** in `fj auth setup-git`. The hostname now must
|
||||||
|
match a strict DNS pattern before being interpolated into the
|
||||||
|
credential-helper string, and we call `git config` directly with
|
||||||
|
separate args instead of going through `sh -c`.
|
||||||
|
- **Pager won't compile on Windows**. The libc-based `dup2` redirect
|
||||||
|
now lives behind `#[cfg(unix)]`; non-Unix gets a no-op stub that
|
||||||
|
returns `None` from `maybe_start`.
|
||||||
|
- Removed the unsafe `std::env::set_var("FJ_NO_PAGER")` from dispatch.
|
||||||
|
`--no-pager` is now threaded into `pager::maybe_start(force_disabled)`.
|
||||||
|
- Replaced the panicking `.expect("token contains invalid header chars")`
|
||||||
|
in `auth_headers` with a typed error.
|
||||||
|
|
||||||
|
## 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`.
|
||||||
21
dist/v0.1.0/fj-v0.1.0-darwin-x86_64/LICENSE
vendored
Normal file
21
dist/v0.1.0/fj-v0.1.0-darwin-x86_64/LICENSE
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 Stephen Way
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
149
dist/v0.1.0/fj-v0.1.0-darwin-x86_64/README.md
vendored
Normal file
149
dist/v0.1.0/fj-v0.1.0-darwin-x86_64/README.md
vendored
Normal file
|
|
@ -0,0 +1,149 @@
|
||||||
|
# fj
|
||||||
|
|
||||||
|
A command-line tool for [Forgejo](https://forgejo.org) instances, in the spirit of GitHub's `gh`.
|
||||||
|
|
||||||
|
Multi-host from day one. Tokens are stored in your OS keychain.
|
||||||
|
|
||||||
|
<!--
|
||||||
|
TODO: replace the line below with an asciinema embed once the v0.1.0
|
||||||
|
release is tagged. Run `./scripts/record-demo.sh` then
|
||||||
|
`asciinema upload dist/demo.cast` and paste the returned URL.
|
||||||
|
-->
|
||||||
|
<!-- [](https://asciinema.org/a/REPLACE) -->
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Homebrew (macOS + Linux):
|
||||||
|
brew tap rasterandstate/tap
|
||||||
|
brew install fj
|
||||||
|
|
||||||
|
# From source:
|
||||||
|
cargo install --path .
|
||||||
|
# or
|
||||||
|
cargo build --release && cp target/release/fj ~/.local/bin/fj
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quick start
|
||||||
|
|
||||||
|
```sh
|
||||||
|
fj auth login # add a host and token
|
||||||
|
fj auth status # show signed-in hosts
|
||||||
|
fj repo list # repos you own
|
||||||
|
fj repo view owner/name # repo overview
|
||||||
|
fj issue list -R owner/name # issues
|
||||||
|
fj pr list -R owner/name --state open # pull requests
|
||||||
|
fj api /repos/search -X GET -f q=foo # raw API escape hatch
|
||||||
|
```
|
||||||
|
|
||||||
|
`-R/--repo` is optional inside a git clone. fj detects the upstream from
|
||||||
|
`upstream`, then `origin`, then any other remote.
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
| Group | Common operations |
|
||||||
|
| ---------- | --------------------------------------------------------------------------------- |
|
||||||
|
| `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, watch, unwatch, star, unstar, starred |
|
||||||
|
| `issue` | list, view, create, edit, close, reopen, comment, edit-comment, delete-comment, develop |
|
||||||
|
| `pr` | list, view, create, edit, diff, commits, files, checks, ready, review, request-review, unrequest-review, status, checkout, merge, close, reopen |
|
||||||
|
| `release` | list, view, create, edit, delete, upload, download |
|
||||||
|
| `label` | list, create, edit, delete |
|
||||||
|
| `milestone`| list, view, create, edit, close, reopen, delete, assign |
|
||||||
|
| `run` | list, view, rerun, cancel (Forgejo Actions workflow runs) |
|
||||||
|
| `secret` | list, set, delete (Actions secrets) |
|
||||||
|
| `variable` | list, set, delete (Actions variables) |
|
||||||
|
| `search` | repos, issues, prs, users, code |
|
||||||
|
| `browse` | open the current repo (or a path within it) in your browser |
|
||||||
|
| `status` | notifications inbox + `--mark-read` |
|
||||||
|
| `org` | list, view, teams |
|
||||||
|
| `ssh-key` | list, add, delete |
|
||||||
|
| `gpg-key` | list, add, delete |
|
||||||
|
| `alias` | list, set, delete |
|
||||||
|
| `config` | get, set, list, path |
|
||||||
|
| `protect` | list, view, set, delete branch protection rules |
|
||||||
|
| `hook` | list, create, delete, test webhooks |
|
||||||
|
| `gist` | list, create (thin wrapper over `gist-*` repos) |
|
||||||
|
| `extension`| list, run discovered `fj-<name>` plugins on PATH |
|
||||||
|
| `api` | raw HTTP with `-X`, `-f`, `-F`, `-H`, `-q` (jq-ish), `--paginate`, `--include` |
|
||||||
|
| `completion`| Print shell completions (bash, zsh, fish, powershell, elvish) |
|
||||||
|
| `man` | Generate man pages into a directory |
|
||||||
|
|
||||||
|
## Global flags
|
||||||
|
|
||||||
|
- `--host <hostname>` (or `FJ_HOST`): pick the host explicitly.
|
||||||
|
- `--debug` (or `FJ_DEBUG=1`): log every HTTP request to stderr.
|
||||||
|
- `--no-pager` (or `FJ_NO_PAGER=1`): skip the pager.
|
||||||
|
- `--json-fields foo,bar`: gh-style projection on top of any `--json`
|
||||||
|
output. Dotted paths supported (`--json-fields owner.login,id`).
|
||||||
|
- `--web` on most list/view subcommands: open the relevant page in
|
||||||
|
your default browser.
|
||||||
|
|
||||||
|
## Config
|
||||||
|
|
||||||
|
- Hosts and the current host live in `$XDG_CONFIG_HOME/fj/hosts.toml`
|
||||||
|
(`~/Library/Application Support/fj/hosts.toml` on macOS).
|
||||||
|
- Aliases in `aliases.toml`. Preferences in `config.toml`.
|
||||||
|
- Tokens live in the OS keychain under service `fj`, keyed by hostname.
|
||||||
|
|
||||||
|
## Aliases
|
||||||
|
|
||||||
|
```sh
|
||||||
|
fj alias set co "pr checkout"
|
||||||
|
fj co 42 # equivalent to: fj pr checkout 42
|
||||||
|
```
|
||||||
|
|
||||||
|
## Extensions
|
||||||
|
|
||||||
|
Any executable on `$PATH` named `fj-<name>` is invokable as `fj <name> [args]`.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
fj extension list # show discovered plugins
|
||||||
|
fj my-plugin some-arg # invokes `fj-my-plugin some-arg`
|
||||||
|
```
|
||||||
|
|
||||||
|
## Plugin-style usage of `fj api`
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Auto-paginate a list endpoint into a single JSON array:
|
||||||
|
fj api /repos/search -f q=fj --paginate -q .
|
||||||
|
|
||||||
|
# Pass a custom request header:
|
||||||
|
fj api /user -H "X-Trace-Id: foo"
|
||||||
|
|
||||||
|
# Show response headers along with the body:
|
||||||
|
fj api /version --include
|
||||||
|
|
||||||
|
# Send a literal JSON body:
|
||||||
|
fj api /repos/migrate --input '{"clone_addr":"...","repo_name":"x","repo_owner":"y"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Hooks
|
||||||
|
|
||||||
|
- `hooks/pre-push` runs `cargo fmt --check`, `cargo clippy -D warnings`,
|
||||||
|
`cargo test`, and a release build before any push.
|
||||||
|
- With `FJ_E2E=1`, the hook also runs `scripts/e2e-smoke.sh` against the
|
||||||
|
configured host.
|
||||||
|
- Install via `./scripts/install-hooks.sh`.
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
```sh
|
||||||
|
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
|
||||||
BIN
dist/v0.1.0/fj-v0.1.0-darwin-x86_64/fj
vendored
Executable file
BIN
dist/v0.1.0/fj-v0.1.0-darwin-x86_64/fj
vendored
Executable file
Binary file not shown.
|
|
@ -1,38 +0,0 @@
|
||||||
#!/usr/bin/env bash
|
|
||||||
# Drives an asciinema recording. Don't invoke directly; run record-demo.sh.
|
|
||||||
|
|
||||||
set -u
|
|
||||||
PS1='\$ '
|
|
||||||
export PS1
|
|
||||||
|
|
||||||
PAUSE_BETWEEN=0.6
|
|
||||||
PAUSE_AFTER=1.2
|
|
||||||
|
|
||||||
say() { printf '\033[1;34m# %s\033[0m\n' "$*"; sleep "$PAUSE_BETWEEN"; }
|
|
||||||
do_() { printf '\033[1m$ %s\033[0m\n' "$*"; sleep "$PAUSE_BETWEEN"; eval "$@"; sleep "$PAUSE_AFTER"; }
|
|
||||||
|
|
||||||
clear
|
|
||||||
say "fj: a CLI for Forgejo. Multi-host, tokens in the keychain."
|
|
||||||
do_ "fj --version"
|
|
||||||
|
|
||||||
say "Inside a clone, no flags needed: fj infers the repo from your git remote."
|
|
||||||
do_ "fj repo view | head -8"
|
|
||||||
|
|
||||||
say "Issues, PRs, releases all work the same way."
|
|
||||||
do_ "fj issue list --state all -L 5"
|
|
||||||
do_ "fj pr list --state all -L 5"
|
|
||||||
|
|
||||||
say "The api escape hatch with a jq-ish projector."
|
|
||||||
do_ "fj api /version"
|
|
||||||
do_ "fj api /user -q .login"
|
|
||||||
|
|
||||||
say "Selective JSON for scripting."
|
|
||||||
do_ "fj repo list -L 3 --json --json-fields full_name,private"
|
|
||||||
|
|
||||||
say "Or just browse on the web."
|
|
||||||
say " fj browse src/main.rs"
|
|
||||||
sleep 1.5
|
|
||||||
|
|
||||||
clear
|
|
||||||
say "60+ subcommands. Try fj --help."
|
|
||||||
sleep 2
|
|
||||||
93
scripts/demo.tape
Normal file
93
scripts/demo.tape
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
# vhs tape file for the fj README demo.
|
||||||
|
#
|
||||||
|
# Reproduce with:
|
||||||
|
# brew install vhs
|
||||||
|
# ./scripts/record-demo.sh
|
||||||
|
#
|
||||||
|
# The tape is the source of truth. Edit it, re-record, commit the
|
||||||
|
# updated assets/demo.gif and (optionally) assets/demo.mp4.
|
||||||
|
#
|
||||||
|
# Pre-requisites the recorder needs:
|
||||||
|
# * `fj` on PATH (build it via `cargo build --release` and symlink)
|
||||||
|
# * `fj auth login --host rasterhub.com` already completed
|
||||||
|
# * Run from inside the fj clone (so `fj repo view` resolves via the
|
||||||
|
# git remote)
|
||||||
|
|
||||||
|
Output assets/demo.gif
|
||||||
|
Output assets/demo.mp4
|
||||||
|
|
||||||
|
Set Theme "Catppuccin Mocha"
|
||||||
|
Set FontSize 16
|
||||||
|
Set Width 1100
|
||||||
|
Set Height 720
|
||||||
|
Set TypingSpeed 50ms
|
||||||
|
Set Padding 24
|
||||||
|
Set PlaybackSpeed 1.0
|
||||||
|
Set Shell zsh
|
||||||
|
|
||||||
|
# Establish a clean prompt.
|
||||||
|
Hide
|
||||||
|
Type "clear"
|
||||||
|
Enter
|
||||||
|
Show
|
||||||
|
Sleep 600ms
|
||||||
|
|
||||||
|
Type "fj --version"
|
||||||
|
Sleep 300ms
|
||||||
|
Enter
|
||||||
|
Sleep 1200ms
|
||||||
|
|
||||||
|
Type "# inside a clone, fj infers the repo from the git remote"
|
||||||
|
Sleep 200ms
|
||||||
|
Enter
|
||||||
|
Sleep 600ms
|
||||||
|
|
||||||
|
Type "fj repo view | head -10"
|
||||||
|
Sleep 200ms
|
||||||
|
Enter
|
||||||
|
Sleep 2200ms
|
||||||
|
|
||||||
|
Type "# issues, PRs, releases — same shape"
|
||||||
|
Sleep 200ms
|
||||||
|
Enter
|
||||||
|
Sleep 500ms
|
||||||
|
|
||||||
|
Type "fj issue list --state all -L 5"
|
||||||
|
Sleep 200ms
|
||||||
|
Enter
|
||||||
|
Sleep 2000ms
|
||||||
|
|
||||||
|
Type "fj pr list --state all -L 5"
|
||||||
|
Sleep 200ms
|
||||||
|
Enter
|
||||||
|
Sleep 2200ms
|
||||||
|
|
||||||
|
Type "# api escape hatch with jq-ish projection"
|
||||||
|
Sleep 200ms
|
||||||
|
Enter
|
||||||
|
Sleep 500ms
|
||||||
|
|
||||||
|
Type "fj api /version"
|
||||||
|
Sleep 200ms
|
||||||
|
Enter
|
||||||
|
Sleep 1500ms
|
||||||
|
|
||||||
|
Type "fj api /user -q .login"
|
||||||
|
Sleep 200ms
|
||||||
|
Enter
|
||||||
|
Sleep 1500ms
|
||||||
|
|
||||||
|
Type "# gh-style selective JSON for scripts"
|
||||||
|
Sleep 200ms
|
||||||
|
Enter
|
||||||
|
Sleep 500ms
|
||||||
|
|
||||||
|
Type "fj repo list -L 3 --json --json-fields full_name,private"
|
||||||
|
Sleep 200ms
|
||||||
|
Enter
|
||||||
|
Sleep 2800ms
|
||||||
|
|
||||||
|
Type "fj --help | head -20"
|
||||||
|
Sleep 200ms
|
||||||
|
Enter
|
||||||
|
Sleep 3000ms
|
||||||
Loading…
Reference in a new issue