Compare commits
No commits in common. "main" and "v0.1.0" have entirely different histories.
6
.gitignore
vendored
6
.gitignore
vendored
|
|
@ -6,9 +6,3 @@ Cargo.lock.bak
|
||||||
.vscode/
|
.vscode/
|
||||||
*.swp
|
*.swp
|
||||||
*.swo
|
*.swo
|
||||||
|
|
||||||
# Release build outputs (uploaded to Forgejo releases, never tracked)
|
|
||||||
/dist/v*/
|
|
||||||
|
|
||||||
# Local Claude / agent state
|
|
||||||
.claude/settings.local.json
|
|
||||||
|
|
|
||||||
38
README.md
38
README.md
|
|
@ -1,26 +1,15 @@
|
||||||
<p align="center">
|
# fj
|
||||||
<img src="assets/logo.png" alt="fj logo" width="128">
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h1 align="center">fj</h1>
|
A command-line tool for [Forgejo](https://forgejo.org) instances, in the spirit of GitHub's `gh`.
|
||||||
|
|
||||||
<p align="center">
|
|
||||||
A command-line tool for <a href="https://forgejo.org">Forgejo</a>, in the spirit of GitHub's <code>gh</code>.<br>
|
|
||||||
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 align="center">
|
|
||||||
<img src="assets/demo.gif" alt="fj demo" width="800">
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
The GIF above is generated from scripts/demo.tape via charmbracelet/vhs.
|
TODO: replace the line below with an asciinema embed once the v0.1.0
|
||||||
To refresh it after a feature change:
|
release is tagged. Run `./scripts/record-demo.sh` then
|
||||||
brew install vhs
|
`asciinema upload dist/demo.cast` and paste the returned URL.
|
||||||
./scripts/record-demo.sh
|
|
||||||
git commit -am 'docs: refresh demo recording'
|
|
||||||
-->
|
-->
|
||||||
|
<!-- [](https://asciinema.org/a/REPLACE) -->
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
|
|
@ -145,21 +134,6 @@ cargo build --release # ~4 MB stripped binary at target/release/fj
|
||||||
./target/release/fj man -o ~/man/man1
|
./target/release/fj man -o ~/man/man1
|
||||||
```
|
```
|
||||||
|
|
||||||
## Claude Code plugin
|
|
||||||
|
|
||||||
`fj` ships a [Claude Code](https://docs.claude.com/en/docs/claude-code)
|
|
||||||
plugin so AI agents and developers using Claude Code can drive fj
|
|
||||||
directly from natural-language requests.
|
|
||||||
|
|
||||||
```text
|
|
||||||
/plugin marketplace add rasterandstate/fj-claude-plugin
|
|
||||||
/plugin install fj@rasterandstate
|
|
||||||
```
|
|
||||||
|
|
||||||
The source of truth for the plugin lives in [`claude/`](claude/) inside
|
|
||||||
this repo; the canonical install URL points at the mirror at
|
|
||||||
[`rasterandstate/fj-claude-plugin`](https://github.com/rasterandstate/fj-claude-plugin).
|
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
- [`docs/architecture.md`](docs/architecture.md) — module graph, HTTP
|
- [`docs/architecture.md`](docs/architecture.md) — module graph, HTTP
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
## Reporting a vulnerability
|
## Reporting a vulnerability
|
||||||
|
|
||||||
Please report security issues to **security@rasterstate.com** rather than
|
Please report security issues to **stephen@rasterstate.com** rather than
|
||||||
opening a public issue. Encrypt with the GPG key below if the issue is
|
opening a public issue. Encrypt with the GPG key below if the issue is
|
||||||
sensitive. Expect a response within 72 hours.
|
sensitive. Expect a response within 72 hours.
|
||||||
|
|
||||||
|
|
|
||||||
BIN
assets/demo.gif
BIN
assets/demo.gif
Binary file not shown.
|
Before Width: | Height: | Size: 139 KiB |
BIN
assets/logo.png
BIN
assets/logo.png
Binary file not shown.
|
Before Width: | Height: | Size: 53 KiB |
|
|
@ -1,20 +0,0 @@
|
||||||
{
|
|
||||||
"name": "rasterandstate",
|
|
||||||
"owner": {
|
|
||||||
"name": "Raster & State",
|
|
||||||
"email": "hello@rasterstate.com"
|
|
||||||
},
|
|
||||||
"plugins": [
|
|
||||||
{
|
|
||||||
"name": "fj",
|
|
||||||
"description": "Teach Claude Code to use fj, a CLI for Forgejo and Gitea-compatible instances. Activates when the user mentions fj, Forgejo, Gitea, or any forge-side action (issues, PRs, releases, code search, labels, milestones, webhooks).",
|
|
||||||
"author": {
|
|
||||||
"name": "Raster & State",
|
|
||||||
"email": "hello@rasterstate.com"
|
|
||||||
},
|
|
||||||
"source": "./plugins/fj",
|
|
||||||
"category": "developer-tools",
|
|
||||||
"homepage": "https://rasterhub.com/rasterstate/fj"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
@ -1,69 +0,0 @@
|
||||||
# fj Claude Code plugin
|
|
||||||
|
|
||||||
A [Claude Code](https://docs.claude.com/en/docs/claude-code) plugin that
|
|
||||||
teaches Claude how to use `fj`, the CLI for Forgejo and Gitea-compatible
|
|
||||||
instances.
|
|
||||||
|
|
||||||
The plugin ships a single skill (`fj`) that activates when the user
|
|
||||||
mentions fj, Forgejo, Gitea, or any forge-side action (open a PR, list
|
|
||||||
issues, cut a release, request a review, etc.) on a non-GitHub host.
|
|
||||||
|
|
||||||
## Install
|
|
||||||
|
|
||||||
This directory is both a Claude Code plugin AND a single-plugin
|
|
||||||
marketplace. Add the marketplace once, then install:
|
|
||||||
|
|
||||||
```text
|
|
||||||
/plugin marketplace add rasterandstate/fj-claude-plugin
|
|
||||||
/plugin install fj@rasterandstate
|
|
||||||
```
|
|
||||||
|
|
||||||
After install, the skill activates automatically when relevant.
|
|
||||||
|
|
||||||
## Layout
|
|
||||||
|
|
||||||
```
|
|
||||||
claude/
|
|
||||||
├── .claude-plugin/
|
|
||||||
│ └── marketplace.json marketplace manifest — declares plugins[]
|
|
||||||
├── README.md this file
|
|
||||||
└── plugins/
|
|
||||||
└── fj/
|
|
||||||
├── .claude-plugin/
|
|
||||||
│ └── plugin.json plugin manifest (name, version, keywords)
|
|
||||||
└── skills/
|
|
||||||
└── fj/
|
|
||||||
└── SKILL.md the skill body — what Claude reads
|
|
||||||
```
|
|
||||||
|
|
||||||
The marketplace lives at the root; each plugin lives in `plugins/<name>/`.
|
|
||||||
`marketplace.json` references plugins via `"source": "./plugins/<name>"`.
|
|
||||||
This is the working pattern used by `anthropics/claude-plugins-official`
|
|
||||||
and other shipping marketplaces.
|
|
||||||
|
|
||||||
## Updating
|
|
||||||
|
|
||||||
This `claude/` directory is the source of truth, inside the fj repo at
|
|
||||||
[rasterhub.com/rasterstate/fj](https://rasterhub.com/rasterstate/fj).
|
|
||||||
|
|
||||||
A mirror lives at
|
|
||||||
[github.com/rasterandstate/fj-claude-plugin](https://github.com/rasterandstate/fj-claude-plugin),
|
|
||||||
synced from this directory. To cut a new version:
|
|
||||||
|
|
||||||
1. Bump `version` in `.claude-plugin/plugin.json` here.
|
|
||||||
2. Update SKILL.md if new fj commands or workflows exist.
|
|
||||||
3. Commit + push the fj repo.
|
|
||||||
4. Sync the contents of `claude/` over to the mirror repo and tag it:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
# From the fj repo root:
|
|
||||||
rsync -av --delete --exclude=.git claude/ /path/to/fj-claude-plugin/
|
|
||||||
cd /path/to/fj-claude-plugin
|
|
||||||
git commit -am "sync from fj vX.Y.Z"
|
|
||||||
git tag vX.Y.Z
|
|
||||||
git push origin main vX.Y.Z
|
|
||||||
```
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
MIT, matching fj.
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
{
|
|
||||||
"name": "fj",
|
|
||||||
"description": "Forge-side helper skill for fj, a CLI for Forgejo",
|
|
||||||
"author": {
|
|
||||||
"name": "Raster & State",
|
|
||||||
"email": "hello@rasterstate.com"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,207 +0,0 @@
|
||||||
---
|
|
||||||
name: fj
|
|
||||||
description: How to use `fj`, a CLI for Forgejo (the gh equivalent). Use this skill when the user mentions fj, Forgejo, Gitea, or wants to script repository, issue, pull-request, release, label, milestone, webhook, or branch-protection operations against a self-hosted Forgejo/Gitea instance. Triggers also include "open a PR on rasterhub", "list my Forgejo issues", and similar.
|
|
||||||
---
|
|
||||||
|
|
||||||
# Using `fj`
|
|
||||||
|
|
||||||
`fj` is a CLI for [Forgejo](https://forgejo.org) instances and
|
|
||||||
Gitea-compatible forks, in the spirit of GitHub's `gh`. Tokens live in
|
|
||||||
the OS keychain. Multi-host. Repo auto-detection from the git remote.
|
|
||||||
|
|
||||||
When the user is operating against a Forgejo or Gitea host (not
|
|
||||||
github.com), prefer `fj` over `curl` or `git` for forge-side actions:
|
|
||||||
issues, PRs, releases, labels, milestones, webhooks, branch protection,
|
|
||||||
search, notifications.
|
|
||||||
|
|
||||||
## Quick orientation
|
|
||||||
|
|
||||||
```
|
|
||||||
fj auth login --host <host> # one-time setup; token in keychain
|
|
||||||
fj auth status # which hosts you're signed in to
|
|
||||||
fj --version
|
|
||||||
fj --help # 25 top-level subcommands
|
|
||||||
```
|
|
||||||
|
|
||||||
Inside a clone, `fj` infers the repo from `git remote -v` (prefers
|
|
||||||
`upstream` then `origin`). `-R/--repo` is always accepted as an
|
|
||||||
override.
|
|
||||||
|
|
||||||
## Core commands
|
|
||||||
|
|
||||||
| Group | Most-reached-for subcommands |
|
|
||||||
| --- | --- |
|
|
||||||
| `repo` | list, view, clone, create, fork, sync, edit, rename, archive, delete, branches, topics, mirror, watch, star, 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, status, checkout, merge, close |
|
|
||||||
| `release` | list, view, create, edit, delete, upload, download |
|
|
||||||
| `label` | list, create, edit, delete |
|
|
||||||
| `milestone` | list, view, create, edit, close, delete, assign |
|
|
||||||
| `run` / `secret` / `variable` | Forgejo Actions workflows + their config |
|
|
||||||
| `search` | repos, issues, prs, users, code |
|
|
||||||
| `browse` | open the current repo (or a path within it) in $BROWSER |
|
|
||||||
| `status` | notifications inbox |
|
|
||||||
| `protect` / `hook` | branch protection rules, webhooks |
|
|
||||||
| `api` | raw HTTP escape hatch with `-X`, `-f`, `-F`, `-H`, `-q`, `--paginate`, `--include` |
|
|
||||||
|
|
||||||
## Global flags
|
|
||||||
|
|
||||||
- `--host <name>` or `FJ_HOST`: pick the host explicitly. When omitted
|
|
||||||
fj uses (1) the host from the autodetected remote, then (2) the
|
|
||||||
configured default from `fj auth switch`.
|
|
||||||
- `--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 `--json`. Dotted
|
|
||||||
paths supported (`--json-fields owner.login,id`).
|
|
||||||
- `--web` on most list/view commands: open the relevant page in
|
|
||||||
`$BROWSER`.
|
|
||||||
|
|
||||||
## Scripting patterns
|
|
||||||
|
|
||||||
### Get one field
|
|
||||||
|
|
||||||
```sh
|
|
||||||
fj api /user -q .login
|
|
||||||
fj api /repos/owner/name -q .default_branch
|
|
||||||
fj api /repos/owner/name/pulls/3 -q .merged
|
|
||||||
```
|
|
||||||
|
|
||||||
`-q` accepts dot paths, `[N]`/`[-N]` brackets, and `|` pipes. Not full
|
|
||||||
jq. For complex queries, pipe to real `jq`:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
fj api /repos/.../pulls --paginate | jq '[.[] | select(.draft == false) | .number]'
|
|
||||||
```
|
|
||||||
|
|
||||||
### Selective JSON output
|
|
||||||
|
|
||||||
```sh
|
|
||||||
fj repo list --json --json-fields full_name,private,default_branch
|
|
||||||
fj pr list -R foo/bar --json --json-fields number,title,head.label
|
|
||||||
```
|
|
||||||
|
|
||||||
### Auto-pagination
|
|
||||||
|
|
||||||
`fj api --paginate` follows `Link: rel=next` and concatenates results.
|
|
||||||
For list commands (`fj pr list -L 200`, etc.), pagination is implicit:
|
|
||||||
when `-L > 50` (Forgejo's per-page cap) the CLI walks pages
|
|
||||||
transparently.
|
|
||||||
|
|
||||||
### Body input
|
|
||||||
|
|
||||||
For commands with `--body`: pass an inline string, `-` for stdin, or
|
|
||||||
omit `--body` to open `$EDITOR`. In non-interactive contexts (CI,
|
|
||||||
scripts) ALWAYS pass `--body` explicitly so the command doesn't hang
|
|
||||||
waiting for an editor.
|
|
||||||
|
|
||||||
```sh
|
|
||||||
fj issue create -R foo/bar --title "..." --body "..." # inline
|
|
||||||
echo "long body" | fj issue create -R foo/bar --title "..." --body - # stdin
|
|
||||||
fj issue create -R foo/bar --title "..." # interactive only
|
|
||||||
```
|
|
||||||
|
|
||||||
## Common workflows
|
|
||||||
|
|
||||||
### Find a PR to review
|
|
||||||
|
|
||||||
```sh
|
|
||||||
fj pr status # cross-repo dashboard
|
|
||||||
fj pr list -R foo/bar --state open --json \
|
|
||||||
--json-fields number,title,head.label,user.login
|
|
||||||
```
|
|
||||||
|
|
||||||
### Review a PR (inside a clone)
|
|
||||||
|
|
||||||
```sh
|
|
||||||
fj pr checkout 42 # checks out the head branch
|
|
||||||
fj pr diff 42 | less # or fj pr files 42 for a summary
|
|
||||||
fj pr review 42 --event approve --body "LGTM"
|
|
||||||
fj pr review 42 --event request-changes --body "see comments"
|
|
||||||
fj pr review 42 --event comment --body "..."
|
|
||||||
fj pr request-review 42 alice bob # request specific reviewers
|
|
||||||
```
|
|
||||||
|
|
||||||
### Open + iterate on an issue
|
|
||||||
|
|
||||||
```sh
|
|
||||||
fj issue create --title "..." --body "..." # auto-detects repo
|
|
||||||
fj issue view 7
|
|
||||||
fj issue comment 7 --body "more info"
|
|
||||||
fj issue edit-comment 12345 --body "fixed typo" # comment id, not issue number
|
|
||||||
fj issue close 7
|
|
||||||
fj issue develop 7 # create a branch for it
|
|
||||||
```
|
|
||||||
|
|
||||||
### Cut a release
|
|
||||||
|
|
||||||
```sh
|
|
||||||
fj release create v1.2.3 --title "1.2.3" --body "..." --asset dist/foo.tar.gz
|
|
||||||
fj release upload v1.2.3 dist/extra.txt
|
|
||||||
fj release view v1.2.3 --json
|
|
||||||
```
|
|
||||||
|
|
||||||
### Search
|
|
||||||
|
|
||||||
```sh
|
|
||||||
fj search code "use crate::foo" -R owner/name
|
|
||||||
fj search issues "is:open label:bug"
|
|
||||||
fj search repos "rust forgejo"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Inspect notifications
|
|
||||||
|
|
||||||
```sh
|
|
||||||
fj status # unread inbox
|
|
||||||
fj status --all # include read
|
|
||||||
fj status --mark-read # mark all as read
|
|
||||||
```
|
|
||||||
|
|
||||||
### Raw API
|
|
||||||
|
|
||||||
```sh
|
|
||||||
fj api /version # GET
|
|
||||||
fj api /repos/foo/bar/issues -f state=all --paginate
|
|
||||||
fj api /repos/foo/bar/issues -X POST -F labels='[1,2]' --input '{"title":"x","body":"y"}'
|
|
||||||
fj api /user -H "X-Trace-Id: $(uuidgen)" # custom headers
|
|
||||||
fj api /repos/foo/bar/branches --include # response headers + body
|
|
||||||
```
|
|
||||||
|
|
||||||
## Things to avoid
|
|
||||||
|
|
||||||
- **Don't paste tokens into the shell history.** `fj auth token` refuses
|
|
||||||
to print to a TTY by default. Pipe to a file or another command:
|
|
||||||
`fj auth token | pbcopy`. Use `--force` if absolutely needed.
|
|
||||||
- **Don't shell out to `curl` against `/api/v1`** when `fj api` works.
|
|
||||||
`fj api` handles auth, retries on 5xx and 429, jq projection, and
|
|
||||||
pagination.
|
|
||||||
- **Don't `git push` and call it forge-side.** `fj` doesn't have a
|
|
||||||
`push` subcommand because that's git's job. But `fj pr create`,
|
|
||||||
`fj release create` etc. are how you forge-side artifacts.
|
|
||||||
- **Don't omit `--body` in non-interactive scripts.** It'll open
|
|
||||||
`$EDITOR` and hang.
|
|
||||||
- **Don't assume gh-equivalence everywhere.** Check `docs/gh-to-fj.md`
|
|
||||||
if uncertain. Notable gaps: no `gh codespace`, no `gh attestation`,
|
|
||||||
no `gh ruleset` (Forgejo uses branch protection via `fj protect`).
|
|
||||||
|
|
||||||
## When things go wrong
|
|
||||||
|
|
||||||
- HTTP 401: `fj auth refresh` (re-verifies) or `fj auth refresh --token
|
|
||||||
NEW` (replaces). If a token is revoked, `fj auth login --host <h>`
|
|
||||||
to start over.
|
|
||||||
- "no host selected": pass `--host` or `fj auth switch <h>` to set a
|
|
||||||
default.
|
|
||||||
- Mysterious 404s on commands like `fj pr ready` or `fj search code`:
|
|
||||||
the target server may be older than the Forgejo 7.x baseline. Check
|
|
||||||
`fj api /version`. See [`docs/compatibility.md`](https://rasterhub.com/rasterstate/fj/src/branch/main/docs/compatibility.md).
|
|
||||||
- Hangs in CI: probably waiting on `$EDITOR`. Always pass `--body`.
|
|
||||||
- "shadowed by other commands": you have two `fj` binaries on PATH.
|
|
||||||
Pick one (usually `~/.local/bin/fj` for source builds vs.
|
|
||||||
`/opt/homebrew/bin/fj` for Homebrew installs).
|
|
||||||
|
|
||||||
## Reference
|
|
||||||
|
|
||||||
- Repo: <https://rasterhub.com/rasterstate/fj>
|
|
||||||
- gh→fj mapping: <https://rasterhub.com/rasterstate/fj/src/branch/main/docs/gh-to-fj.md>
|
|
||||||
- Architecture: <https://rasterhub.com/rasterstate/fj/src/branch/main/docs/architecture.md>
|
|
||||||
- Troubleshooting: <https://rasterhub.com/rasterstate/fj/src/branch/main/docs/troubleshooting.md>
|
|
||||||
- jq syntax: <https://rasterhub.com/rasterstate/fj/src/branch/main/docs/jq.md>
|
|
||||||
38
scripts/_demo-session.sh
Executable file
38
scripts/_demo-session.sh
Executable file
|
|
@ -0,0 +1,38 @@
|
||||||
|
#!/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
|
||||||
|
|
@ -1,93 +0,0 @@
|
||||||
# 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
|
|
||||||
|
|
@ -1,68 +1,51 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Record the fj README demo from scripts/demo.tape using vhs.
|
# Record an asciinema demo of fj. The result is a .cast file you can:
|
||||||
|
# - Upload to asciinema.org via `asciinema upload dist/demo.cast`
|
||||||
|
# - Embed as a static SVG/GIF via `agg` (https://github.com/asciinema/agg)
|
||||||
|
# - Link from README.md
|
||||||
#
|
#
|
||||||
# vhs is from Charmbracelet (https://github.com/charmbracelet/vhs):
|
# Requires: asciinema, fj (installed and authenticated), `jq` for nicer output.
|
||||||
# a declarative tape format that drives a headless terminal and writes
|
|
||||||
# GIF/MP4/WebM directly. Reproducible, scriptable, no manual typing.
|
|
||||||
#
|
|
||||||
# Pre-flight:
|
|
||||||
# * `brew install vhs` (or download from the vhs releases page)
|
|
||||||
# * `cargo build --release && ln -sf $PWD/target/release/fj ~/.local/bin/fj`
|
|
||||||
# * `fj auth login --host rasterhub.com`
|
|
||||||
#
|
|
||||||
# Outputs:
|
|
||||||
# assets/demo.gif embed in README
|
|
||||||
# assets/demo.mp4 optional, higher-fidelity for social cards
|
|
||||||
#
|
#
|
||||||
# Usage:
|
# Usage:
|
||||||
# ./scripts/record-demo.sh # records both outputs
|
# ./scripts/record-demo.sh # records dist/demo.cast
|
||||||
# ./scripts/record-demo.sh --gif-only # GIF only (faster)
|
# ./scripts/record-demo.sh foo.cast # records to foo.cast
|
||||||
|
#
|
||||||
|
# The session below is deliberately short (~30 seconds) and covers what
|
||||||
|
# someone reading the README in their first 10 seconds cares about.
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
cd "$(git rev-parse --show-toplevel)"
|
OUT="${1:-dist/demo.cast}"
|
||||||
|
mkdir -p "$(dirname "$OUT")"
|
||||||
|
|
||||||
GIF_ONLY=0
|
if ! command -v asciinema >/dev/null 2>&1; then
|
||||||
case "${1:-}" in
|
cat >&2 <<EOF
|
||||||
--gif-only) GIF_ONLY=1 ;;
|
asciinema not found.
|
||||||
"") ;;
|
macOS: brew install asciinema
|
||||||
--help|-h)
|
Linux: pipx install asciinema
|
||||||
sed -n '/^# /{s/^# \{0,1\}//;p;}' "$0" | head -25
|
EOF
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "unknown arg: $1 (try --help)" >&2
|
|
||||||
exit 2
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
require() {
|
|
||||||
local cmd=$1 hint=$2
|
|
||||||
if ! command -v "$cmd" >/dev/null 2>&1; then
|
|
||||||
echo "missing $cmd. $hint" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
require vhs "Install with: brew install vhs"
|
|
||||||
require fj "Build with: cargo build --release && ln -sf \$PWD/target/release/fj ~/.local/bin/fj"
|
|
||||||
|
|
||||||
if ! fj auth status >/dev/null 2>&1; then
|
|
||||||
echo "fj is not authenticated. Run: fj auth login --host rasterhub.com" >&2
|
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
mkdir -p assets
|
if ! command -v fj >/dev/null 2>&1; then
|
||||||
|
echo "fj not found on PATH; build it first (cargo build --release)" >&2
|
||||||
if [ "$GIF_ONLY" = 1 ]; then
|
exit 1
|
||||||
TAPE=$(mktemp -t fj-demo.XXXXXX.tape)
|
|
||||||
trap 'rm -f "$TAPE"' EXIT
|
|
||||||
grep -v '^Output assets/demo\.mp4' scripts/demo.tape > "$TAPE"
|
|
||||||
vhs "$TAPE"
|
|
||||||
else
|
|
||||||
vhs scripts/demo.tape
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Drive a representative session through asciinema.
|
||||||
|
# Idle pauses keep readability up; trim them in post if you want a snappier
|
||||||
|
# embedded GIF.
|
||||||
|
asciinema rec \
|
||||||
|
--overwrite \
|
||||||
|
--idle-time-limit 2 \
|
||||||
|
--title "fj: a CLI for Forgejo" \
|
||||||
|
--command "bash $(dirname "$0")/_demo-session.sh" \
|
||||||
|
"$OUT"
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "✓ wrote assets/demo.gif"
|
echo "✓ Recorded $OUT"
|
||||||
[ "$GIF_ONLY" = 1 ] || echo "✓ wrote assets/demo.mp4"
|
echo ""
|
||||||
|
echo "Next:"
|
||||||
|
echo " asciinema play $OUT # preview"
|
||||||
|
echo " asciinema upload $OUT # publish (returns a URL)"
|
||||||
|
echo " agg $OUT $OUT.gif # render to GIF"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue