$ forgemap clone kirchDev/laravel-pbac
✔ Cloned kirchDev/laravel-pbac → ~/projects/comGithub/kirchDev/laravel-pbacThat's it. Every repo lands at a predictable <root>/<forge.dir>/<owner>/<repo> path, and forgemap cd <slug> jumps into any of them from anywhere — exact slug, fuzzy match, or interactive picker.
- 🗂️ Predictable layout — every clone goes to
<root>/<forge.dir>/<owner>/<repo>, configured once. - 🚪 Flexible slug syntax —
owner/repo,forge:owner/repo, full HTTPS URLs, or SSH (git@…:…). - 🔍 List & fuzzy search —
forgemap listshows every local repo,forgemap list <term>filters by owner or repo name (powered by Fuse.js);cd,pathandopentake the same fuzzy terms. - 🤖 Forge-aware —
type: 'github'shells out togh;type: 'git'uses plaingit clonewith no extra dependencies. - 🔁 Mass sync + status —
forgemap syncfetches every clone in parallel,forgemap statusshows branch / dirty / ahead / behind per repo — narrow either to given owners or forges with a repeatable--filter. - 📥 Import existing trees —
forgemap import <path>adopts a folder already laid out as<server>/<owner>/<repo>, reconciles each repo against its git remote (spotting moved or deleted remotes), and derives a config. - 🧹 Safe cleanup —
forgemap cleanupdeletes long-idle clones,forgemap delete <slug>drops a single one — both only when it is clean, fully pushed, free of stashed work, and still on its remote, so nothing unbacked-up is ever lost. - 🛡️ Preflight validate —
forgemap validatechecks the config schema and required CLIs before you discover a problem mid-clone. - 🧰 Typed config —
forgemap.config.tswithdefineForgeMapConfig(), parent walk-up discovery, and a global fallback. - 🚀 Shell-friendly —
forgemap shell-init --installwires up realforgemap cd <slug>and tab-completion in one step.
Important
Needs Node 24+ and git on PATH. gh (GitHub CLI) is only required when a type: 'github' forge is configured — run forgemap validate for an exact rundown of what your config needs.
npm install -g forgemap # or: pnpm add -g forgemap
cd ~/projects # the directory that should hold all your repos
forgemap config init # write a starter forgemap.config.ts
forgemap shell-init --install # cd wrapper + completion → your rc file (idempotent)
source ~/.zshrc # re-source once, then it's automatic
forgemap clone kirchDev/laravel-pbac # any slug form works: owner/repo, forge:owner/repo, URL, SSH
forgemap cd laravel # fuzzy match → jump in; bare `forgemap cd` opens a pickerforgemap cd resolves the slug, walks/picks across your cloned repos, and actually changes directory because the shell wrapper from shell-init intercepts it before the binary runs. Every other subcommand falls through to the real binary unchanged. Hacking on forgemap itself? See CONTRIBUTING.md → Trying the CLI locally — covers pnpm setup, pnpm link --global . and the shell-wrapper source.
All commands — clone, cd, path, list, pick, open, sync/status, import, cleanup, delete, validate, shell-init, config
forgemap clone kirchDev/laravel-pbac # default forge
forgemap clone github:TitusKirch/forgemap
forgemap clone https://github.com/foo/bar
forgemap cd kirchDev/laravel-pbac # exact slug → direct cd
forgemap cd laravel # fuzzy single match → direct cd
forgemap cd kirch # multiple matches → interactive picker
forgemap cd # no arg → picker over every cloneforgemap path kirchDev/laravel-pbac # → ~/projects/comGithub/kirchDev/laravel-pbac
forgemap path laravel # fuzzy single match → same path
cd "$(forgemap path laravel)" # the manual form of `forgemap cd`
code "$(forgemap path forgemap)" # feed any tool that takes a directoryPrints where a repo lives — or would live, if it isn't cloned yet, which makes it useful for scripting a clone target. An exact owner/repo never touches the disk, so a strict slug always wins over a fuzzy match; ambiguous fuzzy terms error with the candidate list rather than guessing.
forgemap list # every repo as a pretty tree
forgemap list forgemap # pretty tree (one line per match)
forgemap list forgemap | fzf # pipe-friendly path output
forgemap pick # interactive picker (consola prompt)
forgemap pick kirch # picker pre-filtered by fuzzy queryforgemap open kirchDev/laravel-pbac # exact slug
forgemap open laravel # fuzzy single match → same folder- WSL → launches
explorer.exeagainst\\wsl$\<distro>\…, Explorer opens the folder - macOS →
open <path>(Finder) - Linux →
xdg-open <path>
forgemap sync # git fetch --all --prune, 4 in parallel
forgemap sync --pull # git pull --ff-only (skips dirty trees)
forgemap sync --forge work --query api # restrict scope
forgemap status # tree: branch / dirty / ahead↑ / behind↓ / last commit
forgemap status --format json # structured for jq + scripts
# --filter keeps only matching owners/forges. Repeatable, OR-combined.
forgemap status --format json --filter kirchDev --filter TitusKirch
forgemap status --no-cache # rescan the disk instead of using the cached tree--filter works on status, sync and list. It matches an owner or a forge name exactly (case-insensitive) — unlike --query, which is fuzzy.
status, sync and cleanup read a cached repo tree so repeated runs stay fast. Pass --no-cache to skip it and walk the disk again — the cache refreshes on its own, so you only need this right after cloning or moving repos by hand.
All tree output (status, list, import) groups as forge → owner → repo. Network operations (sync, import, cleanup) run with a hard timeout and non-interactive SSH, so an unreachable host can never wedge a run.
Already have a folder full of repos laid out as <server>/<owner>/<repo>? Adopt it without re-cloning:
forgemap import ~/projects # reconcile + derive/write forgemap.config.ts
forgemap import ~/projects --no-remote-check # offline: folder-vs-origin only (instant)
forgemap import ~/projects --fix # move folders / fix origin URLs to match the remote
forgemap import ~/projects --no-write-config # only report, don't touch the config
forgemap import ~/projects --format jsonFor each repo import compares the folder's <owner>/<repo> against the git origin, checks whether the remote still exists or was moved/renamed (GitHub via a batched gh GraphQL query, other forges via git ls-remote), and derives root + one forge per server directory. Read-only by default — --fix is the only thing that touches the filesystem.
forgemap cleanup # list deletable clones, then type "yes" to confirm
forgemap cleanup --dry-run # show candidates + why every other idle repo is kept
forgemap cleanup --days 540 # idle threshold in days (default 365)
forgemap cleanup --include-dirty --include-unpushed --include-stashed # also delete repos with local-only work (lost!)A repo is only deleted when it is idle for --days+ days (by last local commit), has a clean working tree, has nothing unpushed, has no stashed work, and its remote still exists — so everything removed is provably backed up. Repos without a remote (or with a gone/unreachable one) are never touched; empty owner directories left behind are pruned automatically. Deletion needs an explicit typed yes (or --yes).
forgemap delete kirchDev/laravel-pbac # gates, then type "yes" to confirm
forgemap delete github:foo/bar # forge-qualified slug or full URL
forgemap delete foo/bar --dry-run # report the gates; never prompt or delete
forgemap delete foo/bar --yes # non-interactive (scripts)
forgemap delete foo/bar --include-dirty --include-unpushed --include-stashed # delete local-only work too (lost!)The targeted counterpart to cleanup: same safety gates, no staleness requirement — for the repo you are done with today. It runs the identical checks (clean tree, nothing unpushed, no stashed work, remote still exists) and prints the local-only work at stake, naming the branches that carry unpushed commits and the stash count rather than just saying "unpushed". A gone or unreachable remote is a hard stop that no flag overrides — that local copy may be the only one left. After deleting, the repo is evicted from the scan cache and emptied owner directories are pruned.
forgemap validate # pretty checklist with ✓ / ! / ✗ per check
forgemap validate --json | jq # machine-readable for pre-commit hooksValidates the schema, required CLI tools (git always, gh when a type: 'github' forge is configured), gh auth status, and that the configured root exists.
forgemap shell-init --install # cd wrapper + completion → your rc file (idempotent)
forgemap completion --install # completion only, if you don't want the cd wrapper
forgemap shell-init # print the wrapper (manual: eval "$(…)")
forgemap shell-init fish | source # fish: source the wrapper directly
forgemap completion bash # print the completion script for bash/zsh/fish--install appends a marker-guarded block to the right rc file (~/.zshrc, ~/.bashrc, or ~/.config/fish/config.fish) — re-source it once and you're done. Tab-completion suggests every subcommand, and slugs for the commands that take one (cd, clone, open, …).
forgemap config init # write a starter forgemap.config.ts
forgemap config show # print the resolved config + which file it came fromforgemap config init writes a forgemap.config.ts like this:
import { defineForgeMapConfig } from 'forgemap/config';
export default defineForgeMapConfig({
root: '.',
defaultForge: 'github',
forges: {
github: {
type: 'github', // uses `gh repo clone`
host: 'github.com',
dir: 'comGithub'
},
work: {
type: 'git', // plain `git clone` — no gh needed
host: 'gitlab.acme.com',
dir: 'comGitlabAcme',
protocol: 'ssh' // optional, ssh is the default
}
}
});The config is discovered by walking up from your current directory (so forgemap cd works from inside any clone), then falling back to ~/.config/forgemap/. Override with --config <path> or the FORGEMAP_CONFIG env var.
Tip
Already have a directory full of repos? Skip writing this by hand — forgemap import <path> derives root + forges from the existing layout.
All configuration options
| Key | What it controls |
|---|---|
root |
Base directory for all clones. Relative paths resolve against the config file's directory. |
defaultForge |
Forge alias used when a slug is just owner/repo (no host or forge prefix). |
forges.<name>.type |
'github' (shells out to gh) or 'git' (plain git clone). gitlab / gitea / codeberg reserved. |
forges.<name>.host |
Hostname used to map full URLs and (for git) build the clone URL. |
forges.<name>.dir |
Subdirectory under root where this forge's clones live. |
forges.<name>.protocol |
git-type only. 'ssh' (default) or 'https'. Override per call with --ssh / --https. |
The config file is discovered by walking up from your current directory (so forgemap cd works from inside any clone, not just the root), then falling back to a global $XDG_CONFIG_HOME/forgemap/forgemap.config.* (i.e. ~/.config/forgemap/) so commands work from anywhere. Override with --config <path> or the FORGEMAP_CONFIG env var.
Repo layout & slug syntax
<root>/
└── <forge.dir>/
└── <owner>/
└── <repo>/
Example with the default config rooted at ~/projects:
~/projects/
├── forgemap.config.ts
└── comGithub/
├── kirchDev/
│ └── laravel-pbac/
└── TitusKirch/
└── forgemap/
| Form | Resolves to |
|---|---|
kirchDev/laravel-pbac |
Default forge, kirchDev/laravel-pbac. |
work:team/api |
Named forge work, team/api. |
https://github.com/foo/bar |
Host matched against forges[].host. |
https://github.com/foo/bar.git |
Same, .git suffix stripped. |
git@github.com:foo/bar.git |
SSH form, host matched against forges[].host. |
PRs welcome. Conventional Commits required (enforced via commitlint); Husky runs lint-staged on every commit. Run pnpm check:fix before pushing — CI will catch what husky missed.
Dev scripts
pnpm install
pnpm test # vitest
pnpm typecheck # tsc --noEmit
pnpm check # lint + format
pnpm bench # microbench scanRepos / cache hit / cache rebuildTune the bench layout via env vars (FORGEMAP_BENCH_FORGES, FORGEMAP_BENCH_OWNERS, FORGEMAP_BENCH_REPOS, FORGEMAP_BENCH_RUNS).
Semantic Versioning via release-please — see CHANGELOG.md.
