gflows 0.1.11 → 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
 
@@ -171,7 +171,7 @@ gflows finish hotfix --push # merge to main, then dev; tag v1.3.1;
171
171
  | `delete` | `-L` | Delete local workflow branch(es). Never main/dev. |
172
172
  | `list` | `-l` | List workflow branches; optional type filter and remote. |
173
173
  | `bump` | — | Bump or rollback package version (patch/minor/major). |
174
- | `completion` | — | Print shell completion script (bash | zsh | fish). |
174
+ | `completion` | — | Print shell completion script (bash/zsh/fish). |
175
175
  | `status` | `-t` | Show current branch, type, base, merge target(s), ahead/behind. |
176
176
  | `help` | `-h` | Show usage and quick reference. |
177
177
  | `version` | `-V` | Show version. |
@@ -179,13 +179,32 @@ gflows finish hotfix --push # merge to main, then dev; tag v1.3.1;
179
179
 
180
180
  **Branch types (for start/finish/list):** `feature` (`-f`), `bugfix` (`-b`), `chore` (`-c`), `release` (`-r`), `hotfix` (`-x`), `spike` (`-e`).
181
181
 
182
+ **Common flags** (used by multiple commands):
183
+
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
+
200
+
182
201
  ---
183
202
 
184
203
  ### init
185
204
 
186
205
  Ensures the **main** branch exists (exits with error if not). Creates **dev** from main if it does not exist; does nothing if dev already exists. Does not rewrite or force-push.
187
206
 
188
- You can set and persist config with `**--main`**, `**--dev**`, and `**-R`/`--remote**`. Any of these flags cause init to write or update `.gflows.json` with the given values (after a successful init; skipped with `--dry-run`).
207
+ You can set and persist config with `**--main`**, `**--dev`**, and `**-R`/`--remote**`. Any of these flags cause init to write or update `.gflows.json` with the given values (after a successful init; skipped with `--dry-run`).
189
208
 
190
209
  **Examples:**
191
210
 
@@ -197,7 +216,20 @@ gflows init -C ../other-repo # run in another directory
197
216
  gflows init --dry-run # log intended actions only
198
217
  ```
199
218
 
200
- **Flags:** `--push`, `--main <name>`, `--dev <name>`, `-R`/`--remote <name>` (main/dev/remote are persisted to `.gflows.json` when provided), `-C`/`--path <dir>`, `--dry-run`, `-v`/`--verbose`, `-q`/`--quiet`.
219
+ **Flags:**
220
+
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
+
201
233
 
202
234
  ---
203
235
 
@@ -220,7 +252,20 @@ gflows start feature api-v2 --push # create branch and push to rem
220
252
  gflows start chore deps-update -C ./backend # run in subdirectory
221
253
  ```
222
254
 
223
- **Flags:** `--force` (allow dirty working tree), `--push`, `-o`/`--from <branch>` (base override, e.g. `-o main` for bugfix), `-R`/`--remote`, `-C`/`--path`, `--dry-run`, `-v`, `-q`.
255
+ **Flags:**
256
+
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
+
224
269
 
225
270
  ---
226
271
 
@@ -245,7 +290,27 @@ gflows finish -y # skip "Delete branch after finish?"
245
290
 
246
291
  **Branch resolution:** If you omit the branch name, gflows uses the current branch. With `-B` and no value in a TTY, it shows a picker of workflow branches. Without a TTY, you must pass the branch name explicitly.
247
292
 
248
- **Flags:** `-B`/`--branch <name>`, `--no-ff`, `-D`/`--delete` (delete branch after finish), `-N`/`--no-delete`, `--push`, `-s`/`--sign`, `-T`/`--no-tag`, `-M`/`--tag-message`, `-m`/`--message`, `-y`/`--yes`, `-C`, `--dry-run`, `-v`, `-q`.
293
+ **Flags:**
294
+
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
+
249
314
 
250
315
  ---
251
316
 
@@ -261,7 +326,15 @@ gflows switch feature/auth-refactor
261
326
  gflows -W feature/auth-refactor # same with short command
262
327
  ```
263
328
 
264
- **Flags:** `-C`/`--path`, `-v`, `-q`.
329
+ **Flags:**
330
+
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
+
265
338
 
266
339
  ---
267
340
 
@@ -277,7 +350,15 @@ gflows delete feature/old-spike
277
350
  gflows delete feature/one feature/two # delete multiple
278
351
  ```
