@errhythm/gitmux 1.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,219 @@
1
+ # gitmux πŸ”„
2
+
3
+ Multi-repo Git & GitLab workflow CLI β€” switch branches, manage epics & issues, create MRs, all from the terminal.
4
+
5
+ ```
6
+ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ•— β–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ•— β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—
7
+ β–ˆβ–ˆβ•”β•β•β•β•β• β–ˆβ–ˆβ•”β•β•β•β•β•β•šβ–ˆβ–ˆβ•— β–ˆβ–ˆβ•”β•β–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•”β•β•β•β•β•
8
+ β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•— β•šβ–ˆβ–ˆβ–ˆβ–ˆβ•”β• β–ˆβ–ˆβ•”β–ˆβ–ˆβ•— β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘
9
+ β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘β•šβ•β•β•β•β–ˆβ–ˆβ•‘ β•šβ–ˆβ–ˆβ•”β• β–ˆβ–ˆβ•‘β•šβ–ˆβ–ˆβ•—β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘
10
+ β•šβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•”β•β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘ β•šβ–ˆβ–ˆβ–ˆβ–ˆβ•‘β•šβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—
11
+ β•šβ•β•β•β•β•β• β•šβ•β•β•β•β•β•β• β•šβ•β• β•šβ•β• β•šβ•β•β•β• β•šβ•β•β•β•β•β•
12
+ ```
13
+
14
+ `gitmux` finds every `.git` repository up to 4 levels deep from your current folder and lets you act on all of them at once β€” branch switching, status checks, fetch, GitLab merge requests, and a full GitLab development portal (epics, issues, branches) β€” all from a single interactive TUI.
15
+
16
+ Made by E.R.Rhythm.
17
+
18
+ ## Features
19
+
20
+ - **Blazing Fast** β€” Parallel workers (up to CPU count) for all git operations.
21
+ - **Live UI** β€” Real-time spinners showing `main β†’ develop` transitions per repo.
22
+ - **`gitmux switch`** β€” Switch branches across all repos simultaneously with pull, stash, create, fuzzy match, and dry-run support.
23
+ - **`gitmux status`** β€” Table view of all repos: current branch, dirty file count, ahead/behind remote.
24
+ - **`gitmux fetch`** β€” Fetch all remotes across repos in parallel.
25
+ - **`gitmux mr`** β€” Interactively create GitLab merge requests via `glab` CLI for one or multiple repos at once.
26
+ - **`gitmux portal`** β€” GitLab Development Portal: browse assigned Epics, view & create Issues per project, create branches, and checkout primary branches across repos.
27
+ - **`gitmux settings`** β€” Configure branch suggestion templates, MR defaults, and portal defaults interactively.
28
+ - **Branch suggestions** β€” Configure interactive switch presets like `sprint/{yyyy}-{mm}-W{w}` in settings.
29
+ - **Auto-stash** β€” Stash dirty repos before switching, pop after (`--stash`).
30
+ - **Create branch** β€” Create the branch if it doesn't exist (`--create`).
31
+ - **Fuzzy matching** β€” Partial branch name resolution with interactive picker for ambiguous matches.
32
+ - **Dry-run mode** β€” Preview what would happen without touching anything (`--dry-run`).
33
+ - **Smart skipping** β€” Repos without the target branch are skipped cleanly.
34
+ - **Exclude / filter** β€” Skip or include repos matching a name pattern.
35
+ - **Color-coded branches** β€” `main/master` red, `feature/` cyan, `hotfix/` orange, `develop/` purple.
36
+ - **Scriptable** β€” Pass flags directly for use in CI/bash pipelines.
37
+
38
+ ## Installation
39
+
40
+ ```bash
41
+ npm install -g @rhythm/gitmux
42
+ ```
43
+
44
+ *(Requires Node.js 18+)*
45
+
46
+ ## Usage
47
+
48
+ ### Interactive mode
49
+
50
+ ```bash
51
+ gitmux
52
+ ```
53
+
54
+ Launches a mode selector: **Switch branches** or **GitLab** (portal + MRs).
55
+
56
+ If configured, interactive switch mode first shows computed branch suggestions from `~/.config/gitmux/gitmux.json`, then offers `Custom branch...` if you want to type something else.
57
+
58
+ ### Switch branches
59
+
60
+ ```bash
61
+ gitmux develop
62
+ gitmux main --pull
63
+ gitmux feature/auth --stash --pull
64
+ gitmux feat --fuzzy
65
+ gitmux experiment --create
66
+ ```
67
+
68
+ ### Show repo status
69
+
70
+ ```bash
71
+ gitmux status
72
+ ```
73
+
74
+ Displays a table of every repo with its current branch, dirty file count, and ahead/behind remote sync status.
75
+
76
+ ### Fetch all remotes
77
+
78
+ ```bash
79
+ gitmux fetch
80
+ ```
81
+
82
+ Runs `git fetch --all --prune` across all repos in parallel and shows ahead/behind per repo.
83
+
84
+ ### Create merge requests (GitLab)
85
+
86
+ ```bash
87
+ gitmux mr
88
+ ```
89
+
90
+ Requires [`glab`](https://gitlab.com/gitlab-org/cli#installation) to be installed and authenticated.
91
+
92
+ Shows all repos with their current branch. Select one or more repos, fill in the details once (title, description, target branch, labels, draft, push first). `gitmux` builds the `glab mr create` command for each repo and runs them, printing each MR URL on completion.
93
+
94
+ Last-used MR settings (target branch, labels, draft mode, push preference, scope) are remembered between runs.
95
+
96
+ ### GitLab Development Portal
97
+
98
+ ```bash
99
+ gitmux portal
100
+ gitmux portal --settings
101
+ ```
102
+
103
+ Requires [`glab`](https://gitlab.com/gitlab-org/cli#installation) installed and authenticated.
104
+
105
+ Opens an interactive TUI that:
106
+
107
+ 1. **Auto-detects your GitLab group** from local repo remote URLs (saved to config on first run).
108
+ 2. **Shows Epics assigned to you** β€” searchable list from the GitLab API.
109
+ 3. **Browse issues** under each epic β€” view branches, set a primary branch per issue.
110
+ 4. **Checkout primary branches** β€” switch all matching local repos to their primary branches in one step.
111
+ 5. **Create an Issue** β€” default title is `Epic Name - Project Name`, pre-fills configured milestone, iteration, and labels.
112
+ 6. **Create a Branch** β€” immediately after issue creation, with a default name of `feature/{iid}-{slug}` cut from the configured base branch.
113
+
114
+ Run with `--settings` to configure portal defaults (see Settings below).
115
+
116
+ ### Settings
117
+
118
+ ```bash
119
+ gitmux settings
120
+ ```
121
+
122
+ Opens an interactive settings menu with three sections:
123
+
124
+ | Section | What you can configure |
125
+ |---------|----------------------|
126
+ | **Portal** | GitLab group path, epic filter label, default milestone, default iteration, default issue labels, base branch |
127
+ | **Switch** | Branch suggestion templates (with date tokens) |
128
+ | **Merge Requests** | Default labels, draft mode, push-before-MR |
129
+
130
+ ### Dry-run
131
+
132
+ ```bash
133
+ gitmux --dry-run develop
134
+ ```
135
+
136
+ Preview what would happen without making any changes.
137
+
138
+ ## All flags
139
+
140
+ | Flag | Short | Description |
141
+ |------|-------|-------------|
142
+ | `--pull` | `-p` | Pull latest on the target branch after switching |
143
+ | `--fuzzy` | `-f` | Partial branch name matching |
144
+ | `--create` | `-c` | Create the branch if it doesn't exist |
145
+ | `--stash` | `-s` | Auto-stash dirty repos before switching, pop after |
146
+ | `--fetch` | | Fetch all remotes before switching |
147
+ | `--dry-run` | | Preview actions without executing |
148
+ | `--depth n` | | Repo search depth (default: `4`) |
149
+ | `--exclude p` | | Exclude repos whose name contains pattern `p` |
150
+ | `--filter p` | | Only include repos whose name contains pattern `p` |
151
+ | `--settings` | | Open portal settings (use with `gitmux portal`) |
152
+ | `--version` | `-v` | Show version number |
153
+ | `--help` | `-h` | Show help |
154
+
155
+ ## Requirements for `gitmux mr` and `gitmux portal`
156
+
157
+ - [`glab`](https://gitlab.com/gitlab-org/cli#installation) installed and on `$PATH`
158
+ - `glab auth login` completed for your GitLab instance
159
+
160
+ ## Configuration
161
+
162
+ `gitmux` reads optional settings from `~/.config/gitmux/gitmux.json`.
163
+
164
+ Example:
165
+
166
+ ```json
167
+ {
168
+ "switch": {
169
+ "branchSuggestions": [
170
+ "sprint/{yyyy}-{mm}-W{w}",
171
+ "release/{yyyy}-{mm}",
172
+ "hotfix/{yyyy}-{mm}-{dd}",
173
+ "main"
174
+ ]
175
+ },
176
+ "mr": {
177
+ "scope": "multi",
178
+ "targetBranch": "develop",
179
+ "labels": "backend,sprint",
180
+ "isDraft": false,
181
+ "pushFirst": true
182
+ },
183
+ "portal": {
184
+ "group": "company/subgroup",
185
+ "epicLabelFilter": "TECH::BACKEND",
186
+ "defaultMilestone": { "id": 42, "title": "Sprint 5" },
187
+ "defaultIteration": { "id": "__current__", "title": "Current iteration" },
188
+ "defaultLabels": "backend",
189
+ "defaultBaseBranch": "develop"
190
+ }
191
+ }
192
+ ```
193
+
194
+ ### Switch template tokens
195
+
196
+ | Token | Description | Example |
197
+ |-------|-------------|---------|
198
+ | `{yyyy}` | 4-digit year | `2026` |
199
+ | `{yy}` | 2-digit year | `26` |
200
+ | `{mm}` | Zero-padded month | `03` |
201
+ | `{m}` | Month | `3` |
202
+ | `{dd}` | Zero-padded day | `07` |
203
+ | `{d}` | Day | `7` |
204
+ | `{q}` | Quarter | `1` |
205
+ | `{w}` | Week of month | `1` |
206
+ | `{ww}` | ISO week of year | `09` |
207
+
208
+ ## How it works
209
+
210
+ 1. Searches for `.git` folders from the current directory up to `--depth` levels deep.
211
+ 2. Applies any `--exclude` / `--filter`, then prints a session info box.
212
+ 3. In fuzzy mode, resolves partial branch names per repo (interactive picker for ambiguity).
213
+ 4. Runs all git operations concurrently using parallel workers.
214
+ 5. Each task shows live `current β†’ target` branch transitions with timing.
215
+ 6. Prints a clean summary: done / pulled / stashed / skipped / failed.
216
+
217
+ ---
218
+
219
+ **License**: MIT
package/bin/gitmux.js ADDED
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { main } from "../src/main.js";
4
+
5
+ main()
6
+ .then((code) => process.exit(code ?? 0))
7
+ .catch((err) => {
8
+ process.stderr.write((err.message || String(err)) + "\n");
9
+ process.exit(1);
10
+ });
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@errhythm/gitmux",
3
+ "version": "1.6.2",
4
+ "description": "Multi-repo Git & GitLab workflow CLI β€” switch branches, manage epics & issues, create MRs, all from the terminal",
5
+ "type": "module",
6
+ "bin": {
7
+ "gitmux": "bin/gitmux.js",
8
+ "gmux": "bin/gitmux.js"
9
+ },
10
+ "files": [
11
+ "bin",
12
+ "src"
13
+ ],
14
+ "engines": {
15
+ "node": ">=18"
16
+ },
17
+ "keywords": [
18
+ "git",
19
+ "gitlab",
20
+ "branch",
21
+ "multi-repo",
22
+ "polyrepo",
23
+ "monorepo",
24
+ "merge-request",
25
+ "cli",
26
+ "developer-tools",
27
+ "gitlab-cli"
28
+ ],
29
+ "author": "rhythm",
30
+ "license": "MIT",
31
+ "main": "src/main.js",
32
+ "scripts": {
33
+ "test": "echo \"Error: no test specified\" && exit 1"
34
+ },
35
+ "dependencies": {
36
+ "@inquirer/prompts": "^8.3.0",
37
+ "boxen": "^8.0.1",
38
+ "chalk": "^5.6.2",
39
+ "enquirer": "^2.4.1",
40
+ "figlet": "^1.10.0",
41
+ "listr2": "^10.2.1"
42
+ }
43
+ }
@@ -0,0 +1,76 @@
1
+ import { basename } from "path";
2
+ import chalk from "chalk";
3
+ import boxen from "boxen";
4
+ import { Listr } from "listr2";
5
+
6
+ import { getAheadBehind } from "../git/core.js";
7
+ import { execAsync, extractMsg } from "../utils/exec.js";
8
+ import { MAX_JOBS } from "../constants.js";
9
+ import { p } from "../ui/theme.js";
10
+
11
+ export async function cmdFetch(repos) {
12
+ const fetchResults = [];
13
+ const padWidth = String(repos.length).length;
14
+
15
+ const tasks = new Listr(
16
+ repos.map((repo, idx) => {
17
+ const name = basename(repo);
18
+ const idx_ = p.muted(`[${String(idx + 1).padStart(padWidth)}/${repos.length}]`);
19
+ return {
20
+ title: idx_ + " " + chalk.bold(p.white(name)) + p.muted(" fetching…"),
21
+ task: async (_, task) => {
22
+ const t0 = Date.now();
23
+ try {
24
+ await execAsync("git fetch --all --prune", { cwd: repo });
25
+ const { ahead, behind } = await getAheadBehind(repo);
26
+ const elapsed = ((Date.now() - t0) / 1000).toFixed(1);
27
+ fetchResults.push({ name, ok: true, ahead, behind, elapsed });
28
+
29
+ const syncParts = [];
30
+ if (ahead > 0) syncParts.push(p.cyan(`↑${ahead}`));
31
+ if (behind > 0) syncParts.push(p.red(`↓${behind}`));
32
+ const syncStr = syncParts.length ? " " + syncParts.join(" ") : "";
33
+
34
+ task.title =
35
+ idx_ + " " + chalk.bold(p.white(name)) +
36
+ " " + p.green("βœ”") +
37
+ " " + p.muted(elapsed + "s") + syncStr;
38
+ } catch (e) {
39
+ const msg = extractMsg(e);
40
+ fetchResults.push({ name, ok: false, msg });
41
+ task.title =
42
+ idx_ + " " + chalk.bold(p.white(name)) +
43
+ " " + p.red("✘") +
44
+ " " + p.muted(msg.slice(0, 55));
45
+ throw new Error(msg);
46
+ }
47
+ },
48
+ };
49
+ }),
50
+ { concurrent: MAX_JOBS, exitOnError: false },
51
+ );
52
+
53
+ await tasks.run().catch(() => {});
54
+
55
+ const failed = fetchResults.filter((r) => !r.ok).length;
56
+ const avgMs = fetchResults
57
+ .filter((r) => r.ok && r.elapsed)
58
+ .reduce((s, r) => s + parseFloat(r.elapsed), 0) /
59
+ (fetchResults.filter((r) => r.ok).length || 1);
60
+
61
+ console.log();
62
+ console.log(
63
+ boxen(
64
+ (failed === 0
65
+ ? chalk.bold(p.green(`βœ” ${repos.length} repos fetched`))
66
+ : p.green(`βœ” ${repos.length - failed} fetched`) + p.slate(" Β· ") + p.red(`✘ ${failed} failed`)) +
67
+ "\n" + p.muted(`avg ${avgMs.toFixed(1)}s/repo`),
68
+ {
69
+ padding: { top: 0, bottom: 0, left: 2, right: 2 },
70
+ borderStyle: "round",
71
+ borderColor: failed > 0 ? "#f87171" : "#4ade80",
72
+ },
73
+ ),
74
+ );
75
+ console.log();
76
+ }