git-tidy-cli 1.0.0
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/LICENSE +21 -0
- package/README.md +117 -0
- package/bin/git-tidy.js +2 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1154 -0
- package/dist/index.js.map +1 -0
- package/package.json +63 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1154 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.tsx
|
|
4
|
+
import { render } from "ink";
|
|
5
|
+
|
|
6
|
+
// src/app.tsx
|
|
7
|
+
import { useState as useState5, useEffect as useEffect2, useCallback as useCallback2 } from "react";
|
|
8
|
+
import { Box as Box8, Text as Text8 } from "ink";
|
|
9
|
+
import { Spinner as Spinner2 } from "@inkjs/ui";
|
|
10
|
+
|
|
11
|
+
// src/components/Header.tsx
|
|
12
|
+
import { Box, Text } from "ink";
|
|
13
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
14
|
+
function Header({ repoInfo }) {
|
|
15
|
+
return /* @__PURE__ */ jsxs(
|
|
16
|
+
Box,
|
|
17
|
+
{
|
|
18
|
+
flexDirection: "column",
|
|
19
|
+
borderStyle: "round",
|
|
20
|
+
borderColor: "cyan",
|
|
21
|
+
paddingX: 2,
|
|
22
|
+
paddingY: 0,
|
|
23
|
+
marginBottom: 1,
|
|
24
|
+
children: [
|
|
25
|
+
/* @__PURE__ */ jsxs(Box, { children: [
|
|
26
|
+
/* @__PURE__ */ jsx(Text, { color: "cyan", bold: true, children: "git-tidy" }),
|
|
27
|
+
/* @__PURE__ */ jsx(Text, { color: "gray", children: " - Branch Cleanup Tool" })
|
|
28
|
+
] }),
|
|
29
|
+
repoInfo && /* @__PURE__ */ jsxs(Box, { gap: 2, children: [
|
|
30
|
+
/* @__PURE__ */ jsxs(Text, { color: "gray", children: [
|
|
31
|
+
"Repository:",
|
|
32
|
+
" ",
|
|
33
|
+
/* @__PURE__ */ jsxs(Text, { color: "white", children: [
|
|
34
|
+
repoInfo.owner,
|
|
35
|
+
"/",
|
|
36
|
+
repoInfo.repo
|
|
37
|
+
] })
|
|
38
|
+
] }),
|
|
39
|
+
/* @__PURE__ */ jsxs(Text, { color: "gray", children: [
|
|
40
|
+
"Branch:",
|
|
41
|
+
" ",
|
|
42
|
+
/* @__PURE__ */ jsx(Text, { color: "green", children: repoInfo.currentBranch })
|
|
43
|
+
] }),
|
|
44
|
+
/* @__PURE__ */ jsxs(Text, { color: "gray", children: [
|
|
45
|
+
"Default:",
|
|
46
|
+
" ",
|
|
47
|
+
/* @__PURE__ */ jsx(Text, { color: "yellow", children: repoInfo.defaultBranch })
|
|
48
|
+
] })
|
|
49
|
+
] })
|
|
50
|
+
]
|
|
51
|
+
}
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// src/components/ScopeStep.tsx
|
|
56
|
+
import { Box as Box2, Text as Text2 } from "ink";
|
|
57
|
+
import { Select } from "@inkjs/ui";
|
|
58
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
59
|
+
function ScopeStep({ onSelect }) {
|
|
60
|
+
const options2 = [
|
|
61
|
+
{
|
|
62
|
+
label: "Both local and remote branches",
|
|
63
|
+
value: "both"
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
label: "Local branches only",
|
|
67
|
+
value: "local"
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
label: "Remote branches only",
|
|
71
|
+
value: "remote"
|
|
72
|
+
}
|
|
73
|
+
];
|
|
74
|
+
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", gap: 1, children: [
|
|
75
|
+
/* @__PURE__ */ jsxs2(Box2, { children: [
|
|
76
|
+
/* @__PURE__ */ jsx2(Text2, { color: "cyan", bold: true, children: "Step 1:" }),
|
|
77
|
+
/* @__PURE__ */ jsx2(Text2, { children: " What would you like to clean up?" })
|
|
78
|
+
] }),
|
|
79
|
+
/* @__PURE__ */ jsx2(Box2, { marginLeft: 2, children: /* @__PURE__ */ jsx2(
|
|
80
|
+
Select,
|
|
81
|
+
{
|
|
82
|
+
options: options2,
|
|
83
|
+
onChange: (value) => onSelect(value)
|
|
84
|
+
}
|
|
85
|
+
) }),
|
|
86
|
+
/* @__PURE__ */ jsx2(Box2, { marginTop: 1, children: /* @__PURE__ */ jsx2(Text2, { color: "gray", dimColor: true, children: "Use \u2191\u2193 to navigate, Enter to select" }) })
|
|
87
|
+
] });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// src/components/CriteriaStep.tsx
|
|
91
|
+
import { useState } from "react";
|
|
92
|
+
import { Box as Box3, Text as Text3, useInput } from "ink";
|
|
93
|
+
import { MultiSelect, TextInput } from "@inkjs/ui";
|
|
94
|
+
|
|
95
|
+
// src/utils/config.ts
|
|
96
|
+
var DEFAULT_STALE_DAYS = 30;
|
|
97
|
+
var DEFAULT_AGE_DAYS = 60;
|
|
98
|
+
var PROTECTED_PATTERNS = [
|
|
99
|
+
"main",
|
|
100
|
+
"master",
|
|
101
|
+
"develop",
|
|
102
|
+
"development",
|
|
103
|
+
"staging",
|
|
104
|
+
"production",
|
|
105
|
+
"release/*"
|
|
106
|
+
];
|
|
107
|
+
function matchesProtectedPattern(branchName) {
|
|
108
|
+
return PROTECTED_PATTERNS.some((pattern) => {
|
|
109
|
+
if (pattern.endsWith("/*")) {
|
|
110
|
+
const prefix = pattern.slice(0, -2);
|
|
111
|
+
return branchName.startsWith(prefix + "/");
|
|
112
|
+
}
|
|
113
|
+
return branchName === pattern;
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// src/components/CriteriaStep.tsx
|
|
118
|
+
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
119
|
+
function CriteriaStep({ onSelect, onBack }) {
|
|
120
|
+
const [selectedCriteria, setSelectedCriteria] = useState([]);
|
|
121
|
+
const [inputMode, setInputMode] = useState("select");
|
|
122
|
+
const [staleDays, setStaleDays] = useState(String(DEFAULT_STALE_DAYS));
|
|
123
|
+
const [ageDays, setAgeDays] = useState(String(DEFAULT_AGE_DAYS));
|
|
124
|
+
const [pattern, setPattern] = useState("feature/*");
|
|
125
|
+
const [inputError, setInputError] = useState(null);
|
|
126
|
+
const validateNumber = (value) => {
|
|
127
|
+
const num = parseInt(value, 10);
|
|
128
|
+
if (isNaN(num) || num <= 0) {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
return num;
|
|
132
|
+
};
|
|
133
|
+
const options2 = [
|
|
134
|
+
{
|
|
135
|
+
label: "Merged branches (already merged into default branch)",
|
|
136
|
+
value: "merged"
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
label: `Stale branches (no commits in ${staleDays} days)`,
|
|
140
|
+
value: "stale"
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
label: `Branches older than ${ageDays} days`,
|
|
144
|
+
value: "age"
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
label: `Pattern matching: ${pattern}`,
|
|
148
|
+
value: "pattern"
|
|
149
|
+
}
|
|
150
|
+
];
|
|
151
|
+
useInput((input, key) => {
|
|
152
|
+
if (key.escape) {
|
|
153
|
+
if (inputMode !== "select") {
|
|
154
|
+
setInputMode("select");
|
|
155
|
+
} else {
|
|
156
|
+
onBack();
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (key.return && inputMode === "select") {
|
|
160
|
+
handleSubmit();
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
const handleCriteriaChange = (values) => {
|
|
164
|
+
setSelectedCriteria(values);
|
|
165
|
+
};
|
|
166
|
+
const handleSubmit = () => {
|
|
167
|
+
if (selectedCriteria.includes("stale") && inputMode === "select") {
|
|
168
|
+
setInputMode("staleDays");
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
if (selectedCriteria.includes("age") && inputMode === "staleDays") {
|
|
172
|
+
setInputMode("ageDays");
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
if (selectedCriteria.includes("pattern") && inputMode === "ageDays") {
|
|
176
|
+
setInputMode("pattern");
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
if (selectedCriteria.includes("pattern") && inputMode === "select") {
|
|
180
|
+
setInputMode("pattern");
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
const filters = {
|
|
184
|
+
merged: selectedCriteria.includes("merged"),
|
|
185
|
+
stale: selectedCriteria.includes("stale"),
|
|
186
|
+
staleDays: parseInt(staleDays, 10) || DEFAULT_STALE_DAYS,
|
|
187
|
+
pattern: selectedCriteria.includes("pattern"),
|
|
188
|
+
patternValue: pattern,
|
|
189
|
+
age: selectedCriteria.includes("age"),
|
|
190
|
+
ageDays: parseInt(ageDays, 10) || DEFAULT_AGE_DAYS
|
|
191
|
+
};
|
|
192
|
+
onSelect(filters);
|
|
193
|
+
};
|
|
194
|
+
if (inputMode === "staleDays") {
|
|
195
|
+
return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", gap: 1, children: [
|
|
196
|
+
/* @__PURE__ */ jsx3(Text3, { color: "cyan", children: "How many days without commits is considered stale?" }),
|
|
197
|
+
/* @__PURE__ */ jsxs3(Box3, { children: [
|
|
198
|
+
/* @__PURE__ */ jsx3(
|
|
199
|
+
TextInput,
|
|
200
|
+
{
|
|
201
|
+
defaultValue: staleDays,
|
|
202
|
+
onSubmit: (value) => {
|
|
203
|
+
const num = validateNumber(value);
|
|
204
|
+
if (num === null) {
|
|
205
|
+
setInputError("Please enter a valid number greater than 0");
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
setInputError(null);
|
|
209
|
+
setStaleDays(String(num));
|
|
210
|
+
if (selectedCriteria.includes("age")) {
|
|
211
|
+
setInputMode("ageDays");
|
|
212
|
+
} else if (selectedCriteria.includes("pattern")) {
|
|
213
|
+
setInputMode("pattern");
|
|
214
|
+
} else {
|
|
215
|
+
handleSubmit();
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
),
|
|
220
|
+
/* @__PURE__ */ jsx3(Text3, { color: "gray", children: " days" })
|
|
221
|
+
] }),
|
|
222
|
+
inputError && /* @__PURE__ */ jsx3(Text3, { color: "red", children: inputError })
|
|
223
|
+
] });
|
|
224
|
+
}
|
|
225
|
+
if (inputMode === "ageDays") {
|
|
226
|
+
return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", gap: 1, children: [
|
|
227
|
+
/* @__PURE__ */ jsx3(Text3, { color: "cyan", children: "How many days old should a branch be?" }),
|
|
228
|
+
/* @__PURE__ */ jsxs3(Box3, { children: [
|
|
229
|
+
/* @__PURE__ */ jsx3(
|
|
230
|
+
TextInput,
|
|
231
|
+
{
|
|
232
|
+
defaultValue: ageDays,
|
|
233
|
+
onSubmit: (value) => {
|
|
234
|
+
const num = validateNumber(value);
|
|
235
|
+
if (num === null) {
|
|
236
|
+
setInputError("Please enter a valid number greater than 0");
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
setInputError(null);
|
|
240
|
+
setAgeDays(String(num));
|
|
241
|
+
if (selectedCriteria.includes("pattern")) {
|
|
242
|
+
setInputMode("pattern");
|
|
243
|
+
} else {
|
|
244
|
+
handleSubmit();
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
),
|
|
249
|
+
/* @__PURE__ */ jsx3(Text3, { color: "gray", children: " days" })
|
|
250
|
+
] }),
|
|
251
|
+
inputError && /* @__PURE__ */ jsx3(Text3, { color: "red", children: inputError })
|
|
252
|
+
] });
|
|
253
|
+
}
|
|
254
|
+
if (inputMode === "pattern") {
|
|
255
|
+
return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", gap: 1, children: [
|
|
256
|
+
/* @__PURE__ */ jsx3(Text3, { color: "cyan", children: "Enter branch name pattern (use * as wildcard):" }),
|
|
257
|
+
/* @__PURE__ */ jsx3(
|
|
258
|
+
TextInput,
|
|
259
|
+
{
|
|
260
|
+
defaultValue: pattern,
|
|
261
|
+
onSubmit: (value) => {
|
|
262
|
+
setPattern(value);
|
|
263
|
+
const filters = {
|
|
264
|
+
merged: selectedCriteria.includes("merged"),
|
|
265
|
+
stale: selectedCriteria.includes("stale"),
|
|
266
|
+
staleDays: parseInt(staleDays, 10) || DEFAULT_STALE_DAYS,
|
|
267
|
+
pattern: selectedCriteria.includes("pattern"),
|
|
268
|
+
patternValue: value,
|
|
269
|
+
age: selectedCriteria.includes("age"),
|
|
270
|
+
ageDays: parseInt(ageDays, 10) || DEFAULT_AGE_DAYS
|
|
271
|
+
};
|
|
272
|
+
onSelect(filters);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
),
|
|
276
|
+
/* @__PURE__ */ jsx3(Text3, { color: "gray", dimColor: true, children: "Examples: feature/*, hotfix/*, *-old, test-*" })
|
|
277
|
+
] });
|
|
278
|
+
}
|
|
279
|
+
return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", gap: 1, children: [
|
|
280
|
+
/* @__PURE__ */ jsxs3(Box3, { children: [
|
|
281
|
+
/* @__PURE__ */ jsx3(Text3, { color: "cyan", bold: true, children: "Step 2:" }),
|
|
282
|
+
/* @__PURE__ */ jsx3(Text3, { children: " Which branches should be included? (select multiple)" })
|
|
283
|
+
] }),
|
|
284
|
+
/* @__PURE__ */ jsx3(Box3, { marginLeft: 2, children: /* @__PURE__ */ jsx3(
|
|
285
|
+
MultiSelect,
|
|
286
|
+
{
|
|
287
|
+
options: options2,
|
|
288
|
+
onChange: handleCriteriaChange
|
|
289
|
+
}
|
|
290
|
+
) }),
|
|
291
|
+
/* @__PURE__ */ jsxs3(Box3, { marginTop: 1, flexDirection: "column", children: [
|
|
292
|
+
/* @__PURE__ */ jsx3(Text3, { color: "gray", dimColor: true, children: "Use \u2191\u2193 to navigate, Space to toggle, Enter to confirm" }),
|
|
293
|
+
/* @__PURE__ */ jsx3(Text3, { color: "gray", dimColor: true, children: "Press Esc to go back" })
|
|
294
|
+
] }),
|
|
295
|
+
selectedCriteria.length > 0 && /* @__PURE__ */ jsx3(Box3, { marginTop: 1, children: /* @__PURE__ */ jsxs3(Text3, { color: "green", children: [
|
|
296
|
+
"Press Enter to continue with ",
|
|
297
|
+
selectedCriteria.length,
|
|
298
|
+
" filter(s)"
|
|
299
|
+
] }) })
|
|
300
|
+
] });
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// src/components/BranchSelectStep.tsx
|
|
304
|
+
import { useState as useState2, useMemo } from "react";
|
|
305
|
+
import { Box as Box4, Text as Text4, useInput as useInput2 } from "ink";
|
|
306
|
+
import { MultiSelect as MultiSelect2 } from "@inkjs/ui";
|
|
307
|
+
|
|
308
|
+
// src/utils/date.ts
|
|
309
|
+
function daysAgo(date) {
|
|
310
|
+
const now = /* @__PURE__ */ new Date();
|
|
311
|
+
const diffTime = Math.abs(now.getTime() - date.getTime());
|
|
312
|
+
const diffDays = Math.floor(diffTime / (1e3 * 60 * 60 * 24));
|
|
313
|
+
return diffDays;
|
|
314
|
+
}
|
|
315
|
+
function formatDaysAgo(date) {
|
|
316
|
+
const days = daysAgo(date);
|
|
317
|
+
if (days === 0) {
|
|
318
|
+
return "today";
|
|
319
|
+
} else if (days === 1) {
|
|
320
|
+
return "1 day ago";
|
|
321
|
+
} else if (days < 7) {
|
|
322
|
+
return `${days} days ago`;
|
|
323
|
+
} else if (days < 30) {
|
|
324
|
+
const weeks = Math.floor(days / 7);
|
|
325
|
+
return weeks === 1 ? "1 week ago" : `${weeks} weeks ago`;
|
|
326
|
+
} else if (days < 365) {
|
|
327
|
+
const months = Math.floor(days / 30);
|
|
328
|
+
return months === 1 ? "1 month ago" : `${months} months ago`;
|
|
329
|
+
} else {
|
|
330
|
+
const years = Math.floor(days / 365);
|
|
331
|
+
return years === 1 ? "1 year ago" : `${years} years ago`;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
function isOlderThan(date, days) {
|
|
335
|
+
return daysAgo(date) > days;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// src/components/BranchSelectStep.tsx
|
|
339
|
+
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
340
|
+
function BranchSelectStep({
|
|
341
|
+
branches,
|
|
342
|
+
onSelect,
|
|
343
|
+
onBack
|
|
344
|
+
}) {
|
|
345
|
+
const [selectedValues, setSelectedValues] = useState2([]);
|
|
346
|
+
const options2 = useMemo(() => {
|
|
347
|
+
return branches.map((branch) => {
|
|
348
|
+
const location = branch.isLocal && branch.isRemote ? "local+remote" : branch.isLocal ? "local" : "remote";
|
|
349
|
+
const status = branch.isMerged ? "merged" : "not merged";
|
|
350
|
+
const age = formatDaysAgo(branch.lastCommitDate);
|
|
351
|
+
return {
|
|
352
|
+
label: `${branch.name}`,
|
|
353
|
+
value: branch.name,
|
|
354
|
+
hint: `(${status}, ${age}, ${location})`
|
|
355
|
+
};
|
|
356
|
+
});
|
|
357
|
+
}, [branches]);
|
|
358
|
+
useInput2((input, key) => {
|
|
359
|
+
if (key.escape) {
|
|
360
|
+
onBack();
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
if (key.return && selectedValues.length > 0) {
|
|
364
|
+
const selected = branches.filter((b) => selectedValues.includes(b.name));
|
|
365
|
+
onSelect(selected);
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
if (input === "a") {
|
|
369
|
+
setSelectedValues(branches.map((b) => b.name));
|
|
370
|
+
}
|
|
371
|
+
if (input === "n") {
|
|
372
|
+
setSelectedValues([]);
|
|
373
|
+
}
|
|
374
|
+
if (input === "i") {
|
|
375
|
+
const inverted = branches.filter((b) => !selectedValues.includes(b.name)).map((b) => b.name);
|
|
376
|
+
setSelectedValues(inverted);
|
|
377
|
+
}
|
|
378
|
+
});
|
|
379
|
+
const handleChange = (values) => {
|
|
380
|
+
setSelectedValues(values);
|
|
381
|
+
};
|
|
382
|
+
const handleSubmit = () => {
|
|
383
|
+
const selected = branches.filter((b) => selectedValues.includes(b.name));
|
|
384
|
+
onSelect(selected);
|
|
385
|
+
};
|
|
386
|
+
if (branches.length === 0) {
|
|
387
|
+
return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", gap: 1, children: [
|
|
388
|
+
/* @__PURE__ */ jsxs4(Box4, { children: [
|
|
389
|
+
/* @__PURE__ */ jsx4(Text4, { color: "cyan", bold: true, children: "Step 3:" }),
|
|
390
|
+
/* @__PURE__ */ jsx4(Text4, { children: " Select branches to delete" })
|
|
391
|
+
] }),
|
|
392
|
+
/* @__PURE__ */ jsx4(Box4, { marginLeft: 2, children: /* @__PURE__ */ jsx4(Text4, { color: "yellow", children: "No branches match your criteria." }) }),
|
|
393
|
+
/* @__PURE__ */ jsx4(Text4, { color: "gray", dimColor: true, children: "Press Esc to go back and adjust filters" })
|
|
394
|
+
] });
|
|
395
|
+
}
|
|
396
|
+
return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", gap: 1, children: [
|
|
397
|
+
/* @__PURE__ */ jsxs4(Box4, { children: [
|
|
398
|
+
/* @__PURE__ */ jsx4(Text4, { color: "cyan", bold: true, children: "Step 3:" }),
|
|
399
|
+
/* @__PURE__ */ jsxs4(Text4, { children: [
|
|
400
|
+
" Select branches to delete (",
|
|
401
|
+
branches.length,
|
|
402
|
+
" found)"
|
|
403
|
+
] })
|
|
404
|
+
] }),
|
|
405
|
+
/* @__PURE__ */ jsxs4(Box4, { marginLeft: 2, gap: 2, children: [
|
|
406
|
+
/* @__PURE__ */ jsx4(Text4, { color: "gray", children: "[a] Select all" }),
|
|
407
|
+
/* @__PURE__ */ jsx4(Text4, { color: "gray", children: "[n] Select none" }),
|
|
408
|
+
/* @__PURE__ */ jsx4(Text4, { color: "gray", children: "[i] Invert" })
|
|
409
|
+
] }),
|
|
410
|
+
/* @__PURE__ */ jsx4(Box4, { marginLeft: 2, flexDirection: "column", children: /* @__PURE__ */ jsx4(
|
|
411
|
+
MultiSelect2,
|
|
412
|
+
{
|
|
413
|
+
options: options2,
|
|
414
|
+
defaultValue: selectedValues,
|
|
415
|
+
onChange: handleChange
|
|
416
|
+
}
|
|
417
|
+
) }),
|
|
418
|
+
/* @__PURE__ */ jsx4(Box4, { marginTop: 1, flexDirection: "column", children: /* @__PURE__ */ jsx4(Text4, { color: "gray", dimColor: true, children: "\u2191\u2193 navigate, Space toggle, Enter confirm, Esc back" }) }),
|
|
419
|
+
/* @__PURE__ */ jsxs4(Box4, { marginTop: 1, children: [
|
|
420
|
+
/* @__PURE__ */ jsxs4(Text4, { color: selectedValues.length > 0 ? "green" : "gray", children: [
|
|
421
|
+
selectedValues.length,
|
|
422
|
+
" branch(es) selected"
|
|
423
|
+
] }),
|
|
424
|
+
selectedValues.length > 0 && /* @__PURE__ */ jsx4(Text4, { color: "gray", children: " - Press Enter to continue" })
|
|
425
|
+
] })
|
|
426
|
+
] });
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// src/components/ConfirmStep.tsx
|
|
430
|
+
import { Box as Box5, Text as Text5, useInput as useInput3 } from "ink";
|
|
431
|
+
import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
432
|
+
function ConfirmStep({
|
|
433
|
+
branches,
|
|
434
|
+
isDryRun,
|
|
435
|
+
onConfirm,
|
|
436
|
+
onCancel
|
|
437
|
+
}) {
|
|
438
|
+
const localCount = branches.filter((b) => b.isLocal).length;
|
|
439
|
+
const remoteCount = branches.filter((b) => b.isRemote).length;
|
|
440
|
+
useInput3((input, key) => {
|
|
441
|
+
if (key.return) {
|
|
442
|
+
onConfirm();
|
|
443
|
+
}
|
|
444
|
+
if (key.escape || input === "n" || input === "N") {
|
|
445
|
+
onCancel();
|
|
446
|
+
}
|
|
447
|
+
if (input === "y" || input === "Y") {
|
|
448
|
+
onConfirm();
|
|
449
|
+
}
|
|
450
|
+
});
|
|
451
|
+
return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", gap: 1, children: [
|
|
452
|
+
/* @__PURE__ */ jsxs5(Box5, { children: [
|
|
453
|
+
/* @__PURE__ */ jsx5(Text5, { color: "cyan", bold: true, children: "Step 4:" }),
|
|
454
|
+
/* @__PURE__ */ jsx5(Text5, { children: " Confirm deletion" })
|
|
455
|
+
] }),
|
|
456
|
+
/* @__PURE__ */ jsxs5(
|
|
457
|
+
Box5,
|
|
458
|
+
{
|
|
459
|
+
flexDirection: "column",
|
|
460
|
+
borderStyle: "round",
|
|
461
|
+
borderColor: "yellow",
|
|
462
|
+
paddingX: 2,
|
|
463
|
+
paddingY: 1,
|
|
464
|
+
marginY: 1,
|
|
465
|
+
children: [
|
|
466
|
+
/* @__PURE__ */ jsxs5(Text5, { bold: true, children: [
|
|
467
|
+
"Ready to delete ",
|
|
468
|
+
branches.length,
|
|
469
|
+
" branch(es)"
|
|
470
|
+
] }),
|
|
471
|
+
/* @__PURE__ */ jsxs5(Box5, { marginTop: 1, gap: 2, children: [
|
|
472
|
+
/* @__PURE__ */ jsxs5(Text5, { children: [
|
|
473
|
+
"Local: ",
|
|
474
|
+
/* @__PURE__ */ jsx5(Text5, { color: "cyan", children: localCount })
|
|
475
|
+
] }),
|
|
476
|
+
/* @__PURE__ */ jsxs5(Text5, { children: [
|
|
477
|
+
"Remote: ",
|
|
478
|
+
/* @__PURE__ */ jsx5(Text5, { color: "magenta", children: remoteCount })
|
|
479
|
+
] })
|
|
480
|
+
] }),
|
|
481
|
+
isDryRun && /* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: /* @__PURE__ */ jsx5(Text5, { color: "yellow", bold: true, children: "DRY RUN MODE - No branches will actually be deleted" }) }),
|
|
482
|
+
!isDryRun && /* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: /* @__PURE__ */ jsx5(Text5, { color: "red", bold: true, children: "WARNING: This will permanently delete these branches!" }) })
|
|
483
|
+
]
|
|
484
|
+
}
|
|
485
|
+
),
|
|
486
|
+
/* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", children: [
|
|
487
|
+
/* @__PURE__ */ jsx5(Text5, { color: "gray", children: "Branches to delete:" }),
|
|
488
|
+
/* @__PURE__ */ jsxs5(Box5, { marginLeft: 2, flexDirection: "column", children: [
|
|
489
|
+
branches.slice(0, 10).map((branch) => /* @__PURE__ */ jsxs5(Text5, { color: "gray", children: [
|
|
490
|
+
"- ",
|
|
491
|
+
branch.name,
|
|
492
|
+
" ",
|
|
493
|
+
/* @__PURE__ */ jsxs5(Text5, { dimColor: true, children: [
|
|
494
|
+
"(",
|
|
495
|
+
branch.isLocal && "local",
|
|
496
|
+
branch.isLocal && branch.isRemote && ", ",
|
|
497
|
+
branch.isRemote && "remote",
|
|
498
|
+
")"
|
|
499
|
+
] })
|
|
500
|
+
] }, branch.name)),
|
|
501
|
+
branches.length > 10 && /* @__PURE__ */ jsxs5(Text5, { color: "gray", dimColor: true, children: [
|
|
502
|
+
"... and ",
|
|
503
|
+
branches.length - 10,
|
|
504
|
+
" more"
|
|
505
|
+
] })
|
|
506
|
+
] })
|
|
507
|
+
] }),
|
|
508
|
+
/* @__PURE__ */ jsxs5(Box5, { marginTop: 1, gap: 2, children: [
|
|
509
|
+
/* @__PURE__ */ jsx5(Text5, { color: "green", children: "[Enter/Y] Confirm" }),
|
|
510
|
+
/* @__PURE__ */ jsx5(Text5, { color: "red", children: "[Esc/N] Cancel" })
|
|
511
|
+
] })
|
|
512
|
+
] });
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// src/components/ExecutionStep.tsx
|
|
516
|
+
import { useEffect, useState as useState3 } from "react";
|
|
517
|
+
import { Box as Box6, Text as Text6 } from "ink";
|
|
518
|
+
import { Spinner } from "@inkjs/ui";
|
|
519
|
+
|
|
520
|
+
// src/services/git.ts
|
|
521
|
+
import simpleGit from "simple-git";
|
|
522
|
+
var git;
|
|
523
|
+
function initGit(cwd) {
|
|
524
|
+
git = simpleGit(cwd);
|
|
525
|
+
return git;
|
|
526
|
+
}
|
|
527
|
+
async function isGitRepo() {
|
|
528
|
+
try {
|
|
529
|
+
await git.revparse(["--git-dir"]);
|
|
530
|
+
return true;
|
|
531
|
+
} catch {
|
|
532
|
+
return false;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
async function getCurrentBranch() {
|
|
536
|
+
const result = await git.revparse(["--abbrev-ref", "HEAD"]);
|
|
537
|
+
return result.trim();
|
|
538
|
+
}
|
|
539
|
+
async function getRemoteInfo() {
|
|
540
|
+
try {
|
|
541
|
+
const remotes = await git.getRemotes(true);
|
|
542
|
+
const origin = remotes.find((r) => r.name === "origin");
|
|
543
|
+
if (!origin?.refs?.fetch) {
|
|
544
|
+
return null;
|
|
545
|
+
}
|
|
546
|
+
const url = origin.refs.fetch;
|
|
547
|
+
const sshMatch = url.match(/git@github\.com:([^/]+)\/(.+?)(?:\.git)?$/);
|
|
548
|
+
const httpsMatch = url.match(/https:\/\/github\.com\/([^/]+)\/(.+?)(?:\.git)?$/);
|
|
549
|
+
const match = sshMatch || httpsMatch;
|
|
550
|
+
if (match) {
|
|
551
|
+
return {
|
|
552
|
+
owner: match[1],
|
|
553
|
+
repo: match[2],
|
|
554
|
+
isGitHub: true
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
return { owner: "", repo: "", isGitHub: false };
|
|
558
|
+
} catch {
|
|
559
|
+
return null;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
async function getLocalBranches(defaultBranch, currentBranch) {
|
|
563
|
+
const branches = [];
|
|
564
|
+
try {
|
|
565
|
+
const branchSummary = await git.branchLocal();
|
|
566
|
+
for (const branchName of branchSummary.all) {
|
|
567
|
+
const logResult = await git.log({
|
|
568
|
+
[branchName]: null,
|
|
569
|
+
maxCount: 1,
|
|
570
|
+
format: { date: "%aI" }
|
|
571
|
+
});
|
|
572
|
+
const lastCommitDate = logResult.latest?.date ? new Date(logResult.latest.date) : /* @__PURE__ */ new Date();
|
|
573
|
+
const isMerged = await isBranchMerged(branchName, defaultBranch);
|
|
574
|
+
const isProtected = branchName === defaultBranch || matchesProtectedPattern(branchName);
|
|
575
|
+
branches.push({
|
|
576
|
+
name: branchName,
|
|
577
|
+
isLocal: true,
|
|
578
|
+
isRemote: false,
|
|
579
|
+
lastCommitDate,
|
|
580
|
+
isMerged,
|
|
581
|
+
isProtected,
|
|
582
|
+
isCurrentBranch: branchName === currentBranch
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
} catch (error) {
|
|
586
|
+
console.error("Error getting local branches:", error);
|
|
587
|
+
}
|
|
588
|
+
return branches;
|
|
589
|
+
}
|
|
590
|
+
async function getRemoteBranches(defaultBranch) {
|
|
591
|
+
const branches = [];
|
|
592
|
+
try {
|
|
593
|
+
await git.fetch(["--prune"]);
|
|
594
|
+
const result = await git.branch(["-r"]);
|
|
595
|
+
for (const branchName of result.all) {
|
|
596
|
+
if (branchName.includes("HEAD")) continue;
|
|
597
|
+
const shortName = branchName.replace(/^origin\//, "");
|
|
598
|
+
if (shortName === defaultBranch) continue;
|
|
599
|
+
const logResult = await git.log({
|
|
600
|
+
[branchName]: null,
|
|
601
|
+
maxCount: 1,
|
|
602
|
+
format: { date: "%aI" }
|
|
603
|
+
});
|
|
604
|
+
const lastCommitDate = logResult.latest?.date ? new Date(logResult.latest.date) : /* @__PURE__ */ new Date();
|
|
605
|
+
const isMerged = await isBranchMerged(branchName, `origin/${defaultBranch}`);
|
|
606
|
+
const isProtected = shortName === defaultBranch || matchesProtectedPattern(shortName);
|
|
607
|
+
branches.push({
|
|
608
|
+
name: shortName,
|
|
609
|
+
isLocal: false,
|
|
610
|
+
isRemote: true,
|
|
611
|
+
lastCommitDate,
|
|
612
|
+
isMerged,
|
|
613
|
+
isProtected,
|
|
614
|
+
isCurrentBranch: false
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
} catch (error) {
|
|
618
|
+
console.error("Error getting remote branches:", error);
|
|
619
|
+
}
|
|
620
|
+
return branches;
|
|
621
|
+
}
|
|
622
|
+
async function isBranchMerged(branch, target) {
|
|
623
|
+
try {
|
|
624
|
+
const result = await git.raw(["branch", "--merged", target]);
|
|
625
|
+
const mergedBranches = result.split("\n").map((b) => b.trim().replace(/^\*\s*/, ""));
|
|
626
|
+
return mergedBranches.includes(branch) || mergedBranches.includes(branch.replace(/^origin\//, ""));
|
|
627
|
+
} catch {
|
|
628
|
+
return false;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
async function deleteLocalBranch(branchName, force = false) {
|
|
632
|
+
const flag = force ? "-D" : "-d";
|
|
633
|
+
await git.branch([flag, branchName]);
|
|
634
|
+
}
|
|
635
|
+
async function deleteRemoteBranch(branchName, remote = "origin") {
|
|
636
|
+
await git.push([remote, "--delete", branchName]);
|
|
637
|
+
}
|
|
638
|
+
async function getRepoInfo(defaultBranch) {
|
|
639
|
+
try {
|
|
640
|
+
const currentBranch = await getCurrentBranch();
|
|
641
|
+
const remoteInfo = await getRemoteInfo();
|
|
642
|
+
return {
|
|
643
|
+
owner: remoteInfo?.owner || "",
|
|
644
|
+
repo: remoteInfo?.repo || "",
|
|
645
|
+
defaultBranch: defaultBranch || "main",
|
|
646
|
+
currentBranch,
|
|
647
|
+
isGitHub: remoteInfo?.isGitHub || false
|
|
648
|
+
};
|
|
649
|
+
} catch {
|
|
650
|
+
return null;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// src/components/ExecutionStep.tsx
|
|
655
|
+
import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
656
|
+
function ExecutionStep({
|
|
657
|
+
branches,
|
|
658
|
+
isDryRun,
|
|
659
|
+
onComplete
|
|
660
|
+
}) {
|
|
661
|
+
const [currentIndex, setCurrentIndex] = useState3(0);
|
|
662
|
+
const [results, setResults] = useState3([]);
|
|
663
|
+
const [isDeleting, setIsDeleting] = useState3(true);
|
|
664
|
+
useEffect(() => {
|
|
665
|
+
const deleteBranches = async () => {
|
|
666
|
+
const allResults = [];
|
|
667
|
+
for (let i = 0; i < branches.length; i++) {
|
|
668
|
+
const branch = branches[i];
|
|
669
|
+
setCurrentIndex(i);
|
|
670
|
+
const result = {
|
|
671
|
+
branch,
|
|
672
|
+
success: true,
|
|
673
|
+
deletedLocal: false,
|
|
674
|
+
deletedRemote: false
|
|
675
|
+
};
|
|
676
|
+
if (isDryRun) {
|
|
677
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
678
|
+
result.deletedLocal = branch.isLocal;
|
|
679
|
+
result.deletedRemote = branch.isRemote;
|
|
680
|
+
} else {
|
|
681
|
+
try {
|
|
682
|
+
if (branch.isLocal) {
|
|
683
|
+
await deleteLocalBranch(branch.name, true);
|
|
684
|
+
result.deletedLocal = true;
|
|
685
|
+
}
|
|
686
|
+
} catch (error) {
|
|
687
|
+
result.success = false;
|
|
688
|
+
result.error = `Local: ${error instanceof Error ? error.message : "Unknown error"}`;
|
|
689
|
+
}
|
|
690
|
+
try {
|
|
691
|
+
if (branch.isRemote) {
|
|
692
|
+
await deleteRemoteBranch(branch.name);
|
|
693
|
+
result.deletedRemote = true;
|
|
694
|
+
}
|
|
695
|
+
} catch (error) {
|
|
696
|
+
if (!result.error) {
|
|
697
|
+
result.success = false;
|
|
698
|
+
result.error = `Remote: ${error instanceof Error ? error.message : "Unknown error"}`;
|
|
699
|
+
} else {
|
|
700
|
+
result.error += `; Remote: ${error instanceof Error ? error.message : "Unknown error"}`;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
allResults.push(result);
|
|
705
|
+
setResults([...allResults]);
|
|
706
|
+
}
|
|
707
|
+
setIsDeleting(false);
|
|
708
|
+
const summary = {
|
|
709
|
+
total: branches.length,
|
|
710
|
+
successful: allResults.filter((r) => r.success).length,
|
|
711
|
+
failed: allResults.filter((r) => !r.success).length,
|
|
712
|
+
skipped: 0,
|
|
713
|
+
results: allResults
|
|
714
|
+
};
|
|
715
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
716
|
+
onComplete(summary);
|
|
717
|
+
};
|
|
718
|
+
deleteBranches();
|
|
719
|
+
}, [branches, isDryRun, onComplete]);
|
|
720
|
+
const currentBranch = branches[currentIndex];
|
|
721
|
+
const completedCount = results.length;
|
|
722
|
+
return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", gap: 1, children: [
|
|
723
|
+
/* @__PURE__ */ jsxs6(Box6, { children: [
|
|
724
|
+
/* @__PURE__ */ jsx6(Text6, { color: "cyan", bold: true, children: "Step 5:" }),
|
|
725
|
+
/* @__PURE__ */ jsxs6(Text6, { children: [
|
|
726
|
+
" ",
|
|
727
|
+
isDryRun ? "Simulating deletion..." : "Deleting branches..."
|
|
728
|
+
] })
|
|
729
|
+
] }),
|
|
730
|
+
/* @__PURE__ */ jsxs6(
|
|
731
|
+
Box6,
|
|
732
|
+
{
|
|
733
|
+
flexDirection: "column",
|
|
734
|
+
borderStyle: "round",
|
|
735
|
+
borderColor: "blue",
|
|
736
|
+
paddingX: 2,
|
|
737
|
+
paddingY: 1,
|
|
738
|
+
children: [
|
|
739
|
+
isDeleting && currentBranch && /* @__PURE__ */ jsxs6(Box6, { gap: 1, children: [
|
|
740
|
+
/* @__PURE__ */ jsx6(Spinner, { label: "" }),
|
|
741
|
+
/* @__PURE__ */ jsxs6(Text6, { children: [
|
|
742
|
+
isDryRun ? "Processing" : "Deleting",
|
|
743
|
+
" ",
|
|
744
|
+
currentBranch.name
|
|
745
|
+
] }),
|
|
746
|
+
/* @__PURE__ */ jsxs6(Text6, { color: "gray", children: [
|
|
747
|
+
"(",
|
|
748
|
+
completedCount + 1,
|
|
749
|
+
"/",
|
|
750
|
+
branches.length,
|
|
751
|
+
")"
|
|
752
|
+
] })
|
|
753
|
+
] }),
|
|
754
|
+
!isDeleting && /* @__PURE__ */ jsxs6(Text6, { color: "green", children: [
|
|
755
|
+
isDryRun ? "Simulation" : "Deletion",
|
|
756
|
+
" complete!"
|
|
757
|
+
] })
|
|
758
|
+
]
|
|
759
|
+
}
|
|
760
|
+
),
|
|
761
|
+
/* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", marginTop: 1, children: [
|
|
762
|
+
/* @__PURE__ */ jsx6(Text6, { color: "gray", children: "Results:" }),
|
|
763
|
+
/* @__PURE__ */ jsxs6(Box6, { marginLeft: 2, flexDirection: "column", children: [
|
|
764
|
+
results.slice(-8).map((result, index) => /* @__PURE__ */ jsxs6(Box6, { gap: 1, children: [
|
|
765
|
+
/* @__PURE__ */ jsx6(Text6, { color: result.success ? "green" : "red", children: result.success ? "\u2713" : "\u2717" }),
|
|
766
|
+
/* @__PURE__ */ jsx6(Text6, { color: result.success ? "gray" : "red", children: result.branch.name }),
|
|
767
|
+
result.deletedLocal && /* @__PURE__ */ jsx6(Text6, { color: "cyan", dimColor: true, children: "(local)" }),
|
|
768
|
+
result.deletedRemote && /* @__PURE__ */ jsx6(Text6, { color: "magenta", dimColor: true, children: "(remote)" }),
|
|
769
|
+
result.error && /* @__PURE__ */ jsxs6(Text6, { color: "red", dimColor: true, children: [
|
|
770
|
+
"- ",
|
|
771
|
+
result.error
|
|
772
|
+
] })
|
|
773
|
+
] }, result.branch.name)),
|
|
774
|
+
results.length > 8 && /* @__PURE__ */ jsxs6(Text6, { color: "gray", dimColor: true, children: [
|
|
775
|
+
"... and ",
|
|
776
|
+
results.length - 8,
|
|
777
|
+
" more"
|
|
778
|
+
] })
|
|
779
|
+
] })
|
|
780
|
+
] })
|
|
781
|
+
] });
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// src/components/SummaryStep.tsx
|
|
785
|
+
import { Box as Box7, Text as Text7, useInput as useInput4, useApp } from "ink";
|
|
786
|
+
import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
787
|
+
function SummaryStep({ summary, isDryRun, onDeleteForReal }) {
|
|
788
|
+
const { exit } = useApp();
|
|
789
|
+
useInput4((input, key) => {
|
|
790
|
+
if (isDryRun && (input === "d" || input === "D")) {
|
|
791
|
+
onDeleteForReal?.();
|
|
792
|
+
return;
|
|
793
|
+
}
|
|
794
|
+
if (input === "q" || input === "Q" || key.escape) {
|
|
795
|
+
exit();
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
if (!isDryRun) {
|
|
799
|
+
exit();
|
|
800
|
+
}
|
|
801
|
+
});
|
|
802
|
+
const successColor = summary.successful > 0 ? "green" : "gray";
|
|
803
|
+
const failedColor = summary.failed > 0 ? "red" : "gray";
|
|
804
|
+
return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", gap: 1, children: [
|
|
805
|
+
/* @__PURE__ */ jsx7(Box7, { children: /* @__PURE__ */ jsx7(Text7, { color: "cyan", bold: true, children: isDryRun ? "Dry Run Complete!" : "Cleanup Complete!" }) }),
|
|
806
|
+
/* @__PURE__ */ jsx7(
|
|
807
|
+
Box7,
|
|
808
|
+
{
|
|
809
|
+
flexDirection: "column",
|
|
810
|
+
borderStyle: "round",
|
|
811
|
+
borderColor: summary.failed > 0 ? "yellow" : "green",
|
|
812
|
+
paddingX: 2,
|
|
813
|
+
paddingY: 1,
|
|
814
|
+
children: /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", gap: 0, children: [
|
|
815
|
+
/* @__PURE__ */ jsxs7(Box7, { gap: 1, children: [
|
|
816
|
+
/* @__PURE__ */ jsx7(Text7, { color: successColor, children: "\u2713" }),
|
|
817
|
+
/* @__PURE__ */ jsxs7(Text7, { children: [
|
|
818
|
+
summary.successful,
|
|
819
|
+
" branch(es)",
|
|
820
|
+
" ",
|
|
821
|
+
isDryRun ? "would be deleted" : "deleted successfully"
|
|
822
|
+
] })
|
|
823
|
+
] }),
|
|
824
|
+
summary.failed > 0 && /* @__PURE__ */ jsxs7(Box7, { gap: 1, children: [
|
|
825
|
+
/* @__PURE__ */ jsx7(Text7, { color: failedColor, children: "\u2717" }),
|
|
826
|
+
/* @__PURE__ */ jsxs7(Text7, { color: failedColor, children: [
|
|
827
|
+
summary.failed,
|
|
828
|
+
" branch(es) failed"
|
|
829
|
+
] })
|
|
830
|
+
] }),
|
|
831
|
+
summary.skipped > 0 && /* @__PURE__ */ jsxs7(Box7, { gap: 1, children: [
|
|
832
|
+
/* @__PURE__ */ jsx7(Text7, { color: "gray", children: "\u25CB" }),
|
|
833
|
+
/* @__PURE__ */ jsxs7(Text7, { color: "gray", children: [
|
|
834
|
+
summary.skipped,
|
|
835
|
+
" branch(es) skipped"
|
|
836
|
+
] })
|
|
837
|
+
] })
|
|
838
|
+
] })
|
|
839
|
+
}
|
|
840
|
+
),
|
|
841
|
+
isDryRun && summary.successful > 0 && /* @__PURE__ */ jsx7(Box7, { marginTop: 1, flexDirection: "column", children: /* @__PURE__ */ jsxs7(Box7, { gap: 2, children: [
|
|
842
|
+
/* @__PURE__ */ jsx7(Text7, { color: "red", bold: true, children: "[d] Delete for real" }),
|
|
843
|
+
/* @__PURE__ */ jsx7(Text7, { color: "gray", children: "[q] Quit" })
|
|
844
|
+
] }) }),
|
|
845
|
+
isDryRun && summary.successful === 0 && /* @__PURE__ */ jsx7(Box7, { marginTop: 1, children: /* @__PURE__ */ jsx7(Text7, { color: "gray", dimColor: true, children: "Press any key to exit" }) }),
|
|
846
|
+
summary.failed > 0 && /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", marginTop: 1, children: [
|
|
847
|
+
/* @__PURE__ */ jsx7(Text7, { color: "red", children: "Failed branches:" }),
|
|
848
|
+
/* @__PURE__ */ jsxs7(Box7, { marginLeft: 2, flexDirection: "column", children: [
|
|
849
|
+
summary.results.filter((r) => !r.success).slice(0, 5).map((result) => /* @__PURE__ */ jsxs7(Text7, { color: "red", children: [
|
|
850
|
+
"- ",
|
|
851
|
+
result.branch.name,
|
|
852
|
+
": ",
|
|
853
|
+
result.error
|
|
854
|
+
] }, result.branch.name)),
|
|
855
|
+
summary.results.filter((r) => !r.success).length > 5 && /* @__PURE__ */ jsxs7(Text7, { color: "gray", dimColor: true, children: [
|
|
856
|
+
"... and ",
|
|
857
|
+
summary.results.filter((r) => !r.success).length - 5,
|
|
858
|
+
" more"
|
|
859
|
+
] })
|
|
860
|
+
] })
|
|
861
|
+
] }),
|
|
862
|
+
!isDryRun && /* @__PURE__ */ jsx7(Box7, { marginTop: 1, children: /* @__PURE__ */ jsx7(Text7, { color: "gray", dimColor: true, children: "Press any key to exit" }) })
|
|
863
|
+
] });
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
// src/hooks/useWizard.ts
|
|
867
|
+
import { useState as useState4, useCallback } from "react";
|
|
868
|
+
var STEP_ORDER = [
|
|
869
|
+
"init",
|
|
870
|
+
"scope",
|
|
871
|
+
"criteria",
|
|
872
|
+
"loading",
|
|
873
|
+
"select",
|
|
874
|
+
"confirm",
|
|
875
|
+
"execute",
|
|
876
|
+
"summary"
|
|
877
|
+
];
|
|
878
|
+
function useWizard(initialStep = "init") {
|
|
879
|
+
const [step, setStep] = useState4(initialStep);
|
|
880
|
+
const nextStep = useCallback(() => {
|
|
881
|
+
const currentIndex = STEP_ORDER.indexOf(step);
|
|
882
|
+
if (currentIndex < STEP_ORDER.length - 1) {
|
|
883
|
+
setStep(STEP_ORDER[currentIndex + 1]);
|
|
884
|
+
}
|
|
885
|
+
}, [step]);
|
|
886
|
+
const prevStep = useCallback(() => {
|
|
887
|
+
const currentIndex = STEP_ORDER.indexOf(step);
|
|
888
|
+
if (currentIndex > 0) {
|
|
889
|
+
setStep(STEP_ORDER[currentIndex - 1]);
|
|
890
|
+
}
|
|
891
|
+
}, [step]);
|
|
892
|
+
const goToStep = useCallback((newStep) => {
|
|
893
|
+
setStep(newStep);
|
|
894
|
+
}, []);
|
|
895
|
+
return {
|
|
896
|
+
step,
|
|
897
|
+
nextStep,
|
|
898
|
+
prevStep,
|
|
899
|
+
goToStep
|
|
900
|
+
};
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
// src/services/github.ts
|
|
904
|
+
import { Octokit } from "@octokit/rest";
|
|
905
|
+
var octokit = null;
|
|
906
|
+
function initGitHub(token) {
|
|
907
|
+
const authToken = token || process.env.GITHUB_TOKEN;
|
|
908
|
+
octokit = new Octokit({
|
|
909
|
+
auth: authToken
|
|
910
|
+
});
|
|
911
|
+
return octokit;
|
|
912
|
+
}
|
|
913
|
+
async function getDefaultBranch(owner, repo) {
|
|
914
|
+
if (!octokit) {
|
|
915
|
+
return "main";
|
|
916
|
+
}
|
|
917
|
+
try {
|
|
918
|
+
const { data } = await octokit.repos.get({ owner, repo });
|
|
919
|
+
return data.default_branch;
|
|
920
|
+
} catch (error) {
|
|
921
|
+
console.error("Error fetching default branch:", error);
|
|
922
|
+
return "main";
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
// src/services/branch-analyzer.ts
|
|
927
|
+
async function fetchBranches(scope, defaultBranch, currentBranch) {
|
|
928
|
+
let branches = [];
|
|
929
|
+
if (scope === "local" || scope === "both") {
|
|
930
|
+
const localBranches = await getLocalBranches(defaultBranch, currentBranch);
|
|
931
|
+
branches = [...branches, ...localBranches];
|
|
932
|
+
}
|
|
933
|
+
if (scope === "remote" || scope === "both") {
|
|
934
|
+
const remoteBranches = await getRemoteBranches(defaultBranch);
|
|
935
|
+
if (scope === "both") {
|
|
936
|
+
for (const remoteBranch of remoteBranches) {
|
|
937
|
+
const existingIndex = branches.findIndex(
|
|
938
|
+
(b) => b.name === remoteBranch.name
|
|
939
|
+
);
|
|
940
|
+
if (existingIndex >= 0) {
|
|
941
|
+
branches[existingIndex].isRemote = true;
|
|
942
|
+
} else {
|
|
943
|
+
branches.push(remoteBranch);
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
} else {
|
|
947
|
+
branches = remoteBranches;
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
return branches;
|
|
951
|
+
}
|
|
952
|
+
function filterBranches(branches, filters) {
|
|
953
|
+
let filtered = [...branches];
|
|
954
|
+
filtered = filtered.filter((b) => !b.isProtected && !b.isCurrentBranch);
|
|
955
|
+
const hasFilters = filters.merged || filters.stale || filters.pattern || filters.age;
|
|
956
|
+
if (!hasFilters) {
|
|
957
|
+
return filtered;
|
|
958
|
+
}
|
|
959
|
+
filtered = filtered.filter((branch) => {
|
|
960
|
+
const matches = [];
|
|
961
|
+
if (filters.merged) {
|
|
962
|
+
matches.push(branch.isMerged);
|
|
963
|
+
}
|
|
964
|
+
if (filters.stale) {
|
|
965
|
+
matches.push(isOlderThan(branch.lastCommitDate, filters.staleDays));
|
|
966
|
+
}
|
|
967
|
+
if (filters.age) {
|
|
968
|
+
matches.push(isOlderThan(branch.lastCommitDate, filters.ageDays));
|
|
969
|
+
}
|
|
970
|
+
if (filters.pattern && filters.patternValue) {
|
|
971
|
+
matches.push(matchesPattern(branch.name, filters.patternValue));
|
|
972
|
+
}
|
|
973
|
+
return matches.some((m) => m === true);
|
|
974
|
+
});
|
|
975
|
+
return filtered;
|
|
976
|
+
}
|
|
977
|
+
function matchesPattern(branchName, pattern) {
|
|
978
|
+
const regexPattern = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
|
|
979
|
+
const regex = new RegExp(`^${regexPattern}$`, "i");
|
|
980
|
+
return regex.test(branchName);
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
// src/app.tsx
|
|
984
|
+
import { jsx as jsx8, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
985
|
+
function App({ options: options2 }) {
|
|
986
|
+
const { step, goToStep } = useWizard("init");
|
|
987
|
+
const [repoInfo, setRepoInfo] = useState5(null);
|
|
988
|
+
const [scope, setScope] = useState5("both");
|
|
989
|
+
const [filters, setFilters] = useState5({
|
|
990
|
+
merged: false,
|
|
991
|
+
stale: false,
|
|
992
|
+
staleDays: DEFAULT_STALE_DAYS,
|
|
993
|
+
pattern: false,
|
|
994
|
+
patternValue: "",
|
|
995
|
+
age: false,
|
|
996
|
+
ageDays: DEFAULT_AGE_DAYS
|
|
997
|
+
});
|
|
998
|
+
const [allBranches, setAllBranches] = useState5([]);
|
|
999
|
+
const [filteredBranches, setFilteredBranches] = useState5([]);
|
|
1000
|
+
const [selectedBranches, setSelectedBranches] = useState5([]);
|
|
1001
|
+
const [deletionSummary, setDeletionSummary] = useState5(null);
|
|
1002
|
+
const [error, setError] = useState5(null);
|
|
1003
|
+
const [loadingMessage, setLoadingMessage] = useState5("Initializing...");
|
|
1004
|
+
const [executeForReal, setExecuteForReal] = useState5(false);
|
|
1005
|
+
useEffect2(() => {
|
|
1006
|
+
const initialize = async () => {
|
|
1007
|
+
try {
|
|
1008
|
+
initGit();
|
|
1009
|
+
const isRepo = await isGitRepo();
|
|
1010
|
+
if (!isRepo) {
|
|
1011
|
+
setError("Not a git repository. Please run this command in a git repository.");
|
|
1012
|
+
return;
|
|
1013
|
+
}
|
|
1014
|
+
if (options2.token || process.env.GITHUB_TOKEN) {
|
|
1015
|
+
initGitHub(options2.token);
|
|
1016
|
+
}
|
|
1017
|
+
setLoadingMessage("Detecting repository...");
|
|
1018
|
+
let info = await getRepoInfo();
|
|
1019
|
+
if (info?.isGitHub && (options2.token || process.env.GITHUB_TOKEN)) {
|
|
1020
|
+
setLoadingMessage("Fetching repository info from GitHub...");
|
|
1021
|
+
const defaultBranch = await getDefaultBranch(info.owner, info.repo);
|
|
1022
|
+
info = { ...info, defaultBranch };
|
|
1023
|
+
}
|
|
1024
|
+
setRepoInfo(info);
|
|
1025
|
+
goToStep("scope");
|
|
1026
|
+
} catch (err) {
|
|
1027
|
+
setError(err instanceof Error ? err.message : "Unknown error occurred");
|
|
1028
|
+
}
|
|
1029
|
+
};
|
|
1030
|
+
initialize();
|
|
1031
|
+
}, [options2.token, goToStep]);
|
|
1032
|
+
const handleScopeSelect = useCallback2((selectedScope) => {
|
|
1033
|
+
setScope(selectedScope);
|
|
1034
|
+
goToStep("criteria");
|
|
1035
|
+
}, [goToStep]);
|
|
1036
|
+
const handleCriteriaSelect = useCallback2(async (selectedFilters) => {
|
|
1037
|
+
setFilters(selectedFilters);
|
|
1038
|
+
goToStep("loading");
|
|
1039
|
+
setLoadingMessage("Fetching branches...");
|
|
1040
|
+
try {
|
|
1041
|
+
if (!repoInfo) {
|
|
1042
|
+
throw new Error("Repository info not available");
|
|
1043
|
+
}
|
|
1044
|
+
const branches = await fetchBranches(
|
|
1045
|
+
scope,
|
|
1046
|
+
repoInfo.defaultBranch,
|
|
1047
|
+
repoInfo.currentBranch
|
|
1048
|
+
);
|
|
1049
|
+
setAllBranches(branches);
|
|
1050
|
+
setLoadingMessage("Analyzing branches...");
|
|
1051
|
+
const filtered = filterBranches(branches, selectedFilters);
|
|
1052
|
+
setFilteredBranches(filtered);
|
|
1053
|
+
goToStep("select");
|
|
1054
|
+
} catch (err) {
|
|
1055
|
+
setError(err instanceof Error ? err.message : "Failed to fetch branches");
|
|
1056
|
+
}
|
|
1057
|
+
}, [repoInfo, scope, goToStep]);
|
|
1058
|
+
const handleBranchSelect = useCallback2((selected) => {
|
|
1059
|
+
setSelectedBranches(selected);
|
|
1060
|
+
goToStep("confirm");
|
|
1061
|
+
}, [goToStep]);
|
|
1062
|
+
const handleConfirm = useCallback2(() => {
|
|
1063
|
+
goToStep("execute");
|
|
1064
|
+
}, [goToStep]);
|
|
1065
|
+
const handleDeletionComplete = useCallback2((summary) => {
|
|
1066
|
+
setDeletionSummary(summary);
|
|
1067
|
+
goToStep("summary");
|
|
1068
|
+
}, [goToStep]);
|
|
1069
|
+
const handleDeleteForReal = useCallback2(() => {
|
|
1070
|
+
setExecuteForReal(true);
|
|
1071
|
+
setDeletionSummary(null);
|
|
1072
|
+
goToStep("execute");
|
|
1073
|
+
}, [goToStep]);
|
|
1074
|
+
const handleBack = useCallback2(() => {
|
|
1075
|
+
switch (step) {
|
|
1076
|
+
case "criteria":
|
|
1077
|
+
goToStep("scope");
|
|
1078
|
+
break;
|
|
1079
|
+
case "select":
|
|
1080
|
+
goToStep("criteria");
|
|
1081
|
+
break;
|
|
1082
|
+
case "confirm":
|
|
1083
|
+
goToStep("select");
|
|
1084
|
+
break;
|
|
1085
|
+
default:
|
|
1086
|
+
break;
|
|
1087
|
+
}
|
|
1088
|
+
}, [step, goToStep]);
|
|
1089
|
+
if (error) {
|
|
1090
|
+
return /* @__PURE__ */ jsx8(Box8, { flexDirection: "column", padding: 1, children: /* @__PURE__ */ jsxs8(Text8, { color: "red", bold: true, children: [
|
|
1091
|
+
"Error: ",
|
|
1092
|
+
error
|
|
1093
|
+
] }) });
|
|
1094
|
+
}
|
|
1095
|
+
return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", padding: 1, children: [
|
|
1096
|
+
/* @__PURE__ */ jsx8(Header, { repoInfo }),
|
|
1097
|
+
step === "init" && /* @__PURE__ */ jsx8(Box8, { gap: 1, children: /* @__PURE__ */ jsx8(Spinner2, { label: loadingMessage }) }),
|
|
1098
|
+
step === "loading" && /* @__PURE__ */ jsx8(Box8, { gap: 1, children: /* @__PURE__ */ jsx8(Spinner2, { label: loadingMessage }) }),
|
|
1099
|
+
step === "scope" && /* @__PURE__ */ jsx8(ScopeStep, { onSelect: handleScopeSelect }),
|
|
1100
|
+
step === "criteria" && /* @__PURE__ */ jsx8(CriteriaStep, { onSelect: handleCriteriaSelect, onBack: handleBack }),
|
|
1101
|
+
step === "select" && /* @__PURE__ */ jsx8(
|
|
1102
|
+
BranchSelectStep,
|
|
1103
|
+
{
|
|
1104
|
+
branches: filteredBranches,
|
|
1105
|
+
onSelect: handleBranchSelect,
|
|
1106
|
+
onBack: handleBack
|
|
1107
|
+
}
|
|
1108
|
+
),
|
|
1109
|
+
step === "confirm" && /* @__PURE__ */ jsx8(
|
|
1110
|
+
ConfirmStep,
|
|
1111
|
+
{
|
|
1112
|
+
branches: selectedBranches,
|
|
1113
|
+
isDryRun: !options2.execute,
|
|
1114
|
+
onConfirm: handleConfirm,
|
|
1115
|
+
onCancel: handleBack
|
|
1116
|
+
}
|
|
1117
|
+
),
|
|
1118
|
+
step === "execute" && /* @__PURE__ */ jsx8(
|
|
1119
|
+
ExecutionStep,
|
|
1120
|
+
{
|
|
1121
|
+
branches: selectedBranches,
|
|
1122
|
+
isDryRun: !options2.execute && !executeForReal,
|
|
1123
|
+
onComplete: handleDeletionComplete
|
|
1124
|
+
}
|
|
1125
|
+
),
|
|
1126
|
+
step === "summary" && deletionSummary && /* @__PURE__ */ jsx8(
|
|
1127
|
+
SummaryStep,
|
|
1128
|
+
{
|
|
1129
|
+
summary: deletionSummary,
|
|
1130
|
+
isDryRun: !options2.execute && !executeForReal,
|
|
1131
|
+
onDeleteForReal: handleDeleteForReal
|
|
1132
|
+
}
|
|
1133
|
+
)
|
|
1134
|
+
] });
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
// src/cli.ts
|
|
1138
|
+
import { Command } from "commander";
|
|
1139
|
+
function createCLI() {
|
|
1140
|
+
const program = new Command();
|
|
1141
|
+
program.name("git-tidy").description("Interactive CLI tool for cleaning up unused git branches").version("1.0.0").option("-x, --execute", "Actually delete branches (default: dry-run mode)", false).option("-y, --yes", "Skip all confirmations (for scripting)", false).option("-t, --token <token>", "GitHub personal access token (or use GITHUB_TOKEN env)").parse();
|
|
1142
|
+
const opts = program.opts();
|
|
1143
|
+
return {
|
|
1144
|
+
execute: opts.execute || false,
|
|
1145
|
+
yes: opts.yes || false,
|
|
1146
|
+
token: opts.token
|
|
1147
|
+
};
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
// src/index.tsx
|
|
1151
|
+
import { jsx as jsx9 } from "react/jsx-runtime";
|
|
1152
|
+
var options = createCLI();
|
|
1153
|
+
render(/* @__PURE__ */ jsx9(App, { options }));
|
|
1154
|
+
//# sourceMappingURL=index.js.map
|