No description
  • Go 99.7%
  • Pkl 0.3%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Tim Hårek Andreassen aca40184e4
All checks were successful
check / check (push) Successful in 42s
issue list: refuse a list the API cannot answer, and offer --web
The list flags went to the API as written, so a misspelled --state or
--sort answered the open list as if nothing had been asked, and the
empty message then reported the word that was never a state as one:
'No bogus issues on tim/heimdarr'. state and sort are enums in the
API's own document, so they are checked before the request, and page
and limit are checked with them: a 0 was quietly dropped from the
query, which is how --page 0 answered page 1 and a negative --limit
answered the API's own small page.

The empty answer read the state back out raw: --state all said 'No all
issues on ...', naming all as if it were a state of its own.

--web opens the list in the browser, which every other command here
has and gh issue list has; the URL is spelled from the host and the
path directly, so it opens with the API unreachable.
2026-09-18 10:32:00 +02:00
.forgejo/workflows Initial commit 2026-09-11 13:23:00 +02:00
cmd/fj issue list: refuse a list the API cannot answer, and offer --web 2026-09-18 10:32:00 +02:00
forgejo issue: add fj issue list|view|create|edit|comment|close|reopen|delete 2026-09-15 16:09:58 +02:00
internal table: use text/tabwriter, and cap a cell's width 2026-09-15 16:10:34 +02:00
.gitignore Initial commit 2026-09-11 13:23:00 +02:00
.golangci.toml Initial commit 2026-09-11 13:23:00 +02:00
AGENTS.md table: use text/tabwriter, and cap a cell's width 2026-09-15 16:10:34 +02:00
go.mod table: use text/tabwriter, and cap a cell's width 2026-09-15 16:10:34 +02:00
go.sum table: use text/tabwriter, and cap a cell's width 2026-09-15 16:10:34 +02:00
hk.pkl Initial commit 2026-09-11 13:23:00 +02:00
LICENSE.md Initial commit 2026-09-11 13:23:00 +02:00
mise.toml Initial commit 2026-09-11 13:23:00 +02:00
README.md issue list: refuse a list the API cannot answer, and offer --web 2026-09-18 10:32:00 +02:00

[fj]

Quick Start:

fj auth add src.timharek.no
fj run ci.yml

fj run ci.yml dispatches a workflow file named in .forgejo/workflows/; a repository without any has nothing to run.

A CLI for Forgejo, in the shape of the CLIs it replaces: gh for GitHub and hut for Sourcehut. Commands work on the repository the current directory belongs to, via the git remote called origin.

Install CLI

Using go:

go install src.timharek.no/tim/forgejo-cli/cmd/fj@latest

Using Homebrew

brew tap timharek/tap
brew trust timharek/tap
brew install timharek/tap/fj

Using pre-compiled binary

Select the version you want to install available releases and download it and add it to your $PATH or something similar.

Example:

wget https://src.timharek.no/tim/forgejo-cli/releases/download/<release>/fj-<version>-linux-amd64.tar.gz
tar xf fj-<version>-linux-amd64.tar.gz
cd fj-<version>
cp fj /usr/local/bin

Setup

Create a token on your instance under Settings → Applications → Access tokens, with at least repository and actions read access, then add the host:

fj auth add src.timharek.no

The token is stored in your OS keyring; the host and login in ~/.config/forgejo-cli/config. List and manage them with fj auth list, fj auth remove.

fj auth add prompts for anything missing. For scripts, pass --user and --token, or skip storing a token at all by setting $FORGEJO_TOKEN. Environment variables read:

  • $FORGEJO_TOKEN — API token, tried before the keyring
  • $FORGEJO_HOST — host to use with --repo when --host isn't given
  • $FORGEJO_CONFIG_DIR — where the config file lives ($XDG_CONFIG_HOME/forgejo-cli or ~/.config/forgejo-cli by default)

Usage

Every command works on the repository the current directory belongs to, unless --repo owner/name says otherwise.

# Dispatch a workflow and watch it until it finishes
fj run ci.yml
fj run --ref staging ci.yml
fj run --no-watch ci.yml

# Follow the latest run of a workflow, then show the log
fj run watch ci.yml
fj run watch              # the latest run of anything
fj run watch 12           # run number 12
fj run watch --web ci.yml # open the run in the browser instead

# Logs, listings, housekeeping — watch, logs, cancel and delete all take a
# workflow file name or a run number; list filters by workflow instead
fj run logs ci.yml --job check
fj run list [--workflow ci.yml] [--page N]
fj run cancel 12
fj run delete ci.yml

# Repositories
fj repo list              # yours and the ones you collaborate on
fj repo list timharek     # a user's or an organization's
fj repo view              # details in the terminal
fj repo view --web        # open in the browser
fj repo edit -d "a CLI"   # change settings, only the flags given are changed
fj repo edit --visibility private
fj repo edit --enable-issues=false

# Pull requests
fj pr list                # open ones, --state closed or all for the rest
fj pr status              # the current branch's and yours, open only
fj pr view 3              # details in the terminal
fj pr view --web 3        # open in the browser
fj pr create              # head is the current branch, base the default branch
fj pr create -t "a CLI" -b "why" --head feature --base main
fj pr edit 3 -t "a better title"
fj pr edit --reopen=false 3   # close it without merging
fj pr merge 3             # asks first; --style squash, --delete-branch

# Issues
fj issue list             # open ones, --state closed or all for the rest
fj issue list -l bug -a tim --milestone v1
fj issue list --web       # the list in the browser instead
fj issue view 3           # details in the terminal
fj issue view --comments 3    # with the comments under it
fj issue view --web 3     # open in the browser
fj issue create -t "a CLI" -b "why" -l bug -a tim   # body from stdin when piped
fj issue edit 3 -t "a better title"
fj issue edit 3 --add-label docs --remove-label bug
fj issue close 3 -c "shipped in v1"
fj issue reopen 3
fj issue comment 3 -b "on it"
fj issue delete 3         # asks first; --force

# Secrets, on the repository or, with --org, on an organization
fj secret list
fj secret set DEPLOY_KEY          # hidden prompt, or piped in
fj secret set NPM_TOKEN --body "$NPM_TOKEN"
fj secret set -o timharek ORG_TOKEN --body "$TOKEN"
fj secret delete DEPLOY_KEY

fj secret set reads the value from --body, from stdin when it is piped, or from a hidden prompt. Names are checked here, so a name the instance would refuse is a sentence rather than a 400.

fj run picks the job to show the log of: the one job when the run has exactly one, otherwise you are asked which. With no workflow given and exactly one in the repository, that one is dispatched. Shell completion is generated by fj completion bash|zsh|fish|powershell.

fj pr create takes the head branch from the checkout and the base branch from the repository, so fj pr create -t "a title" is usually all of it.

Scripting

Global flags: --repo owner/name (or -R), --host, --remote, --config, --debug.

--json prints machine-readable output: repo list, repo view, repo edit, pr list, pr status, pr view, pr create, pr edit, pr merge, auth list, secret list and the run subcommands print the API objects; run watch prints a status snapshot after each poll and the final run.

cancel, delete, merge, secret delete and auth remove ask for confirmation unless --force is given. Without a terminal there is nobody to ask, so they fail instead of guessing — a script that means it passes --force.

Why

Forgejo ships forgejo-cli, whose commands are shaped differently from the CLIs most people know. fj follows gh and hut instead: dispatch with run, follow with watch, open with repo view --web or pr view --web.

License

MIT, see LICENSE.md.