@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/src/main.js ADDED
@@ -0,0 +1,418 @@
1
+ import { basename } from "path";
2
+ import { execSync } from "child_process";
3
+ import chalk from "chalk";
4
+ import boxen from "boxen";
5
+ import { input, confirm, select } from "@inquirer/prompts";
6
+
7
+ import { VERSION, DEFAULT_DEPTH, SUBCOMMANDS } from "./constants.js";
8
+ import { findRepos } from "./git/core.js";
9
+ import { getSwitchBranchSuggestions } from "./git/templates.js";
10
+ import { loadConfig, saveConfig } from "./config/index.js";
11
+ import { printLogo, printHelp, printAbout } from "./ui/print.js";
12
+ import { p, THEME } from "./ui/theme.js";
13
+ import { colorBranch } from "./ui/colors.js";
14
+ import { cmdStatus } from "./commands/status.js";
15
+ import { cmdFetch } from "./commands/fetch.js";
16
+ import { cmdSwitch } from "./commands/switch.js";
17
+ import { cmdMr } from "./commands/mr.js";
18
+ import { cmdPortal } from "./commands/portal.js";
19
+ import { cmdSettings } from "./commands/settings.js";
20
+
21
+ // ── Dependency checks ─────────────────────────────────────────────────────────
22
+
23
+ function checkRequiredDeps() {
24
+ // git — required by every command
25
+ try { execSync("git --version", { stdio: "pipe" }); } catch {
26
+ const isMac = process.platform === "darwin";
27
+ const isWin = process.platform === "win32";
28
+ const isLinux = process.platform === "linux";
29
+
30
+ const installLines = isMac
31
+ ? p.muted(" brew ") + p.cyan("brew install git") + "\n" +
32
+ p.muted(" direct ") + p.cyan("https://git-scm.com/download/mac")
33
+ : isWin
34
+ ? p.muted(" winget ") + p.cyan("winget install --id Git.Git") + "\n" +
35
+ p.muted(" direct ") + p.cyan("https://git-scm.com/download/win")
36
+ : isLinux
37
+ ? p.muted(" apt ") + p.cyan("sudo apt install git") + "\n" +
38
+ p.muted(" dnf ") + p.cyan("sudo dnf install git") + "\n" +
39
+ p.muted(" pacman ") + p.cyan("sudo pacman -S git")
40
+ : p.muted(" ") + p.cyan("https://git-scm.com/downloads");
41
+
42
+ console.log(
43
+ boxen(
44
+ chalk.bold(p.red("git not found")) + "\n\n" +
45
+ p.white("gitmux requires git. Install it:\n\n") +
46
+ installLines,
47
+ {
48
+ padding: { top: 1, bottom: 1, left: 3, right: 3 },
49
+ borderStyle: "round",
50
+ borderColor: "#f87171",
51
+ title: p.red(" missing dependency: git "),
52
+ titleAlignment: "center",
53
+ },
54
+ ),
55
+ );
56
+ process.exit(1);
57
+ }
58
+ }
59
+
60
+ // ── Argument parser ───────────────────────────────────────────────────────────
61
+
62
+ // Options that take a value argument
63
+ const VALUE_OPTIONS = new Set([
64
+ "--depth", "--exclude", "--filter",
65
+ // mr
66
+ "--target", "-t", "--repo", "--title", "--description", "--labels",
67
+ // portal
68
+ "--epic", "--issue-project", "--issue-title", "--issue-description",
69
+ "--issue-labels", "--branch-name", "--base-branch",
70
+ ]);
71
+
72
+ function parseArgs(rawArgs) {
73
+ const flags = new Set();
74
+ const options = {};
75
+ const positional = [];
76
+ // Multi-value: --repo can appear multiple times
77
+ const repos = [];
78
+
79
+ for (let i = 0; i < rawArgs.length; i++) {
80
+ const arg = rawArgs[i];
81
+
82
+ if (VALUE_OPTIONS.has(arg) && rawArgs[i + 1] !== undefined) {
83
+ const key = arg.replace(/^-+/, "").replace(/-([a-z])/g, (_, c) => c.toUpperCase());
84
+ if (arg === "--repo") {
85
+ repos.push(rawArgs[++i]);
86
+ } else {
87
+ options[key] = rawArgs[++i];
88
+ }
89
+ } else if (arg.includes("=") && arg.startsWith("--")) {
90
+ const eqIdx = arg.indexOf("=");
91
+ const key = arg.slice(2, eqIdx).replace(/-([a-z])/g, (_, c) => c.toUpperCase());
92
+ options[key] = arg.slice(eqIdx + 1);
93
+ } else if (!arg.startsWith("--") && arg.startsWith("-") && arg.length > 2 && arg !== "-t") {
94
+ for (const c of arg.slice(1)) flags.add(`-${c}`);
95
+ } else if (arg.startsWith("-")) {
96
+ flags.add(arg);
97
+ } else {
98
+ positional.push(arg);
99
+ }
100
+ }
101
+
102
+ const first = positional[0] ?? "";
103
+ const isSubcmd = SUBCOMMANDS.has(first);
104
+
105
+ return {
106
+ // ── Core ────────────────────────────────────────────────────────────────
107
+ subcommand: isSubcmd ? first : null,
108
+ branch: isSubcmd ? (positional[1] ?? "") : first,
109
+
110
+ // ── Switch flags ────────────────────────────────────────────────────────
111
+ pull: flags.has("--pull") || flags.has("-p"),
112
+ fuzzy: flags.has("--fuzzy") || flags.has("-f"),
113
+ create: flags.has("--create") || flags.has("-c"),
114
+ stash: flags.has("--stash") || flags.has("-s"),
115
+ fetch: flags.has("--fetch"),
116
+ dryRun: flags.has("--dry-run"),
117
+
118
+ // ── Global ──────────────────────────────────────────────────────────────
119
+ version: flags.has("--version") || flags.has("-v"),
120
+ help: flags.has("--help") || flags.has("-h"),
121
+ settings: flags.has("--settings"),
122
+ debug: flags.has("--debug"),
123
+ yes: flags.has("--yes") || flags.has("-y"),
124
+ depth: parseInt(options.depth ?? String(DEFAULT_DEPTH), 10) || DEFAULT_DEPTH,
125
+ exclude: options.exclude ?? null,
126
+ filter: options.filter ?? null,
127
+
128
+ // ── MR flags ─────────────────────────────────────────────────────────────
129
+ // --target / -t <branch> — MR target branch
130
+ target: options.target ?? options.t ?? null,
131
+ // --repo <name> (repeatable) — restrict MR to these repos
132
+ mrRepos: repos.length > 0 ? repos : null,
133
+ // --title / --description / --labels — MR or issue metadata
134
+ title: options.title ?? null,
135
+ description: options.description ?? null,
136
+ labels: options.labels ?? null,
137
+ // --draft — mark MR as draft
138
+ draft: flags.has("--draft"),
139
+ // --no-push — skip git push before MR
140
+ noPush: flags.has("--no-push"),
141
+
142
+ // ── Portal flags ──────────────────────────────────────────────────────────
143
+ // --epic <iid> — select epic by IID non-interactively
144
+ epic: options.epic ?? null,
145
+ // --checkout — run epic checkout without menus
146
+ checkout: flags.has("--checkout"),
147
+ // --create-mr — create bulk MRs for epic without menus
148
+ createMr: flags.has("--create-mr"),
149
+ // --create-issue — go straight to issue creation
150
+ createIssue: flags.has("--create-issue"),
151
+ // --review — run cr review on all primary branches of the epic
152
+ review: flags.has("--review"),
153
+ // Issue creation params
154
+ issueProject: options.issueProject ?? null,
155
+ issueTitle: options.issueTitle ?? null,
156
+ issueDescription: options.issueDescription ?? null,
157
+ issueLabels: options.issueLabels ?? null,
158
+ branchName: options.branchName ?? null,
159
+ baseBranch: options.baseBranch ?? null,
160
+ };
161
+ }
162
+
163
+ // ── Entry point ───────────────────────────────────────────────────────────────
164
+
165
+ export async function main() {
166
+ process.on("SIGINT", () => { console.log("\n\n" + p.muted(" Interrupted.") + "\n"); process.exit(0); });
167
+ process.on("SIGTERM", () => { console.log("\n\n" + p.muted(" Terminated.") + "\n"); process.exit(0); });
168
+
169
+ const opts = parseArgs(process.argv.slice(2));
170
+ if (opts.debug) process.env.GITMUX_DEBUG = "1"; // set before any module uses it
171
+ const config = loadConfig();
172
+
173
+ if (opts.version) {
174
+ console.log(`gitmux v${VERSION}`);
175
+ return 0;
176
+ }
177
+
178
+ if (opts.help) {
179
+ printHelp();
180
+ return 0;
181
+ }
182
+
183
+ if (opts.subcommand === "about") {
184
+ printAbout();
185
+ return 0;
186
+ }
187
+
188
+ process.stdout.write("\x1Bc");
189
+ printLogo();
190
+ checkRequiredDeps();
191
+
192
+ const cwd = process.cwd();
193
+ let repos = findRepos(cwd, opts.depth);
194
+
195
+ if (repos.length === 0) {
196
+ console.log(
197
+ boxen(p.red("No git repositories found in the current directory."), {
198
+ padding: { top: 0, bottom: 0, left: 2, right: 2 },
199
+ borderStyle: "round",
200
+ borderColor: "red",
201
+ }),
202
+ );
203
+ return 1;
204
+ }
205
+
206
+ if (opts.filter) {
207
+ const pat = opts.filter.toLowerCase();
208
+ const before = repos.length;
209
+ repos = repos.filter((r) => basename(r).toLowerCase().includes(pat));
210
+ if (repos.length === 0) {
211
+ console.log(p.yellow(` No repos matched --filter "${opts.filter}". Nothing to do.\n`));
212
+ return 0;
213
+ }
214
+ console.log(
215
+ p.muted(` ${repos.length} repos matched --filter "${opts.filter}" (${before - repos.length} hidden)\n`),
216
+ );
217
+ }
218
+
219
+ if (opts.exclude) {
220
+ const pat = opts.exclude.toLowerCase();
221
+ const before = repos.length;
222
+ repos = repos.filter((r) => !basename(r).toLowerCase().includes(pat));
223
+ if (repos.length === 0) {
224
+ console.log(
225
+ p.yellow(` All ${before} repos matched --exclude "${opts.exclude}". Nothing to do.\n`),
226
+ );
227
+ return 0;
228
+ }
229
+ console.log(
230
+ p.muted(` ${repos.length} repos · ${before - repos.length} excluded via "${opts.exclude}"\n`),
231
+ );
232
+ }
233
+
234
+ // ── Subcommands ───────────────────────────────────────────────────────────
235
+
236
+ if (opts.subcommand === "status") {
237
+ await cmdStatus(repos);
238
+ return 0;
239
+ }
240
+
241
+ if (opts.subcommand === "fetch") {
242
+ console.log();
243
+ await cmdFetch(repos);
244
+ return 0;
245
+ }
246
+
247
+ if (opts.subcommand === "mr") {
248
+ return await cmdMr(repos, {
249
+ target: opts.target,
250
+ mrRepos: opts.mrRepos,
251
+ title: opts.title,
252
+ description: opts.description,
253
+ labels: opts.labels,
254
+ draft: opts.draft,
255
+ noPush: opts.noPush,
256
+ yes: opts.yes,
257
+ });
258
+ }
259
+
260
+ if (opts.subcommand === "portal") {
261
+ return await cmdPortal(repos, {
262
+ settings: opts.settings,
263
+ epic: opts.epic,
264
+ checkout: opts.checkout,
265
+ createMr: opts.createMr,
266
+ createIssue: opts.createIssue,
267
+ review: opts.review,
268
+ target: opts.target,
269
+ title: opts.title,
270
+ description: opts.description,
271
+ labels: opts.labels,
272
+ draft: opts.draft,
273
+ noPush: opts.noPush,
274
+ issueProject: opts.issueProject,
275
+ issueTitle: opts.issueTitle,
276
+ issueDescription: opts.issueDescription,
277
+ issueLabels: opts.issueLabels,
278
+ branchName: opts.branchName,
279
+ baseBranch: opts.baseBranch,
280
+ yes: opts.yes,
281
+ });
282
+ }
283
+
284
+ if (opts.subcommand === "settings") {
285
+ return await cmdSettings(repos);
286
+ }
287
+
288
+ // ── Interactive switch mode ────────────────────────────────────────────────
289
+
290
+ let {
291
+ branch: targetBranch,
292
+ pull: pullChanges,
293
+ fuzzy: fuzzyMode,
294
+ create: createBranch,
295
+ stash: autoStash,
296
+ fetch: doFetch,
297
+ dryRun,
298
+ } = opts;
299
+
300
+ if (!targetBranch) {
301
+ console.log(
302
+ " " + p.muted("Found ") + p.white(String(repos.length)) +
303
+ p.muted(` ${repos.length === 1 ? "repo" : "repos"} in scope\n`),
304
+ );
305
+
306
+ // Mode selector — loop so "go back" from sub-commands returns here
307
+ modeLoop: while (true) {
308
+ const mode = await select({
309
+ message: p.white("What do you want to do?"),
310
+ choices: [
311
+ {
312
+ value: "switch",
313
+ name: p.cyan("⇌ Switch branches") + p.muted(" checkout a branch across all repos"),
314
+ },
315
+ {
316
+ value: "portal",
317
+ name: chalk.hex("#FC6D26")("◈ GitLab") + p.muted(" development portal — epics, MRs & branches"),
318
+ },
319
+ ],
320
+ theme: THEME,
321
+ });
322
+
323
+ console.log();
324
+
325
+ if (mode === "portal") {
326
+ const result = await cmdPortal(repos);
327
+ if (result !== "__back__") return result;
328
+ continue;
329
+ }
330
+
331
+ // ── Switch branch flow (with back support) ─────────────────────────────
332
+ const branchSuggestions = getSwitchBranchSuggestions(config);
333
+
334
+ if (branchSuggestions.length > 0) {
335
+ const branchChoice = await select({
336
+ message: p.white("Suggested branch:"),
337
+ choices: [
338
+ { value: "__back__", name: p.yellow("← Go back"), description: p.muted("return to main menu") },
339
+ ...branchSuggestions.map((item) => ({
340
+ value: item.value,
341
+ name: colorBranch(item.value),
342
+ description: p.muted(item.template),
343
+ })),
344
+ {
345
+ value: "__custom__",
346
+ name: p.white("Custom branch..."),
347
+ description: p.muted("type any branch name or partial"),
348
+ },
349
+ ],
350
+ theme: THEME,
351
+ });
352
+
353
+ console.log();
354
+
355
+ if (branchChoice === "__back__") continue modeLoop;
356
+
357
+ if (branchChoice === "__custom__") {
358
+ targetBranch = await input({
359
+ message: p.white("Branch name") + p.muted(" (or partial):"),
360
+ theme: THEME,
361
+ validate: (v) => v.trim() !== "" || "Branch name cannot be empty",
362
+ });
363
+ } else {
364
+ targetBranch = branchChoice;
365
+ }
366
+ } else {
367
+ targetBranch = await input({
368
+ message: p.white("Branch name") + p.muted(" (or partial):"),
369
+ theme: THEME,
370
+ validate: (v) => v.trim() !== "" || "Branch name cannot be empty",
371
+ });
372
+ }
373
+
374
+ break modeLoop; // branch was chosen — proceed to confirm prompts
375
+ }
376
+
377
+ const switchDefaults = config.switch?.lastOptions ?? {};
378
+
379
+ pullChanges = await confirm({
380
+ message: p.white("Pull") + p.muted(" after switching?"),
381
+ default: switchDefaults.pull ?? false,
382
+ theme: THEME,
383
+ });
384
+
385
+ fuzzyMode = await confirm({
386
+ message: p.white("Fuzzy") + p.muted(" / partial branch matching?"),
387
+ default: switchDefaults.fuzzy ?? false,
388
+ theme: THEME,
389
+ });
390
+
391
+ autoStash = await confirm({
392
+ message: p.white("Auto-stash") + p.muted(" dirty repos before switching?"),
393
+ default: switchDefaults.stash ?? false,
394
+ theme: THEME,
395
+ });
396
+
397
+ saveConfig({
398
+ ...config,
399
+ switch: {
400
+ ...config.switch,
401
+ lastOptions: { pull: pullChanges, fuzzy: fuzzyMode, stash: autoStash },
402
+ },
403
+ });
404
+
405
+ console.log();
406
+ }
407
+
408
+
409
+ return await cmdSwitch(repos, {
410
+ targetBranch,
411
+ pullChanges,
412
+ fuzzyMode,
413
+ createBranch,
414
+ autoStash,
415
+ doFetch,
416
+ dryRun,
417
+ });
418
+ }
@@ -0,0 +1,54 @@
1
+ import chalk from "chalk";
2
+ import { p } from "./theme.js";
3
+
4
+ // Convert HSL (h: 0-360, s: 0-100, l: 0-100) to a hex colour string
5
+ function hslToHex(h, s, l) {
6
+ s /= 100;
7
+ l /= 100;
8
+ const k = (n) => (n + h / 30) % 12;
9
+ const a = s * Math.min(l, 1 - l);
10
+ const f = (n) =>
11
+ l - a * Math.max(-1, Math.min(k(n) - 3, Math.min(9 - k(n), 1)));
12
+ const hex = (x) =>
13
+ Math.round(x * 255)
14
+ .toString(16)
15
+ .padStart(2, "0");
16
+ return `#${hex(f(0))}${hex(f(8))}${hex(f(4))}`;
17
+ }
18
+
19
+ // Deterministically map a prefix string to a hue (0–359)
20
+ function prefixHue(str) {
21
+ let h = 0;
22
+ for (let i = 0; i < str.length; i++) h = (h * 31 + str.charCodeAt(i)) >>> 0;
23
+ return h % 360;
24
+ }
25
+
26
+ // Colour a GitLab label by hashing its namespace prefix (e.g. "STA", "PRI", "TECH")
27
+ // Works for any prefix — same prefix always gets the same colour.
28
+ export function colorLabel(label) {
29
+ const prefix = label.split("::")[0].trim();
30
+ const hex = hslToHex(prefixHue(prefix.toUpperCase()), 65, 72);
31
+ return chalk.hex(hex)(label);
32
+ }
33
+
34
+ export function colorBranch(name) {
35
+ const n = name.trimEnd();
36
+ const pad = name.slice(n.length);
37
+ const color =
38
+ n === "main" || n === "master"
39
+ ? p.red
40
+ : /^(feature|feat)/.test(n)
41
+ ? p.cyan
42
+ : /^(hotfix|fix|bugfix)/.test(n)
43
+ ? p.orange
44
+ : /^(release|rel)/.test(n)
45
+ ? p.teal
46
+ : /^(develop|dev)$/.test(n)
47
+ ? p.purple
48
+ : /^sprint/.test(n)
49
+ ? p.yellow
50
+ : /^(chore|refactor|docs|test)/.test(n)
51
+ ? p.muted
52
+ : p.white;
53
+ return color(n) + pad;
54
+ }
@@ -0,0 +1,177 @@
1
+ import figlet from "figlet";
2
+ import boxen from "boxen";
3
+ import chalk from "chalk";
4
+
5
+ import { p } from "./theme.js";
6
+ import { VERSION } from "../constants.js";
7
+
8
+ export function printLogo() {
9
+ const art = figlet.textSync("gitmux", { font: "ANSI Shadow" });
10
+ console.log();
11
+ console.log(chalk.white(art));
12
+ console.log(
13
+ " " + p.slate("─".repeat(3)) +
14
+ " " + p.muted(`v${VERSION}`) +
15
+ " " + p.slate("─".repeat(3)),
16
+ );
17
+ console.log();
18
+ }
19
+
20
+ export function printAbout() {
21
+ printLogo();
22
+
23
+ const hr = " " + p.dim("─".repeat(58));
24
+ const gap = "";
25
+
26
+ const features = [
27
+ [p.cyan("⇌ Branch switching"), "checkout any branch across all repos instantly"],
28
+ [p.teal("↓ Pull & stash"), "pull latest or auto-stash dirty repos on switch"],
29
+ [p.purple("~ Fuzzy matching"), "partial branch name matching across all repos"],
30
+ [p.cyan("↺ Parallel fetch"), "fetch all remotes concurrently"],
31
+ [chalk.hex("#FC6D26")("◈ GitLab portal"), "browse epics, issues & primary branches"],
32
+ [p.purple("⎇ Merge requests"), "open MRs straight from the CLI via glab"],
33
+ [p.green("✚ Issue creation"), "create GitLab issues linked to epics"],
34
+ [p.teal("★ Primary branches"), "pin a branch per issue for fast checkout"],
35
+ ];
36
+
37
+ const lines = [
38
+ // About
39
+ " " + chalk.bold(p.white("ABOUT")),
40
+ gap,
41
+ " " + p.muted("gitmux is a multi-repo Git workflow CLI."),
42
+ " " + p.muted("Run it from any directory to operate across all git"),
43
+ " " + p.muted("repositories found within the configured search depth."),
44
+ gap,
45
+ hr,
46
+ gap,
47
+
48
+ // Features
49
+ " " + chalk.bold(p.white("FEATURES")),
50
+ gap,
51
+ ...features.map(([label, desc]) =>
52
+ " " + label + " " + p.muted(desc),
53
+ ),
54
+ gap,
55
+ hr,
56
+ gap,
57
+
58
+ // Commands
59
+ " " + chalk.bold(p.white("COMMANDS")),
60
+ gap,
61
+ ` ${p.cyan("gitmux <branch>")} ${p.muted("Switch branch across all repos")}`,
62
+ ` ${p.cyan("gitmux status")} ${p.muted("Show branch & dirty state for all repos")}`,
63
+ ` ${p.cyan("gitmux fetch")} ${p.muted("Fetch all remotes in parallel")}`,
64
+ ` ${p.cyan("gitmux mr")} ${p.muted("Create merge requests via glab")}`,
65
+ ` ${p.cyan("gitmux portal")} ${p.muted("GitLab development portal")}`,
66
+ ` ${p.cyan("gitmux settings")} ${p.muted("Configure portal & workflow defaults")}`,
67
+ ` ${p.cyan("gitmux about")} ${p.muted("This page")}`,
68
+ gap,
69
+ hr,
70
+ gap,
71
+
72
+ // Meta
73
+ " " + chalk.bold(p.white("VERSION")) + " " + p.white(`v${VERSION}`),
74
+ gap,
75
+ " " + chalk.bold(p.white("AUTHOR")) + " " + p.white("E.R.Rhythm"),
76
+ " " + chalk.bold(p.white("EMAIL")) + " " + p.muted("errhythm.me@gmail.com"),
77
+ " " + chalk.bold(p.white("GITHUB")) + " " + p.muted("github.com/") + p.cyan("e.r.rhythm"),
78
+ gap,
79
+ ];
80
+
81
+ for (const line of lines) console.log(line);
82
+ }
83
+
84
+
85
+ export function printHelp() {
86
+ printLogo();
87
+ const hr = p.dim("─".repeat(52));
88
+ console.log(
89
+ boxen(
90
+ [
91
+ chalk.bold(p.white("USAGE")),
92
+ ` ${p.cyan("gitmux")} ${p.muted("[branch] [options]")}`,
93
+ ` ${p.cyan("gitmux status")}`,
94
+ ` ${p.cyan("gitmux fetch")}`,
95
+ ` ${p.cyan("gitmux mr [options]")}`,
96
+ ` ${p.cyan("gitmux portal [options]")}`,
97
+ ` ${p.cyan("gitmux settings")}`,
98
+ ` ${p.cyan("gitmux about")}`,
99
+ "",
100
+ hr,
101
+ "",
102
+ chalk.bold(p.white("COMMANDS")),
103
+ ` ${p.cyan("status")} ${p.muted("Show branch & dirty status for all repos")}`,
104
+ ` ${p.cyan("fetch")} ${p.muted("Fetch all remotes across repos in parallel")}`,
105
+ ` ${p.cyan("mr")} ${p.muted("Create GitLab merge requests directly (via glab)")}`,
106
+ ` ${p.cyan("portal")} ${p.muted("GitLab Portal — epics, MRs, issues & branches")}`,
107
+ ` ${p.cyan("settings")} ${p.muted("Configure portal, switch & MR defaults")}`,
108
+ ` ${p.cyan("about")} ${p.muted("Show version, features & author info")}`,
109
+ "",
110
+ hr,
111
+ "",
112
+ chalk.bold(p.white("SWITCH OPTIONS")),
113
+ ` ${p.purple("-p, --pull")} ${p.muted("Pull latest on the target branch after switching")}`,
114
+ ` ${p.purple("-f, --fuzzy")} ${p.muted("Fuzzy / partial branch name matching")}`,
115
+ ` ${p.purple("-c, --create")} ${p.muted("Create branch if it doesn't exist")}`,
116
+ ` ${p.purple("-s, --stash")} ${p.muted("Auto-stash dirty repos before switching")}`,
117
+ ` ${p.purple(" --fetch")} ${p.muted("Fetch all remotes before switching")}`,
118
+ ` ${p.purple(" --dry-run")} ${p.muted("Preview what would happen, no changes made")}`,
119
+ "",
120
+ chalk.bold(p.white("MR OPTIONS")),
121
+ ` ${p.purple("-t, --target <b>")} ${p.muted("Target branch to merge into (skips scope picker)")}`,
122
+ ` ${p.purple(" --repo <name>")} ${p.muted("Restrict to this repo (repeatable)")}`,
123
+ ` ${p.purple(" --title <s>")} ${p.muted("MR title (skips prompt)")}`,
124
+ ` ${p.purple(" --description")} ${p.muted("MR description (skips prompt)")}`,
125
+ ` ${p.purple(" --labels <csv>")}${p.muted("Comma-separated labels")}`,
126
+ ` ${p.purple(" --draft")} ${p.muted("Mark MR as draft")}`,
127
+ ` ${p.purple(" --no-push")} ${p.muted("Skip git push before creating MR")}`,
128
+ "",
129
+ chalk.bold(p.white("PORTAL OPTIONS")),
130
+ ` ${p.purple(" --epic <iid>")} ${p.muted("Select epic by IID (skips epic picker)")}`,
131
+ ` ${p.purple(" --checkout")} ${p.muted("Checkout primary branches for epic's repos")}`,
132
+ ` ${p.purple(" --create-mr")} ${p.muted("Bulk-create MRs for epic issues")}`,
133
+ ` ${p.purple(" --create-issue")} ${p.muted("Create a new issue in the epic")}`,
134
+ ` ${p.purple(" --issue-project <path>")} ${p.muted("Project name or path for issue creation")}`,
135
+ ` ${p.purple(" --issue-title <text>")} ${p.muted("Issue title")}`,
136
+ ` ${p.purple(" --issue-description")} ${p.muted("Issue description")}`,
137
+ ` ${p.purple(" --issue-labels <csv>")} ${p.muted("Issue labels")}`,
138
+ ` ${p.purple(" --branch-name <name>")} ${p.muted("Create this branch for the new issue")}`,
139
+ ` ${p.purple(" --base-branch <name>")} ${p.muted("Base branch for branch/MR creation")}`,
140
+ "",
141
+ chalk.bold(p.white("GLOBAL OPTIONS")),
142
+ ` ${p.purple("-y, --yes")} ${p.muted("Auto-confirm all prompts (non-interactive)")}`,
143
+ ` ${p.purple(" --depth n")} ${p.muted("Repo search depth (default: 4)")}`,
144
+ ` ${p.purple(" --filter p")} ${p.muted("Only include repos whose name contains pattern")}`,
145
+ ` ${p.purple(" --exclude p")} ${p.muted("Exclude repos whose name contains pattern")}`,
146
+ ` ${p.purple(" --settings")} ${p.muted("Open portal settings (group, milestone, iteration…)")}`,
147
+ ` ${p.purple("-v, --version")} ${p.muted("Show version number")}`,
148
+ ` ${p.purple("-h, --help")} ${p.muted("Show this help")}`,
149
+ "",
150
+ hr,
151
+ "",
152
+ chalk.bold(p.white("EXAMPLES")),
153
+ ` ${p.muted("$")} ${p.cyan("gitmux develop")}`,
154
+ ` ${p.muted("$")} ${p.cyan("gitmux main --pull")}`,
155
+ ` ${p.muted("$")} ${p.cyan("gitmux feature/auth --stash --pull")}`,
156
+ ` ${p.muted("$")} ${p.cyan("gitmux feat --fuzzy")}`,
157
+ ` ${p.muted("$")} ${p.cyan("gitmux main --create")}`,
158
+ ` ${p.muted("$")} ${p.cyan("gitmux --dry-run develop")}`,
159
+ ` ${p.muted("$")} ${p.cyan("gitmux --exclude legacy develop")}`,
160
+ ` ${p.muted("$")} ${p.cyan("gitmux status")}`,
161
+ ` ${p.muted("$")} ${p.cyan("gitmux fetch")}`,
162
+ ` ${p.muted("$")} ${p.cyan("gitmux mr --target develop --title \"Fix auth\" --labels \"bug\" --yes")}`,
163
+ ` ${p.muted("$")} ${p.cyan("gitmux portal --epic 42 --checkout --yes")}`,
164
+ ` ${p.muted("$")} ${p.cyan("gitmux portal --epic 42 --create-mr --target develop --yes")}`,
165
+ ` ${p.muted("$")} ${p.cyan("gitmux portal --epic 42 --create-issue --issue-project group/repo --issue-title \"Task\" --yes")}`,
166
+ ].join("\n"),
167
+ {
168
+ padding: { top: 1, bottom: 1, left: 3, right: 3 },
169
+ borderStyle: "round",
170
+ borderColor: "#334155",
171
+ title: p.muted(" help "),
172
+ titleAlignment: "right",
173
+ },
174
+ ),
175
+ );
176
+ console.log();
177
+ }
@@ -0,0 +1,26 @@
1
+ import chalk from "chalk";
2
+
3
+ export const p = {
4
+ cyan: chalk.hex("#67e8f9"),
5
+ teal: chalk.hex("#4ecdc4"),
6
+ purple: chalk.hex("#c084fc"),
7
+ green: chalk.hex("#4ade80"),
8
+ yellow: chalk.hex("#fbbf24"),
9
+ red: chalk.hex("#f87171"),
10
+ orange: chalk.hex("#fb923c"),
11
+ muted: chalk.hex("#64748b"),
12
+ slate: chalk.hex("#475569"),
13
+ dim: chalk.hex("#1e293b"),
14
+ white: chalk.hex("#f1f5f9"),
15
+ bold: chalk.bold,
16
+ };
17
+
18
+ export const THEME = {
19
+ prefix: p.purple("◆"),
20
+ icon: { cursor: " " },
21
+ style: {
22
+ highlight: (s) => chalk.bgHex("#5b21b6").white.bold(s.replace(/\x1B\[[0-9;]*[a-zA-Z]/g, "")),
23
+ answer: (s) => p.cyan(s),
24
+ defaultAnswer: (s) => p.muted(s),
25
+ },
26
+ };
@@ -0,0 +1,12 @@
1
+ import { exec, execFile } from "child_process";
2
+ import { promisify } from "util";
3
+
4
+ export const execAsync = promisify(exec);
5
+ export const execFileAsync = promisify(execFile);
6
+
7
+ export function extractMsg(e) {
8
+ return (e.stderr || e.stdout || e.message || "")
9
+ .toString()
10
+ .replace(/\n/g, " ")
11
+ .trim();
12
+ }