gflows 0.1.12 → 0.1.14
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 +328 -341
- package/package.json +6 -2
- package/src/cli.ts +64 -21
- package/src/commands/bump.ts +81 -31
- package/src/commands/completion.ts +21 -30
- package/src/commands/delete.ts +10 -30
- package/src/commands/finish.ts +34 -58
- package/src/commands/help.ts +1 -1
- package/src/commands/init.ts +8 -13
- package/src/commands/list.ts +10 -27
- package/src/commands/start.ts +15 -18
- package/src/commands/status.ts +11 -28
- package/src/commands/switch.ts +12 -23
- package/src/config.ts +43 -23
- package/src/constants.ts +1 -1
- package/src/errors.ts +4 -2
- package/src/git.ts +20 -28
- package/src/index.ts +59 -63
- package/src/out.ts +10 -2
- package/src/types.ts +1 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gflows",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.14",
|
|
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,7 +10,10 @@
|
|
|
10
10
|
},
|
|
11
11
|
"scripts": {
|
|
12
12
|
"test": "bun test",
|
|
13
|
-
"
|
|
13
|
+
"typecheck": "tsc --noEmit",
|
|
14
|
+
"lint": "biome check .",
|
|
15
|
+
"lint:fix": "biome check . --write",
|
|
16
|
+
"format": "biome format . --write",
|
|
14
17
|
"gflows": "bun run src/cli.ts",
|
|
15
18
|
"publish:all": "bun run scripts/publish.ts",
|
|
16
19
|
"publish:npm": "bun run scripts/publish.ts -- --npm-only",
|
|
@@ -33,6 +36,7 @@
|
|
|
33
36
|
"@inquirer/prompts": "^8"
|
|
34
37
|
},
|
|
35
38
|
"devDependencies": {
|
|
39
|
+
"@biomejs/biome": "^2.4.4",
|
|
36
40
|
"@types/bun": "latest",
|
|
37
41
|
"typescript": "^5"
|
|
38
42
|
}
|
package/src/cli.ts
CHANGED
|
@@ -9,9 +9,9 @@
|
|
|
9
9
|
import { existsSync, statSync } from "node:fs";
|
|
10
10
|
import { resolve } from "node:path";
|
|
11
11
|
import { parseArgs } from "node:util";
|
|
12
|
-
import
|
|
13
|
-
import { EXIT_OK, EXIT_USER, EXIT_GIT } from "./constants.js";
|
|
12
|
+
import { EXIT_GIT, EXIT_OK, EXIT_USER } from "./constants.js";
|
|
14
13
|
import { exitCodeForError } from "./errors.js";
|
|
14
|
+
import type { BranchType, Command, ParsedArgs } from "./types.js";
|
|
15
15
|
|
|
16
16
|
/** Last parsed args, set at start of run(); used by catch/rejection to respect -v for stack trace. */
|
|
17
17
|
let lastParsedArgs: ParsedArgs | null = null;
|
|
@@ -30,14 +30,7 @@ const COMMANDS: Command[] = [
|
|
|
30
30
|
"version",
|
|
31
31
|
];
|
|
32
32
|
|
|
33
|
-
const BRANCH_TYPES: BranchType[] = [
|
|
34
|
-
"feature",
|
|
35
|
-
"bugfix",
|
|
36
|
-
"chore",
|
|
37
|
-
"release",
|
|
38
|
-
"hotfix",
|
|
39
|
-
"spike",
|
|
40
|
-
];
|
|
33
|
+
const BRANCH_TYPES: BranchType[] = ["feature", "bugfix", "chore", "release", "hotfix", "spike"];
|
|
41
34
|
|
|
42
35
|
/** Short flag → command (when used as main command). */
|
|
43
36
|
const SHORT_TO_COMMAND: Record<string, Command> = {
|
|
@@ -80,6 +73,7 @@ function buildParseArgsOptions() {
|
|
|
80
73
|
// Common
|
|
81
74
|
push: { type: "boolean" as const, short: "p" },
|
|
82
75
|
noPush: { type: "boolean" as const, short: "P" },
|
|
76
|
+
"no-push": { type: "boolean" as const },
|
|
83
77
|
main: { type: "string" as const },
|
|
84
78
|
dev: { type: "string" as const },
|
|
85
79
|
remote: { type: "string" as const, short: "R" },
|
|
@@ -123,12 +117,57 @@ function resolveCwd(pathFlag: string | undefined): string {
|
|
|
123
117
|
return absolute;
|
|
124
118
|
}
|
|
125
119
|
|
|
120
|
+
/**
|
|
121
|
+
* Returns the command name closest to `input` by edit distance, or undefined if no close match.
|
|
122
|
+
* Used for "did you mean?" when the user mistypes a command.
|
|
123
|
+
*/
|
|
124
|
+
function closestCommand(input: string): Command | undefined {
|
|
125
|
+
if (!input || input.length < 2) return undefined;
|
|
126
|
+
const target = input.toLowerCase();
|
|
127
|
+
let best: Command | undefined;
|
|
128
|
+
let bestDistance = 3; // only suggest if within 2 edits
|
|
129
|
+
for (const cmd of COMMANDS) {
|
|
130
|
+
const d = editDistance(target, cmd);
|
|
131
|
+
if (d < bestDistance) {
|
|
132
|
+
bestDistance = d;
|
|
133
|
+
best = cmd as Command;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return best;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Levenshtein edit distance between two strings. */
|
|
140
|
+
function editDistance(a: string, b: string): number {
|
|
141
|
+
const m = a.length;
|
|
142
|
+
const n = b.length;
|
|
143
|
+
const dp: number[][] = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
|
|
144
|
+
for (let i = 0; i <= m; i++) {
|
|
145
|
+
const row = dp[i];
|
|
146
|
+
if (row) row[0] = i;
|
|
147
|
+
}
|
|
148
|
+
for (let j = 0; j <= n; j++) {
|
|
149
|
+
const row = dp[0];
|
|
150
|
+
if (row) row[j] = j;
|
|
151
|
+
}
|
|
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
|
+
const v1 = dp[i - 1]?.[j] ?? 0;
|
|
156
|
+
const v2 = dp[i]?.[j - 1] ?? 0;
|
|
157
|
+
const v3 = dp[i - 1]?.[j - 1] ?? 0;
|
|
158
|
+
const rowI = dp[i];
|
|
159
|
+
if (rowI) rowI[j] = Math.min(v1 + 1, v2 + 1, v3 + cost);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return dp[m]?.[n] ?? 0;
|
|
163
|
+
}
|
|
164
|
+
|
|
126
165
|
/** Resolve command from positionals and short flags. Short wins if both present. */
|
|
127
166
|
function resolveCommand(
|
|
128
167
|
positionals: string[],
|
|
129
|
-
values: Record<string, string | boolean | undefined
|
|
168
|
+
values: Record<string, string | boolean | undefined>,
|
|
130
169
|
): Command | undefined {
|
|
131
|
-
for (const [
|
|
170
|
+
for (const [_short, cmd] of Object.entries(SHORT_TO_COMMAND)) {
|
|
132
171
|
const key = cmd === "delete" ? "delete" : cmd;
|
|
133
172
|
if (values[key] === true) return cmd as Command;
|
|
134
173
|
}
|
|
@@ -143,7 +182,7 @@ function resolveCommand(
|
|
|
143
182
|
function resolveType(
|
|
144
183
|
command: Command,
|
|
145
184
|
positionals: string[],
|
|
146
|
-
values: Record<string, string | boolean | undefined
|
|
185
|
+
values: Record<string, string | boolean | undefined>,
|
|
147
186
|
): BranchType | undefined {
|
|
148
187
|
if (command !== "start" && command !== "finish" && command !== "list") {
|
|
149
188
|
return undefined;
|
|
@@ -179,7 +218,7 @@ function resolveType(
|
|
|
179
218
|
function resolveName(
|
|
180
219
|
command: Command,
|
|
181
220
|
positionals: string[],
|
|
182
|
-
values: Record<string, string | boolean | undefined
|
|
221
|
+
values: Record<string, string | boolean | undefined>,
|
|
183
222
|
): string | undefined {
|
|
184
223
|
const branch = values.branch;
|
|
185
224
|
if (typeof branch === "string" && branch.trim() !== "") {
|
|
@@ -200,7 +239,7 @@ function resolveName(
|
|
|
200
239
|
}
|
|
201
240
|
if (command === "bump") {
|
|
202
241
|
const dir = positionals[skip];
|
|
203
|
-
const
|
|
242
|
+
const _typ = positionals[skip + 1];
|
|
204
243
|
if (dir === "up" || dir === "down") {
|
|
205
244
|
return dir;
|
|
206
245
|
}
|
|
@@ -215,7 +254,7 @@ function resolveName(
|
|
|
215
254
|
/** Resolve bump direction and type from positionals (bump [up|down] [patch|minor|major]). */
|
|
216
255
|
function resolveBump(
|
|
217
256
|
positionals: string[],
|
|
218
|
-
|
|
257
|
+
_values: Record<string, string | boolean | undefined>,
|
|
219
258
|
): { direction?: "up" | "down"; type?: "patch" | "minor" | "major" } {
|
|
220
259
|
const skip = positionals[0] === "bump" ? 1 : 0;
|
|
221
260
|
const a = positionals[skip];
|
|
@@ -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
|
-
|
|
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
|
|
|
@@ -259,9 +304,7 @@ export function parse(argv: string[] = Bun.argv.slice(2)): ParsedArgs {
|
|
|
259
304
|
// -r context: for list → includeRemote; for start/finish → already used as type release
|
|
260
305
|
const includeRemote =
|
|
261
306
|
command === "list"
|
|
262
|
-
?
|
|
263
|
-
v["include-remote"] === true ||
|
|
264
|
-
v.release === true)
|
|
307
|
+
? v.includeRemote === true || v["include-remote"] === true || v.release === true
|
|
265
308
|
: false;
|
|
266
309
|
|
|
267
310
|
let completionShell: "bash" | "zsh" | "fish" | undefined;
|
|
@@ -279,7 +322,7 @@ export function parse(argv: string[] = Bun.argv.slice(2)): ParsedArgs {
|
|
|
279
322
|
bumpDirection,
|
|
280
323
|
bumpType,
|
|
281
324
|
push: v.push === true,
|
|
282
|
-
noPush: v.noPush === true,
|
|
325
|
+
noPush: v.noPush === true || v["no-push"] === true,
|
|
283
326
|
main: typeof v.main === "string" && v.main.trim() !== "" ? v.main.trim() : undefined,
|
|
284
327
|
dev: typeof v.dev === "string" && v.dev.trim() !== "" ? v.dev.trim() : undefined,
|
|
285
328
|
remote: typeof v.remote === "string" ? v.remote : undefined,
|
package/src/commands/bump.ts
CHANGED
|
@@ -1,20 +1,51 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Bump command: bump or rollback
|
|
2
|
+
* Bump command: bump or rollback package version (patch/minor/major).
|
|
3
|
+
* Supports monorepos: discovers all package.json and jsr.json under cwd and bumps them to the same version.
|
|
3
4
|
* Keeps package.json and jsr.json in sync; no git operations.
|
|
4
5
|
* @module commands/bump
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
|
-
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
8
|
-
import { join } from "node:path";
|
|
9
|
-
import type { ParsedArgs } from "../types.js";
|
|
10
|
-
import type { BumpDirection, BumpType } from "../types.js";
|
|
8
|
+
import { existsSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
9
|
+
import { join, relative } from "node:path";
|
|
11
10
|
import { EXIT_OK, EXIT_USER } from "../constants.js";
|
|
12
11
|
import { InvalidVersionError } from "../errors.js";
|
|
13
12
|
import { hint, success } from "../out.js";
|
|
13
|
+
import type { BumpDirection, BumpType, ParsedArgs } from "../types.js";
|
|
14
14
|
|
|
15
15
|
const PACKAGE_JSON = "package.json";
|
|
16
16
|
const JSR_JSON = "jsr.json";
|
|
17
17
|
|
|
18
|
+
/** Directory names to skip when discovering package roots (monorepo). */
|
|
19
|
+
const SKIP_DIRS = new Set(["node_modules", ".git"]);
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Recursively finds all directories under `root` that contain a package.json.
|
|
23
|
+
* Skips node_modules and .git.
|
|
24
|
+
*/
|
|
25
|
+
function findPackageRoots(root: string): string[] {
|
|
26
|
+
const acc: string[] = [];
|
|
27
|
+
if (!existsSync(root) || !statSync(root, { throwIfNoEntry: false })?.isDirectory()) {
|
|
28
|
+
return acc;
|
|
29
|
+
}
|
|
30
|
+
if (existsSync(join(root, PACKAGE_JSON))) {
|
|
31
|
+
acc.push(root);
|
|
32
|
+
}
|
|
33
|
+
let entries: Array<{ isDirectory(): boolean; name: string }>;
|
|
34
|
+
try {
|
|
35
|
+
entries = readdirSync(root, { withFileTypes: true }) as Array<{
|
|
36
|
+
isDirectory(): boolean;
|
|
37
|
+
name: string;
|
|
38
|
+
}>;
|
|
39
|
+
} catch {
|
|
40
|
+
return acc;
|
|
41
|
+
}
|
|
42
|
+
for (const e of entries) {
|
|
43
|
+
if (!e.isDirectory() || SKIP_DIRS.has(e.name)) continue;
|
|
44
|
+
acc.push(...findPackageRoots(join(root, e.name)));
|
|
45
|
+
}
|
|
46
|
+
return acc;
|
|
47
|
+
}
|
|
48
|
+
|
|
18
49
|
/** Semver triplet. */
|
|
19
50
|
interface Semver {
|
|
20
51
|
major: number;
|
|
@@ -30,18 +61,16 @@ function parseVersion(version: string): Semver {
|
|
|
30
61
|
const trimmed = version.trim();
|
|
31
62
|
const normalized = trimmed.startsWith("v") ? trimmed.slice(1) : trimmed;
|
|
32
63
|
const parts = normalized.split(".");
|
|
33
|
-
if (
|
|
34
|
-
parts.length !== 3 ||
|
|
35
|
-
parts.some((p) => !/^\d+$/.test(p))
|
|
36
|
-
) {
|
|
64
|
+
if (parts.length !== 3 || parts.some((p) => !/^\d+$/.test(p))) {
|
|
37
65
|
throw new InvalidVersionError(
|
|
38
|
-
`Invalid version '${version}'. Expected format: X.Y.Z or vX.Y.Z (e.g. 1.2.3 or v1.2.3)
|
|
66
|
+
`Invalid version '${version}'. Expected format: X.Y.Z or vX.Y.Z (e.g. 1.2.3 or v1.2.3).`,
|
|
39
67
|
);
|
|
40
68
|
}
|
|
69
|
+
const [p0, p1, p2] = parts;
|
|
41
70
|
return {
|
|
42
|
-
major: parseInt(
|
|
43
|
-
minor: parseInt(
|
|
44
|
-
patch: parseInt(
|
|
71
|
+
major: parseInt(p0 ?? "0", 10),
|
|
72
|
+
minor: parseInt(p1 ?? "0", 10),
|
|
73
|
+
patch: parseInt(p2 ?? "0", 10),
|
|
45
74
|
};
|
|
46
75
|
}
|
|
47
76
|
|
|
@@ -103,7 +132,7 @@ function readPackageVersion(dir: string): { raw: string; semver: Semver } {
|
|
|
103
132
|
const path = join(dir, PACKAGE_JSON);
|
|
104
133
|
if (!existsSync(path)) {
|
|
105
134
|
throw new InvalidVersionError(
|
|
106
|
-
`No package.json found at ${path}. Run from project root or use -C <dir
|
|
135
|
+
`No package.json found at ${path}. Run from project root or use -C <dir>.`,
|
|
107
136
|
);
|
|
108
137
|
}
|
|
109
138
|
const raw = readFileSync(path, "utf-8");
|
|
@@ -111,7 +140,7 @@ function readPackageVersion(dir: string): { raw: string; semver: Semver } {
|
|
|
111
140
|
const version = data.version;
|
|
112
141
|
if (typeof version !== "string" || version.trim() === "") {
|
|
113
142
|
throw new InvalidVersionError(
|
|
114
|
-
|
|
143
|
+
'package.json has no valid \'version\' field. Add a "version" field (e.g. "0.0.0") to package.json.',
|
|
115
144
|
);
|
|
116
145
|
}
|
|
117
146
|
const semver = parseVersion(version);
|
|
@@ -127,7 +156,7 @@ function writePackageVersion(dir: string, newVersion: string): void {
|
|
|
127
156
|
const raw = readFileSync(path, "utf-8");
|
|
128
157
|
const data = JSON.parse(raw) as Record<string, unknown>;
|
|
129
158
|
data.version = newVersion;
|
|
130
|
-
writeFileSync(path, JSON.stringify(data, null, 2)
|
|
159
|
+
writeFileSync(path, `${JSON.stringify(data, null, 2)}\n`, "utf-8");
|
|
131
160
|
}
|
|
132
161
|
|
|
133
162
|
/**
|
|
@@ -139,7 +168,7 @@ function syncJsrVersion(dir: string, newVersion: string): boolean {
|
|
|
139
168
|
const raw = readFileSync(path, "utf-8");
|
|
140
169
|
const data = JSON.parse(raw) as Record<string, unknown>;
|
|
141
170
|
data.version = newVersion;
|
|
142
|
-
writeFileSync(path, JSON.stringify(data, null, 2)
|
|
171
|
+
writeFileSync(path, `${JSON.stringify(data, null, 2)}\n`, "utf-8");
|
|
143
172
|
return true;
|
|
144
173
|
}
|
|
145
174
|
|
|
@@ -154,15 +183,14 @@ export async function run(args: ParsedArgs): Promise<void> {
|
|
|
154
183
|
let direction: BumpDirection;
|
|
155
184
|
let type: BumpType;
|
|
156
185
|
|
|
157
|
-
const isTTY =
|
|
158
|
-
typeof process.stdin.isTTY === "boolean" && process.stdin.isTTY;
|
|
186
|
+
const isTTY = typeof process.stdin.isTTY === "boolean" && process.stdin.isTTY;
|
|
159
187
|
|
|
160
188
|
if (bumpDirection && bumpType) {
|
|
161
189
|
direction = bumpDirection;
|
|
162
190
|
type = bumpType;
|
|
163
191
|
} else if (!isTTY) {
|
|
164
192
|
console.error(
|
|
165
|
-
"gflows bump: when not in a TTY, both direction and type are required. Example: gflows bump up patch"
|
|
193
|
+
"gflows bump: when not in a TTY, both direction and type are required. Example: gflows bump up patch",
|
|
166
194
|
);
|
|
167
195
|
process.exit(EXIT_USER);
|
|
168
196
|
} else {
|
|
@@ -184,14 +212,33 @@ export async function run(args: ParsedArgs): Promise<void> {
|
|
|
184
212
|
});
|
|
185
213
|
}
|
|
186
214
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
215
|
+
let roots = findPackageRoots(cwd);
|
|
216
|
+
roots = [...roots].sort((a, b) => {
|
|
217
|
+
if (a === cwd && b !== cwd) return -1;
|
|
218
|
+
if (a !== cwd && b === cwd) return 1;
|
|
219
|
+
return a.localeCompare(b);
|
|
220
|
+
});
|
|
221
|
+
if (roots.length === 0) {
|
|
222
|
+
throw new InvalidVersionError(
|
|
223
|
+
`No package.json found under ${cwd}. Run from project root or use -C <dir>.`,
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
const primaryRoot = roots[0];
|
|
227
|
+
if (primaryRoot === undefined) {
|
|
228
|
+
throw new InvalidVersionError(
|
|
229
|
+
`No package.json found under ${cwd}. Run from project root or use -C <dir>.`,
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
const { raw: oldVersion, semver } = readPackageVersion(primaryRoot);
|
|
233
|
+
const newSemver = direction === "up" ? bumpUp(semver, type) : bumpDown(semver, type);
|
|
190
234
|
const newVersion = formatVersion(newSemver);
|
|
191
235
|
|
|
192
|
-
const filesToUpdate: string[] = [
|
|
193
|
-
|
|
194
|
-
filesToUpdate.push(
|
|
236
|
+
const filesToUpdate: string[] = [];
|
|
237
|
+
for (const dir of roots) {
|
|
238
|
+
filesToUpdate.push(relative(cwd, join(dir, PACKAGE_JSON)) || PACKAGE_JSON);
|
|
239
|
+
if (existsSync(join(dir, JSR_JSON))) {
|
|
240
|
+
filesToUpdate.push(relative(cwd, join(dir, JSR_JSON)) || JSR_JSON);
|
|
241
|
+
}
|
|
195
242
|
}
|
|
196
243
|
|
|
197
244
|
if (dryRun) {
|
|
@@ -202,15 +249,18 @@ export async function run(args: ParsedArgs): Promise<void> {
|
|
|
202
249
|
process.exit(EXIT_OK);
|
|
203
250
|
}
|
|
204
251
|
|
|
205
|
-
|
|
206
|
-
const
|
|
252
|
+
const updated: string[] = [];
|
|
253
|
+
for (const dir of roots) {
|
|
254
|
+
writePackageVersion(dir, newVersion);
|
|
255
|
+
updated.push(relative(cwd, join(dir, PACKAGE_JSON)) || PACKAGE_JSON);
|
|
256
|
+
if (syncJsrVersion(dir, newVersion)) {
|
|
257
|
+
updated.push(relative(cwd, join(dir, JSR_JSON)) || JSR_JSON);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
207
260
|
|
|
208
261
|
if (!quiet) {
|
|
209
262
|
success(`Bumped version: ${oldVersion} → ${newVersion}`);
|
|
210
|
-
const updated = [PACKAGE_JSON];
|
|
211
|
-
if (jsrUpdated) updated.push(JSR_JSON);
|
|
212
263
|
success(`Updated: ${updated.join(", ")}`);
|
|
213
|
-
// Hint: suggest next step — commit and start release branch
|
|
214
264
|
hint("Commit the change, then run gflows start release vX.Y.Z to release.");
|
|
215
265
|
}
|
|
216
266
|
}
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* @module commands/completion
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import type { ParsedArgs } from "../types.js";
|
|
9
8
|
import { EXIT_USER } from "../constants.js";
|
|
9
|
+
import type { ParsedArgs } from "../types.js";
|
|
10
10
|
|
|
11
11
|
const COMMANDS = [
|
|
12
12
|
"init",
|
|
@@ -22,14 +22,7 @@ const COMMANDS = [
|
|
|
22
22
|
"version",
|
|
23
23
|
];
|
|
24
24
|
|
|
25
|
-
const BRANCH_TYPES = [
|
|
26
|
-
"feature",
|
|
27
|
-
"bugfix",
|
|
28
|
-
"chore",
|
|
29
|
-
"release",
|
|
30
|
-
"hotfix",
|
|
31
|
-
"spike",
|
|
32
|
-
];
|
|
25
|
+
const BRANCH_TYPES = ["feature", "bugfix", "chore", "release", "hotfix", "spike"];
|
|
33
26
|
|
|
34
27
|
const COMPLETION_SHELLS = ["bash", "zsh", "fish"] as const;
|
|
35
28
|
|
|
@@ -46,7 +39,7 @@ function bashScript(): string {
|
|
|
46
39
|
_gflows() {
|
|
47
40
|
local cur prev words cword cmd_idx cmd
|
|
48
41
|
words=(${D}COMP_WORDS[@]})
|
|
49
|
-
cword
|
|
42
|
+
cword=$COMP_CWORD
|
|
50
43
|
cur="${D}words[cword]:-}"
|
|
51
44
|
prev="${D}words[cword-1]:-}"
|
|
52
45
|
|
|
@@ -72,58 +65,58 @@ _gflows() {
|
|
|
72
65
|
break
|
|
73
66
|
fi
|
|
74
67
|
done
|
|
75
|
-
if [[ -n "
|
|
76
|
-
gflows -C "
|
|
68
|
+
if [[ -n "$path" ]]; then
|
|
69
|
+
gflows -C "$path" list 2>/dev/null
|
|
77
70
|
else
|
|
78
71
|
gflows list 2>/dev/null
|
|
79
72
|
fi
|
|
80
73
|
}
|
|
81
74
|
|
|
82
75
|
# Completing -C/--path value: suggest directories
|
|
83
|
-
if [[ "
|
|
76
|
+
if [[ "$prev" == "-C" ]] || [[ "$prev" == "--path" ]]; then
|
|
84
77
|
compopt -o dirnames 2>/dev/null
|
|
85
|
-
COMPREPLY=($(compgen -d -S / -- "
|
|
78
|
+
COMPREPLY=($(compgen -d -S / -- "$cur"))
|
|
86
79
|
return
|
|
87
80
|
fi
|
|
88
81
|
|
|
89
82
|
# First positional: command
|
|
90
83
|
if (( cword == cmd_idx )); then
|
|
91
|
-
COMPREPLY=($(compgen -W "${COMMANDS.join(" ")}" -- "
|
|
84
|
+
COMPREPLY=($(compgen -W "${COMMANDS.join(" ")}" -- "$cur"))
|
|
92
85
|
return
|
|
93
86
|
fi
|
|
94
87
|
|
|
95
88
|
# After command: type/name/branches/shell/bump by command
|
|
96
89
|
case "$cmd" in
|
|
97
90
|
completion)
|
|
98
|
-
COMPREPLY=($(compgen -W "${COMPLETION_SHELLS.join(" ")}" -- "
|
|
91
|
+
COMPREPLY=($(compgen -W "${COMPLETION_SHELLS.join(" ")}" -- "$cur"))
|
|
99
92
|
;;
|
|
100
93
|
start)
|
|
101
94
|
if (( cword == cmd_idx + 1 )); then
|
|
102
|
-
COMPREPLY=($(compgen -W "${BRANCH_TYPES.join(" ")}" -- "
|
|
95
|
+
COMPREPLY=($(compgen -W "${BRANCH_TYPES.join(" ")}" -- "$cur"))
|
|
103
96
|
else
|
|
104
97
|
COMPREPLY=()
|
|
105
98
|
fi
|
|
106
99
|
;;
|
|
107
100
|
finish)
|
|
108
|
-
if [[ "
|
|
109
|
-
COMPREPLY=($(compgen -W "$(_gflows_path)" -- "
|
|
101
|
+
if [[ "$prev" == "-B" ]] || [[ "$prev" == "--branch" ]]; then
|
|
102
|
+
COMPREPLY=($(compgen -W "$(_gflows_path)" -- "$cur"))
|
|
110
103
|
elif (( cword == cmd_idx + 1 )); then
|
|
111
|
-
COMPREPLY=($(compgen -W "${BRANCH_TYPES.join(" ")}" -- "
|
|
104
|
+
COMPREPLY=($(compgen -W "${BRANCH_TYPES.join(" ")}" -- "$cur"))
|
|
112
105
|
else
|
|
113
106
|
COMPREPLY=()
|
|
114
107
|
fi
|
|
115
108
|
;;
|
|
116
109
|
list)
|
|
117
|
-
COMPREPLY=($(compgen -W "${BRANCH_TYPES.join(" ")}" -- "
|
|
110
|
+
COMPREPLY=($(compgen -W "${BRANCH_TYPES.join(" ")}" -- "$cur"))
|
|
118
111
|
;;
|
|
119
112
|
switch|delete)
|
|
120
|
-
COMPREPLY=($(compgen -W "$(_gflows_path)" -- "
|
|
113
|
+
COMPREPLY=($(compgen -W "$(_gflows_path)" -- "$cur"))
|
|
121
114
|
;;
|
|
122
115
|
bump)
|
|
123
116
|
if (( cword == cmd_idx + 1 )); then
|
|
124
|
-
COMPREPLY=($(compgen -W "${BUMP_DIRECTIONS.join(" ")}" -- "
|
|
117
|
+
COMPREPLY=($(compgen -W "${BUMP_DIRECTIONS.join(" ")}" -- "$cur"))
|
|
125
118
|
elif (( cword == cmd_idx + 2 )); then
|
|
126
|
-
COMPREPLY=($(compgen -W "${BUMP_TYPES.join(" ")}" -- "
|
|
119
|
+
COMPREPLY=($(compgen -W "${BUMP_TYPES.join(" ")}" -- "$cur"))
|
|
127
120
|
else
|
|
128
121
|
COMPREPLY=()
|
|
129
122
|
fi
|
|
@@ -169,7 +162,7 @@ _gflows() {
|
|
|
169
162
|
|
|
170
163
|
case $state in
|
|
171
164
|
args)
|
|
172
|
-
cur
|
|
165
|
+
cur=$words[CURRENT]
|
|
173
166
|
cmd_idx=1
|
|
174
167
|
while (( cmd_idx < CURRENT )); do
|
|
175
168
|
if [[ "${D}words[cmd_idx]}" == "-C" ]] || [[ "${D}words[cmd_idx]}" == "--path" ]]; then
|
|
@@ -190,7 +183,7 @@ _gflows() {
|
|
|
190
183
|
;;
|
|
191
184
|
finish)
|
|
192
185
|
if [[ "${D}words[CURRENT-1]}" == "-B" ]] || [[ "${D}words[CURRENT-1]}" == "--branch" ]]; then
|
|
193
|
-
_values "branch"
|
|
186
|
+
_values "branch" $(_gflows_list_branches)
|
|
194
187
|
else
|
|
195
188
|
_values "type" ${BRANCH_TYPES.map((t) => `"${t}"`).join(" ")}
|
|
196
189
|
fi
|
|
@@ -199,7 +192,7 @@ _gflows() {
|
|
|
199
192
|
_values "type" ${BRANCH_TYPES.map((t) => `"${t}"`).join(" ")}
|
|
200
193
|
;;
|
|
201
194
|
switch|delete)
|
|
202
|
-
_values "branch"
|
|
195
|
+
_values "branch" $(_gflows_list_branches)
|
|
203
196
|
;;
|
|
204
197
|
bump)
|
|
205
198
|
if [[ "${D}words[CURRENT-1]}" == "up" ]] || [[ "${D}words[CURRENT-1]}" == "down" ]]; then
|
|
@@ -326,9 +319,7 @@ complete -c gflows -f -n "__fish_seen_subcommand_from ${COMMANDS.join(" ")}" -l
|
|
|
326
319
|
export async function run(args: ParsedArgs): Promise<void> {
|
|
327
320
|
const shell = args.completionShell;
|
|
328
321
|
if (!shell) {
|
|
329
|
-
console.error(
|
|
330
|
-
"gflows: completion requires a shell. Use: gflows completion bash | zsh | fish"
|
|
331
|
-
);
|
|
322
|
+
console.error("gflows: completion requires a shell. Use: gflows completion bash | zsh | fish");
|
|
332
323
|
process.exit(EXIT_USER);
|
|
333
324
|
}
|
|
334
325
|
|
package/src/commands/delete.ts
CHANGED
|
@@ -3,38 +3,24 @@
|
|
|
3
3
|
* @module commands/delete
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import type { BranchType } from "../types.js";
|
|
7
|
-
import type { ParsedArgs } from "../types.js";
|
|
8
6
|
import { resolveConfig } from "../config.js";
|
|
9
7
|
import { EXIT_OK, EXIT_USER } from "../constants.js";
|
|
10
8
|
import { CannotDeleteMainOrDevError, NotRepoError } from "../errors.js";
|
|
11
|
-
import {
|
|
12
|
-
branchList,
|
|
13
|
-
deleteBranch,
|
|
14
|
-
resolveRepoRoot,
|
|
15
|
-
} from "../git.js";
|
|
9
|
+
import { branchList, deleteBranch, resolveRepoRoot } from "../git.js";
|
|
16
10
|
import { hint, success } from "../out.js";
|
|
11
|
+
import type { BranchType, ParsedArgs } from "../types.js";
|
|
17
12
|
|
|
18
|
-
const BRANCH_TYPES: BranchType[] = [
|
|
19
|
-
"feature",
|
|
20
|
-
"bugfix",
|
|
21
|
-
"chore",
|
|
22
|
-
"release",
|
|
23
|
-
"hotfix",
|
|
24
|
-
"spike",
|
|
25
|
-
];
|
|
13
|
+
const BRANCH_TYPES: BranchType[] = ["feature", "bugfix", "chore", "release", "hotfix", "spike"];
|
|
26
14
|
|
|
27
15
|
/**
|
|
28
16
|
* Returns local branch names that match any workflow prefix (feature/, bugfix/, etc.).
|
|
29
17
|
*/
|
|
30
18
|
function getWorkflowBranches(
|
|
31
19
|
allBranches: string[],
|
|
32
|
-
prefixes: Record<BranchType, string
|
|
20
|
+
prefixes: Record<BranchType, string>,
|
|
33
21
|
): string[] {
|
|
34
22
|
const prefixed = BRANCH_TYPES.map((t) => prefixes[t]).filter(Boolean);
|
|
35
|
-
return allBranches.filter((b) =>
|
|
36
|
-
prefixed.some((p) => p && b.startsWith(p))
|
|
37
|
-
);
|
|
23
|
+
return allBranches.filter((b) => prefixed.some((p) => p && b.startsWith(p)));
|
|
38
24
|
}
|
|
39
25
|
|
|
40
26
|
/**
|
|
@@ -58,16 +44,12 @@ export async function run(args: ParsedArgs): Promise<void> {
|
|
|
58
44
|
});
|
|
59
45
|
const { main, dev, prefixes } = config;
|
|
60
46
|
|
|
61
|
-
const fromPositionals = (rawBranchNames ?? [])
|
|
62
|
-
.map((s) => s.trim())
|
|
63
|
-
.filter(Boolean);
|
|
47
|
+
const fromPositionals = (rawBranchNames ?? []).map((s) => s.trim()).filter(Boolean);
|
|
64
48
|
|
|
65
49
|
if (fromPositionals.length > 0) {
|
|
66
50
|
for (const branch of fromPositionals) {
|
|
67
51
|
if (branch === main || branch === dev) {
|
|
68
|
-
throw new CannotDeleteMainOrDevError(
|
|
69
|
-
`Cannot delete the long-lived branch '${branch}'.`
|
|
70
|
-
);
|
|
52
|
+
throw new CannotDeleteMainOrDevError(`Cannot delete the long-lived branch '${branch}'.`);
|
|
71
53
|
}
|
|
72
54
|
}
|
|
73
55
|
for (const branch of fromPositionals) {
|
|
@@ -89,7 +71,7 @@ export async function run(args: ParsedArgs): Promise<void> {
|
|
|
89
71
|
const isTTY = typeof process.stdin.isTTY === "boolean" && process.stdin.isTTY;
|
|
90
72
|
if (!isTTY) {
|
|
91
73
|
console.error(
|
|
92
|
-
"gflows delete: no branch name(s) given and stdin is not a TTY. Pass branch name(s) (e.g. gflows delete feature/my-branch) or run from an interactive terminal."
|
|
74
|
+
"gflows delete: no branch name(s) given and stdin is not a TTY. Pass branch name(s) (e.g. gflows delete feature/my-branch) or run from an interactive terminal.",
|
|
93
75
|
);
|
|
94
76
|
process.exit(EXIT_USER);
|
|
95
77
|
}
|
|
@@ -103,7 +85,7 @@ export async function run(args: ParsedArgs): Promise<void> {
|
|
|
103
85
|
if (workflowBranches.length === 0) {
|
|
104
86
|
if (!quiet) {
|
|
105
87
|
console.error(
|
|
106
|
-
"No workflow branches to delete. Create one with 'gflows start <type> <name>'."
|
|
88
|
+
"No workflow branches to delete. Create one with 'gflows start <type> <name>'.",
|
|
107
89
|
);
|
|
108
90
|
}
|
|
109
91
|
process.exit(EXIT_USER);
|
|
@@ -124,9 +106,7 @@ export async function run(args: ParsedArgs): Promise<void> {
|
|
|
124
106
|
|
|
125
107
|
for (const branch of chosen) {
|
|
126
108
|
if (branch === main || branch === dev) {
|
|
127
|
-
throw new CannotDeleteMainOrDevError(
|
|
128
|
-
`Cannot delete the long-lived branch '${branch}'.`
|
|
129
|
-
);
|
|
109
|
+
throw new CannotDeleteMainOrDevError(`Cannot delete the long-lived branch '${branch}'.`);
|
|
130
110
|
}
|
|
131
111
|
}
|
|
132
112
|
|