gflows 0.1.10 → 0.1.12

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
@@ -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,30 @@ 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
+ | 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. |
198
+
182
199
  ---
183
200
 
184
201
  ### init
185
202
 
186
203
  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
204
 
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`).
205
+ 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
206
 
190
207
  **Examples:**
191
208
 
@@ -197,7 +214,18 @@ gflows init -C ../other-repo # run in another directory
197
214
  gflows init --dry-run # log intended actions only
198
215
  ```
199
216
 
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`.
217
+ **Flags:**
218
+
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. |
201
229
 
202
230
  ---
203
231
 
@@ -220,7 +248,18 @@ gflows start feature api-v2 --push # create branch and push to rem
220
248
  gflows start chore deps-update -C ./backend # run in subdirectory
221
249
  ```
222
250
 
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`.
251
+ **Flags:**
252
+
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. |
224
263
 
225
264
  ---
226
265
 
@@ -245,7 +284,25 @@ gflows finish -y # skip "Delete branch after finish?"
245
284
 
246
285
  **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
286
 
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`.
287
+ **Flags:**
288
+
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. |
249
306
 
250
307
  ---
251
308
 
@@ -261,7 +318,13 @@ gflows switch feature/auth-refactor
261
318
  gflows -W feature/auth-refactor # same with short command
262
319
  ```
263
320
 
264
- **Flags:** `-C`/`--path`, `-v`, `-q`.
321
+ **Flags:**
322
+
323
+ | Flag | Short | Description |
324
+ |------|-------|-------------|
325
+ | `--path <dir>` | `-C` | Run as if in `<dir>`. |
326
+ | `--verbose` | `-v` | Verbose output. |
327
+ | `--quiet` | `-q` | Minimal output. |
265
328
 
266
329
  ---
267
330
 
@@ -277,7 +340,13 @@ gflows delete feature/old-spike
277
340
  gflows delete feature/one feature/two # delete multiple
278
341
  ```
279
342
 
280
- **Flags:** `-C`/`--path`, `-v`, `-q`.
343
+ **Flags:**
344
+
345
+ | Flag | Short | Description |
346
+ |------|-------|-------------|
347
+ | `--path <dir>` | `-C` | Run as if in `<dir>`. |
348
+ | `--verbose` | `-v` | Verbose output. |
349
+ | `--quiet` | `-q` | Minimal output. |
281
350
 
282
351
  ---
283
352
 
@@ -295,7 +364,15 @@ gflows list -r feature # remote + local feature branches
295
364
  gflows list --include-remote
296
365
  ```
297
366
 
298
- **Flags:** `-r`/`--include-remote`, `-C`/`--path`, `--dry-run`, `-v`, `-q`.
367
+ **Flags:**
368
+
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. |
299
376
 
300
377
  ---
301
378
 
@@ -317,7 +394,14 @@ gflows bump # interactive (direction + type) when T
317
394
  gflows bump --dry-run # print old → new, no file writes
318
395
  ```
319
396
 
320
- **Flags:** `--dry-run`, `-C`/`--path`, `-v`, `-q`.
397
+ **Flags:**
398
+
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. |
321
405
 
322
406
  ---
323
407
 
@@ -332,7 +416,13 @@ gflows status
332
416
  gflows -t
333
417
  ```
334
418
 
335
- **Flags:** `-C`/`--path`, `-v`, `-q`.
419
+ **Flags:**
420
+
421
+ | Flag | Short | Description |
422
+ |------|-------|-------------|
423
+ | `--path <dir>` | `-C` | Run as if in `<dir>`. |
424
+ | `--verbose` | `-v` | Verbose output. |
425
+ | `--quiet` | `-q` | Minimal output. |
336
426
 
337
427
  ---
338
428
 
@@ -386,7 +476,7 @@ Configuration is **optional**. Override branch names, remote, and branch **prefi
386
476
  **Resolution order** (later overrides earlier):
387
477
 
388
478
  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**`.
479
+ 2. Repo config file: `**.gflows.json`** in repo root, or `**gflows`** key in `**package.json**`.
390
480
  3. CLI (e.g. `--main`, `--dev`, `-R`/`--remote`).
391
481
 
392
482
  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.10",
