| manifests | ||
| src | ||
| test | ||
| .gitignore | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
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 globalfetchAPI) - npm
Installation
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.jsonwhenXDG_CONFIG_HOMEis set) - Override with
--config <path>orSTOKE_CONFIG_FILE(orFORGEJO_CONFIG_FILEas 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
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
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:
- Calls
GET /api/v1/userto verify credentials and resolve the canonicalloginname. - Calls
POST /api/v1/users/{login}/tokensto generate a personal access token. - Requests the standard non-admin scopes:
read/writeforactivitypub,issue,misc,organization,package,repository, anduser. - Writes the token, token id, user details and URL to the config file.
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:
- Calls
DELETE /api/v1/users/{login}/tokens/{id}using Basic auth. - 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 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 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 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}.
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
cli.jsdefines commands and options, handles prompts and prints results.api.jsencapsulates 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.jsreads/writes JSON config and enforces restrictive file permissions. Paths are resolved lazily so the global--configflag works.
Testing
The test suite uses the Node.js built-in test runner — no extra dependencies:
npm test
Security notes
- Tokens are stored on disk with
0600permissions. - 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-fileorSTOKE_PASSWORDover-pto 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, butdiecarries a strong negative meaning andmoldhas 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.