git-cleanup-merged 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,473 @@
1
+ # Git Cleanup Merged
2
+
3
+ [![CI](https://github.com/ondrovic/git-cleanup-merged/actions/workflows/ci.yml/badge.svg)](https://github.com/ondrovic/git-cleanup-merged/actions/workflows/ci.yml)
4
+ [![codecov](https://codecov.io/gh/ondrovic/git-cleanup-merged/graph/badge.svg?token=x3cYga3d2E)](https://codecov.io/gh/ondrovic/git-cleanup-merged)
5
+
6
+ A Node.js command-line tool that automatically identifies and deletes local Git branches that have been merged via GitHub Pull Requests.
7
+
8
+ ---
9
+
10
+ ## ๐Ÿงช Testing & Quality Assurance
11
+
12
+ - [![CI](https://github.com/ondrovic/git-cleanup-merged/actions/workflows/ci.yml/badge.svg)](https://github.com/ondrovic/git-cleanup-merged/actions/workflows/ci.yml) **CI runs on all pull requests and on push to `main`/`master` only** via GitHub Actions (tests Node.js 18.x, 20.x)
13
+ - This avoids duplicate runs for feature branches and is the recommended best practice for open source projects.
14
+ - [![codecov](https://codecov.io/gh/ondrovic/git-cleanup-merged/graph/badge.svg?token=x3cYga3d2E)](https://codecov.io/gh/ondrovic/git-cleanup-merged) **Live coverage tracking** via Codecov
15
+ - ๐Ÿšฆ **Branch coverage threshold:** CI will fail if branch coverage drops below 75%
16
+ - ๐Ÿ“ **JUnit test results and coverage are uploaded to Codecov for every CI run**
17
+ - ๐Ÿงช **Run tests locally:**
18
+ ```bash
19
+ npm test
20
+ npm run test:coverage
21
+ ```
22
+ - ๐Ÿ“ˆ **Check coverage report:**
23
+ After running `npm run test:coverage`, open `coverage/lcov-report/index.html` in your browser for a detailed report.
24
+ - ๐Ÿ” **Code quality:** ESLint and Prettier configured for consistent code style
25
+
26
+ ---
27
+
28
+ ## โœจ Features
29
+
30
+ - ๐Ÿ” **Smart Detection**: Automatically checks GitHub PR status for tracked branches
31
+ - ๐Ÿท๏ธ **Untracked Branch Support**: Clean up local-only branches with `--untracked-only` mode
32
+ - โœ… **Safe Deletion**: Only deletes branches with merged or closed PRs, or untracked branches
33
+ - ๐Ÿ”’ **Protection**: Never deletes `main`, `master`, or your current branch
34
+ - ๐Ÿ‘€ **Preview Mode**: Dry-run option to see what would be deleted
35
+ - ๐Ÿ“‚ **Directory Support**: Operate on any git repo by passing a directory as the first argument
36
+ - ๐ŸŽจ **Colorful Output**: Clear visual indicators with icons and colors
37
+ - ๐Ÿ“Š **Status Overview**: Shows comprehensive branch status table
38
+ - โšก **Interactive Spinner**: Real-time progress updates with animated spinner
39
+ - ๐Ÿ›ก๏ธ **Comprehensive Testing**: 100% test coverage with 97 test cases and live coverage tracking
40
+ - ๐ŸŽฏ **Code Quality**: ESLint and Prettier for consistent code style
41
+ - ๐Ÿง  **Smart UX**: Focused modes - main mode for PR cleanup, untracked mode for local cleanup
42
+
43
+ ## Prerequisites
44
+
45
+ Before installing, make sure you have:
46
+
47
+ - **Node.js** (version 18 or higher - tested on 18.x and 20.x)
48
+ - **Git** installed and configured
49
+ - **GitHub CLI** (`gh`) installed and authenticated (only required for main mode, not for `--untracked-only`)
50
+ - Active internet connection for GitHub API calls (only required for main mode)
51
+
52
+ ### Installing GitHub CLI
53
+
54
+ If you don't have GitHub CLI installed:
55
+
56
+ **macOS (Homebrew):**
57
+
58
+ ```bash
59
+ brew install gh
60
+ ```
61
+
62
+ **Windows (Chocolatey):**
63
+
64
+ ```bash
65
+ choco install gh
66
+ ```
67
+
68
+ **Linux (Ubuntu/Debian):**
69
+
70
+ ```bash
71
+ curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg
72
+ echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null
73
+ sudo apt update
74
+ sudo apt install gh
75
+ ```
76
+
77
+ **Authenticate GitHub CLI:**
78
+
79
+ ```bash
80
+ gh auth login
81
+ ```
82
+
83
+ ## Installation
84
+
85
+ ### Option 1: Global Installation via NPM
86
+
87
+ ```bash
88
+ # Install globally from npm (if published)
89
+ npm install -g git-cleanup-merged
90
+
91
+ # Or install globally from GitHub
92
+ npm install -g https://github.com/ondro/git-cleanup-merged.git
93
+ ```
94
+
95
+ ### Option 2: Local Installation
96
+
97
+ ```bash
98
+ # Clone the repository
99
+ git clone https://github.com/ondro/git-cleanup-merged.git
100
+ cd git-cleanup-merged
101
+
102
+ # Install dependencies
103
+ npm install
104
+
105
+ # Make executable (Unix/macOS/Linux)
106
+ chmod +x index.js
107
+
108
+ # Create symlink for global access (optional)
109
+ npm link
110
+ ```
111
+
112
+ ### Option 3: Direct Download
113
+
114
+ ```bash
115
+ # Download the script directly
116
+ curl -o git-cleanup-merged https://raw.githubusercontent.com/ondro/git-cleanup-merged/main/index.js
117
+ chmod +x git-cleanup-merged
118
+
119
+ # Move to your PATH
120
+ sudo mv git-cleanup-merged /usr/local/bin/
121
+ ```
122
+
123
+ ### Option 4: Using npx (No Installation)
124
+
125
+ ```bash
126
+ # Run directly without installing
127
+ npx git-cleanup-merged
128
+
129
+ # Or from GitHub
130
+ npx https://github.com/ondro/git-cleanup-merged.git
131
+ ```
132
+
133
+ ## Usage
134
+
135
+ ### Basic Usage
136
+
137
+ > **Note:** At the start of every run, the tool will display the name of the repository directory being scanned (e.g. 'git-local-branch-cleanup' or 'ollama-git-commit'), so you always know which directory is being operated on.
138
+
139
+ #### Main Mode (Default) - Clean up branches with merged PRs
140
+
141
+ ```bash
142
+ # Clean up merged branches (with confirmation)
143
+ git-cleanup-merged
144
+
145
+ # Clean up merged branches in a different directory
146
+ git-cleanup-merged ../path/to/repo
147
+
148
+ # Preview what would be deleted (dry run)
149
+ git-cleanup-merged --dry-run
150
+
151
+ # Show detailed processing information
152
+ git-cleanup-merged --verbose
153
+
154
+ # Combine options
155
+ git-cleanup-merged ../path/to/repo --dry-run --verbose
156
+ ```
157
+
158
+ #### Untracked Mode - Clean up local-only branches
159
+
160
+ ```bash
161
+ # Clean up untracked branches (local branches without remote tracking)
162
+ git-cleanup-merged --untracked-only
163
+
164
+ # Same as above using shorthand
165
+ git-cleanup-merged -u
166
+
167
+ # Preview untracked branches (dry run)
168
+ git-cleanup-merged --untracked-only --dry-run
169
+
170
+ # Same as above using shorthand
171
+ git-cleanup-merged -u -n
172
+
173
+ # Show detailed processing for untracked branches
174
+ git-cleanup-merged --untracked-only --verbose
175
+
176
+ # Same as above using shorthand
177
+ git-cleanup-merged -u -v
178
+
179
+ # Clean up untracked branches in a different directory
180
+ git-cleanup-merged ../path/to/repo --untracked-only
181
+
182
+ # Same as above using shorthand
183
+ git-cleanup-merged ../path/to/repo -u
184
+ ```
185
+
186
+ ### Command Line Options
187
+
188
+ | Option | Short | Description |
189
+ | ------------------ | ----- | ------------------------------------------------------------------------------------ |
190
+ | `[DIRECTORY]` | | Path to a git repository to operate on. Defaults to the current directory if omitted |
191
+ | `--dry-run` | `-n` | Show what would be deleted without actually deleting |
192
+ | `--verbose` | `-v` | Show detailed information during processing |
193
+ | `--untracked-only` | `-u` | Only process untracked local branches (no remote tracking branch) |
194
+ | `--help` | `-h` | Show help message |
195
+
196
+ ### Example Output
197
+
198
+ #### Main Mode (Default) - Branches with merged PRs
199
+
200
+ ```
201
+ ๐Ÿ“‚ Scanning repository: my-project
202
+ โœ… Dependencies checked
203
+ โœ… Current branch: main
204
+ โœ… Finished checking 3 tracked branches
205
+
206
+ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
207
+ Branch Icon Status
208
+ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
209
+ feature/user-authentication โœ… Merged
210
+ bugfix/header-layout โœ… Merged
211
+ feature/experimental ๐Ÿ”’ Closed
212
+ feature/dark-mode โณ Open
213
+ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
214
+
215
+ โŒ The following branches have merged or closed PRs and will be deleted:
216
+ feature/user-authentication
217
+ bugfix/header-layout
218
+ feature/experimental
219
+
220
+ Proceed with deletion? (y/N): y
221
+
222
+ โœ… Deleted branch feature/user-authentication
223
+ โœ… Deleted branch bugfix/header-layout
224
+ โœ… Deleted branch feature/experimental
225
+ โœ… Successfully deleted 3 branches
226
+ ```
227
+
228
+ #### Untracked Mode - Local-only branches
229
+
230
+ ```
231
+ ๐Ÿ“‚ Scanning repository: my-project
232
+ โœ… Dependencies checked
233
+ โœ… Current branch: main
234
+ โœ… Finished processing 2 untracked branches
235
+
236
+ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
237
+ Branch Icon Status
238
+ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
239
+ debug/test-branch ๐Ÿท๏ธ Untracked
240
+ temp/experiment ๐Ÿท๏ธ Untracked
241
+ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
242
+
243
+ โŒ The following untracked local branches will be deleted:
244
+ debug/test-branch
245
+ temp/experiment
246
+
247
+ Proceed with deletion of untracked branches? (y/N): y
248
+
249
+ โœ… Deleted branch debug/test-branch
250
+ โœ… Deleted branch temp/experiment
251
+ โœ… Successfully deleted 2 branches
252
+ ```
253
+
254
+ ## How It Works
255
+
256
+ ### Main Mode (Default)
257
+
258
+ 1. **Dependency Check**: Verifies you're in a Git repository and GitHub CLI is installed/authenticated
259
+ 2. **Current Branch Detection**: Identifies and protects your current working branch
260
+ 3. **Tracked Branch Discovery**: Lists only branches that have remote tracking (excluding `main`, `master`, current branch)
261
+ - Uses Git's upstream tracking information to accurately detect tracked branches
262
+ - Works with any remote name (origin, upstream, etc.) - not hard-coded to "origin"
263
+ - Robust parsing handles multiple consecutive spaces in Git output
264
+ 4. **PR Status Check**: Queries GitHub API for each branch's PR status with progress indication
265
+ 5. **Results Display**: Shows a comprehensive status table with clear visual indicators
266
+ 6. **Safe Deletion**: Only deletes branches with merged or closed PRs (with user confirmation)
267
+
268
+ ### Untracked Mode (`--untracked-only`)
269
+
270
+ 1. **Dependency Check**: Verifies you're in a Git repository (GitHub CLI not required)
271
+ 2. **Current Branch Detection**: Identifies and protects your current working branch
272
+ 3. **Untracked Branch Discovery**: Lists only local branches without remote tracking (excluding `main`, `master`, current branch)
273
+ - Uses Git's upstream tracking information to accurately detect untracked branches
274
+ - Works with any remote name (origin, upstream, etc.) - not hard-coded to "origin"
275
+ - Robust parsing handles multiple consecutive spaces in Git output
276
+ 4. **Results Display**: Shows untracked branches with ๐Ÿท๏ธ icon
277
+ 5. **Safe Deletion**: Deletes untracked branches (with user confirmation)
278
+
279
+ ## Branch Status Indicators
280
+
281
+ ### Main Mode
282
+
283
+ | Icon | Status | Description |
284
+ | ---- | ------ | ------------------------------------------------------ |
285
+ | โœ… | Merged | PR has been merged - branch is safe to delete |
286
+ | ๐Ÿ”’ | Closed | PR has been closed without merging - branch is safe to delete |
287
+ | โณ | Open | PR is still open - branch will be preserved |
288
+ | โŒ | No PR | No PR found for this branch - branch will be preserved |
289
+
290
+ ### Untracked Mode
291
+
292
+ | Icon | Status | Description |
293
+ | ---- | --------- | ----------------------------------------------------- |
294
+ | ๐Ÿท๏ธ | Untracked | Local branch without remote tracking - safe to delete |
295
+
296
+ ## Safety Features
297
+
298
+ - **Protected Branches**: Never touches `main`, `master`, or your current branch
299
+ - **Confirmation Required**: Always asks before deleting (unless in dry-run mode)
300
+ - **GitHub Verification**: Only deletes branches with confirmed merged or closed PRs (main mode)
301
+ - **Untracked Detection**: Only deletes local branches without remote tracking (untracked mode)
302
+ - **Robust Parsing**: Handles various Git output formats including multiple consecutive spaces
303
+ - **Error Handling**: Graceful failure handling with informative messages
304
+ - **Progress Feedback**: Real-time spinner shows current operation status
305
+ - **Smart UX**: Main mode focuses on PR cleanup, untracked mode focuses on local cleanup
306
+
307
+ ## Troubleshooting
308
+
309
+ ### Common Issues
310
+
311
+ **"Not in a git repository"**
312
+
313
+ - Make sure you're running the command from within a Git repository
314
+
315
+ **"GitHub CLI (gh) is not installed"**
316
+
317
+ - Install GitHub CLI following the prerequisites section above
318
+
319
+ **"GitHub CLI is not authenticated"**
320
+
321
+ - Run `gh auth login` and follow the authentication process
322
+
323
+ **"Failed to get current branch"**
324
+
325
+ - Ensure you're in a valid Git repository with at least one commit
326
+
327
+ **Branch names appearing garbled in terminal**
328
+
329
+ - This was a known issue with the spinner display that has been fixed in recent versions
330
+
331
+ ### Debug Mode
332
+
333
+ For troubleshooting, use verbose mode to see detailed processing:
334
+
335
+ ```bash
336
+ # Main mode with verbose output
337
+ git-cleanup-merged --verbose --dry-run
338
+
339
+ # Untracked mode with verbose output
340
+ git-cleanup-merged --untracked-only --verbose --dry-run
341
+
342
+ # Same as above using shorthand
343
+ git-cleanup-merged -u -v -n
344
+ ```
345
+
346
+ ## Development
347
+
348
+ ### Project Structure
349
+
350
+ ```
351
+ git-cleanup-merged/
352
+ โ”œโ”€โ”€ __tests__/ # Test files
353
+ โ”‚ โ”œโ”€โ”€ index.test.js # Main functionality tests
354
+ โ”‚ โ”œโ”€โ”€ spinner.test.js # Spinner component tests
355
+ โ”‚ โ””โ”€โ”€ utils.test.js # Utility function tests
356
+ โ”œโ”€โ”€ coverage/ # Coverage reports (generated)
357
+
358
+ โ”œโ”€โ”€ src/
359
+ โ”‚ โ”œโ”€โ”€ bin.js # CLI entry point
360
+ โ”‚ โ”œโ”€โ”€ index.js # Main GitCleanupTool class
361
+ โ”‚ โ””โ”€โ”€ utils/
362
+ โ”‚ โ”œโ”€โ”€ index.js # Utility functions
363
+ โ”‚ โ””โ”€โ”€ spinner.js # Spinner component
364
+ โ”œโ”€โ”€ package.json
365
+ โ”œโ”€โ”€ package-lock.json
366
+ โ”œโ”€โ”€ eslint.config.mjs # ESLint configuration
367
+ โ”œโ”€โ”€ .prettierrc # Prettier configuration
368
+ โ””โ”€โ”€ README.md
369
+ ```
370
+
371
+ ### Key Components
372
+
373
+ - **GitCleanupTool**: Main class that orchestrates the cleanup process
374
+ - **Spinner**: Enhanced spinner class with proper terminal handling and testability
375
+ - **CLI Entry Point**: Separate `bin.js` file for clean CLI execution
376
+ - **Test Suite**: Comprehensive tests covering all functionality and edge cases
377
+
378
+ ### Contributing
379
+
380
+ 1. Fork the repository
381
+ 2. Create a feature branch
382
+ 3. Make your changes
383
+ 4. Test thoroughly with both `--dry-run` and actual deletion
384
+ 5. Submit a pull request
385
+
386
+ ### Running Tests
387
+
388
+ ```bash
389
+ # Run all tests
390
+ npm test
391
+
392
+ # Run tests with coverage
393
+ npm run test:coverage
394
+
395
+ # Run linting
396
+ npm run lint
397
+
398
+ # Run linting with auto-fix
399
+ npm run lint -- --fix
400
+
401
+ # Format code
402
+ npm run format
403
+ ```
404
+
405
+ ## License
406
+
407
+ MIT License - see LICENSE file for details.
408
+
409
+ ## ๐Ÿ“‹ Changelog
410
+
411
+ ### v1.3.1
412
+
413
+ - ๐Ÿ› **Critical Bug Fix**: Fixed whitespace parsing issue in branch tracking detection
414
+ - The `line.split(" ")` logic was not robust and could misclassify tracked branches as untracked when `git for-each-ref` output contained multiple consecutive spaces
415
+ - Replaced with `line.split(/\s+/)` and proper array handling to correctly parse branch names and upstream information
416
+ - Added comprehensive tests to verify the fix works with various whitespace scenarios
417
+ - ๐Ÿงช **Enhanced Testing**: Added 2 new test cases specifically for whitespace parsing edge cases
418
+ - โœ… **Maintained Quality**: 100% test coverage preserved with 97 test cases
419
+
420
+ ### v1.3.0
421
+
422
+ - ๐Ÿท๏ธ **New Feature**: Added `--untracked-only` mode to clean up local branches without remote tracking
423
+ - ๐Ÿง  **Improved UX**: Main mode now only shows tracked branches with PRs, untracked mode handles local-only branches
424
+ - ๐Ÿ”ง **Smart Dependencies**: GitHub CLI only required for main mode, not for untracked mode
425
+ - ๐Ÿ’ก **Helpful Guidance**: Suggests `--untracked-only` when no tracked branches found in main mode
426
+ - ๐ŸŽฏ **100% Test Coverage**: Achieved complete test coverage with 97 comprehensive test cases
427
+ - ๐Ÿ› **Bug Fixes**: Fixed branch tracking detection logic and improved deletion feedback
428
+ - ๐Ÿ“Š **Enhanced Testing**: Added tests for all new functionality and edge cases
429
+ - ๐Ÿ”ง **Critical Fix**: Fixed branch tracking detection to use proper Git upstream relationships instead of hard-coded remote names
430
+ - ๐Ÿ› ๏ธ **Robust Parsing**: Fixed whitespace parsing bug that could misclassify tracked branches as untracked
431
+
432
+ ### v1.2.1
433
+
434
+ - ๐Ÿ”ง **Node.js Compatibility**: Updated to require Node.js 18+ for ESLint 9.x compatibility
435
+ - ๐Ÿงช **CI Updates**: Removed Node.js 16.x from CI matrix (reached end-of-life)
436
+ - ๐Ÿ“ฆ **Dependencies**: Updated to use modern ESLint flat config format
437
+ - ๐Ÿšฆ **Workflow Optimization**: CI now only runs on pull requests and on push to `main`/`master` to avoid duplicate runs for feature branches
438
+
439
+ ### v1.2.0
440
+
441
+ - ๐ŸŽฏ **100% Test Coverage**: Achieved complete test coverage across all code paths
442
+ - ๐Ÿงช **Enhanced Test Suite**: Added 76 comprehensive test cases covering all functionality
443
+ - ๐Ÿ”ง **Code Quality**: Added ESLint and Prettier for consistent code style
444
+ - ๐Ÿ—๏ธ **Architecture Improvements**: Separated CLI entry point for better testability
445
+ - ๐Ÿ› **Bug Fixes**: Fixed spinner component and improved error handling
446
+ - ๐Ÿ“Š **Coverage Thresholds**: Set minimum 75% branch coverage requirement
447
+
448
+ ### v1.1.0
449
+
450
+ - Fixed spinner display issue where branch names would merge together
451
+ - Improved terminal output clearing with proper ANSI escape sequences
452
+ - Enhanced progress indicators during branch checking and deletion
453
+ - Added directory argument support for operating on different repositories
454
+
455
+ ### v1.0.0
456
+
457
+ - Initial release
458
+ - Basic branch cleanup functionality
459
+ - GitHub PR integration
460
+ - Dry-run mode
461
+ - Verbose logging
462
+ - Interactive spinner with progress feedback
463
+
464
+ ## ๐Ÿค Support
465
+
466
+ - ๐Ÿ› **Bug Reports**: [GitHub Issues](https://github.com/ondro/git-cleanup-merged/issues)
467
+ - ๐Ÿ’ก **Feature Requests**: [GitHub Discussions](https://github.com/ondro/git-cleanup-merged/discussions)
468
+ - ๐Ÿ“ง **Contact**: ondrovic@gmail.com
469
+ - ๐Ÿ“š **Documentation**: This README contains comprehensive usage examples and troubleshooting
470
+
471
+ ---
472
+
473
+ **โš ๏ธ Important**: Always run with `--dry-run` first to preview changes before actual deletion. This tool is designed to be safe, but you should always verify the branches it wants to delete.
package/package.json ADDED
@@ -0,0 +1,84 @@
1
+ {
2
+ "name": "git-cleanup-merged",
3
+ "version": "1.0.1",
4
+ "description": "Clean up local Git branches that have merged PRs on GitHub",
5
+ "main": "index.js",
6
+ "bin": {
7
+ "git-cleanup-merged": "./src/bin.js"
8
+ },
9
+ "scripts": {
10
+ "test": "jest",
11
+ "test:coverage": "jest --coverage",
12
+ "test:coverage:ci": "jest --coverage && node scripts/fix-junit.js",
13
+ "lint": "eslint .",
14
+ "format": "prettier --write ."
15
+ },
16
+ "keywords": [
17
+ "git",
18
+ "github",
19
+ "cli",
20
+ "cleanup",
21
+ "branches",
22
+ "merge"
23
+ ],
24
+ "author": "Chris Ondrovic",
25
+ "license": "MIT",
26
+ "engines": {
27
+ "node": ">=18.0.0"
28
+ },
29
+ "preferGlobal": true,
30
+ "files": [
31
+ "src/**/*.js",
32
+ "index.js",
33
+ "README.md",
34
+ "LICENSE"
35
+ ],
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "https://github.com/ondro/git-cleanup-merged.git"
39
+ },
40
+ "bugs": {
41
+ "url": "https://github.com/ondro/git-cleanup-merged/issues"
42
+ },
43
+ "homepage": "https://github.com/ondro/git-cleanup-merged#readme",
44
+ "devDependencies": {
45
+ "@eslint/js": "^9.30.1",
46
+ "eslint": "^9.30.1",
47
+ "globals": "^16.3.0",
48
+ "jest": "^30.0.3",
49
+ "jest-junit": "^16.0.0",
50
+ "prettier": "^3.6.2"
51
+ },
52
+ "jest": {
53
+ "coverageThreshold": {
54
+ "global": {
55
+ "branches": 75
56
+ }
57
+ },
58
+ "collectCoverageFrom": [
59
+ "src/**/*.js",
60
+ "!src/bin.js"
61
+ ],
62
+ "coverageReporters": [
63
+ "text",
64
+ "lcov",
65
+ "html"
66
+ ],
67
+ "reporters": [
68
+ "default",
69
+ [
70
+ "jest-junit",
71
+ {
72
+ "outputDirectory": "coverage",
73
+ "outputName": "junit.xml",
74
+ "classNameTemplate": "{filepath}",
75
+ "titleTemplate": "{title}",
76
+ "ancestorSeparator": " โ€บ ",
77
+ "usePathForSuiteName": true,
78
+ "useFullFilePath": true,
79
+ "classNameTemplate": "{filepath}"
80
+ }
81
+ ]
82
+ ]
83
+ }
84
+ }
package/src/bin.js ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ const GitCleanupTool = require("./index");
3
+
4
+ const tool = new GitCleanupTool();
5
+ const code = tool.parseArguments();
6
+ if (typeof code === "number") process.exit(code);
7
+ if (code === undefined) tool.run();
package/src/index.js ADDED
@@ -0,0 +1,605 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { execSync } = require("child_process");
4
+ const readline = require("readline");
5
+ const clearTerminal = require("./utils");
6
+ const Spinner = require("./utils/spinner");
7
+ const path = require("path");
8
+
9
+ // Colors for terminal output
10
+ const colors = {
11
+ green: "\x1b[32m",
12
+ red: "\x1b[31m",
13
+ yellow: "\x1b[33m",
14
+ blue: "\x1b[34m",
15
+ reset: "\x1b[0m",
16
+ bold: "\x1b[1m",
17
+ };
18
+
19
+ class GitCleanupTool {
20
+ constructor() {
21
+ this.dryRun = false;
22
+ this.verbose = false;
23
+ this.untrackedOnly = false;
24
+ this.countOnly = false;
25
+ this.branchesToDelete = [];
26
+ this.prResults = [];
27
+ this.currentBranch = "";
28
+ this.spinner = new Spinner();
29
+ }
30
+
31
+ // Helper method for minimum spinner visibility
32
+ async sleep(ms) {
33
+ return new Promise((resolve) => setTimeout(resolve, ms));
34
+ }
35
+
36
+ async execCommand(command, options = {}) {
37
+ try {
38
+ const result = execSync(command, {
39
+ encoding: "utf8",
40
+ stdio: options.silent ? "pipe" : "inherit",
41
+ ...options,
42
+ });
43
+ return result.trim();
44
+ } catch (error) {
45
+ if (!options.silent) {
46
+ throw error;
47
+ }
48
+ return null;
49
+ }
50
+ }
51
+
52
+ async checkDependencies() {
53
+ this.spinner.updateMessage("Checking dependencies...");
54
+ this.spinner.start();
55
+
56
+ // Check if we're in a git repository
57
+ if (
58
+ (await this.execCommand("git rev-parse --git-dir", { silent: true })) ===
59
+ null
60
+ ) {
61
+ this.spinner.error("Not in a git repository");
62
+ process.exit(1);
63
+ }
64
+ await this.sleep(300); // Minimum spinner time
65
+
66
+ // Only check GitHub CLI dependencies if not in untracked-only or count-only mode
67
+ if (!this.untrackedOnly && !this.countOnly) {
68
+ this.spinner.updateMessage("Checking GitHub CLI...");
69
+ // Check for GitHub CLI
70
+ if ((await this.execCommand("gh --version", { silent: true })) === null) {
71
+ this.spinner.error(
72
+ "GitHub CLI (gh) is not installed. Please install it from https://cli.github.com/",
73
+ );
74
+ process.exit(1);
75
+ }
76
+ await this.sleep(200);
77
+
78
+ this.spinner.updateMessage("Verifying GitHub authentication...");
79
+ // Check GitHub CLI authentication
80
+ if (
81
+ (await this.execCommand("gh auth status", { silent: true })) === null
82
+ ) {
83
+ this.spinner.error(
84
+ "GitHub CLI is not authenticated. Run: gh auth login",
85
+ );
86
+ process.exit(1);
87
+ }
88
+ await this.sleep(200);
89
+ }
90
+
91
+ this.spinner.success("Dependencies checked");
92
+ }
93
+
94
+ async getCurrentBranch() {
95
+ this.spinner.updateMessage("Getting current branch...");
96
+ this.spinner.start();
97
+
98
+ try {
99
+ this.currentBranch = await this.execCommand("git branch --show-current", {
100
+ silent: true,
101
+ });
102
+ await this.sleep(200); // Let the spinner show
103
+ this.spinner.success(`Current branch: ${this.currentBranch}`);
104
+ } catch {
105
+ this.spinner.error("Failed to get current branch");
106
+ process.exit(1);
107
+ }
108
+ }
109
+
110
+ async getLocalBranches() {
111
+ try {
112
+ const branches = await this.execCommand(
113
+ 'git for-each-ref --format="%(refname:short)" refs/heads/',
114
+ { silent: true },
115
+ );
116
+
117
+ return branches
118
+ .split("\n")
119
+ .filter((branch) => branch.trim() !== "")
120
+ .filter(
121
+ (branch) =>
122
+ !["main", "master", this.currentBranch].includes(branch.trim()),
123
+ )
124
+ .map((branch) => branch.trim());
125
+ } catch {
126
+ this.spinner.error("Failed to get local branches");
127
+ return [];
128
+ }
129
+ }
130
+
131
+ async getTrackedBranches() {
132
+ try {
133
+ // Get all local branches with their upstream tracking information
134
+ const branches = await this.execCommand(
135
+ 'git for-each-ref --format="%(refname:short) %(upstream:short)" refs/heads/',
136
+ { silent: true },
137
+ );
138
+
139
+ const trackedBranches = [];
140
+
141
+ const branchLines = branches
142
+ .split("\n")
143
+ .filter((line) => line.trim() !== "")
144
+ .map((line) => line.trim());
145
+
146
+ for (const line of branchLines) {
147
+ // Use regex to split on one or more whitespace characters
148
+ const parts = line.split(/\s+/);
149
+ const branchName = parts[0];
150
+ const upstream = parts.slice(1).join(" "); // Join remaining parts in case upstream contains spaces
151
+
152
+ // Skip main, master, and current branch
153
+ if (["main", "master", this.currentBranch].includes(branchName)) {
154
+ continue;
155
+ }
156
+
157
+ // If upstream is not empty, the branch is tracking a remote branch
158
+ if (upstream && upstream.trim() !== "") {
159
+ trackedBranches.push(branchName);
160
+ }
161
+ }
162
+
163
+ return trackedBranches;
164
+ } catch {
165
+ this.spinner.error("Failed to get tracked branches");
166
+ return [];
167
+ }
168
+ }
169
+
170
+ async getUntrackedBranches() {
171
+ try {
172
+ // Get all local branches with their upstream tracking information
173
+ const branches = await this.execCommand(
174
+ 'git for-each-ref --format="%(refname:short) %(upstream:short)" refs/heads/',
175
+ { silent: true },
176
+ );
177
+
178
+ const untrackedBranches = [];
179
+
180
+ const branchLines = branches
181
+ .split("\n")
182
+ .filter((line) => line.trim() !== "")
183
+ .map((line) => line.trim());
184
+
185
+ for (const line of branchLines) {
186
+ // Use regex to split on one or more whitespace characters
187
+ const parts = line.split(/\s+/);
188
+ const branchName = parts[0];
189
+ const upstream = parts.slice(1).join(" "); // Join remaining parts in case upstream contains spaces
190
+
191
+ // Skip main, master, and current branch
192
+ if (["main", "master", this.currentBranch].includes(branchName)) {
193
+ continue;
194
+ }
195
+
196
+ // If upstream is empty, the branch is not tracking any remote branch
197
+ if (!upstream || upstream.trim() === "") {
198
+ untrackedBranches.push(branchName);
199
+ }
200
+ }
201
+
202
+ return untrackedBranches;
203
+ } catch {
204
+ this.spinner.error("Failed to get untracked branches");
205
+ return [];
206
+ }
207
+ }
208
+
209
+ async countBranches() {
210
+ this.spinner.updateMessage("Counting branches...");
211
+ this.spinner.start();
212
+
213
+ const trackedBranches = await this.getTrackedBranches();
214
+ const untrackedBranches = await this.getUntrackedBranches();
215
+ const total = trackedBranches.length + untrackedBranches.length;
216
+
217
+ await this.sleep(300); // Minimum spinner time
218
+ this.spinner.success("Branch count complete");
219
+
220
+ console.log("");
221
+ console.log(`${colors.bold}๐Ÿ“Š Branch Count Summary${colors.reset}`);
222
+ console.log(` Total branches: ${total}`);
223
+ console.log(` Tracked: ${trackedBranches.length}`);
224
+ console.log(` Untracked: ${untrackedBranches.length}`);
225
+ }
226
+
227
+ async getPRStatus(branch) {
228
+ try {
229
+ const result = await this.execCommand(
230
+ `gh pr view "${branch}" --json state --jq .state`,
231
+ { silent: true },
232
+ );
233
+ return result;
234
+ } catch {
235
+ return null;
236
+ }
237
+ }
238
+
239
+ async checkBranches() {
240
+ this.spinner.updateMessage("Fetching tracked branches...");
241
+ this.spinner.start();
242
+
243
+ const branches = await this.getTrackedBranches();
244
+
245
+ if (branches.length === 0) {
246
+ this.spinner.warning("No tracked branches found to check.");
247
+ this.spinner.info(
248
+ "You might want to use --untracked-only to see local-only branches.",
249
+ );
250
+ return;
251
+ }
252
+
253
+ this.spinner.updateMessage(
254
+ `Checking ${branches.length} tracked branches against GitHub...`,
255
+ );
256
+
257
+ // Process each branch
258
+ for (let i = 0; i < branches.length; i++) {
259
+ const branch = branches[i];
260
+ this.spinner.updateMessage(
261
+ `Checking branch ${i + 1}/${branches.length}: ${branch}`,
262
+ );
263
+
264
+ const prStatus = await this.getPRStatus(branch);
265
+
266
+ // Only call debug if verbose is enabled to avoid stopping spinner
267
+ if (this.verbose) {
268
+ this.spinner.debug(
269
+ `Checking branch ${branch} -> PR state: ${prStatus || "unknown"}`,
270
+ this.verbose,
271
+ );
272
+ // Restart spinner after debug output
273
+ this.spinner.updateMessage(
274
+ `Checking branch ${i + 1}/${branches.length}: ${branch}`,
275
+ );
276
+ this.spinner.start();
277
+ }
278
+
279
+ let icon, label;
280
+ switch (prStatus) {
281
+ case "MERGED":
282
+ icon = "โœ…";
283
+ label = "Merged";
284
+ this.branchesToDelete.push(branch);
285
+ break;
286
+ case "CLOSED":
287
+ icon = "๐Ÿ”’";
288
+ label = "Closed";
289
+ this.branchesToDelete.push(branch);
290
+ break;
291
+ case "OPEN":
292
+ icon = "โณ";
293
+ label = "Open";
294
+ break;
295
+ default:
296
+ icon = "โŒ";
297
+ label = "No PR";
298
+ break;
299
+ }
300
+
301
+ this.prResults.push({ branch, icon, label });
302
+
303
+ // Add a small delay so we can see the spinner working
304
+ await this.sleep(300);
305
+ }
306
+
307
+ this.spinner.success(
308
+ `Finished checking ${branches.length} tracked branches`,
309
+ );
310
+ console.log(""); // Empty line for spacing
311
+ }
312
+
313
+ async checkUntrackedBranches() {
314
+ this.spinner.updateMessage("Fetching untracked local branches...");
315
+ this.spinner.start();
316
+
317
+ const untrackedBranches = await this.getUntrackedBranches();
318
+
319
+ if (untrackedBranches.length === 0) {
320
+ this.spinner.warning("No untracked local branches found.");
321
+ return;
322
+ }
323
+
324
+ this.spinner.updateMessage(
325
+ `Found ${untrackedBranches.length} untracked local branches...`,
326
+ );
327
+
328
+ // Process each untracked branch
329
+ for (let i = 0; i < untrackedBranches.length; i++) {
330
+ const branch = untrackedBranches[i];
331
+ this.spinner.updateMessage(
332
+ `Processing untracked branch ${i + 1}/${untrackedBranches.length}: ${branch}`,
333
+ );
334
+
335
+ // Only call debug if verbose is enabled to avoid stopping spinner
336
+ if (this.verbose) {
337
+ this.spinner.debug(
338
+ `Processing untracked branch ${branch}`,
339
+ this.verbose,
340
+ );
341
+ // Restart spinner after debug output
342
+ this.spinner.updateMessage(
343
+ `Processing untracked branch ${i + 1}/${untrackedBranches.length}: ${branch}`,
344
+ );
345
+ this.spinner.start();
346
+ }
347
+
348
+ const icon = "๐Ÿท๏ธ";
349
+ const label = "Untracked";
350
+ this.branchesToDelete.push(branch);
351
+
352
+ this.prResults.push({ branch, icon, label });
353
+
354
+ // Add a small delay so we can see the spinner working
355
+ await this.sleep(300);
356
+ }
357
+
358
+ this.spinner.success(
359
+ `Finished processing ${untrackedBranches.length} untracked branches`,
360
+ );
361
+ console.log(""); // Empty line for spacing
362
+ }
363
+
364
+ displayResults() {
365
+ this.spinner.log("โ”€".repeat(60));
366
+ this.spinner.log(
367
+ `${"Branch".padEnd(40)} ${"Icon".padEnd(6)} ${"Status".padEnd(10)}`,
368
+ );
369
+ this.spinner.log("โ”€".repeat(60));
370
+
371
+ this.prResults.forEach(({ branch, icon, label }) => {
372
+ this.spinner.log(
373
+ `${branch.padEnd(40)} ${icon.padEnd(6)} ${label.padEnd(10)}`,
374
+ );
375
+ });
376
+
377
+ this.spinner.log("โ”€".repeat(60));
378
+ }
379
+
380
+ async deleteBranches() {
381
+ if (this.branchesToDelete.length === 0) {
382
+ if (this.untrackedOnly) {
383
+ this.spinner.warning("No untracked local branches found.");
384
+ } else {
385
+ this.spinner.warning("No branches with merged or closed PRs found.");
386
+ }
387
+ return;
388
+ }
389
+
390
+ console.log(""); // Empty line for spacing
391
+ if (this.dryRun) {
392
+ this.spinner.warning("DRY RUN โ€” branches eligible for deletion:");
393
+ } else {
394
+ if (this.untrackedOnly) {
395
+ this.spinner.error(
396
+ "The following untracked local branches will be deleted:",
397
+ );
398
+ } else {
399
+ this.spinner.error(
400
+ "The following branches have merged or closed PRs and will be deleted:",
401
+ );
402
+ }
403
+ }
404
+
405
+ // List branches without icons since the spinner methods handle them
406
+ this.branchesToDelete.forEach((branch) => {
407
+ this.spinner.log(` ${branch}`, colors.red);
408
+ });
409
+
410
+ if (this.dryRun) {
411
+ if (this.untrackedOnly) {
412
+ this.spinner.info(
413
+ "Run without --dry-run to actually delete untracked branches.",
414
+ );
415
+ } else {
416
+ this.spinner.info("Run without --dry-run to actually delete them.");
417
+ }
418
+ return;
419
+ }
420
+
421
+ const confirmationMessage = this.untrackedOnly
422
+ ? "Proceed with deletion of untracked branches? (y/N): "
423
+ : "Proceed with deletion? (y/N): ";
424
+
425
+ const confirmed = await this.askConfirmation(confirmationMessage);
426
+
427
+ if (confirmed) {
428
+ console.log(""); // Empty line for spacing
429
+
430
+ let deletedCount = 0;
431
+ let failedBranches = [];
432
+
433
+ for (let i = 0; i < this.branchesToDelete.length; i++) {
434
+ const branch = this.branchesToDelete[i];
435
+ this.spinner.updateMessage(
436
+ `Deleting branch ${i + 1}/${this.branchesToDelete.length}: ${branch}`,
437
+ );
438
+ this.spinner.start();
439
+
440
+ try {
441
+ await this.execCommand(`git branch -d "${branch}"`, { silent: true });
442
+ deletedCount++;
443
+ // Stop spinner before printing confirmation to avoid overlap
444
+ this.spinner.stop();
445
+ this.spinner.log(`โœ… Deleted branch ${branch}`, colors.green);
446
+ // Brief pause to show progress
447
+ await this.sleep(200);
448
+ } catch {
449
+ failedBranches.push(branch);
450
+ // Stop spinner before printing error to avoid overlap
451
+ this.spinner.stop();
452
+ this.spinner.log(`โŒ Failed to delete branch ${branch}`, colors.red);
453
+ await this.sleep(200);
454
+ }
455
+ }
456
+
457
+ // Final status
458
+ if (failedBranches.length === 0) {
459
+ this.spinner.success(`Successfully deleted ${deletedCount} branches`);
460
+ } else {
461
+ this.spinner.warning(
462
+ `Deleted ${deletedCount} branches, ${failedBranches.length} failed`,
463
+ );
464
+ failedBranches.forEach((branch) => {
465
+ this.spinner.log(` Failed: ${branch}`, colors.red);
466
+ });
467
+ }
468
+ } else {
469
+ this.spinner.info("Cancelled.");
470
+ }
471
+ }
472
+
473
+ async askConfirmation(question) {
474
+ const rl = readline.createInterface({
475
+ input: process.stdin,
476
+ output: process.stdout,
477
+ });
478
+
479
+ return new Promise((resolve) => {
480
+ rl.question(question, (answer) => {
481
+ rl.close();
482
+ resolve(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
483
+ });
484
+ });
485
+ }
486
+
487
+ showHelp() {
488
+ console.log(`
489
+ ${colors.bold}git-cleanup-merged${colors.reset} - Clean up merged Git branches
490
+
491
+ ${colors.bold}USAGE:${colors.reset}
492
+ git-cleanup-merged [DIRECTORY] [OPTIONS]
493
+
494
+ ${colors.bold}DIRECTORY (optional):${colors.reset}
495
+ Path to a git repository to operate on. Defaults to the current directory if omitted.
496
+
497
+ ${colors.bold}OPTIONS:${colors.reset}
498
+ -n, --dry-run Show what would be deleted without actually deleting
499
+ -v, --verbose Show detailed information during processing
500
+ -u, --untracked-only Only process untracked local branches (no remote tracking branch)
501
+ -c, --count Display branch count summary and exit (no deletion)
502
+ -h, --help Show this help message
503
+
504
+ ${colors.bold}DESCRIPTION:${colors.reset}
505
+ This tool checks your local Git branches against GitHub PRs to find
506
+ branches that have been merged and are safe to delete locally.
507
+
508
+ When using --untracked-only, it will only process local branches that
509
+ don't have a corresponding remote tracking branch.
510
+
511
+ ${colors.bold}REQUIREMENTS:${colors.reset}
512
+ - Git repository
513
+ - GitHub CLI (gh) installed and authenticated (only for normal mode)
514
+ - Internet connection to check GitHub PR status (only for normal mode)
515
+
516
+ ${colors.bold}EXAMPLES:${colors.reset}
517
+ git-cleanup-merged # Clean up merged branches in current directory
518
+ git-cleanup-merged ../my/repo # Clean up merged branches in another repo
519
+ git-cleanup-merged --dry-run # Preview what would be deleted
520
+ git-cleanup-merged --verbose # Show detailed processing info
521
+ git-cleanup-merged --untracked-only # Clean up untracked local branches only
522
+ git-cleanup-merged -u # Same as --untracked-only
523
+ git-cleanup-merged --untracked-only --dry-run # Preview untracked branches
524
+ git-cleanup-merged -u -n # Same as above with shorthand
525
+ git-cleanup-merged --count # Display branch count summary
526
+ git-cleanup-merged -c # Same as --count
527
+ `);
528
+ }
529
+
530
+ parseArguments() {
531
+ const args = process.argv.slice(2);
532
+ // Check for a directory as the first positional argument (not a flag)
533
+ if (args[0] && !args[0].startsWith("-")) {
534
+ try {
535
+ process.chdir(args[0]);
536
+ } catch {
537
+ this.spinner.error(`Failed to change directory to: ${args[0]}`);
538
+ process.exit(1);
539
+ }
540
+ args.shift();
541
+ }
542
+ for (const arg of args) {
543
+ switch (arg) {
544
+ case "--dry-run":
545
+ case "-n":
546
+ this.dryRun = true;
547
+ break;
548
+ case "--verbose":
549
+ case "-v":
550
+ this.verbose = true;
551
+ break;
552
+ case "--untracked-only":
553
+ case "-u":
554
+ this.untrackedOnly = true;
555
+ break;
556
+ case "--count":
557
+ case "-c":
558
+ this.countOnly = true;
559
+ break;
560
+ case "--help":
561
+ case "-h":
562
+ this.showHelp();
563
+ return 0;
564
+ default:
565
+ this.spinner.error(`Unknown option: ${arg}`);
566
+ this.spinner.info("Use --help for usage information.");
567
+ process.exit(1);
568
+ }
569
+ }
570
+ }
571
+
572
+ async run() {
573
+ clearTerminal();
574
+ // Display just the directory name being scanned
575
+ this.spinner.log(
576
+ `๐Ÿ“‚ Scanning repository: ${path.basename(process.cwd())}`,
577
+ "\x1b[34m",
578
+ );
579
+ try {
580
+ this.parseArguments();
581
+ await this.checkDependencies();
582
+ await this.getCurrentBranch();
583
+
584
+ // Handle count-only mode and exit early
585
+ if (this.countOnly) {
586
+ await this.countBranches();
587
+ return;
588
+ }
589
+
590
+ if (this.untrackedOnly) {
591
+ await this.checkUntrackedBranches();
592
+ } else {
593
+ await this.checkBranches();
594
+ }
595
+
596
+ this.displayResults();
597
+ await this.deleteBranches();
598
+ } catch (error) {
599
+ this.spinner.error(`An error occurred: ${error.message}`);
600
+ process.exit(1);
601
+ }
602
+ }
603
+ }
604
+
605
+ module.exports = GitCleanupTool;
@@ -0,0 +1,22 @@
1
+ const { execSync } = require("child_process");
2
+
3
+ function clearTerminal() {
4
+ // Clear screen and scroll-back buffer on all platforms
5
+ if (process.platform === "win32") {
6
+ // Windows: Use cls command and clear scroll-back
7
+ try {
8
+ execSync("cls", { stdio: "inherit" });
9
+ } catch {
10
+ // Fallback for Windows
11
+ process.stdout.write("\x1B[2J\x1B[0f\x1B[3J");
12
+ }
13
+ } else {
14
+ // Unix-like systems (macOS, Linux): Clear screen and scroll-back
15
+ process.stdout.write("\x1B[2J\x1B[0f\x1B[3J");
16
+ }
17
+
18
+ // Additional reset for cursor position
19
+ process.stdout.write("\x1B[H");
20
+ }
21
+
22
+ module.exports = clearTerminal;
@@ -0,0 +1,75 @@
1
+ const colors = {
2
+ green: "\x1b[32m",
3
+ red: "\x1b[31m",
4
+ yellow: "\x1b[33m",
5
+ blue: "\x1b[34m",
6
+ reset: "\x1b[0m",
7
+ bold: "\x1b[1m",
8
+ };
9
+
10
+ class Spinner {
11
+ constructor(message = "Loading...") {
12
+ this.message = message;
13
+ this.frames = ["โ ‹", "โ ™", "โ น", "โ ธ", "โ ผ", "โ ด", "โ ฆ", "โ ง", "โ ‡", "โ "];
14
+ this.current = 0;
15
+ this.interval = null;
16
+ this.isSpinning = false;
17
+ }
18
+
19
+ start() {
20
+ if (this.isSpinning) return;
21
+ this.isSpinning = true;
22
+ process.stdout.write("\x1b[?25l"); // Hide cursor
23
+ this.interval = setInterval(() => {
24
+ process.stdout.write(
25
+ `\r\x1b[K${colors.blue}${this.frames[this.current]} ${this.message}${colors.reset}`,
26
+ );
27
+ this.current = (this.current + 1) % this.frames.length;
28
+ }, 100);
29
+ }
30
+
31
+ stop() {
32
+ if (!this.isSpinning) return;
33
+ this.isSpinning = false;
34
+ clearInterval(this.interval);
35
+ this.interval = null;
36
+ process.stdout.write("\r\x1b[K"); // Clear line
37
+ process.stdout.write("\x1b[?25h"); // Show cursor
38
+ }
39
+
40
+ updateMessage(message) {
41
+ this.message = message;
42
+ }
43
+
44
+ success(message) {
45
+ this.stop();
46
+ console.log(`${colors.green}โœ… ${message}${colors.reset}`);
47
+ }
48
+
49
+ error(message) {
50
+ this.stop();
51
+ console.log(`${colors.red}โŒ ${message}${colors.reset}`);
52
+ }
53
+
54
+ warning(message) {
55
+ this.stop();
56
+ console.log(`${colors.yellow}โš ๏ธ ${message}${colors.reset}`);
57
+ }
58
+
59
+ info(message) {
60
+ this.stop();
61
+ console.log(`${colors.blue}โ„น๏ธ ${message}${colors.reset}`);
62
+ }
63
+
64
+ debug(message, verbose = false) {
65
+ if (!verbose) return;
66
+ this.stop();
67
+ console.log(`${colors.blue}๐Ÿ” DEBUG: ${message}${colors.reset}`);
68
+ }
69
+
70
+ log(message, color = "") {
71
+ console.log(`${color}${message}${colors.reset}`);
72
+ }
73
+ }
74
+
75
+ module.exports = Spinner;