Find a file
2026-08-18 00:45:00 +00:00
.forgejo/workflows Add CI workflow for PRs and main pushes 2026-07-26 21:39:57 +00:00
assets design: stoke brand system, without the 23MB of binaries 2026-07-26 21:26:30 +00:00
docs design: stoke brand system, without the 23MB of binaries 2026-07-26 21:26:30 +00:00
manifests feat: forgejo-cli with auth, repo, issue, pr and branch commands 2026-07-22 15:03:32 +00:00
scripts install-apt: fail fast with a clear message when the registry has no Release file 2026-07-26 21:41:16 +00:00
src Add labels to issue creation 2026-08-18 00:45:00 +00:00
test Add labels to issue creation 2026-08-18 00:45:00 +00:00
.gitignore Add apt distribution: deb packaging, registry publish, docs (#1) 2026-07-22 19:41:02 +00:00
package-lock.json Add release, label, and api commands (v1.3.0) 2026-07-26 20:31:07 +00:00
package.json design: stoke brand system, without the 23MB of binaries 2026-07-26 21:26:30 +00:00
README.md Add issue show/comment, --json output, and pr review --commit 2026-07-26 22:00:47 +00:00

stoke

CLI for the heavy-duty forge (Forgejo). Named after the act of feeding and tending a fire — the operator's hand on the forge.

A command-line interface for Forgejo, built with Commander.js. It targets the instance at https://forgejo.heavyduty.builders and is designed so that every real operation performed against Forgejo becomes a new CLI command.

Requirements

  • Node.js >= 22.12 (required by commander@15; the CLI also uses the global fetch API)
  • npm

Installation

The package is published to the Debian registry of the forge itself. One-time setup:

curl -fsSL https://forgejo.heavyduty.builders/heavy-duty/stoke/raw/branch/main/scripts/install-apt.sh | bash

or manually. First add the forge's registry as an apt source:

sudo install -d /etc/apt/keyrings
curl -fsSL https://forgejo.heavyduty.builders/api/packages/heavy-duty/debian/repository.key \
  | sudo tee /etc/apt/keyrings/forgejo-heavy-duty.asc >/dev/null
echo "deb [signed-by=/etc/apt/keyrings/forgejo-heavy-duty.asc] https://forgejo.heavyduty.builders/api/packages/heavy-duty/debian stable main" \
  | sudo tee /etc/apt/sources.list.d/forgejo-heavy-duty.list

The package depends on nodejs (>= 22.12), which the distro archives of Debian 13 (Node 20) and Ubuntu 24.04 (Node 18) cannot satisfy — on those distros, also add a Node 22 source such as NodeSource:

curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key \
  | sudo tee /etc/apt/keyrings/nodesource.asc >/dev/null
echo "deb [signed-by=/etc/apt/keyrings/nodesource.asc] https://deb.nodesource.com/node_22.x nodistro main" \
  | sudo tee /etc/apt/sources.list.d/nodesource.list

Then install:

sudo apt-get update && sudo apt-get install stoke

Upgrades then arrive through regular apt-get upgrade. install-apt.sh performs all of the above, adding the NodeSource repository only when no already-configured apt source offers a new-enough nodejs.

Note: apt releases that verify OpenPGP with sqv (Debian 13+, apt >= 2.9) currently reject the signature Forgejo generates for its Debian registry (an upstream signing bug). install-apt.sh detects this and falls back to a [trusted=yes] source — integrity then relies on HTTPS to the forge. The script prefers the signed source, so setups heal automatically once the forge is fixed.

As a fallback, each release also has the .deb attached for direct install: sudo dpkg -i stoke_<version>_all.deb.

From source

cd stoke
npm install
npm link        # makes the `stoke` binary available globally

Or run it directly without linking:

node src/cli.js <command>

Configuration

Authentication state is stored in a JSON file:

  • Default: ~/.config/stoke/config.json (or $XDG_CONFIG_HOME/stoke/config.json when XDG_CONFIG_HOME is set)
  • Override with --config <path> or STOKE_CONFIG_FILE (or FORGEJO_CONFIG_FILE as a fallback)

Note: the global --config flag must be passed before the subcommand, e.g. stoke --config /path/to/config.json auth status.

The configuration directory is created with permissions 0700 and the file with 0600 so only the owner can read the token.

Example stored config:

{
  "url": "https://forgejo.heavyduty.builders",
  "login": "kimi-reviewer-andresmgsl",
  "username": "kimi-reviewer-andresmgsl",
  "email": "andres+4@heavyduty.builders",
  "token": "<sha1>",
  "tokenId": 42
}

Environment variables

Variable Purpose
STOKE_URL Default Forgejo base URL
STOKE_USERNAME Default username/email for auth login
STOKE_PASSWORD Default password for auth login
STOKE_CONFIG_FILE Path to the config file
STOKE_CONFIG_DIR Directory for the config file
XDG_CONFIG_HOME Followed when resolving the default config directory ($XDG_CONFIG_HOME/stoke)
GITHUB_TOKEN GitHub token used by repo import and repo import-batch when --github-token is omitted

FORGEJO_* variants are still accepted as fallbacks for backward compatibility.

Commands

Read commands (auth status, repo list, issue list, issue show, pr list, pr show, release list, release view, label list, branch list, org repos, org team list, org team member-list, user list, user show) accept a --json flag that prints the raw API response, pretty-printed, instead of the human-readable format — useful for scripting.

Global options

-c, --config <path>   path to configuration file
-h, --help            display help
-V, --version         display version

stoke auth login

Authenticate and persist an access token.

Options:
  -u, --url <url>            Forgejo base URL (default: https://forgejo.heavyduty.builders)
  -n, --username <username>  account username or email
  -p, --password <password>  account password
  --password-file <path>     read account password from a file
  -t, --token <token>        use an existing personal access token instead of generating one
  --token-file <path>        read an existing personal access token from a file
  --token-name <name>        name for the generated token
  --full-scopes              grant full read/write access on all non-admin scopes
  --scopes <csv>             comma-separated list of scopes for the generated token

Interactive example:

stoke auth login
# prompts for username and password

Non-interactive example using environment variables:

export STOKE_USERNAME='kimi-reviewer-andresmgsl'
export STOKE_PASSWORD='...'
stoke auth login

Password file example (avoids shell history and special-character issues):

chmod 600 /run/secrets/stoke-password
stoke auth login -n kimi-reviewer-andresmgsl --password-file /run/secrets/stoke-password

Existing token example:

stoke auth login -t <personal-access-token>

Flow:

  1. Calls GET /api/v1/user to verify credentials and resolve the canonical login name.
  2. Calls POST /api/v1/users/{login}/tokens to generate a personal access token.
  3. Requests the default least-privilege scopes (see "Token scopes" below).
  4. Writes the token, token id, user details and URL to the config file.

Token scopes:

By default the generated token is least-privilege, covering the common issue/PR/repository commands:

  • read:issue, write:issue — issues, PR comments/reviews, labels
  • read:repository, write:repository — repositories, branches, releases, collaborators, pull requests
  • read:userauth status, user list, user show
  • read:organizationorg repos, org team list, org team member-list

Organization administration (org create, org avatar, org team create, org team member-add, org team member-remove) and package publishing need broader access. Pass --full-scopes for the previous all-scopes behavior (read/write on activitypub, issue, misc, organization, package, repository, user), or --scopes <csv> for a custom list:

stoke auth login --scopes read:issue,write:issue,read:repository

The scopes the token was created with are printed after a successful login.

stoke auth logout

Revoke the stored token remotely and delete the local config.

Options:
  -p, --password <password>  account password, needed to revoke the token remotely
  --password-file <path>     read account password from a file
  --local-only               skip remote revocation and only delete the local config
stoke auth logout                                   # prompts for the password on a TTY
stoke auth logout --password-file /run/secrets/pw   # non-interactive
stoke auth logout --local-only                      # keep the token active, just forget it locally

Forgejo only accepts Basic auth on its token endpoints — a token cannot revoke itself — so remote revocation needs the account password. Without one (or with --local-only), the token stays active on the server and can be revoked from the web UI under Settings > Applications.

Flow:

  1. Calls DELETE /api/v1/users/{login}/tokens/{id} using Basic auth.
  2. Removes ~/.config/stoke/config.json.

stoke auth status

Display the currently authenticated user.

stoke auth status

Calls GET /api/v1/user with the stored token.

stoke repo clone

Clone a repository from the configured Forgejo instance using the stored credentials.

Arguments:
  [directory]          destination directory (default: repository name)

Options:
  -o, --owner <owner>  repository owner (required)
  -r, --repo <repo>    repository name (required)
  --branch <branch>    checkout this branch instead of the default branch
  --depth <depth>      create a shallow clone with the given history depth
  --origin <name>      name for the created remote (default: origin)
stoke repo clone -o heavy-duty -r stoke
stoke repo clone -o heavy-duty -r stoke ~/src/stoke --depth 1

The stored token is handed to git ephemerally through environment-based config (GIT_CONFIG_*): it never appears in the remote URL, on the command line, or in the cloned repository's .git/config. Git's output is streamed directly and its exit status is forwarded, so failures behave exactly like a plain git clone.

stoke repo create

Create a new repository for the authenticated user.

Options:
  --name <name>                    repository name (required)
  -d, --description <description>  repository description
  --private                        make the repository private
  --public                         make the repository public
  --auto-init                      initialize with a README (default)
  --no-auto-init                   create an empty repository without a README
  --default-branch <branch>        default branch name (default: "main")

Example:

stoke repo create --name stoke-test --private \
  -d "Test repository created via stoke"

Calls POST /api/v1/user/repos.

stoke repo list

List repositories for the authenticated user.

Options:
  -l, --limit <number>   maximum repositories to display (default: 50; use 0 for all)
stoke repo list

Calls GET /api/v1/user/repos and auto-paginates.

stoke repo import

Import a remote repository into Forgejo, including git history, issues, pull requests, labels, milestones, releases and wiki.

Options:
  --from <clone-addr>      source clone URL (required)
  --name <name>            repository name in Forgejo (required)
  --service <service>      source service type: git, github, gitea, gitlab, ... (default: github)
  --owner <owner>          Forgejo owner for the imported repo (default: current user)
  -d, --description <description>  repository description
  --private / --public     visibility
  --issues                 migrate issues (default)
  --no-issues              skip issues
  --labels                 migrate labels (default)
  --no-labels              skip labels
  --milestones             migrate milestones (default)
  --no-milestones          skip milestones
  --pull-requests          migrate pull requests (default)
  --no-pull-requests       skip pull requests
  --releases               migrate releases (default)
  --no-releases            skip releases
  --wiki                   migrate wiki (default)
  --no-wiki                skip wiki
  --lfs                    migrate LFS objects
  --github-token <token>   source service token (for GitHub defaults to GITHUB_TOKEN or `gh auth token`)

A source token is only required when --service github (the default): it raises rate limits and enables private repositories. For other services (git, gitlab, gitea, ...) no token is sent unless one is explicitly provided.

Example used to mirror heavy-duty/box:

stoke repo import \
  --from https://github.com/heavy-duty/box.git \
  --name box \
  --service github \
  --public \
  --issues --labels --milestones --pull-requests --releases --wiki

Calls POST /api/v1/repos/migrate.

stoke repo import-batch

Import multiple repositories from a JSON manifest.

Options:
  -f, --file <path>   path to JSON manifest (required)
  --dry-run           print the manifest without importing

Manifest format:

[
  {
    "name": "rig",
    "from": "https://github.com/heavy-duty/rig.git",
    "service": "github",
    "private": false,
    "issues": true,
    "labels": true,
    "milestones": true,
    "pull_requests": true,
    "releases": true,
    "wiki": true
  }
]

Example:

stoke repo import-batch -f repos.json
stoke repo import-batch -f repos.json --dry-run

Calls POST /api/v1/repos/migrate once per entry.

stoke repo rename

Rename a repository.

Options:
  -o, --owner <owner>    repository owner (required)
  -r, --repo <repo>      current repository name (required)
  --name <new-name>      new repository name (required)
stoke repo rename -o heavy-duty -r old-name --name new-name

Calls PATCH /api/v1/repos/{owner}/{repo}.

stoke repo transfer

Transfer a repository to a new owner (a user or an organization). The authenticated user must have admin rights on the repository and permission to create repositories under the new owner (e.g. be an organization owner), in which case the transfer completes immediately.

Options:
  -o, --owner <owner>    current repository owner (required)
  -r, --repo <repo>      repository name (required)
  --to <new-owner>       new owner: username or organization name (required)

Example used to move the heavy-duty repositories into the heavy-duty organization:

stoke repo transfer -o kimi-reviewer-andresmgsl -r box --to heavy-duty

Calls POST /api/v1/repos/{owner}/{repo}/transfer.

stoke issue list

List issues in a repository.

Options:
  -o, --owner <owner>    repository owner (required)
  -r, --repo <repo>      repository name (required)
  -s, --state <state>    open, closed, all (default: open)
  -t, --type <type>      issues or pulls (default: issues)
  -l, --limit <number>   maximum issues to display (default: 50; use 0 for all)
stoke issue list -o kimi-reviewer-andresmgsl -r box -s all -l 0

Calls GET /api/v1/repos/{owner}/{repo}/issues and auto-paginates.

stoke issue create

Create an issue in a repository.

Options:
  -o, --owner <owner>       repository owner (required)
  -r, --repo <repo>         repository name (required)
  -t, --title <title>       issue title (required)
  -b, --body <body>         issue body (markdown)
  --body-file <path>        read the issue body from a file
  --assignee <username...>  assign the issue to one or more users
stoke issue create -o heavy-duty -r stoke -t "Ship v2" --body-file body.md

Calls POST /api/v1/repos/{owner}/{repo}/issues.

stoke issue show

Show details of an issue.

Options:
  -o, --owner <owner>    repository owner (required)
  -r, --repo <repo>      repository name (required)
  -n, --number <number>  issue number (required)
  --json                 print raw JSON instead of human-readable output
stoke issue show -o heavy-duty -r stoke -n 10

Calls GET /api/v1/repos/{owner}/{repo}/issues/{number}.

stoke issue comment

Add a comment to an issue. Body is required (whitespace-only is rejected). When both -b and --body-file are set, --body-file wins.

Options:
  -o, --owner <owner>    repository owner (required)
  -r, --repo <repo>      repository name (required)
  -n, --number <number>  issue number (required)
  -b, --body <body>      comment body (markdown; required unless --body-file)
  --body-file <path>     read the comment body from a file (wins over -b)
stoke issue comment -o heavy-duty -r stoke -n 10 -b "Confirmed."

Calls POST /api/v1/repos/{owner}/{repo}/issues/{number}/comments.

stoke pr list

List pull requests in a repository.

Options:
  -o, --owner <owner>    repository owner (required)
  -r, --repo <repo>      repository name (required)
  -s, --state <state>    open, closed, all (default: open)
  -l, --limit <number>   maximum PRs to display (default: 50; use 0 for all)
stoke pr list -o kimi-reviewer-andresmgsl -r box -s all -l 0

Calls GET /api/v1/repos/{owner}/{repo}/pulls and auto-paginates.

stoke pr create

Create a pull request in a repository.

Options:
  -o, --owner <owner>    repository owner (required)
  -r, --repo <repo>      repository name (required)
  -t, --title <title>    pull request title (required)
  --head <branch>        source branch (required; for cross-repo PRs use user:branch)
  --base <branch>        target branch (default: main)
  -b, --body <body>      pull request body (markdown)
  --body-file <path>     read the pull request body from a file
stoke pr create -o heavy-duty -r stoke -t "Fix config handling" \
  --head fix/config --base main --body-file pr-body.md

Calls POST /api/v1/repos/{owner}/{repo}/pulls.

stoke pr merge

Merge a pull request.

Options:
  -o, --owner <owner>      repository owner (required)
  -r, --repo <repo>        repository name (required)
  -n, --number <number>    pull request number (required)
  --method <method>        merge, rebase, rebase-merge, squash (default: merge)
  --title <title>          custom merge commit title
  --message <message>      custom merge commit message
  --delete-branch          delete the source branch after merging
stoke pr merge -o heavy-duty -r stoke -n 2 --delete-branch

Calls POST /api/v1/repos/{owner}/{repo}/pulls/{number}/merge.

stoke pr show

Show details of a pull request.

Options:
  -o, --owner <owner>    repository owner (required)
  -r, --repo <repo>      repository name (required)
  -n, --number <number>  pull request number (required)
stoke pr show -o heavy-duty -r stoke -n 3

Calls GET /api/v1/repos/{owner}/{repo}/pulls/{number}.

stoke pr comment

Add a comment to a pull request. Body is required (whitespace-only is rejected). When both -b and --body-file are set, --body-file wins.

Options:
  -o, --owner <owner>    repository owner (required)
  -r, --repo <repo>      repository name (required)
  -n, --number <number>  pull request number (required)
  -b, --body <body>      comment body (markdown; required unless --body-file)
  --body-file <path>     read the comment body from a file (wins over -b)
stoke pr comment -o heavy-duty -r stoke -n 3 -b "Looks good."

Calls POST /api/v1/repos/{owner}/{repo}/issues/{number}/comments.

stoke pr review

Submit a review on a pull request. approve / approved may omit a body; request-changes and comment require a non-empty body (raw body is preserved — only emptiness is checked with trim()). When both -b and --body-file are set, --body-file wins.

Options:
  -o, --owner <owner>      repository owner (required)
  -r, --repo <repo>        repository name (required)
  -n, --number <number>    pull request number (required)
  --event <event>          approve|approved, request-changes|request_changes, comment (required)
  -b, --body <body>        review body (markdown; required for request-changes and comment)
  --body-file <path>       read the review body from a file (wins over -b)
  --commit <sha>           commit SHA the review applies to (sent as commit_id)
stoke pr review -o heavy-duty -r stoke -n 3 --event approve -b "Ship it."
stoke pr review -o heavy-duty -r stoke -n 3 --event request-changes --body-file notes.md
stoke pr review -o heavy-duty -r stoke -n 3 --event approve --commit 9fceb02

Calls POST /api/v1/repos/{owner}/{repo}/pulls/{number}/reviews. --commit is sent as commit_id; when omitted, no commit_id is sent. Prints the review URL when the forge returns one.

stoke release list

List releases in a repository.

Options:
  -o, --owner <owner>    repository owner (required)
  -r, --repo <repo>      repository name (required)
  -l, --limit <number>   maximum releases to display (default: 50; use 0 for all)
stoke release list -o heavy-duty -r stoke

Calls GET /api/v1/repos/{owner}/{repo}/releases and auto-paginates.

stoke release view

Show the release for a tag, including its notes.

Options:
  -o, --owner <owner>    repository owner (required)
  -r, --repo <repo>      repository name (required)
  --tag <tag>            tag name of the release (required)
stoke release view -o heavy-duty -r stoke --tag v1.2.1

Calls GET /api/v1/repos/{owner}/{repo}/releases/tags/{tag}.

stoke release create

Create a release. If the tag does not exist yet, Forgejo creates it from --target (or the repository default branch). When both -b and --body-file are set, --body-file wins.

Options:
  -o, --owner <owner>    repository owner (required)
  -r, --repo <repo>      repository name (required)
  --tag <tag>            tag name for the release (required)
  --target <ref>         branch or commit the tag is created from (default: default branch)
  -t, --title <title>    release title (default: the tag name)
  -b, --body <body>      release notes (markdown)
  --body-file <path>     read the release notes from a file (wins over -b)
  --draft                create as a draft release
  --prerelease           mark as a prerelease
stoke release create -o heavy-duty -r stoke --tag v1.3.0 --body-file release-notes.md

Calls POST /api/v1/repos/{owner}/{repo}/releases.

stoke label list

List labels in a repository.

Options:
  -o, --owner <owner>    repository owner (required)
  -r, --repo <repo>      repository name (required)
  -l, --limit <number>   maximum labels to display (default: 50; use 0 for all)
stoke label list -o heavy-duty -r stoke

Calls GET /api/v1/repos/{owner}/{repo}/labels and auto-paginates.

stoke label create

Create a label in a repository. The color is validated (6 hex digits, with or without a leading #) before any network call.

Options:
  -o, --owner <owner>          repository owner (required)
  -r, --repo <repo>            repository name (required)
  --name <name>                label name (required)
  --color <color>              label color, 6 hex digits (required)
  -d, --description <description>  label description
stoke label create -o heavy-duty -r stoke --name release --color 0E8A16 \
  -d "Release flow and version/packaging work"

Calls POST /api/v1/repos/{owner}/{repo}/labels.

stoke label delete

Delete a label from a repository, by --id or --name (exactly one is required; passing both is rejected).

stoke label delete -o heavy-duty -r stoke --name needs-triage

Calls DELETE /api/v1/repos/{owner}/{repo}/labels/{id}. A --name is resolved to an id via the repository label list first.

stoke label add

Add labels to an issue or pull request (PRs are issues as far as labels are concerned).

Options:
  -o, --owner <owner>    repository owner (required)
  -r, --repo <repo>      repository name (required)
  -n, --number <number>  issue or pull request number (required)
  --name <name...>       one or more label names (required)
stoke label add -o heavy-duty -r stoke -n 12 --name release scope:cli

Calls POST /api/v1/repos/{owner}/{repo}/issues/{number}/labels. Names are resolved to ids first; an unknown name fails with Label not found.

stoke label remove

Remove labels from an issue or pull request.

stoke label remove -o heavy-duty -r stoke -n 12 --name needs-triage

Calls DELETE /api/v1/repos/{owner}/{repo}/issues/{number}/labels/{id} once per label.

stoke branch list

List branches in a repository.

Options:
  -o, --owner <owner>    repository owner (required)
  -r, --repo <repo>      repository name (required)
  -l, --limit <number>   maximum branches to display (default: 50; use 0 for all)
stoke branch list -o kimi-reviewer-andresmgsl -r box

Calls GET /api/v1/repos/{owner}/{repo}/branches and auto-paginates.

stoke collaborator add

Add a collaborator to a repository.

Options:
  -o, --owner <owner>            repository owner (required)
  -r, --repo <repo>              repository name (required)
  -u, --user <username>          username of the collaborator (required)
  --permission <permission>      read, write, or admin (default: write)

Example:

stoke collaborator add -o kimi-reviewer-andresmgsl -r infra -u andres --permission admin
stoke collaborator add -o kimi-reviewer-andresmgsl -r infra -u dan --permission admin

Calls PUT /api/v1/repos/{owner}/{repo}/collaborators/{user}.

stoke org create

Create a new organization. The authenticated user becomes its first owner.

Options:
  --name <name>              organization username, used in URLs (required)
  --full-name <full-name>    display name of the organization
  -d, --description <description>  organization description
  --website <website>        organization website
  --location <location>      organization location
  --visibility <visibility>  public, limited, private (default: public)

Example used to create the Heavy Duty Builders organization:

stoke org create --name heavy-duty --full-name "Heavy Duty Builders" \
  --website https://heavyduty.builders --visibility public

Calls POST /api/v1/orgs.

stoke org repos

List repositories owned by an organization.

Options:
  -o, --org <org>        organization name (required)
  -l, --limit <number>   maximum repositories to display (default: 50; use 0 for all)
stoke org repos -o heavy-duty -l 0

Calls GET /api/v1/orgs/{org}/repos and auto-paginates.

stoke org avatar

Set the avatar (logo) of an organization from a local image file. The image is read, base64-encoded and uploaded; the caller must be an owner of the organization.

Options:
  -o, --org <org>     organization name (required)
  -f, --file <path>   path to the image file (png, jpeg, gif, ...) (required)

Example used to set the Heavy Duty Builders logo (sourced from the official GitHub organization avatar at https://github.com/heavy-duty.png):

stoke org avatar -o heavy-duty -f heavy-duty-logo.png

Calls POST /api/v1/orgs/{org}/avatar.

stoke org team list

List teams in an organization.

stoke org team list -o heavy-duty

Calls GET /api/v1/orgs/{org}/teams and auto-paginates.

stoke org team create

Create a team in an organization.

Options:
  -o, --org <org>              organization name (required)
  --name <name>                team name (required)
  -d, --description <description>  team description
  --permission <permission>    read, write, admin (default: read)
  --all-repos                  grant access to all current and future org repositories
  --can-create-repo            allow members to create repositories in the organization
  --units <units>              comma-separated team units
                               (default: repo.code,repo.issues,repo.pulls,repo.releases,repo.wiki,repo.projects)

Example used to create the regular members team:

stoke org team create -o heavy-duty --name members --permission write --all-repos

Calls POST /api/v1/orgs/{org}/teams.

stoke org team member-list

List members of a team. Requires being a member of that team (or an instance admin).

stoke org team member-list --team-id 2

Calls GET /api/v1/teams/{id}/members and auto-paginates.

stoke org team member-add

Add a user to a team. Adding a user to any team also makes them a member of the organization; membership in the Owners team is what makes a user an organization admin.

Options:
  --team-id <id>          team id, see `stoke org team list` (required)
  -u, --user <username>   username to add (required)

Example used to make Dan and Andres organization admins:

stoke org team member-add --team-id 1 -u andres
stoke org team member-add --team-id 1 -u dan

Calls PUT /api/v1/teams/{id}/members/{username}.

stoke org team member-remove

Remove a user from a team.

Options:
  --team-id <id>          team id, see `stoke org team list` (required)
  -u, --user <username>   username to remove (required)
stoke org team member-remove --team-id 1 -u kimi-reviewer-andresmgsl

Calls DELETE /api/v1/teams/{id}/members/{username}.

stoke user list

Search/list users on the Forgejo instance. Non-admin searches are visibility-limited: an empty query typically returns only the authenticated user.

Options:
  -q, --query <query>    search query (default: empty)
  -l, --limit <number>   maximum users to display (default: 50; use 0 for all)
stoke user list -q andres

Calls GET /api/v1/users/search and auto-paginates.

stoke user show

Show a single user profile. Useful to check whether a username exists.

stoke user show -u andres

Calls GET /api/v1/users/{username}.

stoke api

Make an authenticated request to any Forgejo API endpoint and print the JSON response. The escape hatch for everything stoke does not wrap yet — pass the endpoint path without the /api/v1 prefix.

Arguments:
  <endpoint>               endpoint path starting with / (required)

Options:
  -X, --method <method>    GET, POST, PUT, PATCH or DELETE
                           (default: GET, or POST when --input is given)
  --input <json>           JSON request body, inline or @path to read from a file
  --paginate               fetch all pages (GET endpoints returning a JSON array);
                           overrides any limit/page in the endpoint
stoke api /user
stoke api "/repos/heavy-duty/stoke/pulls?state=closed" --paginate
stoke api /repos/heavy-duty/stoke/issues/12/comments --input '{"body":"hi"}'
stoke api /repos/heavy-duty/stoke/contents/CHANGELOG.md --input @payload.json

Calls {METHOD} /api/v1{endpoint} with the stored token. The endpoint must start with /; the method, --paginate + non-GET, GET + --input (a GET cannot carry a body), and malformed --input JSON are all rejected before any network call.

Security: stoke api is a full authenticated passthrough — it does anything the stored token is allowed to do. Never interpolate untrusted strings (issue titles, PR bodies, user input) into the endpoint or --input; treat every call like the credential it carries.

Architecture

src/
├── cli.js     # Commander program, commands and user I/O
├── api.js     # Forgejo API client (fetch wrapper)
└── config.js  # Secure filesystem-based config storage
test/
├── cli.test.js     # end-to-end CLI behavior (spawned processes)
├── api.test.js     # API client with a mocked fetch
└── config.test.js  # config path resolution and persistence
scripts/
├── build-deb.sh    # build dist/stoke_<version>_all.deb
├── publish-deb.sh  # upload a .deb to the Forgejo Debian registry
└── install-apt.sh  # consumer-side apt source setup + install
.forgejo/workflows/
└── release.yml     # tag-driven build + publish + release attachment
  • cli.js defines commands and options, handles prompts and prints results.
  • api.js encapsulates all HTTP calls to Forgejo. It supports both Basic auth (for the token endpoints, which reject token auth) and token auth (for all other calls).
  • config.js reads/writes JSON config and enforces restrictive file permissions. Paths are resolved lazily so the global --config flag works.

Testing

The test suite uses the Node.js built-in test runner — no extra dependencies:

npm test

Packaging and releasing

The Debian package is a pure-JS Architecture: all package that ships the CLI to /usr/lib/stoke with a /usr/bin/stoke symlink and declares Depends: nodejs (>= 22.12).

Build locally (needs dpkg-deb; runs lintian when installed):

scripts/build-deb.sh                 # -> dist/stoke_<version>_all.deb

Publish to the Forgejo Debian registry (uses STOKE_TOKEN or the stoke auth login token; the account needs package write access on the owner):

scripts/publish-deb.sh dist/stoke_<version>_all.deb heavy-duty stable main

Releases are automated in .forgejo/workflows/release.yml: pushing a v* tag runs the tests, builds the .deb, publishes it to the heavy-duty registry and attaches it to the tag's release page. The workflow needs a Forgejo Actions runner and a RELEASE_TOKEN secret (package + repository write for the heavy-duty org); adjust runs-on to a label your runner advertises.

Release checklist:

  1. Bump version in package.json and package-lock.json.
  2. Commit, tag v<version>, push the tag.
  3. CI publishes the package; consumers get it with apt-get update && apt-get upgrade.

Security notes

  • Tokens are stored on disk with 0600 permissions.
  • Passwords are never persisted; they are only used to generate (and revoke) a token, and interactive password prompts do not echo to the terminal.
  • Prefer --password-file or STOKE_PASSWORD over -p to keep passwords out of shell history and avoid ! history-expansion issues.
  • The generated token name includes the hostname and a timestamp to avoid collisions.

Verification: heavy-duty repository imports

The heavy-duty repositories were imported into Forgejo under https://forgejo.heavyduty.builders/kimi-reviewer-andresmgsl and later transferred to the heavy-duty organization (https://forgejo.heavyduty.builders/heavy-duty) using stoke repo transfer.

Repository Visibility Branches Commits Open issues Total issues PRs Labels Milestones Releases
box public 1 306 11 65 92 21 0 4
rig public 1 198 10 49 59 21 0 3
cast public 1 194 7 67 75 21 0 3
infra private 1 86 2 8 22 9 0 0
handbook private 1 29 28 28 13 9 5 0
incubator private 1 1,326 1 1 23 9 0 0

Counts match GitHub for all repositories. Git history, issues, pull requests, labels, milestones and releases are included. Discussions are not enabled on any of the source repositories. The cast wiki is enabled on GitHub but contains no pages, so nothing was migrated.

Next steps

The CLI is authenticated and the first full repository import is complete. Every subsequent Forgejo task you need will be added as a new command under stoke.

Why the name stoke?

The heavy-duty handbook defines a strict naming style for tools:

  • one syllable, plain concrete English — industrial: the shop floor and the site
  • a repurposed common word, never coined jargon
  • a verb⇄noun with a double meaning that hints at the job

Existing tools follow this exactly: cast pours a part and is the part; rig sets up and is the setup; jig holds work and is the fixture; boss runs the site and is the supervisor; hand works and is the worker; gig is a one-off job and the job itself; gauge measures and is the instrument.

The handbook calls the Forgejo instance "the forge" — the coordination home where issues, PRs, reviews, and git state live. A forge only stays useful if someone feeds it fuel and air. To stoke a fire is to tend it and keep it burning. This CLI does the same thing programmatically: it creates repos, opens issues, imports projects, adds collaborators — it keeps the forge active.

stoke also fits the operator relationship. While jig, boss, hand, and gauge are autonomous or daemon-like, this tool is the operator's hand on the forge. You don't forge by accident; you stoke on purpose.

Other names were considered and rejected:

  • forge — too literal; the forge is the platform, not the tool.
  • tap — machining term, but "tapping" is about threading holes, not tending a forge.
  • weld / braze — join-metal verbs, but the CLI is not primarily joining things.
  • die / mold / stamp — shaping tools, but die carries a strong negative meaning and mold has the fungus connotation.
  • quench — forge-related, but quenching cools/finishes; the CLI starts and feeds work.
  • draft / blast — air-feeding the forge, but feel like background services, not an operator CLI.
  • flux — nice metaphor for flow, but reads as chemistry rather than the forge shop.

stoke was the only candidate that was clearly forge-specific, operator-facing, one syllable, and already a common verb/noun.