jeffersongoncalves / repos-cli
A multi-host bulk git repository manager built with Laravel Zero. Clone, pull, and list open issues across hundreds of repos on GitHub, GitLab, and Bitbucket from your terminal.
Package info
github.com/jeffersongoncalves/repos-cli
Type:project
pkg:composer/jeffersongoncalves/repos-cli
Requires
- php: ^8.2
Requires (Dev)
- jeffersongoncalves/laravel-zero-api-client: ^1.0
- jeffersongoncalves/laravel-zero-console: ^1.0
- jeffersongoncalves/laravel-zero-credentials: ^1.0
- jeffersongoncalves/laravel-zero-git: ^1.0
- jeffersongoncalves/laravel-zero-self-update: ^1.0
- laravel-zero/framework: ^12.0.2
- laravel/pint: ^1.25.1
- mockery/mockery: ^1.6.12
- pestphp/pest: ^3.8.4|^4.1.2
- phpstan/phpstan: ^2.1
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
repos-cli
A multi-host bulk git repository manager. Clone, pull, and list open issues
across hundreds of repos on GitHub, GitLab, and Bitbucket from one terminal
— no more cd-ing into 600 folders one at a time.
Built with Laravel Zero and modeled on the other CLIs in this monorepo.
Install
Global (recommended)
composer global require jeffersongoncalves/repos-cli
The binary repos will be on your PATH as long as Composer's global
vendor/bin is in it.
From source
git clone https://github.com/jeffersongoncalves/repos-cli.git
cd repos-cli
composer install
Usage
Update
repos self-update
Authenticate
Clones use your local SSH key, same as any git clone. The saved token is
only used for API calls (listing repos, listing issues) — one login per
host you use.
GitHub
Device flow (recommended — nothing to create or manage):
repos auth:github:login --device
Opens a verification URL and a short code; approve it in the browser. The saved token refreshes itself automatically once it expires.
Personal access token instead:
- Create a classic token with the
reposcope (covers repos, issues, and search) repos auth:github:loginand paste it
GitLab
Device flow (recommended):
repos auth:gitlab:login --device
Personal access token instead:
- GitLab → Settings → Access Tokens → create one with the
read_apiscope repos auth:gitlab:loginand paste it
Bitbucket
No device flow — Bitbucket doesn't support it. API token only:
- Bitbucket → Personal settings → Security → API tokens → Create API token with scopes
- Name it (e.g.
repos-cli), select Bitbucket as the app, and grant these permissions — Write does not imply Read, each is separate:
| Scope | Permission | Scope ID |
|---|---|---|
| User | Read | read:user:bitbucket |
| Repositories | Read | read:repository:bitbucket |
| Issues | Read | read:issue:bitbucket |
User Read is required even though the CLI never reads user data directly —
it's what the login check (GET /2.0/user) needs to verify the token.
repos auth:bitbucket:login— prompts for your Atlassian account email (not your Bitbucket username) and the token
repos auth:show # Confirm every saved credential still works (pings each host's API, # never prints the token/app password) repos auth:show --verify
Credentials are stored in ~/.repos-cli/config.json (mode 0600).
Multiple credentials per host
Every auth:*:login command takes --profile=<name> (default: default), so
you can keep more than one credential for the same host — e.g. a personal
GitHub device-flow login alongside a work PAT for an org with OAuth App
restrictions:
repos auth:github:login --device # profile "default" repos auth:github:login --profile=work # a second, separate token repos clone acme --host=github --profile=work --path=~/code/acme repos issues acme --host=github --profile=work
repos auth:show lists every profile per host. Commands that hit a host's
API (clone for bulk owner/org clones, issues) accept --profile=; it
defaults to default when omitted.
Clone
# Single repo (URL, or owner/repo with --host) repos clone git@github.com:acme/widgets.git repos clone acme/widgets --host=github # Bulk-clone every repo of an owner/org. Already-cloned repos are pulled # instead of re-cloned, so re-running this is how you "update everything". repos clone acme --host=github --path=~/code/acme
Submodules are cloned recursively (git clone --recurse-submodules).
Pull
# git pull --all --recurse-submodules on every repo found one level under a folder repos pull ~/code/acme
Issues
# Single repo repos issues acme/widgets --host=github # Every repo of an owner/org # (GitHub: one search API call. GitLab/Bitbucket: looped per repo.) repos issues acme --host=github
Audit (GitHub)
Hygiene report for every repo of an owner — run it from the folder that holds your clones:
cd ~/code/acme repos audit acme --automerge-template=~/templates/dependabot-auto-merge.yml
| Check | Flags a repo when |
|---|---|
clone |
there's no clone of it under --path (default: current directory) |
website |
its website points to packagist.org |
description |
the description is empty |
branch |
the default branch isn't the highest N.x branch (multi-branch packages) or main |
dependabot |
.github/dependabot.yml is missing |
automerge |
dependabot-auto-merge.yml differs from --automerge-template (skipped without it) |
ci |
a push-triggered workflow run of the default branch's HEAD commit failed |
immutable |
release immutability is disabled (skipped silently when the token can't read the setting) |
tests |
a PHP repo (composer.json at the root of the default branch) has no tests/ folder or no workflow whose file name contains test |
dependabot-prs |
an open Dependabot pull request has failing checks |
social |
a public repo still uses GitHub's generated social preview image (one GraphQL call per 100 repos) |
catalog |
an entry of --catalog (a plugins.json) has no GitHub repo, an archived one, or no Packagist package; or the owner publishes a Packagist package the catalog doesn't list and whose repo isn't archived (skipped without it) |
repos audit acme --only=ci,dependabot-prs # just the red builds repos audit acme --only=catalog --catalog=plugins.json --catalog-ignore=acme/website repos audit acme --skip=clone --filter=filament repos audit acme --exclude=legacy-app,old-site # leave known exceptions out repos audit acme --json > audit.json # pure JSON, no progress bar
The catalog is a JSON object of categories; every nested object with a title and a package (vendor/name) is an entry, and an optional repo gives the GitHub path when it differs from package. Entries under startkit, filament, laravel, laravelZero, cli and cakephp must be on Packagist.
Network errors and GitHub 5xx are retried; a repo that still can't be read is reported as an error finding and the audit moves on. Archived repos and forks are skipped unless you pass --include-archived / --include-forks. The command exits 1 when it finds anything, so it can gate a CI job.
Supported hosts
| Host | Auth | Notes |
|---|---|---|
| GitHub | Personal access token, or --device flow |
Bulk clone/issues via orgs/{org}/repos or users/{user}/repos; issues search via search/issues. |
| GitLab | Personal access token, or --device flow |
Bulk clone/issues via groups/{ns}/projects or users/{ns}/projects. |
| Bitbucket | Account email + API token | Same auth shape as bb-cli. Issues skipped for repos with no issue tracker enabled. No device flow — Bitbucket doesn't support it. |
Development
composer install composer test # Pest tests + Pint lint composer lint # Auto-fix style composer phpstan # Static analysis composer build # Build the PHAR into builds/repos
License
MIT © Jefferson Goncalves