279
352
 
280
- **Flags:** `-C`/`--path`, `-v`, `-q`.
353
+ **Flags:**
354
+
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
+
281
362
 
282
363
  ---
283
364
 
@@ -295,7 +376,17 @@ gflows list -r feature # remote + local feature branches
295
376
  gflows list --include-remote
296
377
  ```
297
378
 
298
- **Flags:** `-r`/`--include-remote`, `-C`/`--path`, `--dry-run`, `-v`, `-q`.
379
+ **Flags:**
380
+
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
+
299
390
 
300
391
  ---
301
392
 
@@ -317,7 +408,16 @@ gflows bump # interactive (direction + type) when T
317
408
  gflows bump --dry-run # print old → new, no file writes
318
409
  ```
319
410
 
320
- **Flags:** `--dry-run`, `-C`/`--path`, `-v`, `-q`.
411
+ **Flags:**
412
+
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
+
321
421
 
322
422
  ---
323
423
 
@@ -332,7 +432,15 @@ gflows status
332
432
  gflows -t
333
433
  ```
334
434
 
335
- **Flags:** `-C`/`--path`, `-v`, `-q`.
435
+ **Flags:**
436
+
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
+
336
444
 
337
445
  ---
338
446
 
@@ -386,7 +494,7 @@ Configuration is **optional**. Override branch names, remote, and branch **prefi
386
494
  **Resolution order** (later overrides earlier):
387
495
 
388
496
  1. Built-in defaults (`main`, `dev`, `origin`, and default prefixes).
389
- 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`**.
390
498
  3. CLI (e.g. `--main`, `--dev`, `-R`/`--remote`).
391
499
 
392
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.11",
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);
@@ -210,6 +210,7 @@ export async function run(args: ParsedArgs): Promise<void> {
210
210
  const updated = [PACKAGE_JSON];
211
211
  if (jsrUpdated) updated.push(JSR_JSON);
212
212
  success(`Updated: ${updated.join(", ")}`);
213
+ // Hint: suggest next step — commit and start release branch
213
214
  hint("Commit the change, then run gflows start release vX.Y.Z to release.");
214
215
  }
215
216
  }
@@ -80,6 +80,7 @@ export async function run(args: ParsedArgs): Promise<void> {
80
80
  }
81
81
  }
82
82
  if (!quiet && !dryRun) {
83
+ // Hint: suggest listing remaining branches
83
84
  hint("Use gflows list to see remaining workflow branches.");
84
85
  }
85
86
  return;
@@ -139,6 +140,7 @@ export async function run(args: ParsedArgs): Promise<void> {
139
140
  }
140
141
  }
141
142
  if (!quiet && !dryRun && chosen.length > 0) {
143
+ // Hint: suggest listing remaining branches
142
144
  hint("Use gflows list to see remaining workflow branches.");
143
145
  }
144
146
  }
@@ -242,10 +242,21 @@ export async function run(args: ParsedArgs): Promise<void> {
242
242
  }
243
243
  }
244
244
 
245
- const doPush = args.push && !args.noPush;
246
- const didCreateTag = !!(
245
+ const createdTagName =
247
246
  meta.mergeTarget === "main-then-dev" && meta.tagOnFinish && version && !args.noTag
248
- );
247
+ ? normalizeTagVersion(version)
248
+ : undefined;
249
+ const didCreateTag = Boolean(createdTagName);
250
+
251
+ let doPush = args.push && !args.noPush;
252
+ if (!args.push && !args.noPush && isTTY) {
253
+ const { confirm } = await import("@inquirer/prompts");
254
+ doPush = await confirm({
255
+ message: "Do you want to push?",
256
+ default: true,
257
+ });
258
+ }
259
+
249
260
  if (doPush) {
250
261
  const remote = args.remote ?? config.remote;
251
262
  const refsToPush: string[] = [config.dev];
@@ -271,7 +282,9 @@ export async function run(args: ParsedArgs): Promise<void> {
271
282
  }
272
283
 
273
284
  if (!args.quiet && !args.dryRun) {
274
- success(`gflows: finished '${branchToFinish}' into ${meta.mergeTarget}.`);
285
+ const tagSuffix = createdTagName ? ` (tag ${createdTagName})` : "";
286
+ success(`gflows: finished '${branchToFinish}' into ${meta.mergeTarget}${tagSuffix}.`);
287
+ // Hint: suggest next step — create a new workflow branch
275
288
  hint("Run gflows start <type> <name> to create a new workflow branch.");
276
289
  }
277
290
  }
