# 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](https://forgejo.org/), built with [Commander.js](https://github.com/tj/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 ### With apt (Debian/Ubuntu — recommended) The package is published to the Debian registry of the forge itself. One-time setup: ```bash curl -fsSL https://forgejo.heavyduty.builders/heavy-duty/stoke/raw/branch/main/scripts/install-apt.sh | bash ``` or manually: ```bash 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 sudo apt-get update && sudo apt-get install stoke ``` Upgrades then arrive through regular `apt-get upgrade`. The package depends on `nodejs (>= 22.12)`, which the distro archives of Debian 13 (Node 20) and Ubuntu 24.04 (Node 18) cannot satisfy. `install-apt.sh` handles this automatically by adding the [NodeSource](https://deb.nodesource.com) Node 22 repository when no configured apt source offers a new-enough nodejs. If you follow the manual steps instead, make sure such a source is available before `apt-get install stoke`: ```bash 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 ``` 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__all.deb`. ### From source ```bash cd stoke npm install npm link # makes the `stoke` binary available globally ``` Or run it directly without linking: ```bash node src/cli.js ``` ## 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 ` 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: ```json { "url": "https://forgejo.heavyduty.builders", "login": "kimi-reviewer-andresmgsl", "username": "kimi-reviewer-andresmgsl", "email": "andres+4@heavyduty.builders", "token": "", "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 ```text -c, --config path to configuration file -h, --help display help -V, --version display version ``` ### `stoke auth login` Authenticate and persist an access token. ```text Options: -u, --url Forgejo base URL (default: https://forgejo.heavyduty.builders) -n, --username account username or email -p, --password account password --password-file read account password from a file -t, --token use an existing personal access token instead of generating one --token-file read an existing personal access token from a file --token-name name for the generated token ``` Interactive example: ```bash stoke auth login # prompts for username and password ``` Non-interactive example using environment variables: ```bash export STOKE_USERNAME='kimi-reviewer-andresmgsl' export STOKE_PASSWORD='...' stoke auth login ``` Password file example (avoids shell history and special-character issues): ```bash chmod 600 /run/secrets/stoke-password stoke auth login -n kimi-reviewer-andresmgsl --password-file /run/secrets/stoke-password ``` Existing token example: ```bash stoke auth login -t ``` 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 standard non-admin scopes: `read/write` for `activitypub`, `issue`, `misc`, `organization`, `package`, `repository`, and `user`. 4. 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. ```text Options: -p, --password account password, needed to revoke the token remotely --password-file read account password from a file --local-only skip remote revocation and only delete the local config ``` ```bash 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. ```bash stoke auth status ``` Calls `GET /api/v1/user` with the stored token. ### `stoke repo create` Create a new repository for the authenticated user. ```text Options: --name repository name (required) -d, --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 default branch name (default: "main") ``` Example: ```bash 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. ```text Options: -l, --limit maximum repositories to display (default: 50; use 0 for all) ``` ```bash 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. ```text Options: --from source clone URL (required) --name repository name in Forgejo (required) --service source service type: git, github, gitea, gitlab, ... (default: github) --owner Forgejo owner for the imported repo (default: current user) -d, --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 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`: ```bash 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. ```text Options: -f, --file path to JSON manifest (required) --dry-run print the manifest without importing ``` Manifest format: ```json [ { "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: ```bash 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. ```text Options: -o, --owner repository owner (required) -r, --repo current repository name (required) --name new repository name (required) ``` ```bash 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. ```text Options: -o, --owner current repository owner (required) -r, --repo repository name (required) --to new owner: username or organization name (required) ``` Example used to move the heavy-duty repositories into the `heavy-duty` organization: ```bash 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. ```text Options: -o, --owner repository owner (required) -r, --repo repository name (required) -s, --state open, closed, all (default: open) -t, --type issues or pulls (default: issues) -l, --limit maximum issues to display (default: 50; use 0 for all) ``` ```bash 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. ```text Options: -o, --owner repository owner (required) -r, --repo repository name (required) -t, --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 ``` ```bash 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. ```text 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) ``` ```bash 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. ```text 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 ``` ```bash 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. ```text 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 ``` ```bash stoke pr merge -o heavy-duty -r stoke -n 2 --delete-branch ``` Calls `POST /api/v1/repos/{owner}/{repo}/pulls/{number}/merge`. ### `stoke branch list` List branches in a repository. ```text 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) ``` ```bash 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. ```text 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: ```bash 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. ```text 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: ```bash 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. ```text Options: -o, --org <org> organization name (required) -l, --limit <number> maximum repositories to display (default: 50; use 0 for all) ``` ```bash 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. ```text 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`): ```bash 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. ```bash 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. ```text 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: ```bash 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). ```bash 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. ```text 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: ```bash 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. ```text Options: --team-id <id> team id, see `stoke org team list` (required) -u, --user <username> username to remove (required) ``` ```bash 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. ```text Options: -q, --query <query> search query (default: empty) -l, --limit <number> maximum users to display (default: 50; use 0 for all) ``` ```bash 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. ```bash stoke user show -u andres ``` Calls `GET /api/v1/users/{username}`. ## Architecture ```text 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: ```bash 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): ```bash 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): ```bash 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.