gflows 0.1.1

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.
@@ -0,0 +1,275 @@
1
+ /**
2
+ * Finish command: merge workflow branch into target(s), optional tag, delete branch, and push.
3
+ * Resolves branch (current, -B <name>, or picker when -B with no value and TTY).
4
+ * Uses normal merge with optional --no-ff; on conflict exits with clear message. Release/hotfix: merge to main then dev, create tag.
5
+ * @module commands/finish
6
+ */
7
+
8
+ import type { BranchType, ParsedArgs } from "../types.js";
9
+ import { EXIT_USER, VERSION_REGEX } from "../constants.js";
10
+ import {
11
+ getBranchTypeMeta,
12
+ getPrefixForType,
13
+ resolveConfig,
14
+ } from "../config.js";
15
+ import { BranchNotFoundError } from "../errors.js";
16
+ import {
17
+ assertNoRebaseOrMerge,
18
+ assertNotDetached,
19
+ branchList,
20
+ checkout,
21
+ deleteBranch,
22
+ getCurrentBranch,
23
+ merge,
24
+ push,
25
+ resolveRepoRoot,
26
+ tag,
27
+ tagExists,
28
+ } from "../git.js";
29
+
30
+ /** Normalizes version to vX.Y.Z for tag name. */
31
+ function normalizeTagVersion(version: string): string {
32
+ const v = version.trim();
33
+ return v.startsWith("v") ? v : `v${v}`;
34
+ }
35
+
36
+ /**
37
+ * Returns workflow branches (local) that match any configured prefix.
38
+ */
39
+ async function getWorkflowBranches(
40
+ cwd: string,
41
+ prefixes: Record<BranchType, string>
42
+ ): Promise<string[]> {
43
+ const all = await branchList(cwd, { dryRun: false, verbose: false });
44
+ const workflow: string[] = [];
45
+ for (const b of all) {
46
+ for (const prefix of Object.values(prefixes)) {
47
+ if (b.startsWith(prefix)) {
48
+ workflow.push(b);
49
+ break;
50
+ }
51
+ }
52
+ }
53
+ return workflow.sort();
54
+ }
55
+
56
+ /**
57
+ * Infers branch type and optional version from branch name using config prefixes.
58
+ */
59
+ function parseBranchTypeAndVersion(
60
+ branchName: string,
61
+ prefixes: Record<BranchType, string>
62
+ ): { type: BranchType; version?: string } | null {
63
+ for (const type of [
64
+ "release",
65
+ "hotfix",
66
+ "feature",
67
+ "bugfix",
68
+ "chore",
69
+ "spike",
70
+ ] as BranchType[]) {
71
+ const prefix = prefixes[type];
72
+ if (prefix && branchName.startsWith(prefix)) {
73
+ const suffix = branchName.slice(prefix.length);
74
+ if (type === "release" || type === "hotfix") {
75
+ const ver = suffix.trim();
76
+ return VERSION_REGEX.test(ver) ? { type, version: ver } : { type };
77
+ }
78
+ return { type };
79
+ }
80
+ }
81
+ return null;
82
+ }
83
+
84
+ /**
85
+ * Runs the finish command: resolve branch, run pre-checks, merge to target(s), optional tag/delete/push.
86
+ * Pre-checks: repo, not detached HEAD, no rebase/merge, branch is not main/dev; for release/hotfix tag must not exist.
87
+ *
88
+ * @param args - Parsed CLI args (cwd, type, branch, push, noPush, remote, dryRun, verbose, quiet, yes, noFf, deleteAfterFinish, noDeleteAfterFinish, signTag, noTag, tagMessage).
89
+ */
90
+ export async function run(args: ParsedArgs): Promise<void> {
91
+ const repoRoot = await resolveRepoRoot(args.cwd);
92
+ const config = resolveConfig(
93
+ repoRoot,
94
+ { remote: args.remote },
95
+ { verbose: args.verbose }
96
+ );
97
+
98
+ const opts: { dryRun: boolean; verbose: boolean } = {
99
+ dryRun: args.dryRun,
100
+ verbose: args.verbose,
101
+ };
102
+
103
+ const isTTY = Boolean(process.stdin.isTTY);
104
+
105
+ // Resolve branch to finish (and guard main/dev early so we can error before picker)
106
+ let branchToFinish: string;
107
+ const explicitBranch =
108
+ typeof args.branch === "string" && args.branch.trim() !== ""
109
+ ? args.branch.trim()
110
+ : undefined;
111
+
112
+ if (explicitBranch) {
113
+ branchToFinish = explicitBranch;
114
+ } else if (isTTY) {
115
+ const workflow = await getWorkflowBranches(repoRoot, config.prefixes);
116
+ if (workflow.length === 0) {
117
+ console.error("gflows finish: no workflow branches found.");
118
+ process.exit(EXIT_USER);
119
+ }
120
+ const { select } = await import("@inquirer/prompts");
121
+ branchToFinish = await select({
122
+ message: "Branch to finish",
123
+ choices: workflow.map((b) => ({ name: b, value: b })),
124
+ });
125
+ } else {
126
+ const current = await getCurrentBranch(repoRoot, opts);
127
+ if (!current) {
128
+ console.error(
129
+ "gflows finish: HEAD is detached. Checkout a branch or specify one with -B <name>."
130
+ );
131
+ process.exit(EXIT_USER);
132
+ }
133
+ branchToFinish = current;
134
+ }
135
+
136
+ // Refuse finish on main or dev
137
+ if (branchToFinish === config.main || branchToFinish === config.dev) {
138
+ console.error(
139
+ `gflows finish: cannot finish the long-lived branch '${branchToFinish}'. Finish a workflow branch (feature, bugfix, etc.) instead.`
140
+ );
141
+ process.exit(2);
142
+ }
143
+
144
+ // Infer type from branch name (or use args.type if provided and consistent)
145
+ const parsed = parseBranchTypeAndVersion(branchToFinish, config.prefixes);
146
+ const type: BranchType | undefined = args.type ?? parsed?.type ?? undefined;
147
+ if (!type) {
148
+ console.error(
149
+ `gflows finish: cannot determine branch type for '${branchToFinish}'. Specify type (e.g. gflows finish feature) or use a branch name with a known prefix.`
150
+ );
151
+ process.exit(EXIT_USER);
152
+ }
153
+ if (parsed && parsed.type !== type) {
154
+ console.error(
155
+ `gflows finish: branch '${branchToFinish}' matches type '${parsed.type}', but '${type}' was specified.`
156
+ );
157
+ process.exit(EXIT_USER);
158
+ }
159
+
160
+ const meta = getBranchTypeMeta(type);
161
+ const version = parsed?.version;
162
+
163
+ // Pre-checks: not detached, no rebase/merge
164
+ await assertNotDetached(repoRoot);
165
+ assertNoRebaseOrMerge(repoRoot);
166
+
167
+ // For release/hotfix: tag must not already exist
168
+ if (meta.tagOnFinish && version) {
169
+ const tagName = normalizeTagVersion(version);
170
+ const exists = await tagExists(repoRoot, tagName, opts);
171
+ if (exists) {
172
+ console.error(`gflows finish: tag '${tagName}' already exists.`);
173
+ process.exit(2);
174
+ }
175
+ } else if (meta.tagOnFinish && !version) {
176
+ console.error(
177
+ `gflows finish: release/hotfix branch '${branchToFinish}' has no valid version segment. Use format release/vX.Y.Z or hotfix/vX.Y.Z.`
178
+ );
179
+ process.exit(EXIT_USER);
180
+ }
181
+
182
+ // Ensure the branch we're finishing exists (e.g. if -B was used)
183
+ const branches = await branchList(repoRoot, { ...opts, dryRun: false });
184
+ if (!branches.includes(branchToFinish)) {
185
+ throw new BranchNotFoundError(
186
+ `Branch '${branchToFinish}' not found. Specify an existing local branch with -B <name>.`
187
+ );
188
+ }
189
+
190
+ const noFf = args.noFf;
191
+
192
+ try {
193
+ if (meta.mergeTarget === "dev") {
194
+ await checkout(repoRoot, config.dev, opts);
195
+ await merge(repoRoot, branchToFinish, { ...opts, noFf });
196
+ } else {
197
+ // main-then-dev: merge into main first, then merge main into dev
198
+ await checkout(repoRoot, config.main, opts);
199
+ await merge(repoRoot, branchToFinish, { ...opts, noFf });
200
+
201
+ if (meta.tagOnFinish && version && !args.noTag) {
202
+ const tagName = normalizeTagVersion(version);
203
+ await tag(repoRoot, tagName, {
204
+ ...opts,
205
+ sign: args.signTag,
206
+ tagMessage: args.tagMessage,
207
+ });
208
+ if (!args.quiet && !args.dryRun) {
209
+ console.error(`gflows: created tag '${tagName}'.`);
210
+ }
211
+ }
212
+
213
+ await checkout(repoRoot, config.dev, opts);
214
+ await merge(repoRoot, config.main, { ...opts, noFf });
215
+ }
216
+ } catch (err) {
217
+ throw err;
218
+ }
219
+
220
+ // Optional: delete the finished branch
221
+ let shouldDelete = args.deleteAfterFinish;
222
+ if (!args.deleteAfterFinish && !args.noDeleteAfterFinish) {
223
+ if (args.yes) {
224
+ shouldDelete = false;
225
+ } else if (isTTY) {
226
+ const { confirm } = await import("@inquirer/prompts");
227
+ shouldDelete = await confirm({
228
+ message: "Delete branch after finish?",
229
+ default: false,
230
+ });
231
+ }
232
+ }
233
+ if (args.noDeleteAfterFinish) {
234
+ shouldDelete = false;
235
+ }
236
+
237
+ if (shouldDelete && !opts.dryRun) {
238
+ await deleteBranch(repoRoot, branchToFinish, opts);
239
+ if (!args.quiet) {
240
+ console.error(`gflows: deleted branch '${branchToFinish}'.`);
241
+ }
242
+ }
243
+
244
+ const doPush = args.push && !args.noPush;
245
+ const didCreateTag = !!(
246
+ meta.mergeTarget === "main-then-dev" && meta.tagOnFinish && version && !args.noTag
247
+ );
248
+ if (doPush) {
249
+ const remote = args.remote ?? config.remote;
250
+ const refsToPush: string[] = [config.dev];
251
+ if (meta.mergeTarget === "main-then-dev") {
252
+ refsToPush.push(config.main);
253
+ }
254
+ const pushCode = await push(
255
+ repoRoot,
256
+ remote,
257
+ refsToPush,
258
+ didCreateTag,
259
+ opts
260
+ );
261
+ if (pushCode !== 0) {
262
+ console.error(
263
+ "gflows: merge and tag succeeded locally, but push failed. Retry with `git push` or `gflows finish ... --push`."
264
+ );
265
+ process.exit(2);
266
+ }
267
+ if (!args.quiet && !args.dryRun) {
268
+ console.error(`gflows: pushed to ${remote}.`);
269
+ }
270
+ }
271
+
272
+ if (!args.quiet && !args.dryRun) {
273
+ console.error(`gflows: finished '${branchToFinish}' into ${meta.mergeTarget}.`);
274
+ }
275
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Help command: print quick reference from the spec and list commands/flags.
3
+ * @module commands/help
4
+ */
5
+
6
+ import type { ParsedArgs } from "../types.js";
7
+
8
+ /**
9
+ * Runs the help command: prints usage, commands, types, flags, and exit codes to stdout.
10
+ * @param _args - Parsed CLI args (unused; kept for command signature consistency).
11
+ */
12
+ export async function run(_args: ParsedArgs): Promise<void> {
13
+ const out = `
14
+ gflows — Modern Git branching workflow CLI
15
+
16
+ Usage: gflows <command> [type] [name] [flags]
17
+
18
+ Commands:
19
+ init, -I Ensure main, create dev
20
+ start, -S Create workflow branch
21
+ finish, -F Merge and close branch
22
+ switch, -W Switch branch (picker or name)
23
+ delete, -L Delete local branch(es)
24
+ list, -l List branches by type
25
+ bump Bump or rollback package version
26
+ completion Print shell completion script
27
+ status, -t Show current branch flow info
28
+ help, -h Show this usage
29
+ version, -V Show version
30
+
31
+ Types: feature (-f), bugfix (-b), chore (-c), release (-r), hotfix (-x), spike (-e)
32
+
33
+ Common flags:
34
+ -p, --push Push after init/start/finish
35
+ -P, --no-push Do not push
36
+ -R, --remote <name> Remote name for push
37
+ -o, --from <branch> Base branch override (e.g. -o main for bugfix)
38
+ -B, --branch <name> Branch name (finish: branch to finish)
39
+ -y, --yes Skip confirmations
40
+ -d, --dry-run Log actions only, no writes
41
+ -v, --verbose Verbose output
42
+ -q, --quiet Minimal output
43
+ -C, --path <dir> Run as if in <dir>
44
+
45
+ Start: --force Allow dirty working tree
46
+ Finish: --no-ff Always create merge commit; -D/--delete, -N/--no-delete;
47
+ -s/--sign, -T/--no-tag, -M/--tag-message, -m/--message
48
+ List: -r, --include-remote Include remote-tracking branches
49
+
50
+ Exit codes: 0 success, 1 usage/validation, 2 Git or system error.
51
+ `;
52
+ console.log(out.trim());
53
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Init command: ensure main exists, create dev from main, optional push and dry-run.
3
+ * @module commands/init
4
+ */
5
+
6
+ import { resolveConfig } from "../config.js";
7
+ import { BranchNotFoundError, NotRepoError } from "../errors.js";
8
+ import {
9
+ branchList,
10
+ push,
11
+ resolveRepoRoot,
12
+ revParse,
13
+ runGit,
14
+ } from "../git.js";
15
+ import type { ParsedArgs } from "../types.js";
16
+
17
+ /**
18
+ * Runs the init command: ensure main exists, create dev from main if missing, optional push.
19
+ * Pre-check: cwd (or -C) must be a git repo; main branch must exist (exit 2 otherwise).
20
+ * Skips creating dev if it already exists. Supports --dry-run and --push.
21
+ *
22
+ * @param args - Parsed CLI args (cwd, dryRun, push, noPush, remote, verbose, quiet).
23
+ */
24
+ export async function run(args: ParsedArgs): Promise<void> {
25
+ const repoRoot = await resolveRepoRoot(args.cwd);
26
+ const config = resolveConfig(repoRoot, {
27
+ remote: args.remote,
28
+ }, { verbose: args.verbose });
29
+
30
+ const opts = {
31
+ dryRun: args.dryRun,
32
+ verbose: args.verbose,
33
+ };
34
+
35
+ // Ensure main branch exists
36
+ try {
37
+ await revParse(repoRoot, config.main, [], { dryRun: false, verbose: opts.verbose });
38
+ } catch (err) {
39
+ if (err instanceof NotRepoError) throw err;
40
+ if (err instanceof BranchNotFoundError) {
41
+ throw new BranchNotFoundError(
42
+ `Main branch '${config.main}' does not exist. Create an initial commit and the main branch first.`
43
+ );
44
+ }
45
+ throw err;
46
+ }
47
+
48
+ const branches = await branchList(repoRoot, { ...opts, dryRun: false });
49
+ const devExists = branches.includes(config.dev);
50
+
51
+ if (!devExists) {
52
+ await runGit(["branch", config.dev, config.main], { cwd: repoRoot, ...opts });
53
+ if (!args.quiet && !args.dryRun) {
54
+ console.error(`gflows: created branch '${config.dev}' from '${config.main}'.`);
55
+ }
56
+ }
57
+
58
+ const doPush = args.push && !args.noPush;
59
+ if (doPush) {
60
+ const pushCode = await push(repoRoot, config.remote, [config.dev], false, opts);
61
+ if (pushCode !== 0) {
62
+ throw new Error(
63
+ `Push failed. Local branch '${config.dev}' was created. Retry with \`git push ${config.remote} ${config.dev}\` or \`gflows init --push\`.`
64
+ );
65
+ }
66
+ if (!args.quiet && !args.dryRun) {
67
+ console.error(`gflows: pushed '${config.dev}' to '${config.remote}'.`);
68
+ }
69
+ }
70
+ }
@@ -0,0 +1,89 @@
1
+ /**
2
+ * List command: list workflow branches using resolved config prefixes;
3
+ * optional type filter and -r/--include-remote for remote-tracking branches.
4
+ * Script-friendly: one branch per line to stdout.
5
+ * @module commands/list
6
+ */
7
+
8
+ import type { BranchType } from "../types.js";
9
+ import type { ParsedArgs } from "../types.js";
10
+ import type { ResolvedConfig } from "../types.js";
11
+ import { resolveConfig } from "../config.js";
12
+ import { NotRepoError } from "../errors.js";
13
+ import { branchList, fetch, resolveRepoRoot } from "../git.js";
14
+
15
+ const BRANCH_TYPES: BranchType[] = [
16
+ "feature",
17
+ "bugfix",
18
+ "chore",
19
+ "release",
20
+ "hotfix",
21
+ "spike",
22
+ ];
23
+
24
+ /**
25
+ * Returns branch names that match any workflow prefix. If typeFilter is set,
26
+ * only branches with that type's prefix are included. Excludes main and dev.
27
+ */
28
+ function filterWorkflowBranches(
29
+ allBranches: string[],
30
+ config: ResolvedConfig,
31
+ typeFilter: BranchType | undefined
32
+ ): string[] {
33
+ const { main, dev, prefixes } = config;
34
+ const prefixesToMatch =
35
+ typeFilter !== undefined
36
+ ? [prefixes[typeFilter]]
37
+ : BRANCH_TYPES.map((t) => prefixes[t]);
38
+
39
+ return allBranches.filter((b) => {
40
+ if (b === main || b === dev) return false;
41
+ return prefixesToMatch.some((p) => p && b.startsWith(p));
42
+ });
43
+ }
44
+
45
+ /**
46
+ * Runs the list command.
47
+ * Lists workflow branches (matching config prefixes), optionally filtered by type
48
+ * and optionally including remote-tracking branches (-r/--include-remote).
49
+ * When includeRemote is true, fetches from the configured remote first so refs are up to date.
50
+ * Output: one branch per line to stdout (script-friendly).
51
+ */
52
+ export async function run(args: ParsedArgs): Promise<void> {
53
+ const { cwd, type: typeFilter, includeRemote, dryRun, verbose, quiet } = args;
54
+
55
+ const root = await resolveRepoRoot(cwd).catch((err: unknown) => {
56
+ if (err instanceof NotRepoError) throw err;
57
+ throw err;
58
+ });
59
+
60
+ const config = resolveConfig(root, undefined, { verbose: !!verbose });
61
+
62
+ if (includeRemote && !dryRun) {
63
+ await fetch(root, config.remote, {
64
+ verbose: !!verbose,
65
+ });
66
+ }
67
+
68
+ const allBranches = await branchList(root, {
69
+ includeRemote: includeRemote ?? false,
70
+ dryRun: !!dryRun,
71
+ verbose: !!verbose,
72
+ });
73
+
74
+ const workflowBranches = filterWorkflowBranches(
75
+ allBranches,
76
+ config,
77
+ typeFilter
78
+ );
79
+
80
+ const sorted = [...workflowBranches].sort();
81
+
82
+ for (const b of sorted) {
83
+ console.log(b);
84
+ }
85
+
86
+ if (!quiet && sorted.length === 0) {
87
+ console.error("No workflow branches found.");
88
+ }
89
+ }
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Start command: create a workflow branch from the appropriate base.
3
+ * Ensures clean tree (or --force), base exists (local or after fetch), creates type/name branch, optional push.
4
+ * @module commands/start
5
+ */
6
+
7
+ import type { BranchType, ParsedArgs } from "../types.js";
8
+ import { EXIT_USER, VERSION_REGEX } from "../constants.js";
9
+ import { getPrefixForType, resolveConfig } from "../config.js";
10
+ import { BranchNotFoundError, DirtyWorkingTreeError, InvalidVersionError } from "../errors.js";
11
+ import {
12
+ assertNoRebaseOrMerge,
13
+ assertNotDetached,
14
+ branchList,
15
+ fetch,
16
+ isClean,
17
+ push,
18
+ resolveRepoRoot,
19
+ revParse,
20
+ runGit,
21
+ validateBranchName,
22
+ } from "../git.js";
23
+
24
+ /**
25
+ * Returns the base branch name for the given type and fromMain flag (main vs dev).
26
+ */
27
+ function getBaseBranch(
28
+ type: BranchType,
29
+ fromMain: boolean,
30
+ main: string,
31
+ dev: string
32
+ ): string {
33
+ if (type === "hotfix") return main;
34
+ if (type === "bugfix" && fromMain) return main;
35
+ return dev;
36
+ }
37
+
38
+ /**
39
+ * Runs the start command: validate pre-conditions, ensure base exists, create branch, optional push.
40
+ * Pre-checks: repo is git, not detached HEAD, no rebase/merge in progress, working tree clean (or --force), base exists.
41
+ * Requires type and name (exit 1 if missing). For release/hotfix, name must match vX.Y.Z or X.Y.Z.
42
+ *
43
+ * @param args - Parsed CLI args (cwd, type, name, push, noPush, remote, dryRun, verbose, quiet, force, fromMain).
44
+ */
45
+ export async function run(args: ParsedArgs): Promise<void> {
46
+ if (!args.type || args.name === undefined || args.name === "") {
47
+ console.error("gflows start: requires type and name (e.g. gflows start feature my-feat). Use 'gflows help' for usage.");
48
+ process.exit(EXIT_USER);
49
+ }
50
+
51
+ const type = args.type;
52
+ const name = args.name.trim();
53
+ const repoRoot = await resolveRepoRoot(args.cwd);
54
+ const config = resolveConfig(
55
+ repoRoot,
56
+ { remote: args.remote },
57
+ { verbose: args.verbose }
58
+ );
59
+
60
+ const opts = {
61
+ dryRun: args.dryRun,
62
+ verbose: args.verbose,
63
+ };
64
+
65
+ // Pre-checks: not detached HEAD, no rebase/merge in progress
66
+ await assertNotDetached(repoRoot);
67
+ assertNoRebaseOrMerge(repoRoot);
68
+
69
+ // Working tree clean or --force
70
+ if (!args.force) {
71
+ const clean = await isClean(repoRoot, { dryRun: false, verbose: opts.verbose });
72
+ if (!clean) {
73
+ throw new DirtyWorkingTreeError();
74
+ }
75
+ }
76
+
77
+ // Validate name: version for release/hotfix, branch name for others
78
+ if (type === "release" || type === "hotfix") {
79
+ if (!VERSION_REGEX.test(name)) {
80
+ throw new InvalidVersionError(
81
+ `Invalid version '${name}'. Use format vX.Y.Z or X.Y.Z (e.g. v1.2.0).`
82
+ );
83
+ }
84
+ } else {
85
+ validateBranchName(name);
86
+ }
87
+
88
+ const base = getBaseBranch(type, args.fromMain, config.main, config.dev);
89
+
90
+ // Ensure base exists (local or create from remote after fetch)
91
+ let baseExists = false;
92
+ try {
93
+ await revParse(repoRoot, base, [], { dryRun: false, verbose: opts.verbose });
94
+ baseExists = true;
95
+ } catch {
96
+ // Fetch and try remote ref
97
+ await fetch(repoRoot, config.remote, opts);
98
+ const remoteRef = `${config.remote}/${base}`;
99
+ try {
100
+ await revParse(repoRoot, remoteRef, [], { dryRun: false, verbose: opts.verbose });
101
+ if (!opts.dryRun) {
102
+ await runGit(["branch", base, remoteRef], { cwd: repoRoot, ...opts, dryRun: false });
103
+ }
104
+ baseExists = true;
105
+ } catch {
106
+ throw new BranchNotFoundError(
107
+ `Base branch '${base}' not found locally or on ${config.remote}. Create it or push it first.`
108
+ );
109
+ }
110
+ }
111
+
112
+ const prefix = getPrefixForType(config, type);
113
+ const fullBranchName = `${prefix}${name}`;
114
+
115
+ const branches = await branchList(repoRoot, { ...opts, dryRun: false });
116
+ if (branches.includes(fullBranchName)) {
117
+ throw new BranchNotFoundError(
118
+ `Branch '${fullBranchName}' already exists. Use a different name or switch to it.`
119
+ );
120
+ }
121
+
122
+ await runGit(["checkout", "-b", fullBranchName, base], { cwd: repoRoot, ...opts });
123
+
124
+ if (!args.quiet && !args.dryRun) {
125
+ console.error(`gflows: created and checked out branch '${fullBranchName}' from '${base}'.`);
126
+ }
127
+
128
+ const doPush = args.push && !args.noPush;
129
+ if (doPush) {
130
+ const remote = args.remote ?? config.remote;
131
+ const pushCode = await push(repoRoot, remote, [fullBranchName], false, opts);
132
+ if (pushCode !== 0) {
133
+ throw new Error(
134
+ `Push failed. Local branch '${fullBranchName}' was created. Retry with \`git push ${remote} ${fullBranchName}\` or \`gflows start ... --push\`.`
135
+ );
136
+ }
137
+ if (!args.quiet && !args.dryRun) {
138
+ console.error(`gflows: pushed '${fullBranchName}' to '${remote}'.`);
139
+ }
140
+ }
141
+ }