gflows 0.1.12 → 0.1.13

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 CHANGED
@@ -1,4 +1,4 @@
1
- # gflows
1
+ # gFlows
2
2
 
3
3
  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](https://bun.sh) and TypeScript; **scriptable** and **safe by default**—no history rewriting, predictable exit codes, and optional interactive pickers only when running in a TTY.
4
4
 
@@ -181,20 +181,22 @@ gflows finish hotfix --push # merge to main, then dev; tag v1.3.1;
181
181
 
182
182
  **Common flags** (used by multiple commands):
183
183
 
184
- | Flag | Short | Description |
185
- |------|-------|-------------|
186
- | `--path <dir>` | `-C` | Run as if in `<dir>`. |
187
- | `--dry-run` | `-d` | Log intended actions only; no writes. |
188
- | `--verbose` | `-v` | Verbose output. |
189
- | `--quiet` | `-q` | Minimal output. |
190
- | `--push` | `-p` | Push after init/start/finish. |
191
- | `--no-push` | `-P` | Do not push. |
192
- | `--main <name>` | | Main branch override. |
193
- | `--dev <name>` | — | Dev branch override. |
194
- | `--remote <name>` | `-R` | Remote for push. |
195
- | `--from <branch>` | `-o` | Base branch override (start). |
196
- | `--branch <name>` | `-B` | Branch name (finish). |
197
- | `--yes` | `-y` | Skip confirmations. |
184
+
185
+ | Flag | Short | Description |
186
+ | ----------------- | ----- | ------------------------------------- |
187
+ | `--path <dir>` | `-C` | Run as if in `<dir>`. |
188
+ | `--dry-run` | `-d` | Log intended actions only; no writes. |
189
+ | `--verbose` | `-v` | Verbose output. |
190
+ | `--quiet` | `-q` | Minimal output. |
191
+ | `--push` | `-p` | Push after init/start/finish. |
192
+ | `--no-push` | `-P` | Do not push. |
193
+ | `--main <name>` | — | Main branch override. |
194
+ | `--dev <name>` | | Dev branch override. |
195
+ | `--remote <name>` | `-R` | Remote for push. |
196
+ | `--from <branch>` | `-o` | Base branch override (start). |
197
+ | `--branch <name>` | `-B` | Branch name (finish). |
198
+ | `--yes` | `-y` | Skip confirmations. |
199
+
198
200
 
199
201
  ---
200
202
 
@@ -216,16 +218,18 @@ gflows init --dry-run # log intended actions only
216
218
 
217
219
  **Flags:**
218
220
 
219
- | Flag | Short | Description |
220
- |------|-------|-------------|
221
- | `--push` | `-p` | Push dev to remote after creating. |
222
- | `--main <name>` | | Main branch name (persisted to `.gflows.json` when provided). |
223
- | `--dev <name>` | — | Dev branch name (persisted to `.gflows.json` when provided). |
224
- | `--remote <name>` | `-R` | Remote name (persisted to `.gflows.json` when provided). |
225
- | `--path <dir>` | `-C` | Run as if in `<dir>`. |
226
- | `--dry-run` | `-d` | Log intended actions only; no writes. |
227
- | `--verbose` | `-v` | Verbose output. |
228
- | `--quiet` | `-q` | Minimal output. |
221
+
222
+ | Flag | Short | Description |
223
+ | ----------------- | ----- | ------------------------------------------------------------- |
224
+ | `--push` | `-p` | Push dev to remote after creating. |
225
+ | `--main <name>` | — | Main branch name (persisted to `.gflows.json` when provided). |
226
+ | `--dev <name>` | | Dev branch name (persisted to `.gflows.json` when provided). |
227
+ | `--remote <name>` | `-R` | Remote name (persisted to `.gflows.json` when provided). |
228
+ | `--path <dir>` | `-C` | Run as if in `<dir>`. |
229
+ | `--dry-run` | `-d` | Log intended actions only; no writes. |
230
+ | `--verbose` | `-v` | Verbose output. |
231
+ | `--quiet` | `-q` | Minimal output. |
232
+
229
233
 
230
234
  ---
231
235
 
@@ -250,16 +254,18 @@ gflows start chore deps-update -C ./backend # run in subdirectory
250
254
 
251
255
  **Flags:**
252
256
 
253
- | Flag | Short | Description |
254
- |------|-------|-------------|
255
- | `--force` | | Allow dirty working tree. |
256
- | `--push` | `-p` | Push new branch to remote after creating. |
257
- | `--from <branch>` | `-o` | Base branch override (e.g. `-o main` for bugfix). |
258
- | `--remote <name>` | `-R` | Remote for push. |
259
- | `--path <dir>` | `-C` | Run as if in `<dir>`. |
260
- | `--dry-run` | `-d` | Log intended actions only; no writes. |
261
- | `--verbose` | `-v` | Verbose output. |
262
- | `--quiet` | `-q` | Minimal output. |
257
+
258
+ | Flag | Short | Description |
259
+ | ----------------- | ----- | ------------------------------------------------- |
260
+ | `--force` | | Allow dirty working tree. |
261
+ | `--push` | `-p` | Push new branch to remote after creating. |
262
+ | `--from <branch>` | `-o` | Base branch override (e.g. `-o main` for bugfix). |
263
+ | `--remote <name>` | `-R` | Remote for push. |
264
+ | `--path <dir>` | `-C` | Run as if in `<dir>`. |
265
+ | `--dry-run` | `-d` | Log intended actions only; no writes. |
266
+ | `--verbose` | `-v` | Verbose output. |
267
+ | `--quiet` | `-q` | Minimal output. |
268
+
263
269
 
264
270
  ---
265
271
 
@@ -286,23 +292,25 @@ gflows finish -y # skip "Delete branch after finish?"
286
292
 
287
293
  **Flags:**
288
294
 
289
- | Flag | Short | Description |
290
- |------|-------|-------------|
291
- | `--branch <name>` | `-B` | Branch to finish (current branch if omitted; picker in TTY when `-B` with no value). |
292
- | `--no-ff` | | Always create a merge commit. |
293
- | `--delete` | `-D` | Delete branch after finish. |
294
- | `--no-delete` | `-N` | Do not delete branch after finish. |
295
- | `--push` | `-p` | Push after merge (finish prompts "Do you want to push?" when neither `-p` nor `-P`). |
296
- | `--no-push` | `-P` | Do not push. |
297
- | `--sign` | `-s` | Sign the tag (release/hotfix; GPG). |
298
- | `--no-tag` | `-T` | Do not create tag (release/hotfix). |
299
- | `--tag-message <msg>` | `-M` | Tag message. |
300
- | `--message <msg>` | `-m` | Merge message. |
301
- | `--yes` | `-y` | Skip confirmations (e.g. "Delete branch after finish?"). |
302
- | `--path <dir>` | `-C` | Run as if in `<dir>`. |
303
- | `--dry-run` | `-d` | Log intended actions only; no writes. |
304
- | `--verbose` | `-v` | Verbose output. |
305
- | `--quiet` | `-q` | Minimal output. |
295
+
296
+ | Flag | Short | Description |
297
+ | --------------------- | ----- | ------------------------------------------------------------------------------------ |
298
+ | `--branch <name>` | `-B` | Branch to finish (current branch if omitted; picker in TTY when `-B` with no value). |
299
+ | `--no-ff` | | Always create a merge commit. |
300
+ | `--delete` | `-D` | Delete branch after finish. |
301
+ | `--no-delete` | `-N` | Do not delete branch after finish. |
302
+ | `--push` | `-p` | Push after merge (finish prompts "Do you want to push?" when neither `-p` nor `-P`). |
303
+ | `--no-push` | `-P` | Do not push. |
304
+ | `--sign` | `-s` | Sign the tag (release/hotfix; GPG). |
305
+ | `--no-tag` | `-T` | Do not create tag (release/hotfix). |
306
+ | `--tag-message <msg>` | `-M` | Tag message. |
307
+ | `--message <msg>` | `-m` | Merge message. |
308
+ | `--yes` | `-y` | Skip confirmations (e.g. "Delete branch after finish?"). |
309
+ | `--path <dir>` | `-C` | Run as if in `<dir>`. |
310
+ | `--dry-run` | `-d` | Log intended actions only; no writes. |
311
+ | `--verbose` | `-v` | Verbose output. |
312
+ | `--quiet` | `-q` | Minimal output. |
313
+
306
314
 
307
315
  ---
308
316
 
@@ -320,11 +328,13 @@ gflows -W feature/auth-refactor # same with short command
320
328
 
321
329
  **Flags:**
322
330
 
323
- | Flag | Short | Description |
324
- |------|-------|-------------|
325
- | `--path <dir>` | `-C` | Run as if in `<dir>`. |
326
- | `--verbose` | `-v` | Verbose output. |
327
- | `--quiet` | `-q` | Minimal output. |
331
+
332
+ | Flag | Short | Description |
333
+ | -------------- | ----- | --------------------- |
334
+ | `--path <dir>` | `-C` | Run as if in `<dir>`. |
335
+ | `--verbose` | `-v` | Verbose output. |
336
+ | `--quiet` | `-q` | Minimal output. |
337
+
328
338
 
329
339
  ---
330
340
 
@@ -342,11 +352,13 @@ gflows delete feature/one feature/two # delete multiple
342
352
 
343
353
  **Flags:**
344
354
 
345
- | Flag | Short | Description |
346
- |------|-------|-------------|
347
- | `--path <dir>` | `-C` | Run as if in `<dir>`. |
348
- | `--verbose` | `-v` | Verbose output. |
349
- | `--quiet` | `-q` | Minimal output. |
355
+
356
+ | Flag | Short | Description |
357
+ | -------------- | ----- | --------------------- |
358
+ | `--path <dir>` | `-C` | Run as if in `<dir>`. |
359
+ | `--verbose` | `-v` | Verbose output. |
360
+ | `--quiet` | `-q` | Minimal output. |
361
+
350
362
 
351
363
  ---
352
364
 
@@ -366,13 +378,15 @@ gflows list --include-remote
366
378
 
367
379
  **Flags:**
368
380
 
369
- | Flag | Short | Description |
370
- |------|-------|-------------|
371
- | `--include-remote` | `-r` | Include remote-tracking branches (may run `git fetch`). |
372
- | `--path <dir>` | `-C` | Run as if in `<dir>`. |
373
- | `--dry-run` | `-d` | Log intended actions only. |
374
- | `--verbose` | `-v` | Verbose output. |
375
- | `--quiet` | `-q` | Minimal output. |
381
+
382
+ | Flag | Short | Description |
383
+ | ------------------ | ----- | ------------------------------------------------------- |
384
+ | `--include-remote` | `-r` | Include remote-tracking branches (may run `git fetch`). |
385
+ | `--path <dir>` | `-C` | Run as if in `<dir>`. |
386
+ | `--dry-run` | `-d` | Log intended actions only. |
387
+ | `--verbose` | `-v` | Verbose output. |
388
+ | `--quiet` | `-q` | Minimal output. |
389
+
376
390
 
377
391
  ---
378
392
 
@@ -396,12 +410,14 @@ gflows bump --dry-run # print old → new, no file writes
396
410
 
397
411
  **Flags:**
398
412
 
399
- | Flag | Short | Description |
400
- |------|-------|-------------|
401
- | `--dry-run` | `-d` | Print old → new version only; no file writes. |
402
- | `--path <dir>` | `-C` | Run as if in `<dir>`. |
403
- | `--verbose` | `-v` | Verbose output. |
404
- | `--quiet` | `-q` | Minimal output. |
413
+
414
+ | Flag | Short | Description |
415
+ | -------------- | ----- | --------------------------------------------- |
416
+ | `--dry-run` | `-d` | Print old new version only; no file writes. |
417
+ | `--path <dir>` | `-C` | Run as if in `<dir>`. |
418
+ | `--verbose` | `-v` | Verbose output. |
419
+ | `--quiet` | `-q` | Minimal output. |
420
+
405
421
 
406
422
  ---
407
423
 
@@ -418,11 +434,13 @@ gflows -t
418
434
 
419
435
  **Flags:**
420
436
 
421
- | Flag | Short | Description |
422
- |------|-------|-------------|
423
- | `--path <dir>` | `-C` | Run as if in `<dir>`. |
424
- | `--verbose` | `-v` | Verbose output. |
425
- | `--quiet` | `-q` | Minimal output. |
437
+
438
+ | Flag | Short | Description |
439
+ | -------------- | ----- | --------------------- |
440
+ | `--path <dir>` | `-C` | Run as if in `<dir>`. |
441
+ | `--verbose` | `-v` | Verbose output. |
442
+ | `--quiet` | `-q` | Minimal output. |
443
+
426
444
 
427
445
  ---
428
446
 
@@ -476,7 +494,7 @@ Configuration is **optional**. Override branch names, remote, and branch **prefi
476
494
  **Resolution order** (later overrides earlier):
477
495
 
478
496
  1. Built-in defaults (`main`, `dev`, `origin`, and default prefixes).
479
- 2. Repo config file: `**.gflows.json`** in repo root, or `**gflows`** key in `**package.json**`.
497
+ 2. Repo config file: `**.gflows.json`** in repo root, or `**gflows`** key in `**package.json`**.
480
498
  3. CLI (e.g. `--main`, `--dev`, `-R`/`--remote`).
481
499
 
482
500
  Only include keys you want to override; the rest stay default. Invalid or malformed config is ignored (with an optional warning when using `-v`).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gflows",
3
- "version": "0.1.12",
3
+ "version": "0.1.13",
4
4
  "description": "A lightweight CLI for consistent Git branching workflows (main + dev, feature/bugfix/chore/release/hotfix).",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/cli.ts CHANGED
@@ -123,6 +123,45 @@ function resolveCwd(pathFlag: string | undefined): string {
123
123
  return absolute;
124
124
  }
125
125
 
126
+ /**
127
+ * Returns the command name closest to `input` by edit distance, or undefined if no close match.
128
+ * Used for "did you mean?" when the user mistypes a command.
129
+ */
130
+ function closestCommand(input: string): Command | undefined {
131
+ if (!input || input.length < 2) return undefined;
132
+ const target = input.toLowerCase();
133
+ let best: Command | undefined;
134
+ let bestDistance = 3; // only suggest if within 2 edits
135
+ for (const cmd of COMMANDS) {
136
+ const d = editDistance(target, cmd);
137
+ if (d < bestDistance) {
138
+ bestDistance = d;
139
+ best = cmd as Command;
140
+ }
141
+ }
142
+ return best;
143
+ }
144
+
145
+ /** Levenshtein edit distance between two strings. */
146
+ function editDistance(a: string, b: string): number {
147
+ const m = a.length;
148
+ const n = b.length;
149
+ const dp: number[][] = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
150
+ for (let i = 0; i <= m; i++) dp[i]![0] = i;
151
+ for (let j = 0; j <= n; j++) dp[0]![j] = j;
152
+ for (let i = 1; i <= m; i++) {
153
+ for (let j = 1; j <= n; j++) {
154
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
155
+ dp[i]![j] = Math.min(
156
+ dp[i - 1]![j]! + 1,
157
+ dp[i]![j - 1]! + 1,
158
+ dp[i - 1]![j - 1]! + cost
159
+ );
160
+ }
161
+ }
162
+ return dp[m]![n]!;
163
+ }
164
+
126
165
  /** Resolve command from positionals and short flags. Short wins if both present. */
127
166
  function resolveCommand(
128
167
  positionals: string[],
@@ -240,7 +279,13 @@ export function parse(argv: string[] = Bun.argv.slice(2)): ParsedArgs {
240
279
 
241
280
  const command = resolveCommand(positionals, v);
242
281
  if (!command) {
243
- console.error("gflows: missing command. Use 'gflows help' for usage.");
282
+ const first = positionals[0];
283
+ const suggestion = typeof first === "string" ? closestCommand(first) : undefined;
284
+ if (suggestion) {
285
+ console.error(`gflows: unknown command '${first}'. Did you mean '${suggestion}'?`);
286
+ } else {
287
+ console.error("gflows: missing command. Use 'gflows help' for usage.");
288
+ }
244
289
  process.exit(EXIT_USER);
245
290
  }
246
291
 
@@ -111,7 +111,7 @@ function readPackageVersion(dir: string): { raw: string; semver: Semver } {
111
111
  const version = data.version;
112
112
  if (typeof version !== "string" || version.trim() === "") {
113
113
  throw new InvalidVersionError(
114
- "package.json has no valid 'version' field."
114
+ "package.json has no valid 'version' field. Add a \"version\" field (e.g. \"0.0.0\") to package.json."
115
115
  );
116
116
  }
117
117
  const semver = parseVersion(version);
@@ -82,17 +82,17 @@ export async function run(args: ParsedArgs): Promise<void> {
82
82
  typeFilter
83
83
  );
84
84
 
85
- const sorted = [...workflowBranches].sort();
85
+ // Show main and dev first (when present), then workflow branches
86
+ const mainAndDev = [config.main, config.dev].filter((b) =>
87
+ allBranches.includes(b)
88
+ );
89
+ const sorted = [...mainAndDev, ...[...workflowBranches].sort()];
86
90
 
87
91
  for (const b of sorted) {
88
92
  console.log(b);
89
93
  }
90
94
 
91
- if (!quiet && sorted.length === 0) {
92
- console.error("No workflow branches found.");
93
- // Hint: suggest creating first workflow branch
94
- hint("Run gflows start <type> <name> to create a workflow branch.");
95
- } else if (!quiet && sorted.length > 0) {
95
+ if (!quiet && sorted.length > 0) {
96
96
  // Hint: suggest switching to a listed branch
97
97
  hint("Use gflows switch <branch> to switch to a branch.");
98
98
  }
@@ -81,10 +81,15 @@ export async function run(args: ParsedArgs): Promise<void> {
81
81
 
82
82
  const allLocal = await branchList(root, { dryRun, verbose: args.verbose });
83
83
  const workflowBranches = getWorkflowBranches(allLocal, config.prefixes);
84
+ // Include main and dev so we always show whatever branches exist
85
+ const mainAndDev = [config.main, config.dev].filter((b) =>
86
+ allLocal.includes(b)
87
+ );
88
+ const choices = [...mainAndDev, ...workflowBranches];
84
89
 
85
- if (workflowBranches.length === 0) {
90
+ if (choices.length === 0) {
86
91
  if (!quiet) {
87
- console.error("No workflow branches found. Create one with 'gflows start <type> <name>'.");
92
+ console.error("No branches found.");
88
93
  }
89
94
  process.exit(EXIT_OK);
90
95
  }
@@ -92,7 +97,7 @@ export async function run(args: ParsedArgs): Promise<void> {
92
97
  const { select } = await import("@inquirer/prompts");
93
98
  const chosen = await select({
94
99
  message: "Switch to branch",
95
- choices: workflowBranches.map((b) => ({ name: b, value: b })),
100
+ choices: choices.map((b) => ({ name: b, value: b })),
96
101
  });
97
102
 
98
103
  if (typeof chosen !== "string") {