3
+ "version": "0.1.12",
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",
@@ -10,6 +10,7 @@ import type { ParsedArgs } from "../types.js";
10
10
  import type { BumpDirection, BumpType } from "../types.js";
11
11
  import { EXIT_OK, EXIT_USER } from "../constants.js";
12
12
  import { InvalidVersionError } from "../errors.js";
13
+ import { hint, success } from "../out.js";
13
14
 
14
15
  const PACKAGE_JSON = "package.json";
15
16
  const JSR_JSON = "jsr.json";
@@ -195,8 +196,8 @@ export async function run(args: ParsedArgs): Promise<void> {
195
196
 
196
197
  if (dryRun) {
197
198
  if (!quiet) {
198
- console.error(`Would bump version: ${oldVersion} → ${newVersion}`);
199
- console.error(`Would update: ${filesToUpdate.join(", ")}`);
199
+ success(`Would bump version: ${oldVersion} → ${newVersion}`);
200
+ success(`Would update: ${filesToUpdate.join(", ")}`);
200
201
  }
201
202
  process.exit(EXIT_OK);
202
203
  }
@@ -205,9 +206,11 @@ export async function run(args: ParsedArgs): Promise<void> {
205
206
  const jsrUpdated = syncJsrVersion(cwd, newVersion);
206
207
 
207
208
  if (!quiet) {
208
- console.error(`Bumped version: ${oldVersion} → ${newVersion}`);
209
+ success(`Bumped version: ${oldVersion} → ${newVersion}`);
209
210
  const updated = [PACKAGE_JSON];
210
211
  if (jsrUpdated) updated.push(JSR_JSON);
211
- console.error(`Updated: ${updated.join(", ")}`);
212
+ success(`Updated: ${updated.join(", ")}`);
213
+ // Hint: suggest next step — commit and start release branch
214
+ hint("Commit the change, then run gflows start release vX.Y.Z to release.");
212
215
  }
213
216
  }
@@ -13,6 +13,7 @@ import {
13
13
  deleteBranch,
14
14
  resolveRepoRoot,
15
15
  } from "../git.js";
16
+ import { hint, success } from "../out.js";
16
17
 
17
18
  const BRANCH_TYPES: BranchType[] = [
18
19
  "feature",
@@ -75,9 +76,13 @@ export async function run(args: ParsedArgs): Promise<void> {
75
76
  verbose: args.verbose,
76
77
  });
77
78
  if (!quiet && !dryRun) {
78
- console.error(`Deleted branch '${branch}'.`);
79
+ success(`Deleted branch '${branch}'.`);
79
80
  }
80
81
  }
82
+ if (!quiet && !dryRun) {
83
+ // Hint: suggest listing remaining branches
84
+ hint("Use gflows list to see remaining workflow branches.");
85
+ }
81
86
  return;
82
87
  }
83
88
 
@@ -131,7 +136,11 @@ export async function run(args: ParsedArgs): Promise<void> {
131
136
  verbose: args.verbose,
132
137
  });
133
138
  if (!quiet && !dryRun) {
134
- console.error(`Deleted branch '${branch}'.`);
139
+ success(`Deleted branch '${branch}'.`);
135
140
  }
136
141
  }
142
+ if (!quiet && !dryRun && chosen.length > 0) {
143
+ // Hint: suggest listing remaining branches
144
+ hint("Use gflows list to see remaining workflow branches.");
145
+ }
137
146
  }
@@ -26,6 +26,7 @@ import {
26
26
  tag,
27
27
  tagExists,
28
28
  } from "../git.js";
29
+ import { hint, success } from "../out.js";
29
30
 
30
31
  /** Normalizes version to vX.Y.Z for tag name. */
31
32
  function normalizeTagVersion(version: string): string {
@@ -206,7 +207,7 @@ export async function run(args: ParsedArgs): Promise<void> {
206
207
  tagMessage: args.tagMessage,
207
208
  });
208
209
  if (!args.quiet && !args.dryRun) {
209
- console.error(`gflows: created tag '${tagName}'.`);
210
+ success(`gflows: created tag '${tagName}'.`);
210
211
  }
211
212
  }
212
213
 
