gflows 0.1.16 → 0.1.18
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 +4 -1
- package/package.json +2 -1
- package/src/cli.ts +7 -4
- package/src/commands/bump.ts +5 -4
- package/src/commands/help.ts +1 -0
- package/src/commands/switch.ts +64 -19
- package/src/types.ts +1 -1
package/README.md
CHANGED
|
@@ -265,7 +265,7 @@ Release and hotfix names must be a version (`vX.Y.Z` or `X.Y.Z`). Branch names u
|
|
|
265
265
|
| `init` | `-I` | Ensure main exists; create dev from main. |
|
|
266
266
|
| `start` | `-S` | Create a workflow branch (requires type + name). |
|
|
267
267
|
| `finish` | `-F` | Merge branch into target(s), optional tag (release/hotfix), delete, push. |
|
|
268
|
-
| `switch` | `-W` | Switch to a workflow branch (picker or name); with uncommitted changes: prompt or `--move` / `--restore` / `--clean` / `--cancel`. |
|
|
268
|
+
| `switch` | `-W` | Switch to a workflow branch (picker or name); with uncommitted changes: prompt or `--move` / `--restore` / `--clean` / `--destroy` / `--cancel`. |
|
|
269
269
|
| `delete` | `-L` | Delete local workflow branch(es). Never main/dev. |
|
|
270
270
|
| `list` | `-l` | List workflow branches; optional type filter and remote. |
|
|
271
271
|
| `bump` | — | Bump or rollback package version (patch/minor/major). |
|
|
@@ -404,6 +404,7 @@ Switch to a workflow branch. With TTY and no branch name, shows a picker; otherw
|
|
|
404
404
|
| **Move** | Move current changes to the target branch. |
|
|
405
405
|
| **Restore** | Save changes for this branch; restore target's saved state (if any). |
|
|
406
406
|
| **Clean** | Discard changes and switch clean at HEAD. |
|
|
407
|
+
| **Destroy** | Delete current branch and switch to the target branch (cannot destroy main or dev). |
|
|
407
408
|
| **Cancel** | Abort switching. |
|
|
408
409
|
|
|
409
410
|
You can skip the prompt by passing exactly one of the flags below. If the target branch has saved changes and you use **Clean**, a warning is shown (unless `-q`).
|
|
@@ -415,6 +416,7 @@ bun gflows switch
|
|
|
415
416
|
bun gflows switch feature/auth-refactor
|
|
416
417
|
bun gflows switch dev --restore
|
|
417
418
|
bun gflows switch main --clean
|
|
419
|
+
bun gflows switch dev --destroy
|
|
418
420
|
bun gflows -W feature/auth-refactor
|
|
419
421
|
```
|
|
420
422
|
|
|
@@ -428,6 +430,7 @@ bun gflows -W feature/auth-refactor
|
|
|
428
430
|
| `--move` | — | Move current changes to the target branch; no prompt. |
|
|
429
431
|
| `--restore` | — | Save for this branch; restore target's saved state (if any); no prompt. |
|
|
430
432
|
| `--clean` | — | Discard changes and switch clean at HEAD; no prompt. |
|
|
433
|
+
| `--destroy` | — | Delete current branch and switch to target; no prompt (not main/dev). |
|
|
431
434
|
| `--cancel` | — | Abort switching; no prompt. |
|
|
432
435
|
| `--verbose` | `-v` | Verbose output. |
|
|
433
436
|
| `--quiet` | `-q` | Minimal output (suppresses Clean warning about saved changes on target). |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gflows",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.18",
|
|
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",
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
"lint": "biome check .",
|
|
15
15
|
"lint:fix": "biome check . --write",
|
|
16
16
|
"format": "biome format . --write",
|
|
17
|
+
"check": "bun run test && bun run typecheck && bun run lint",
|
|
17
18
|
"gflows": "bun run src/cli.ts",
|
|
18
19
|
"publish:all": "bun run scripts/publish.ts",
|
|
19
20
|
"publish:npm": "bun run scripts/publish.ts -- --npm-only",
|
package/src/cli.ts
CHANGED
|
@@ -95,11 +95,12 @@ function buildParseArgsOptions() {
|
|
|
95
95
|
// list (-r is context-dependent: list → include-remote; start/finish → release)
|
|
96
96
|
includeRemote: { type: "boolean" as const },
|
|
97
97
|
"include-remote": { type: "boolean" as const },
|
|
98
|
-
// switch: explicit mode (--restore, --clean, --cancel, --move)
|
|
98
|
+
// switch: explicit mode (--restore, --clean, --cancel, --move, --destroy)
|
|
99
99
|
restore: { type: "boolean" as const },
|
|
100
100
|
clean: { type: "boolean" as const },
|
|
101
101
|
cancel: { type: "boolean" as const },
|
|
102
102
|
move: { type: "boolean" as const },
|
|
103
|
+
destroy: { type: "boolean" as const },
|
|
103
104
|
},
|
|
104
105
|
};
|
|
105
106
|
}
|
|
@@ -317,16 +318,17 @@ export function parse(argv: string[] = Bun.argv.slice(2)): ParsedArgs {
|
|
|
317
318
|
else if (command === "completion" && name === "zsh") completionShell = "zsh";
|
|
318
319
|
else if (command === "completion" && name === "fish") completionShell = "fish";
|
|
319
320
|
|
|
320
|
-
let switchMode: "restore" | "clean" | "cancel" | "move" | undefined;
|
|
321
|
+
let switchMode: "restore" | "clean" | "cancel" | "move" | "destroy" | undefined;
|
|
321
322
|
if (command === "switch") {
|
|
322
323
|
const restore = v.restore === true;
|
|
323
324
|
const clean = v.clean === true;
|
|
324
325
|
const cancel = v.cancel === true;
|
|
325
326
|
const move = v.move === true;
|
|
326
|
-
const
|
|
327
|
+
const destroy = v.destroy === true;
|
|
328
|
+
const count = [restore, clean, cancel, move, destroy].filter(Boolean).length;
|
|
327
329
|
if (count > 1) {
|
|
328
330
|
console.error(
|
|
329
|
-
"gflows switch: only one of --restore, --clean, --cancel, or --
|
|
331
|
+
"gflows switch: only one of --restore, --clean, --cancel, --move, or --destroy may be used at a time.",
|
|
330
332
|
);
|
|
331
333
|
process.exit(EXIT_USER);
|
|
332
334
|
}
|
|
@@ -334,6 +336,7 @@ export function parse(argv: string[] = Bun.argv.slice(2)): ParsedArgs {
|
|
|
334
336
|
else if (clean) switchMode = "clean";
|
|
335
337
|
else if (cancel) switchMode = "cancel";
|
|
336
338
|
else if (move) switchMode = "move";
|
|
339
|
+
else if (destroy) switchMode = "destroy";
|
|
337
340
|
}
|
|
338
341
|
|
|
339
342
|
return {
|
package/src/commands/bump.ts
CHANGED
|
@@ -160,15 +160,16 @@ function writePackageVersion(dir: string, newVersion: string): void {
|
|
|
160
160
|
}
|
|
161
161
|
|
|
162
162
|
/**
|
|
163
|
-
* Updates version in jsr.json if the file exists
|
|
163
|
+
* Updates version in jsr.json if the file exists. Only the version value is changed;
|
|
164
|
+
* the rest of the file (format, commas, spacing) is left unchanged.
|
|
164
165
|
*/
|
|
165
166
|
function syncJsrVersion(dir: string, newVersion: string): boolean {
|
|
166
167
|
const path = join(dir, JSR_JSON);
|
|
167
168
|
if (!existsSync(path)) return false;
|
|
168
169
|
const raw = readFileSync(path, "utf-8");
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
writeFileSync(path,
|
|
170
|
+
const updated = raw.replace(/"version":\s*"[^"]*"/, `"version": "${newVersion}"`);
|
|
171
|
+
if (updated === raw) return false;
|
|
172
|
+
writeFileSync(path, updated, "utf-8");
|
|
172
173
|
return true;
|
|
173
174
|
}
|
|
174
175
|
|
package/src/commands/help.ts
CHANGED
|
@@ -51,6 +51,7 @@ List: -r, --include-remote Include remote-tracking branches
|
|
|
51
51
|
Switch: --move Move current changes to the target branch
|
|
52
52
|
--restore Save for this branch; restore target's saved state (if any)
|
|
53
53
|
--clean Discard changes and switch clean at HEAD
|
|
54
|
+
--destroy Delete current branch and switch to target (not main/dev)
|
|
54
55
|
--cancel Abort switching
|
|
55
56
|
|
|
56
57
|
Exit codes: 0 success, 1 usage/validation, 2 Git or system error.
|
package/src/commands/switch.ts
CHANGED
|
@@ -5,11 +5,12 @@
|
|
|
5
5
|
|
|
6
6
|
import { resolveConfig } from "../config.js";
|
|
7
7
|
import { EXIT_GIT, EXIT_OK, EXIT_USER } from "../constants.js";
|
|
8
|
-
import { NotRepoError } from "../errors.js";
|
|
8
|
+
import { CannotDeleteMainOrDevError, NotRepoError } from "../errors.js";
|
|
9
9
|
import {
|
|
10
10
|
branchList,
|
|
11
11
|
checkout,
|
|
12
12
|
cleanUntracked,
|
|
13
|
+
deleteBranch,
|
|
13
14
|
findStashRefByBranch,
|
|
14
15
|
findStashRefByMessage,
|
|
15
16
|
getCurrentBranch,
|
|
@@ -26,7 +27,7 @@ import { hint, success } from "../out.js";
|
|
|
26
27
|
import type { BranchType, ParsedArgs } from "../types.js";
|
|
27
28
|
|
|
28
29
|
/** User choice when switching with uncommitted changes. */
|
|
29
|
-
type SwitchWhenUncommitted = "cancel" | "restore" | "clean" | "move";
|
|
30
|
+
type SwitchWhenUncommitted = "cancel" | "restore" | "clean" | "move" | "destroy";
|
|
30
31
|
|
|
31
32
|
const BRANCH_TYPES: BranchType[] = ["feature", "bugfix", "chore", "release", "hotfix", "spike"];
|
|
32
33
|
|
|
@@ -87,10 +88,17 @@ export async function run(args: ParsedArgs): Promise<void> {
|
|
|
87
88
|
process.exit(EXIT_OK);
|
|
88
89
|
}
|
|
89
90
|
|
|
91
|
+
const currentBranchForPicker = await getCurrentBranch(root, {
|
|
92
|
+
dryRun,
|
|
93
|
+
verbose: args.verbose,
|
|
94
|
+
});
|
|
90
95
|
const { select } = await import("@inquirer/prompts");
|
|
91
96
|
const chosen = await select({
|
|
92
97
|
message: "Switch to branch",
|
|
93
|
-
choices: choices.map((b) => ({
|
|
98
|
+
choices: choices.map((b) => ({
|
|
99
|
+
name: currentBranchForPicker && b === currentBranchForPicker ? `${b} (current)` : b,
|
|
100
|
+
value: b,
|
|
101
|
+
})),
|
|
94
102
|
});
|
|
95
103
|
|
|
96
104
|
if (typeof chosen !== "string") {
|
|
@@ -117,6 +125,10 @@ export async function run(args: ParsedArgs): Promise<void> {
|
|
|
117
125
|
value: "restore" as const,
|
|
118
126
|
},
|
|
119
127
|
{ name: "Clean — Discard changes and switch clean at HEAD", value: "clean" as const },
|
|
128
|
+
{
|
|
129
|
+
name: "Destroy — Delete current branch and switch to the target branch",
|
|
130
|
+
value: "destroy" as const,
|
|
131
|
+
},
|
|
120
132
|
{ name: "Cancel — Abort switching", value: "cancel" as const },
|
|
121
133
|
],
|
|
122
134
|
});
|
|
@@ -132,6 +144,31 @@ export async function run(args: ParsedArgs): Promise<void> {
|
|
|
132
144
|
process.exit(EXIT_USER);
|
|
133
145
|
}
|
|
134
146
|
|
|
147
|
+
let branchToDestroy: string | undefined;
|
|
148
|
+
if (whenUncommitted === "destroy") {
|
|
149
|
+
const currentBranch = await getCurrentBranch(root, gitOpts);
|
|
150
|
+
if (!currentBranch) {
|
|
151
|
+
console.error("gflows switch: cannot destroy when HEAD is detached.");
|
|
152
|
+
process.exit(EXIT_USER);
|
|
153
|
+
}
|
|
154
|
+
if (currentBranch === config.main || currentBranch === config.dev) {
|
|
155
|
+
throw new CannotDeleteMainOrDevError(
|
|
156
|
+
`Cannot destroy the long-lived branch '${currentBranch}'.`,
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
if (currentBranch === targetBranch) {
|
|
160
|
+
console.error(
|
|
161
|
+
"gflows switch: cannot destroy current branch when switching to the same branch.",
|
|
162
|
+
);
|
|
163
|
+
process.exit(EXIT_USER);
|
|
164
|
+
}
|
|
165
|
+
branchToDestroy = currentBranch;
|
|
166
|
+
if (!treeClean) {
|
|
167
|
+
await restoreTracked(root, gitOpts);
|
|
168
|
+
await cleanUntracked(root, gitOpts);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
135
172
|
if (!treeClean && whenUncommitted === "move") {
|
|
136
173
|
await stashPushMove(root, gitOpts);
|
|
137
174
|
}
|
|
@@ -181,26 +218,34 @@ export async function run(args: ParsedArgs): Promise<void> {
|
|
|
181
218
|
}
|
|
182
219
|
}
|
|
183
220
|
|
|
184
|
-
|
|
221
|
+
const tryRestoreTargetStash = async (): Promise<void> => {
|
|
185
222
|
const targetStashRef = await findStashRefByBranch(root, targetBranch, gitOpts);
|
|
186
|
-
if (targetStashRef)
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
}
|
|
223
|
+
if (!targetStashRef) return;
|
|
224
|
+
try {
|
|
225
|
+
await stashPopRef(root, targetStashRef, gitOpts);
|
|
226
|
+
} catch (err) {
|
|
227
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
228
|
+
console.error(
|
|
229
|
+
`gflows switch: conflicts occurred while restoring saved changes for '${targetBranch}'.`,
|
|
230
|
+
);
|
|
231
|
+
console.error(
|
|
232
|
+
"The stash was not dropped. Resolve conflicts, then commit or run `git stash drop` as needed.",
|
|
233
|
+
);
|
|
234
|
+
if (args.verbose && msg) console.error(msg);
|
|
235
|
+
process.exit(EXIT_GIT);
|
|
200
236
|
}
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
if (whenUncommitted === "restore" || whenUncommitted === undefined) {
|
|
240
|
+
await tryRestoreTargetStash();
|
|
201
241
|
}
|
|
202
242
|
|
|
203
|
-
if (
|
|
243
|
+
if (branchToDestroy) {
|
|
244
|
+
await deleteBranch(root, branchToDestroy, gitOpts);
|
|
245
|
+
if (!quiet && !dryRun) {
|
|
246
|
+
success(`Deleted branch '${branchToDestroy}' and switched to '${targetBranch}'.`);
|
|
247
|
+
}
|
|
248
|
+
} else if (!quiet && !dryRun) {
|
|
204
249
|
success(`Switched to branch '${targetBranch}'.`);
|
|
205
250
|
hint("Use gflows list to see all workflow branches.");
|
|
206
251
|
}
|
package/src/types.ts
CHANGED
|
@@ -120,5 +120,5 @@ export interface ParsedArgs {
|
|
|
120
120
|
// list
|
|
121
121
|
includeRemote: boolean;
|
|
122
122
|
// switch: explicit mode when uncommitted (overrides prompt)
|
|
123
|
-
switchMode?: "restore" | "clean" | "cancel" | "move";
|
|
123
|
+
switchMode?: "restore" | "clean" | "cancel" | "move" | "destroy";
|
|
124
124
|
}
|