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/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