@@ -237,14 +238,25 @@ export async function run(args: ParsedArgs): Promise<void> {
237
238
  if (shouldDelete && !opts.dryRun) {
238
239
  await deleteBranch(repoRoot, branchToFinish, opts);
239
240
  if (!args.quiet) {
240
- console.error(`gflows: deleted branch '${branchToFinish}'.`);
241
+ success(`gflows: deleted branch '${branchToFinish}'.`);
241
242
  }
242
243
  }
243
244
 
244
- const doPush = args.push && !args.noPush;
245
- const didCreateTag = !!(
245
+ const createdTagName =
246
246
  meta.mergeTarget === "main-then-dev" && meta.tagOnFinish && version && !args.noTag
247
- );
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
+
248
260
  if (doPush) {
249
261
  const remote = args.remote ?? config.remote;
250
262
  const refsToPush: string[] = [config.dev];
@@ -265,11 +277,14 @@ export async function run(args: ParsedArgs): Promise<void> {
265
277
  process.exit(2);
266
278
  }
267
279
  if (!args.quiet && !args.dryRun) {
268
- console.error(`gflows: pushed to ${remote}.`);
280
+ success(`gflows: pushed to ${remote}.`);
269
281
  }
270
282
  }
271
283
 
272
284
  if (!args.quiet && !args.dryRun) {
273
- console.error(`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
288
+ hint("Run gflows start <type> <name> to create a new workflow branch.");
274
289
  }
275
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)
@@ -50,6 +50,11 @@ Finish: --no-ff Always create merge commit; -D/--delete, -N/--no-delete;
50
50
  List: -r, --include-remote Include remote-tracking branches
51
51
 
52
52
  Exit codes: 0 success, 1 usage/validation, 2 Git or system error.
53
+
54
+ Hints:
55
+ • gflows init then gflows start feature <name> — set up and create first branch
56
+ • gflows finish <type> — merge current workflow branch (use -B <name> to specify branch)
57
+ • gflows list -r — include remote branches; gflows status — show current branch flow
53
58
  `;
54
59
  console.log(out.trim());
55
60
  }
@@ -13,6 +13,7 @@ import {
13
13
  revParse,
14
14
  runGit,
15
15
  } from "../git.js";
16
+ import { banner, hint, success } from "../out.js";
16
17
  import type { ParsedArgs } from "../types.js";
17
18
 
18
19
  /**
@@ -35,6 +36,18 @@ export async function run(args: ParsedArgs): Promise<void> {
35
36
  { verbose: args.verbose }
36
37
  );
37
38
 
39
+ if (!args.quiet) {
40
+ banner("gflows init", [
41
+ "Setting up main + dev workflow",
42
+ "",
43
+ ` main ${config.main}`,
44
+ ` dev ${config.dev}`,
45
+ ` remote ${config.remote}`,
46
+ "",
47
+ "→ Dev from main. Use --push to push.",
48
+ ]);
49
+ }
50
+
38
51
  const opts = {
39
52
  dryRun: args.dryRun,
40
53
  verbose: args.verbose,
@@ -59,7 +72,7 @@ export async function run(args: ParsedArgs): Promise<void> {
59
72
  if (!devExists) {
60
73
  await runGit(["branch", config.dev, config.main], { cwd: repoRoot, ...opts });
61
74
  if (!args.quiet && !args.dryRun) {
62
- console.error(`gflows: created branch '${config.dev}' from '${config.main}'.`);
75
+ success(`gflows: created branch '${config.dev}' from '${config.main}'.`);
63
76
  }
64
77
  }
65
78
 
@@ -72,7 +85,7 @@ export async function run(args: ParsedArgs): Promise<void> {
72
85
  );
73
86
  }
74
87
  if (!args.quiet && !args.dryRun) {
75
- console.error(`gflows: pushed '${config.dev}' to '${config.remote}'.`);
88
+ success(`gflows: pushed '${config.dev}' to '${config.remote}'.`);
76
89
  }
77
90
  }
78
91
 
@@ -84,7 +97,12 @@ export async function run(args: ParsedArgs): Promise<void> {
84
97
  ...(args.remote !== undefined && { remote: args.remote }),
85
98
  });
86
99
  if (!args.quiet) {
87
- console.error("gflows: updated .gflows.json with provided options.");
100
+ success("gflows: updated .gflows.json with provided options.");
88
101
  }
89
102
  }
103
+
104
+ if (!args.quiet) {
105
+ // Hint: suggest next step — create first workflow branch
106
+ hint("Run gflows start feature <name> to create a workflow branch.");
107
+ }
90
108
  }
@@ -11,6 +11,7 @@ import type { ResolvedConfig } from "../types.js";
11
11
  import { resolveConfig } from "../config.js";
12
12
  import { NotRepoError } from "../errors.js";
13
13
  import { branchList, fetch, resolveRepoRoot } from "../git.js";
14
+ import { hint } from "../out.js";
14
15
 
15
16
  const BRANCH_TYPES: BranchType[] = [
16
17
  "feature",
@@ -89,5 +90,10 @@ export async function run(args: ParsedArgs): Promise<void> {
89
90
 
90
91
  if (!quiet && sorted.length === 0) {
91
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) {
96
+ // Hint: suggest switching to a listed branch
97
+ hint("Use gflows switch <branch> to switch to a branch.");
92
98
  }
93
99
  }
@@ -8,6 +8,7 @@ import type { BranchType, ParsedArgs } from "../types.js";
8
8
  import { EXIT_USER, VERSION_REGEX } from "../constants.js";
9
9
  import { getPrefixForType, resolveConfig } from "../config.js";
10
10
  import { BranchNotFoundError, DirtyWorkingTreeError, InvalidVersionError } from "../errors.js";
11
+ import { hint, success } from "../out.js";
11
12
  import {
12
13
  assertNoRebaseOrMerge,
13
14
  assertNotDetached,
@@ -122,7 +123,7 @@ export async function run(args: ParsedArgs): Promise<void> {
122
123
  await runGit(["checkout", "-b", fullBranchName, base], { cwd: repoRoot, ...opts });
123
124
 
124
125
  if (!args.quiet && !args.dryRun) {
125
- console.error(`gflows: created and checked out branch '${fullBranchName}' from '${base}'.`);
126
+ success(`gflows: created and checked out branch '${fullBranchName}' from '${base}'.`);
126
127
  }
127
128
 
128
129
  const doPush = args.push && !args.noPush;
@@ -135,7 +136,12 @@ export async function run(args: ParsedArgs): Promise<void> {
135
136
  );
136
137
  }
137
138
  if (!args.quiet && !args.dryRun) {
138
- console.error(`gflows: pushed '${fullBranchName}' to '${remote}'.`);
139
+ success(`gflows: pushed '${fullBranchName}' to '${remote}'.`);
139
140
  }
140
141
  }
142
+
143
+ if (!args.quiet && !args.dryRun) {
144
+ // Hint: suggest next step — merge branch when done
145
+ hint(`When done, run gflows finish ${type} to merge into the target branch.`);
146
+ }
141
147
  }
@@ -18,6 +18,7 @@ import {
18
18
  getCurrentBranch,
19
19
  resolveRepoRoot,
20
20
  } from "../git.js";
21
+ import { hint } from "../out.js";
21
22
 
22
23
  const BRANCH_TYPES: BranchType[] = [
23
24
  "feature",
@@ -137,5 +138,7 @@ export async function run(args: ParsedArgs): Promise<void> {
137
138
 
138
139
  if (!quiet) {
139
140
  console.log(`Ahead/behind: ${ahead} ahead, ${behind} behind`);
141
+ // Hint: suggest next step — finish current branch
142
+ hint(`Run gflows finish ${classification} to merge into ${mergeTargetDisplay}.`);
140
143
  }
141
144
  }
@@ -13,6 +13,7 @@ import {
13
13
  checkout,
14
14
  resolveRepoRoot,
15
15
  } from "../git.js";
16
+ import { hint, success } from "../out.js";
16
17
 
17
18
  const BRANCH_TYPES: BranchType[] = [
18
19
  "feature",
@@ -63,7 +64,9 @@ export async function run(args: ParsedArgs): Promise<void> {
63
64
  verbose: args.verbose,
64
65
  });
65
66
  if (!quiet && !dryRun) {
66
- console.error(`Switched to branch '${branchName}'.`);
67
+ success(`Switched to branch '${branchName}'.`);
68
+ // Hint: suggest listing branches
69
+ hint("Use gflows list to see all workflow branches.");
67
70
  }
68
71
  return;
69
72
  }
@@ -101,6 +104,8 @@ export async function run(args: ParsedArgs): Promise<void> {
101
104
  verbose: args.verbose,
102
105
  });
103
106
  if (!quiet && !dryRun) {
104
- console.error(`Switched to branch '${chosen}'.`);
107
+ success(`Switched to branch '${chosen}'.`);
108
+ // Hint: suggest listing branches
109
+ hint("Use gflows list to see all workflow branches.");
105
110
  }
106
111
  }
package/src/out.ts ADDED
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Stdout helpers for success/info messages so they are not sent to stderr (which many terminals show in red).
3
+ * @module out
4
+ */
5
+
6
+ const GREEN = "\x1b[32m";
7
+ const CYAN = "\x1b[36m";
8
+ const BOLD = "\x1b[1m";
9
+ const DIM = "\x1b[2m";
10
+ const RESET = "\x1b[0m";
11
+ const SUCCESS_ICON = "✓";
12
+
13
+ /**
14
+ * Whether stdout is a TTY and can safely use ANSI color codes.
15
+ */
16
+ function isColorCapable(): boolean {
17
+ return typeof process.stdout.isTTY === "boolean" && process.stdout.isTTY;
18
+ }
19
+
20
+ /**
21
+ * Prints a success message to stdout with a green checkmark icon.
22
+ * Uses stdout so terminals do not render it as error (red). Disables color when not a TTY (e.g. CI).
23
+ *
24
+ * @param message - One-line success message to print (no trailing newline added).
25
+ */
26
+ export function success(message: string): void {
27
+ const icon = SUCCESS_ICON;
28
+ const line = isColorCapable() ? `${GREEN}${icon}${RESET} ${message}` : `${icon} ${message}`;
29
+ console.log(line);
30
+ }
31
+
32
+ /**
33
+ * Prints a hint line to stdout (dim when TTY). Use after success messages to suggest next steps.
34
+ * Skips color when not a TTY (e.g. CI). Omit when --quiet to keep output minimal.
35
+ *
36
+ * @param message - One-line hint (e.g. "Run gflows start feature <name> to create a branch").
37
+ */
38
+ export function hint(message: string): void {
39
+ const line = isColorCapable() ? `${DIM}Hint: ${message}${RESET}` : `Hint: ${message}`;
40
+ console.log(line);
41
+ }
42
+
43
+ const BANNER_INNER_WIDTH = 42;
44
+
45
+ /** Box-drawing characters for table-style banner. */
46
+ const BOX = {
47
+ TL: "╔",
48
+ TR: "╗",
49
+ BL: "╚",
50
+ BR: "╝",
51
+ H: "═",
52
+ V: "║",
53
+ } as const;
54
+
55
+ /**
56
+ * Prints a table-style banner to stdout (e.g. for init). Uses cyan/bold when TTY.
57
+ * Skips color when not a TTY (e.g. CI). Supports multiple detail lines.
58
+ *
59
+ * @param title - Main banner line (e.g. "gflows init").
60
+ * @param lines - Optional lines below the title (subtitle, key-value rows, blank "" for spacing).
61
+ */
62
+ export function banner(title: string, lines?: string[]): void {
63
+ const color = isColorCapable();
64
+ const c = (s: string) => (color ? `${CYAN}${s}${RESET}` : s);
65
+ const inner = BANNER_INNER_WIDTH - 4;
66
+ const top = ` ${c(BOX.TL)}${c(BOX.H.repeat(BANNER_INNER_WIDTH))}${c(BOX.TR)}`;
67
+ const bottom = ` ${c(BOX.BL)}${c(BOX.H.repeat(BANNER_INNER_WIDTH))}${c(BOX.BR)}`;
68
+ const row = (text: string) =>
69
+ " " + c(BOX.V) + " " + text + " ".repeat(Math.max(0, inner - text.length)) + " " + c(BOX.V);
70
+ const titleDisplay = color ? `${CYAN}${BOLD}${title}${RESET}` : title;
71
+ console.log("");
72
+ console.log(top);
73
+ console.log(" " + c(BOX.V) + " " + titleDisplay + " ".repeat(Math.max(0, inner - title.length)) + " " + c(BOX.V));
74
+ if (lines?.length) {
75
+ for (const line of lines) {
76
+ console.log(line === "" ? row("") : row(line));
77
+ }
78
+ }
79
+ console.log(bottom);
80
+ console.log("");
81
+ }