@@ -32,7 +32,7 @@ Types: feature (-f), bugfix (-b), chore (-c), release (-r), hotfix (-x), spike (
32
32
 
33
33
  Common flags:
34
34
  -p, --push Push after init/start/finish
35
- -P, --no-push Do not push
35
+ -P, --no-push Do not push (finish: prompts "Do you want to push?" when neither -p nor -P)
36
36
  --main <name> Main branch (init: persist to .gflows.json)
37
37
  --dev <name> Dev branch (init: persist to .gflows.json)
38
38
  -R, --remote <name> Remote for push (init: persist to .gflows.json)
@@ -102,6 +102,7 @@ export async function run(args: ParsedArgs): Promise<void> {
102
102
  }
103
103
 
104
104
  if (!args.quiet) {
105
+ // Hint: suggest next step — create first workflow branch
105
106
  hint("Run gflows start feature <name> to create a workflow branch.");
106
107
  }
107
108
  }
@@ -82,16 +82,18 @@ 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("Run gflows start <type> <name> to create a workflow branch.");
94
- } else if (!quiet && sorted.length > 0) {
95
+ if (!quiet && sorted.length > 0) {
96
+ // Hint: suggest switching to a listed branch
95
97
  hint("Use gflows switch <branch> to switch to a branch.");
96
98
  }
97
99
  }
@@ -141,6 +141,7 @@ export async function run(args: ParsedArgs): Promise<void> {
141
141
  }
142
142
 
143
143
  if (!args.quiet && !args.dryRun) {
144
+ // Hint: suggest next step — merge branch when done
144
145
  hint(`When done, run gflows finish ${type} to merge into the target branch.`);
145
146
  }
146
147
  }
@@ -138,6 +138,7 @@ export async function run(args: ParsedArgs): Promise<void> {
138
138
 
139
139
  if (!quiet) {
140
140
  console.log(`Ahead/behind: ${ahead} ahead, ${behind} behind`);
141
+ // Hint: suggest next step — finish current branch
141
142
  hint(`Run gflows finish ${classification} to merge into ${mergeTargetDisplay}.`);
142
143
  }
143
144
  }
@@ -65,6 +65,7 @@ export async function run(args: ParsedArgs): Promise<void> {
65
65
  });
66
66
  if (!quiet && !dryRun) {
67
67
  success(`Switched to branch '${branchName}'.`);
68
+ // Hint: suggest listing branches
68
69
  hint("Use gflows list to see all workflow branches.");
69
70
  }
70
71
  return;
@@ -80,10 +81,15 @@ export async function run(args: ParsedArgs): Promise<void> {
80
81
 
81
82
  const allLocal = await branchList(root, { dryRun, verbose: args.verbose });
82
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];
83
89
 
84
- if (workflowBranches.length === 0) {
90
+ if (choices.length === 0) {
85
91
  if (!quiet) {
86
- console.error("No workflow branches found. Create one with 'gflows start <type> <name>'.");
92
+ console.error("No branches found.");
87
93
  }
88
94
  process.exit(EXIT_OK);
89
95
  }
@@ -91,7 +97,7 @@ export async function run(args: ParsedArgs): Promise<void> {
91
97
  const { select } = await import("@inquirer/prompts");
92
98
  const chosen = await select({
93
99
  message: "Switch to branch",
94
- choices: workflowBranches.map((b) => ({ name: b, value: b })),
100
+ choices: choices.map((b) => ({ name: b, value: b })),
95
101
  });
96
102
 
97
103
  if (typeof chosen !== "string") {
@@ -104,6 +110,7 @@ export async function run(args: ParsedArgs): Promise<void> {
104
110
  });
105
111
  if (!quiet && !dryRun) {
106
112
  success(`Switched to branch '${chosen}'.`);
113
+ // Hint: suggest listing branches
107
114
  hint("Use gflows list to see all workflow branches.");
108
115
  }
109
116
  }