I keep ending up in the same place. With Libraries.io and ecosyste.ms it was package registries that all do the same thing with different APIs and different metadata formats. With git-pkgs it was lockfile formats. The pattern is always the same: open source infrastructure that does roughly the same job across ecosystems, but with enough differences in the details to make working across all of them painful. So you build a common interface and absorb the differences.
Git forges are the same kind of problem. GitHub has gh, GitLab has glab, Bitbucket has various unofficial options, and Gitea/Forgejo has tea. They all manage repos, issues, pull requests, releases, and CI, with completely different syntax, different flag conventions, and different ideas about which API endpoints deserve a command and which don’t.
I’ve been building something that needs to talk to all of these forges, a project I’m not quite ready to announce yet, and the idea of wrapping four different CLIs with four different output formats and four different authentication flows was not appealing. I wanted one interface that worked the same way everywhere, for humans on the command line and for AI coding agents that need to interact with forges programmatically.
I started with tea, the Gitea/Forgejo CLI, which covers the basics but is missing commands for things the API supports perfectly well: CI pipelines, deploy keys, secrets, milestones. I began adding what I needed, then realized I’d rather build the tool I actually wanted than patch one that only covered a quarter of the problem.
CLI
All four of the existing CLIs are written in Go, which made the review straightforward. I read through gh, glab, tea, and the Bitbucket tools, looked at how each one mapped API concepts to commands, and built forge as a single combined version that detects which forge a given domain is running automatically.
forge repo view
forge issue list --state open
forge pr create --title "Fix bug" --head fix-branch
forge release list
forge ci list
forge branch list
forge label list
These commands work the same way whether the remote points at github.com, a self-hosted GitLab, or a Forgejo instance. The CLI figures out the forge type from the git remote, or you can set it with --forge-type or a .forge file in the repo root. It covers repos, issues, pull requests, releases, branches, CI pipelines, labels, milestones, deploy keys, secrets, and comments, and for anything not wrapped in a named command there’s forge api for raw authenticated requests:
forge api repos/{owner}/{repo}
Authentication works through forge auth login, which can be interactive or scripted:
forge auth login # asks for domain + token
forge auth login --domain gitea.example.com --token abc123 --type gitea
Tokens resolve from CLI flags, environment variables (FORGE_TOKEN, GITHUB_TOKEN, GITLAB_TOKEN, and so on), or a config file, so existing tokens from gh and glab just work. All commands support --output json for scripting and piping.
Go module
Forge is also a Go module with the same unified interface available programmatically:
client := forges.NewClient(
forges.WithToken("github.com", os.Getenv("GITHUB_TOKEN")),
forges.WithToken("gitlab.com", os.Getenv("GITLAB_TOKEN")),
)
repo, _ := client.FetchRepository(ctx, "https://github.com/octocat/hello-world")
f, _ := client.ForgeFor("github.com")
issues, _ := f.Issues().List(ctx, "octocat", "hello-world", forges.ListIssueOpts{State: "open"})
Each forge backend implements a common interface with services for repositories, issues, pull requests, releases, CI, and the rest, so you can write code that works against any forge without conditional logic per provider. It fills a similar role to what ecosyste-ms/repos does as a web service, except it runs locally and authenticates with your own tokens. That programmatic interface is going to be useful for future integrations with git-pkgs, where having a consistent way to query repositories, releases, and CI status across forges matters quite a bit.
I suspect more people than usual are going to need something like this soon. AI coding agents are starting to interact with forges directly, opening issues and creating pull requests and triggering CI, and most of them are hardcoded to GitHub’s API. Having a single interface that works the same way against a Forgejo instance as it does against github.com makes the agent code simpler and the forge choice less of a lock-in decision.