A lightweight CLI for consistent Git branching workflows: long-lived main (production) and dev (integration), plus short-lived workflow branches with clear merge targets. Built for Bun and TypeScript; scriptable and safe by default—no history rewriting, predictable exit codes, and optional interactive pickers only when running in a TTY.
Author: Ali AlNaghmoush · Repository: github.com/alialnaghmoush/gflows
Get started
Understand
Reference
Configure & operate
More
- Bun ≥ 1.0 (recommended). The CLI runs TypeScript directly.
- Git for repository operations.
Check versions:
bun --version
git --versionDev dependency (recommended): bun add --dev gflows or npm install --save-dev gflows.
JSR: npx jsr add --dev @alialnaghmoush/gflows or deno add --dev jsr:@alialnaghmoush/gflows.
Run with bunx gflows ... or npx gflows .... npm · JSR
Global: bun add --global gflows or npm install --global gflows
1. One-time setup — In your repo, ensure main exists and create dev:
bun gflows initUse --no-push to skip push, --dry-run to preview. Pass --main, --dev, --remote to persist to .gflows.json.
bun gflows init --main main --dev develop --remote origin2. Daily development (feature → dev):
bun gflows start feature add-login
# ... code, commit ...
bun gflows finish feature
bun gflows finish feature --push
bun gflows finish feature --push -D3. Release (dev → main, then tag):
bun gflows bump up minor
bun gflows start release v1.3.0
# ... update CHANGELOG, commit ...
bun gflows finish release --push4. Hotfix (main → fix → main + dev):
bun gflows start hotfix v1.3.1
# ... fix, commit ...
bun gflows finish hotfix --push- main — Production branch. Only release and hotfix merge here.
- dev — Integration branch. Feature, bugfix, chore, spike merge here. Created by
gflows initfrom main. - Workflow branches — Short-lived branches with a type prefix; each has a base and merge target(s). No history rewriting.
- Merge targets: feature/chore/spike → dev; bugfix → dev (or main with
-o main); release/hotfix → main then dev + tag.
Override names and prefixes via configuration.
flowchart LR
subgraph setup["One-time setup"]
A[main exists] --> B["bun gflows init"]
B --> C[dev created]
C --> D[optional: push dev]
end
subgraph daily["Daily workflow"]
E["bun gflows start<br/>type name"] --> F[work & commit]
F --> G["bun gflows finish<br/>type"]
G --> H{type?}
H -->|feature/bugfix/chore/spike| I[merged to dev]
H -->|release/hotfix| J[merged to main → dev + tag]
end
setup --> daily
flowchart TD
Start([bun gflows init]) --> Repo{In git repo?}
Repo -->|no| ErrRepo[Exit: Not a repo]
Repo -->|yes| MainExists{main branch exists?}
MainExists -->|no| ErrMain[Exit: Create main first]
MainExists -->|yes| DevExists{dev branch exists?}
DevExists -->|yes| DoneNoOp[Done: nothing to do]
DevExists -->|no| CreateDev[Create dev from main]
CreateDev --> Push{--no-push?}
Push -->|no| DoPush[Push dev to remote]
Push -->|yes| SkipPush[Skip push]
DoPush --> Done[Done]
SkipPush --> Done
DoneNoOp --> Done
flowchart TD
Start([bun gflows start type name]) --> Repo{In git repo?}
Repo -->|no| ErrRepo[Exit: Not a repo]
Repo -->|yes| Clean{Tree clean or --force?}
Clean -->|no| ErrDirty[Exit: Uncommitted changes]
Clean -->|yes| Base[Resolve base branch for type]
Base --> BaseExists{Base exists locally or remote?}
BaseExists -->|no| ErrBase[Exit: Base not found]
BaseExists -->|yes| Create[Create workflow branch from base]
Create --> PushOpt{--push?}
PushOpt -->|yes| Push[Push new branch to remote]
PushOpt -->|no| Done[Done]
Push --> Done
flowchart TD
Start([bun gflows finish type]) --> Resolve[Resolve branch to finish]
Resolve --> T{type}
T -->|feature / chore / spike| M1[Merge → dev]
T -->|bugfix from dev| M1
T -->|bugfix from main| M2[Merge → main]
T -->|release| M3[Merge → main]
M3 --> M3b[Merge main → dev]
M3b --> Tag1[Create tag]
T -->|hotfix| M4[Merge → main]
M4 --> M4b[Merge main → dev]
M4b --> Tag2[Create tag]
M2 --> M2b[Merge main → dev]
M1 --> Opt[Optional: delete branch, push]
M2b --> Opt
Tag1 --> Opt
Tag2 --> Opt
Opt --> Done([Done])
flowchart TB
subgraph longlived["Long-lived branches"]
main[main]
dev[dev]
end
subgraph workflow["Workflow branches"]
feature[feature/...]
bugfix[bugfix/...]
chore[chore/...]
release[release/...]
hotfix[hotfix/...]
spike[spike/...]
end
feature --> dev
chore --> dev
spike --> dev
bugfix --> dev
bugfix -.->|"-o main"| main
release --> main
main --> dev
release -.-> dev
hotfix --> main
main --> dev
hotfix -.-> dev
flowchart LR
Start([bun gflows bump direction type]) --> Discover[Discover package.json, jsr.json]
Discover --> Read[Read version from primary root]
Read --> Calc[Compute new version]
Calc --> Write[Write to all roots]
Write --> Done([Done])
| Type | Short | Base (default) | With -o main |
Merge target(s) | Tag |
|---|---|---|---|---|---|
| feature | -f |
dev | — | dev | no |
| bugfix | -b |
dev | main | dev (or main if from main) | no |
| chore | -c |
dev | — | dev | no |
| release | -r |
dev | — | main, then dev | yes |
| hotfix | -x |
main | — | main, then dev | yes |
| spike | -e |
dev | — | dev | no |
Release and hotfix names must be a version (vX.Y.Z or X.Y.Z). Branch names use default prefixes (e.g. feature/add-login); override in configuration. Invalid names (e.g. .., *, spaces) → exit 1.
| Command | Short | Description |
|---|---|---|
init |
-I |
Ensure main exists; create dev from main. |
start |
-S |
Create a workflow branch (requires type + name). |
finish |
-F |
Merge branch into target(s), optional tag (release/hotfix), delete, push. |
switch |
-W |
Switch to a workflow branch (picker or name); with uncommitted changes: prompt or --move / --restore / --clean / --destroy / --cancel. |
delete |
-L |
Delete local workflow branch(es). Never main/dev. |
list |
-l |
List workflow branches; optional type filter and remote. |
bump |
— | Bump or rollback package version (patch/minor/major). |
completion |
— | Print shell completion script (bash/zsh/fish). |
status |
-t |
Show current branch, type, base, merge target(s), ahead/behind. |
help |
-h |
Show usage and quick reference. |
version |
-V |
Show version. |
Common flags:
| Flag | Short | Description |
|---|---|---|
--path <dir> |
-C |
Run as if in <dir>. |
--dry-run |
-d |
Log intended actions only; no writes. |
--verbose |
-v |
Verbose output. |
--quiet |
-q |
Minimal output. |
--push |
-p |
Push after init/start/finish. |
--no-push |
-P |
Do not push. |
--main <name> |
— | Main branch override. |
--dev <name> |
— | Dev branch override. |
--remote <name> |
-R |
Remote for push. |
--from <branch> |
-o |
Base branch override (start). |
--branch <name> |
-B |
Branch name (finish). |
--yes |
-y |
Skip confirmations. |
Ensures main exists; creates dev from main if missing. Use --main, --dev, -R to persist to .gflows.json.
Examples:
bun gflows init
bun gflows init --main main --dev develop --remote origin
bun gflows init -C ../other-repo --dry-runFlags:
| Flag | Short | Description |
|---|---|---|
--push |
-p |
Push dev to remote after creating (default). |
--main <name> |
— | Main branch name (persisted to .gflows.json when provided). |
--dev <name> |
— | Dev branch name (persisted to .gflows.json when provided). |
--remote <name> |
-R |
Remote name (persisted to .gflows.json when provided). |
--path <dir> |
-C |
Run as if in <dir>. |
--dry-run |
-d |
Log intended actions only; no writes. |
--verbose |
-v |
Verbose output. |
--quiet |
-q |
Minimal output. |
Creates a workflow branch from the correct base. Requires type + name; release/hotfix name must be a version. Pre-checks: git repo, clean tree (or --force), base exists.
Examples:
bun gflows start feature auth-refactor
bun gflows start bugfix fix-login --from main
bun gflows start release v2.0.0
bun gflows start hotfix 1.2.1
bun gflows start feature wip --force --pushFlags:
| Flag | Short | Description |
|---|---|---|
--force |
— | Allow dirty working tree. |
--push |
-p |
Push new branch to remote after creating. |
--from <branch> |
-o |
Base branch override (e.g. -o main for bugfix). |
--remote <name> |
-R |
Remote for push. |
--path <dir> |
-C |
Run as if in <dir>. |
--dry-run |
-d |
Log intended actions only; no writes. |
--verbose |
-v |
Verbose output. |
--quiet |
-q |
Minimal output. |
Merges the branch (current or -B) into its target(s). Release/hotfix: merge to main, then main → dev, create tag. Use --no-ff for a merge commit. On conflict, resolve then git merge --continue or re-run finish.
Examples:
bun gflows finish feature
bun gflows finish feature -B feature/auth --push -D
bun gflows finish release --push
bun gflows finish hotfix -s -T -yFlags:
| Flag | Short | Description |
|---|---|---|
--branch <name> |
-B |
Branch to finish (current branch if omitted; picker in TTY when -B with no value). |
--no-ff |
— | Always create a merge commit. |
--delete |
-D |
Delete branch after finish. |
--no-delete |
-N |
Do not delete branch after finish. |
--push |
-p |
Push after merge (finish prompts "Do you want to push?" when neither -p nor -P). |
--no-push |
-P |
Do not push. |
--sign |
-s |
Sign the tag (release/hotfix; GPG). |
--no-tag |
-T |
Do not create tag (release/hotfix). |
--tag-message <msg> |
-M |
Tag message. |
--message <msg> |
-m |
Merge message. |
--yes |
-y |
Skip confirmations (e.g. "Delete branch after finish?"). |
--path <dir> |
-C |
Run as if in <dir>. |
--dry-run |
-d |
Log intended actions only; no writes. |
--verbose |
-v |
Verbose output. |
--quiet |
-q |
Minimal output. |
Switch to a workflow branch. With TTY and no branch name, shows a picker; otherwise pass the branch name (e.g. gflows switch dev or -B dev).
Uncommitted changes: If the working tree is dirty and stdin is a TTY, you are prompted to choose:
| Option | Description |
|---|---|
| Move | Move current changes to the target branch. |
| Restore | Save changes for this branch; restore target's saved state (if any). |
| Clean | Discard changes and switch clean at HEAD. |
| Destroy | Delete current branch and switch to the target branch (cannot destroy main or dev). |
| Cancel | Abort switching. |
You can skip the prompt by passing exactly one of the flags below. If the target branch has saved changes and you use Clean, a warning is shown (unless -q).
Examples:
bun gflows switch
bun gflows switch feature/auth-refactor
bun gflows switch dev --restore
bun gflows switch main --clean
bun gflows switch dev --destroy
bun gflows -W feature/auth-refactorFlags:
| Flag | Short | Description |
|---|---|---|
--path <dir> |
-C |
Run as if in <dir>. |
--branch <name> |
-B |
Branch to switch to (alternative to positional). |
--move |
— | Move current changes to the target branch; no prompt. |
--restore |
— | Save for this branch; restore target's saved state (if any); no prompt. |
--clean |
— | Discard changes and switch clean at HEAD; no prompt. |
--destroy |
— | Delete current branch and switch to target; no prompt (not main/dev). |
--cancel |
— | Abort switching; no prompt. |
--verbose |
-v |
Verbose output. |
--quiet |
-q |
Minimal output (suppresses Clean warning about saved changes on target). |
Delete local workflow branch(es). Never deletes main/dev. TTY: picker; otherwise pass branch name(s).
Examples:
bun gflows delete
bun gflows delete feature/old-spike
bun gflows delete feature/one feature/twoFlags:
| Flag | Short | Description |
|---|---|---|
--path <dir> |
-C |
Run as if in <dir>. |
--verbose |
-v |
Verbose output. |
--quiet |
-q |
Minimal output. |
List workflow branches (one per line). Filter by type; use -r to include remote (may run git fetch).
Examples:
bun gflows list
bun gflows list feature
bun gflows list -rFlags:
| Flag | Short | Description |
|---|---|---|
--include-remote |
-r |
Include remote-tracking branches (may run git fetch). |
--path <dir> |
-C |
Run as if in <dir>. |
--dry-run |
-d |
Log intended actions only. |
--verbose |
-v |
Verbose output. |
--quiet |
-q |
Minimal output. |
Bump or rollback version in package.json and jsr.json (monorepo: all discovered roots). No git operations. Direction: up|down; type: patch|minor|major. TTY: interactive; non-TTY: both required.
Examples:
bun gflows bump up patch
bun gflows bump down minor
bun gflows bump --dry-runFlags:
| Flag | Short | Description |
|---|---|---|
--dry-run |
-d |
Print old → new version only; no file writes. |
--path <dir> |
-C |
Run as if in <dir>. |
--verbose |
-v |
Verbose output. |
--quiet |
-q |
Minimal output. |
Shows current branch, type, base, merge target(s), ahead/behind. Read-only.
Examples:
bun gflows status
bun gflows -tFlags:
| Flag | Short | Description |
|---|---|---|
--path <dir> |
-C |
Run as if in <dir>. |
--verbose |
-v |
Verbose output. |
--quiet |
-q |
Minimal output. |
Prints completion script for bash/zsh/fish. See Shell completion.
bun gflows help · bun gflows -V
Optional. Override main, dev, remote, prefixes. Resolution: defaults → .gflows.json or package.json "gflows" key → CLI flags. Invalid config is ignored (-v for warning).
{
"main": "main",
"dev": "dev",
"remote": "origin",
"prefixes": {
"feature": "feature/",
"bugfix": "bugfix/",
"chore": "chore/",
"release": "release/",
"hotfix": "hotfix/",
"spike": "spike/"
}
}{ "main": "master", "dev": "develop" }Or in package.json: "gflows": { "main": "main", "dev": "development", "remote": "upstream" }
Non-TTY: no pickers; pass branch names explicitly. Use -y to skip confirmations. Exit codes: 0 success, 1 validation, 2 Git/state. Use -C <dir> to run in another directory.
bun gflows finish feature -B feature/add-login --push -y
bun gflows list feature | while read -r b; do echo "$b"; done
bun gflows -C ./packages/api list| Code | Meaning | Typical causes |
|---|---|---|
| 0 | Success | Command completed without error. |
| 1 | Usage / validation | Missing type or name for start; invalid branch name or version; wrong/missing positionals when not TTY. |
| 2 | Git / system | Not a repo; branch not found; dirty tree; merge conflict; rebase/merge in progress; detached HEAD; finish on main/dev; tag exists; push failed. |
| Situation | What to do |
|---|---|
| "Not a Git repository" | Run from a directory that contains .git, or use -C <path> to point to the repo root. |
| "Working tree has uncommitted changes" | Commit or stash changes before start, or use --force (only when you intend to carry uncommitted work). |
| "Merge conflict while merging into …" | Resolve conflicts in your working tree, then run git add and git merge --continue (or git merge --abort to cancel). Re-run gflows finish after resolving if needed. |
| "Tag v1.2.3 already exists" | Use a new version for the release/hotfix, or delete/move the tag if you know what you’re doing. gflows does not overwrite tags. |
| "Cannot finish the long-lived branch main/dev" | You’re on main or dev. Checkout a workflow branch first, or use -B <branch> to finish another branch. |
| "HEAD is detached" | Checkout a branch (e.g. git checkout dev) before running start or finish. |
| "A rebase or merge is in progress" | Run git rebase --abort or git merge --abort, or complete the operation, then retry gflows. |
| Picker not showing / "requires branch name" | Without a TTY, gflows does not show interactive pickers. Pass the branch name explicitly (e.g. -B feature/xyz or gflows switch feature/xyz). |
| Wrong remote or branch names | Use .gflows.json or package.json "gflows" key, or gflows init --main … --dev … --remote …. Use -R for one-off remote override. |
Use -v for verbose git commands and diagnostics.
Bash:
source <(bun gflows completion bash)
echo 'source <(bun gflows completion bash)' >> ~/.bashrcZsh:
source <(bun gflows completion zsh)
echo 'source <(bun gflows completion zsh)' >> ~/.zshrcFish:
bun gflows completion fish | source
bun gflows completion fish > ~/.config/fish/completions/gflows.fishInternal script scripts/publish.ts: syncs version from package.json to jsr.json, then publishes to npm and/or JSR. Not in published package.
bun run publish:all
bun run publish:all -- --dry-run
bun run publish:npm
bun run publish:jsr
bun run publish:all -- --force- Ensure you’re on main with a clean working tree (or use
--forcewhen intentional). - Bump version and tag in Git yourself, or use gflows bump + your own commit:
gflows bump up minor --dry-run # confirm
gflows bump up minor
git add package.json jsr.json && git commit -m "chore: bump to 1.4.0"- Run the publish script:
bun run publish:all -- --dry-run # verify
bun run publish:all- Optionally push main and tags:
git push origin main --tagsVersion sync: The script reads version from package.json and writes it to jsr.json before publishing so the two registries never drift. Use **gflows bump** to change the version; the script does not bump for you.
To get a 100% score on JSR:
- Description — Set in
jsr.json(already added). If the score still shows 0/1, set the description in package settings on JSR. - Runtime compatibility — In package settings, open “Runtime compatibility” and mark at least Bun and Node.js (or others) as Supported.
- Provenance — The repo includes
[.github/workflows/publish.yml](.github/workflows/publish.yml)(test, lint, then publish to npm and JSR). In JSR package settings, link the package to this GitHub repository. AddNPM_TOKENin repo Secrets for npm. After that, pushes tomainrun CI and publish both registries; JSR records provenance.
See LICENSE in this repository.