@homelab.org/git-ghost 1.0.7
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/.github/workflows/publish.yml +36 -0
- package/README.md +318 -0
- package/bin/git-ghost.js +62 -0
- package/lib/audit.js +60 -0
- package/lib/fix.js +122 -0
- package/lib/history.js +43 -0
- package/lib/restore.js +59 -0
- package/lib/utils.js +66 -0
- package/package.json +30 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
name: Publish to npm and GitHub Packages
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- 'v*' # Se activa cuando pusheas un tag v1.0.2
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
publish-npm:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v4
|
|
13
|
+
- uses: actions/setup-node@v4
|
|
14
|
+
with:
|
|
15
|
+
node-version: '20'
|
|
16
|
+
registry-url: 'https://registry.npmjs.org'
|
|
17
|
+
- run: npm ci
|
|
18
|
+
- run: npm publish --access public
|
|
19
|
+
env:
|
|
20
|
+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
21
|
+
|
|
22
|
+
publish-github:
|
|
23
|
+
runs-on: ubuntu-latest
|
|
24
|
+
permissions:
|
|
25
|
+
contents: read
|
|
26
|
+
packages: write
|
|
27
|
+
steps:
|
|
28
|
+
- uses: actions/checkout@v4
|
|
29
|
+
- uses: actions/setup-node@v4
|
|
30
|
+
with:
|
|
31
|
+
node-version: '20'
|
|
32
|
+
registry-url: 'https://npm.pkg.github.com'
|
|
33
|
+
- run: npm ci
|
|
34
|
+
- run: npm publish
|
|
35
|
+
env:
|
|
36
|
+
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
package/README.md
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
# 👻 GitGhost
|
|
2
|
+
|
|
3
|
+
<p align="center">
|
|
4
|
+
<img src="https://raw.githubusercontent.com/MikeDMart/GitGhost/main/assets/logo.png" alt="GitGhost Logo" width="180">
|
|
5
|
+
</p>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<strong>Automatically detect duplicate files, orphaned images, and dead dependencies — then ship a clean PR.</strong>
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
<p align="center">
|
|
12
|
+
<img src="https://img.shields.io/badge/version-1.0.0-blue?style=flat-square">
|
|
13
|
+
<img src="https://img.shields.io/badge/node-%3E%3D18.0.0-brightgreen?style=flat-square">
|
|
14
|
+
<img src="https://img.shields.io/badge/license-MIT-green?style=flat-square">
|
|
15
|
+
<img src="https://img.shields.io/badge/PRs-welcome-brightgreen?style=flat-square">
|
|
16
|
+
<img src="https://img.shields.io/npm/dm/@mikedmart93/git-ghost?style=flat-square">
|
|
17
|
+
</p>
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## What is GitGhost?
|
|
22
|
+
|
|
23
|
+
GitGhost is a zero-config CLI tool that audits your repository for things that shouldn't be there — duplicate files, images nobody references, and npm packages nobody imports. When it finds them, it opens a Pull Request with everything cleaned up, so your team can review before anything gets deleted for good.
|
|
24
|
+
|
|
25
|
+
Nothing is permanently removed without your approval. Ghost files move to `.ghost/` for safe recovery. Duplicates stay in place until the PR is merged. Your repo stays clean without the risk.
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Features
|
|
30
|
+
|
|
31
|
+
| | Feature | Description |
|
|
32
|
+
|---|---|---|
|
|
33
|
+
| 🔍 | **Duplicate detection** | Finds files with identical content using MD5 hashing |
|
|
34
|
+
| 🖼️ | **Orphaned images** | Detects images not referenced in any HTML, CSS, or JS file |
|
|
35
|
+
| 📦 | **Dead dependencies** | Identifies npm packages installed but never imported |
|
|
36
|
+
| 🌿 | **Auto branch** | Creates a timestamped cleanup branch automatically |
|
|
37
|
+
| 📬 | **Auto PR** | Opens a Pull Request on GitHub via the `gh` CLI |
|
|
38
|
+
| ♻️ | **Safe restore** | Recovers any file moved to `.ghost/` with one command |
|
|
39
|
+
| 📊 | **History** | Shows a log of every cleanup ever run on the repo |
|
|
40
|
+
| 🚀 | **CI/CD mode** | GitHub Actions integration *(coming soon)* |
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Installation
|
|
45
|
+
|
|
46
|
+
**Via npm (recommended)**
|
|
47
|
+
```bash
|
|
48
|
+
npm install -g @mikedmart93/git-ghost
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
**Via GitHub**
|
|
52
|
+
```bash
|
|
53
|
+
git clone https://github.com/MikeDMart/GitGhost.git
|
|
54
|
+
cd GitGhost
|
|
55
|
+
npm link
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
**Without installing**
|
|
59
|
+
```bash
|
|
60
|
+
npx github:MikeDMart/GitGhost audit
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
**Verify**
|
|
64
|
+
```bash
|
|
65
|
+
git-ghost --version
|
|
66
|
+
# git-ghost v1.0.0
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
> **Requires** [GitHub CLI (`gh`)](https://cli.github.com/) for the `--pr` flag. Install it and run `gh auth login` once.
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Quick Start
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
# Step into your project
|
|
77
|
+
cd /path/to/your/repo
|
|
78
|
+
|
|
79
|
+
# See what's haunting it (read-only, nothing changes)
|
|
80
|
+
git-ghost audit
|
|
81
|
+
|
|
82
|
+
# Fix everything and open a PR for review
|
|
83
|
+
git-ghost fix --pr
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
That's it. Review the PR, restore anything you want back, merge when ready.
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Commands
|
|
91
|
+
|
|
92
|
+
| Command | Description |
|
|
93
|
+
|---|---|
|
|
94
|
+
| `git-ghost audit` | Scan and report — no changes made |
|
|
95
|
+
| `git-ghost fix` | Apply fixes on a new local branch |
|
|
96
|
+
| `git-ghost fix --pr` | Apply fixes and open a GitHub Pull Request |
|
|
97
|
+
| `git-ghost restore <file>` | Recover a file from `.ghost/` |
|
|
98
|
+
| `git-ghost history` | Show all previous cleanup commits |
|
|
99
|
+
| `git-ghost help` | Show usage information |
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## How it works
|
|
104
|
+
|
|
105
|
+
### Audit
|
|
106
|
+
|
|
107
|
+
GitGhost scans your working directory and reports three categories of findings:
|
|
108
|
+
|
|
109
|
+
**Duplicate files** — reads every `.js`, `.css`, `.html`, `.json`, and `.md` file, hashes the content with MD5, and flags any file whose hash matches another. The first occurrence is kept; duplicates are marked for removal.
|
|
110
|
+
|
|
111
|
+
**Orphaned images** — finds every `.png`, `.jpg`, `.jpeg`, `.gif`, `.svg`, and `.webp` file, then checks whether its filename or path appears anywhere in your source code. If nothing references it, it's a ghost.
|
|
112
|
+
|
|
113
|
+
**Dead dependencies** — reads `dependencies` and `devDependencies` from `package.json` and checks whether each package name appears in a `require()` or `import` statement anywhere in your JS/TS files. If it doesn't, it's flagged.
|
|
114
|
+
|
|
115
|
+
### Fix
|
|
116
|
+
|
|
117
|
+
When you run `git-ghost fix`:
|
|
118
|
+
|
|
119
|
+
1. Creates a branch named `fix/git-ghost-<timestamp>`
|
|
120
|
+
2. Moves orphaned images to `.ghost/` (preserving directory structure)
|
|
121
|
+
3. Deletes confirmed duplicate files (keeping the first occurrence)
|
|
122
|
+
4. Commits all changes with a detailed message
|
|
123
|
+
5. Pushes the branch to origin
|
|
124
|
+
6. Optionally opens a Pull Request with a review checklist
|
|
125
|
+
|
|
126
|
+
Nothing in `.ghost/` is deleted — it's a quarantine folder, not a trash can.
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## Examples
|
|
131
|
+
|
|
132
|
+
### Basic audit
|
|
133
|
+
|
|
134
|
+
```
|
|
135
|
+
$ git-ghost audit
|
|
136
|
+
|
|
137
|
+
👻 git-ghost audit
|
|
138
|
+
================================
|
|
139
|
+
|
|
140
|
+
📁 DUPLICATE FILES:
|
|
141
|
+
📄 styles/main.css
|
|
142
|
+
identical to: styles/style.css
|
|
143
|
+
📄 utils/helpers.js
|
|
144
|
+
identical to: lib/utils.js
|
|
145
|
+
|
|
146
|
+
🖼️ UNREFERENCED IMAGES:
|
|
147
|
+
📄 assets/old-logo.png
|
|
148
|
+
📄 images/backup/banner.jpg
|
|
149
|
+
|
|
150
|
+
📦 ORPHANED DEPENDENCIES:
|
|
151
|
+
📦 lodash
|
|
152
|
+
📦 moment
|
|
153
|
+
|
|
154
|
+
💡 Tip:
|
|
155
|
+
Run git-ghost fix --pr to open a PR with all fixes applied
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### Automated cleanup with PR
|
|
159
|
+
|
|
160
|
+
```
|
|
161
|
+
$ git-ghost fix --pr
|
|
162
|
+
|
|
163
|
+
👻 git-ghost fix
|
|
164
|
+
================================
|
|
165
|
+
|
|
166
|
+
📊 Running audit...
|
|
167
|
+
|
|
168
|
+
📋 Fix summary:
|
|
169
|
+
🗑️ Duplicates: 2
|
|
170
|
+
👻 Unreferenced images: 2
|
|
171
|
+
📦 Orphaned dependencies: 2
|
|
172
|
+
|
|
173
|
+
🌿 Creating branch: fix/git-ghost-1743872154321
|
|
174
|
+
|
|
175
|
+
🔧 Applying fixes...
|
|
176
|
+
👻 assets/old-logo.png → .ghost/assets/old-logo.png
|
|
177
|
+
👻 images/backup/banner.jpg → .ghost/images/backup/banner.jpg
|
|
178
|
+
🗑️ Removed: styles/main.css
|
|
179
|
+
🗑️ Removed: utils/helpers.js
|
|
180
|
+
|
|
181
|
+
📝 Creating commit...
|
|
182
|
+
📤 Pushing branch...
|
|
183
|
+
📬 Pull Request created: https://github.com/username/my-app/pull/123
|
|
184
|
+
|
|
185
|
+
✅ Branch ready: fix/git-ghost-1743872154321
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### Restoring a file
|
|
189
|
+
|
|
190
|
+
```
|
|
191
|
+
$ git-ghost restore assets/old-logo.png
|
|
192
|
+
|
|
193
|
+
👻 git-ghost restore
|
|
194
|
+
================================
|
|
195
|
+
|
|
196
|
+
✅ Restored: assets/old-logo.png
|
|
197
|
+
|
|
198
|
+
💡 To commit the restoration:
|
|
199
|
+
git add assets/old-logo.png
|
|
200
|
+
git commit -m "restore: recover assets/old-logo.png"
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### Viewing history
|
|
204
|
+
|
|
205
|
+
```
|
|
206
|
+
$ git-ghost history
|
|
207
|
+
|
|
208
|
+
👻 git-ghost history
|
|
209
|
+
================================
|
|
210
|
+
|
|
211
|
+
📋 Previous cleanups:
|
|
212
|
+
|
|
213
|
+
4a3c235 2026-04-05 chore: automated cleanup via git-ghost
|
|
214
|
+
2778562 2026-04-05 feat: v1 git-ghost
|
|
215
|
+
3f2a1b4 2026-04-04 chore: remove unused dependency simple-git
|
|
216
|
+
|
|
217
|
+
👻 Files currently in .ghost/: 21
|
|
218
|
+
💡 To restore: git-ghost restore <file>
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## Project structure
|
|
224
|
+
|
|
225
|
+
```
|
|
226
|
+
GitGhost/
|
|
227
|
+
├── bin/
|
|
228
|
+
│ └── git-ghost.js # CLI entry point
|
|
229
|
+
├── lib/
|
|
230
|
+
│ ├── audit.js # Scan and report
|
|
231
|
+
│ ├── fix.js # Apply fixes and create PR
|
|
232
|
+
│ ├── restore.js # Recover files from .ghost/
|
|
233
|
+
│ ├── history.js # Show cleanup log
|
|
234
|
+
│ └── utils.js # File scanning and hashing logic
|
|
235
|
+
├── .gitignore
|
|
236
|
+
├── package.json
|
|
237
|
+
└── README.md
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
242
|
+
## Workflow
|
|
243
|
+
|
|
244
|
+
```
|
|
245
|
+
your repo
|
|
246
|
+
│
|
|
247
|
+
▼
|
|
248
|
+
git-ghost audit ← read-only scan, nothing changes
|
|
249
|
+
│
|
|
250
|
+
▼
|
|
251
|
+
git-ghost fix --pr ← branch created, fixes applied, PR opened
|
|
252
|
+
│
|
|
253
|
+
▼
|
|
254
|
+
review PR on GitHub ← check what was removed, restore if needed
|
|
255
|
+
│
|
|
256
|
+
▼
|
|
257
|
+
merge ← repo is clean
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
If anything was removed by mistake:
|
|
261
|
+
|
|
262
|
+
```
|
|
263
|
+
git-ghost restore <file> ← pulls it back from .ghost/
|
|
264
|
+
git add <file>
|
|
265
|
+
git commit -m "restore: recover <file>"
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
---
|
|
269
|
+
|
|
270
|
+
## Tech stack
|
|
271
|
+
|
|
272
|
+
- **Node.js** — runtime
|
|
273
|
+
- **glob** — recursive file pattern matching
|
|
274
|
+
- **crypto** — MD5 hashing for duplicate detection
|
|
275
|
+
- **child_process** — git and gh CLI integration
|
|
276
|
+
- **fs / path** — file operations and ghost quarantine
|
|
277
|
+
|
|
278
|
+
---
|
|
279
|
+
|
|
280
|
+
## Contributing
|
|
281
|
+
|
|
282
|
+
Contributions are welcome. Here's how:
|
|
283
|
+
|
|
284
|
+
```bash
|
|
285
|
+
# Fork the repo, then:
|
|
286
|
+
git checkout -b feature/your-feature
|
|
287
|
+
git commit -m 'feat: describe your change'
|
|
288
|
+
git push origin feature/your-feature
|
|
289
|
+
# Open a Pull Request
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
Found a bug? Open an issue in the [issue tracker](https://github.com/MikeDMart/GitGhost/issues).
|
|
293
|
+
|
|
294
|
+
---
|
|
295
|
+
|
|
296
|
+
## Roadmap
|
|
297
|
+
|
|
298
|
+
- [ ] Support for more file types (PDF, DOC, SVG sprites)
|
|
299
|
+
- [ ] GitHub Actions integration for automated CI runs
|
|
300
|
+
- [ ] HTML audit reports
|
|
301
|
+
- [ ] GitLab and Bitbucket support
|
|
302
|
+
- [ ] Config file (`.ghostrc`) for custom ignore patterns
|
|
303
|
+
|
|
304
|
+
---
|
|
305
|
+
|
|
306
|
+
## License
|
|
307
|
+
|
|
308
|
+
MIT © [MikeDMart](https://github.com/MikeDMart)
|
|
309
|
+
|
|
310
|
+
---
|
|
311
|
+
|
|
312
|
+
<p align="center">
|
|
313
|
+
<a href="https://github.com/MikeDMart/GitGhost">⭐ Star on GitHub</a>
|
|
314
|
+
·
|
|
315
|
+
<a href="https://github.com/MikeDMart/GitGhost/issues">🐛 Report a bug</a>
|
|
316
|
+
·
|
|
317
|
+
<a href="https://github.com/MikeDMart/GitGhost/issues">💡 Request a feature</a>
|
|
318
|
+
</p>
|
package/bin/git-ghost.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { execSync } = require('child_process');
|
|
4
|
+
|
|
5
|
+
const colors = {
|
|
6
|
+
red: '\x1b[31m',
|
|
7
|
+
green: '\x1b[32m',
|
|
8
|
+
yellow: '\x1b[33m',
|
|
9
|
+
blue: '\x1b[34m',
|
|
10
|
+
gray: '\x1b[90m',
|
|
11
|
+
reset: '\x1b[0m'
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const command = process.argv[2];
|
|
15
|
+
|
|
16
|
+
if (!command || command === 'help' || command === '--help') {
|
|
17
|
+
console.log(`
|
|
18
|
+
${colors.blue}git-ghost v1.0.0${colors.reset}
|
|
19
|
+
${colors.gray}Repository cleanup tool${colors.reset}
|
|
20
|
+
|
|
21
|
+
${colors.yellow}Usage:${colors.reset}
|
|
22
|
+
git-ghost audit # Detect issues (read-only, no changes)
|
|
23
|
+
git-ghost fix # Create local branch with fixes
|
|
24
|
+
git-ghost fix --pr # Create branch + Pull Request
|
|
25
|
+
git-ghost restore <file> # Restore a file from .ghost/
|
|
26
|
+
git-ghost history # Show cleanup history
|
|
27
|
+
|
|
28
|
+
${colors.yellow}Examples:${colors.reset}
|
|
29
|
+
git-ghost audit
|
|
30
|
+
git-ghost fix --pr
|
|
31
|
+
git-ghost restore styles/old.css
|
|
32
|
+
`);
|
|
33
|
+
process.exit(0);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
execSync('git rev-parse --git-dir', { stdio: 'ignore' });
|
|
38
|
+
} catch {
|
|
39
|
+
console.log(`${colors.red}❌ Not inside a Git repository${colors.reset}`);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
switch (command) {
|
|
44
|
+
case 'audit':
|
|
45
|
+
require('../lib/audit')();
|
|
46
|
+
break;
|
|
47
|
+
case 'fix':
|
|
48
|
+
const createPR = process.argv.includes('--pr');
|
|
49
|
+
require('../lib/fix')({ createPR });
|
|
50
|
+
break;
|
|
51
|
+
case 'restore':
|
|
52
|
+
const file = process.argv[3];
|
|
53
|
+
require('../lib/restore')(file);
|
|
54
|
+
break;
|
|
55
|
+
case 'history':
|
|
56
|
+
require('../lib/history')();
|
|
57
|
+
break;
|
|
58
|
+
default:
|
|
59
|
+
console.log(`${colors.red}❌ Unknown command: ${command}${colors.reset}`);
|
|
60
|
+
console.log(`Run 'git-ghost help' to see available commands`);
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
package/lib/audit.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
const utils = require('./utils');
|
|
2
|
+
|
|
3
|
+
const colors = {
|
|
4
|
+
red: '\x1b[31m',
|
|
5
|
+
green: '\x1b[32m',
|
|
6
|
+
yellow: '\x1b[33m',
|
|
7
|
+
blue: '\x1b[34m',
|
|
8
|
+
gray: '\x1b[90m',
|
|
9
|
+
reset: '\x1b[0m'
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
module.exports = async function audit() {
|
|
13
|
+
console.log(`
|
|
14
|
+
${colors.blue}👻 git-ghost audit${colors.reset}
|
|
15
|
+
${colors.gray}================================${colors.reset}
|
|
16
|
+
`);
|
|
17
|
+
|
|
18
|
+
// Duplicate files
|
|
19
|
+
console.log(`${colors.yellow}📁 DUPLICATE FILES:${colors.reset}`);
|
|
20
|
+
const duplicates = utils.findDuplicateFiles();
|
|
21
|
+
if (duplicates.length === 0) {
|
|
22
|
+
console.log(` ${colors.green}✅ No duplicates found${colors.reset}`);
|
|
23
|
+
} else {
|
|
24
|
+
duplicates.forEach(dup => {
|
|
25
|
+
console.log(` ${colors.red}📄${colors.reset} ${dup.file}`);
|
|
26
|
+
console.log(` ${colors.gray}identical to: ${dup.duplicateOf}${colors.reset}`);
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
console.log('');
|
|
31
|
+
|
|
32
|
+
// Unreferenced images
|
|
33
|
+
console.log(`${colors.yellow}🖼️ UNREFERENCED IMAGES:${colors.reset}`);
|
|
34
|
+
const unusedImages = utils.findUnusedImages();
|
|
35
|
+
if (unusedImages.length === 0) {
|
|
36
|
+
console.log(` ${colors.green}✅ All images are referenced${colors.reset}`);
|
|
37
|
+
} else {
|
|
38
|
+
unusedImages.forEach(img => {
|
|
39
|
+
console.log(` ${colors.gray}📄${colors.reset} ${img}`);
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
console.log('');
|
|
44
|
+
|
|
45
|
+
// Orphaned dependencies
|
|
46
|
+
console.log(`${colors.yellow}📦 ORPHANED DEPENDENCIES:${colors.reset}`);
|
|
47
|
+
const unusedDeps = utils.findUnusedDependencies();
|
|
48
|
+
if (unusedDeps.length === 0) {
|
|
49
|
+
console.log(` ${colors.green}✅ All dependencies are in use${colors.reset}`);
|
|
50
|
+
} else {
|
|
51
|
+
unusedDeps.forEach(dep => {
|
|
52
|
+
console.log(` ${colors.gray}📦${colors.reset} ${dep}`);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
console.log('');
|
|
57
|
+
console.log(`${colors.blue}💡 Tip:${colors.reset}`);
|
|
58
|
+
console.log(` Run ${colors.green}git-ghost fix --pr${colors.reset} to open a PR with all fixes applied`);
|
|
59
|
+
console.log('');
|
|
60
|
+
};
|
package/lib/fix.js
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
const { execSync } = require('child_process');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const utils = require('./utils');
|
|
5
|
+
|
|
6
|
+
const colors = {
|
|
7
|
+
green: '\x1b[32m',
|
|
8
|
+
yellow: '\x1b[33m',
|
|
9
|
+
blue: '\x1b[34m',
|
|
10
|
+
red: '\x1b[31m',
|
|
11
|
+
gray: '\x1b[90m',
|
|
12
|
+
reset: '\x1b[0m'
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const BRANCH_NAME = `fix/git-ghost-${Date.now()}`;
|
|
16
|
+
const GHOST_DIR = '.ghost';
|
|
17
|
+
|
|
18
|
+
module.exports = async function fix({ createPR = false }) {
|
|
19
|
+
console.log(`
|
|
20
|
+
${colors.blue}👻 git-ghost fix${colors.reset}
|
|
21
|
+
${colors.gray}================================${colors.reset}
|
|
22
|
+
`);
|
|
23
|
+
|
|
24
|
+
console.log(`${colors.yellow}📊 Running audit...${colors.reset}`);
|
|
25
|
+
const duplicates = utils.findDuplicateFiles();
|
|
26
|
+
const unusedImages = utils.findUnusedImages();
|
|
27
|
+
const unusedDeps = utils.findUnusedDependencies();
|
|
28
|
+
|
|
29
|
+
if (duplicates.length === 0 && unusedImages.length === 0 && unusedDeps.length === 0) {
|
|
30
|
+
console.log(`${colors.green}✅ Nothing to fix — repository is clean${colors.reset}`);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
console.log(`\n${colors.yellow}📋 Fix summary:${colors.reset}`);
|
|
35
|
+
console.log(` 🗑️ Duplicates: ${duplicates.length}`);
|
|
36
|
+
console.log(` 👻 Unreferenced images: ${unusedImages.length}`);
|
|
37
|
+
console.log(` 📦 Orphaned dependencies: ${unusedDeps.length}`);
|
|
38
|
+
|
|
39
|
+
console.log(`\n${colors.yellow}🌿 Creating branch: ${BRANCH_NAME}${colors.reset}`);
|
|
40
|
+
try {
|
|
41
|
+
execSync(`git checkout -b ${BRANCH_NAME}`, { stdio: 'ignore' });
|
|
42
|
+
} catch {
|
|
43
|
+
console.log(`${colors.red}❌ Could not create branch — make sure you have no uncommitted changes${colors.reset}`);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
console.log(`\n${colors.yellow}🔧 Applying fixes...${colors.reset}`);
|
|
48
|
+
|
|
49
|
+
// Move unreferenced images to .ghost/
|
|
50
|
+
if (unusedImages.length > 0 && !fs.existsSync(GHOST_DIR)) {
|
|
51
|
+
fs.mkdirSync(GHOST_DIR, { recursive: true });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
unusedImages.forEach(img => {
|
|
55
|
+
const ghostPath = path.join(GHOST_DIR, img);
|
|
56
|
+
fs.mkdirSync(path.dirname(ghostPath), { recursive: true });
|
|
57
|
+
if (fs.existsSync(img)) {
|
|
58
|
+
fs.renameSync(img, ghostPath);
|
|
59
|
+
console.log(` 👻 ${img} → ${ghostPath}`);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Remove duplicates (keep the first occurrence)
|
|
64
|
+
const kept = new Set();
|
|
65
|
+
let deletedCount = 0;
|
|
66
|
+
duplicates.forEach(dup => {
|
|
67
|
+
if (!kept.has(dup.duplicateOf)) {
|
|
68
|
+
kept.add(dup.duplicateOf);
|
|
69
|
+
} else if (fs.existsSync(dup.file)) {
|
|
70
|
+
fs.unlinkSync(dup.file);
|
|
71
|
+
console.log(` 🗑️ Removed: ${dup.file}`);
|
|
72
|
+
deletedCount++;
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
if (deletedCount > 0 || unusedImages.length > 0) {
|
|
77
|
+
console.log(`\n${colors.yellow}📝 Creating commit...${colors.reset}`);
|
|
78
|
+
execSync(`git add .`, { stdio: 'ignore' });
|
|
79
|
+
execSync(`git commit -m "chore: automated cleanup via git-ghost
|
|
80
|
+
|
|
81
|
+
- ${deletedCount} duplicate file(s) removed
|
|
82
|
+
- ${unusedImages.length} unreferenced image(s) moved to .ghost/
|
|
83
|
+
- ${unusedDeps.length} orphaned dependency/dependencies flagged
|
|
84
|
+
|
|
85
|
+
Run 'git-ghost restore <file>' to recover any moved file"`, { stdio: 'ignore' });
|
|
86
|
+
|
|
87
|
+
console.log(`\n${colors.yellow}📤 Pushing branch...${colors.reset}`);
|
|
88
|
+
execSync(`git push origin ${BRANCH_NAME}`, { stdio: 'ignore' });
|
|
89
|
+
|
|
90
|
+
if (createPR) {
|
|
91
|
+
console.log(`\n${colors.yellow}📬 Creating Pull Request...${colors.reset}`);
|
|
92
|
+
try {
|
|
93
|
+
execSync(`gh pr create \
|
|
94
|
+
--title "♻️ Automated cleanup via git-ghost" \
|
|
95
|
+
--body "This PR was generated automatically by git-ghost.
|
|
96
|
+
|
|
97
|
+
### Changes
|
|
98
|
+
- Duplicate files removed
|
|
99
|
+
- Unreferenced images moved to \`.ghost/\`
|
|
100
|
+
- Orphaned dependencies flagged
|
|
101
|
+
|
|
102
|
+
### Review checklist
|
|
103
|
+
- [ ] Verify nothing critical was removed
|
|
104
|
+
- [ ] Restore from \`.ghost/\` if needed (\`git-ghost restore <file>\`)
|
|
105
|
+
- [ ] Approve or close" \
|
|
106
|
+
--base main \
|
|
107
|
+
--head ${BRANCH_NAME}`, { stdio: 'inherit' });
|
|
108
|
+
} catch {
|
|
109
|
+
console.log(`${colors.yellow}⚠️ Could not create PR automatically. Run manually:${colors.reset}`);
|
|
110
|
+
console.log(` gh pr create --base main --head ${BRANCH_NAME}`);
|
|
111
|
+
}
|
|
112
|
+
} else {
|
|
113
|
+
console.log(`\n${colors.green}✅ Branch ready: ${BRANCH_NAME}${colors.reset}`);
|
|
114
|
+
console.log(`${colors.blue}💡 To open a PR manually:${colors.reset}`);
|
|
115
|
+
console.log(` gh pr create --base main --head ${BRANCH_NAME}`);
|
|
116
|
+
}
|
|
117
|
+
} else {
|
|
118
|
+
console.log(`${colors.yellow}⚠️ No file changes were made${colors.reset}`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
console.log('');
|
|
122
|
+
};
|
package/lib/history.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const { execSync } = require('child_process');
|
|
3
|
+
|
|
4
|
+
const colors = {
|
|
5
|
+
green: '\x1b[32m',
|
|
6
|
+
yellow: '\x1b[33m',
|
|
7
|
+
blue: '\x1b[34m',
|
|
8
|
+
red: '\x1b[31m',
|
|
9
|
+
gray: '\x1b[90m',
|
|
10
|
+
reset: '\x1b[0m'
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
module.exports = function history() {
|
|
14
|
+
console.log(`
|
|
15
|
+
${colors.blue}👻 git-ghost history${colors.reset}
|
|
16
|
+
${colors.gray}================================${colors.reset}
|
|
17
|
+
`);
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
const commits = execSync(
|
|
21
|
+
`git log --oneline --grep="git-ghost" --format="%h %ad %s" --date=short`,
|
|
22
|
+
{ encoding: 'utf-8' }
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
if (commits.trim() === '') {
|
|
26
|
+
console.log(`${colors.yellow}📭 No previous cleanups found${colors.reset}`);
|
|
27
|
+
console.log(`\n${colors.blue}💡 Run 'git-ghost fix --pr' to create your first cleanup${colors.reset}`);
|
|
28
|
+
} else {
|
|
29
|
+
console.log(`${colors.green}📋 Previous cleanups:${colors.reset}\n`);
|
|
30
|
+
console.log(commits);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (fs.existsSync('.ghost')) {
|
|
34
|
+
const count = execSync(`find .ghost -type f | wc -l`, { encoding: 'utf-8' }).trim();
|
|
35
|
+
console.log(`\n${colors.yellow}👻 Files currently in .ghost/: ${count}${colors.reset}`);
|
|
36
|
+
console.log(`${colors.blue}💡 To restore: git-ghost restore <file>${colors.reset}`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
console.log('');
|
|
40
|
+
} catch {
|
|
41
|
+
console.log(`${colors.red}❌ Could not retrieve history${colors.reset}`);
|
|
42
|
+
}
|
|
43
|
+
};
|
package/lib/restore.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
const colors = {
|
|
5
|
+
green: '\x1b[32m',
|
|
6
|
+
yellow: '\x1b[33m',
|
|
7
|
+
blue: '\x1b[34m',
|
|
8
|
+
red: '\x1b[31m',
|
|
9
|
+
gray: '\x1b[90m',
|
|
10
|
+
reset: '\x1b[0m'
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const GHOST_DIR = '.ghost';
|
|
14
|
+
|
|
15
|
+
module.exports = function restore(file) {
|
|
16
|
+
console.log(`
|
|
17
|
+
${colors.blue}👻 git-ghost restore${colors.reset}
|
|
18
|
+
${colors.gray}================================${colors.reset}
|
|
19
|
+
`);
|
|
20
|
+
|
|
21
|
+
if (!file) {
|
|
22
|
+
console.log(`${colors.red}❌ Please specify a file to restore${colors.reset}`);
|
|
23
|
+
console.log(`${colors.yellow}Usage: git-ghost restore <file>${colors.reset}`);
|
|
24
|
+
console.log(`Example: git-ghost restore styles/old.css`);
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const ghostPath = path.join(GHOST_DIR, file);
|
|
29
|
+
|
|
30
|
+
if (!fs.existsSync(ghostPath)) {
|
|
31
|
+
console.log(`${colors.red}❌ File not found in .ghost/: ${file}${colors.reset}`);
|
|
32
|
+
|
|
33
|
+
if (fs.existsSync(GHOST_DIR)) {
|
|
34
|
+
console.log(`\n${colors.yellow}📁 Available files in .ghost/:${colors.reset}`);
|
|
35
|
+
const listFiles = (dir, prefix = '') => {
|
|
36
|
+
fs.readdirSync(dir).forEach(item => {
|
|
37
|
+
const fullPath = path.join(dir, item);
|
|
38
|
+
const rel = path.join(prefix, item);
|
|
39
|
+
if (fs.statSync(fullPath).isDirectory()) {
|
|
40
|
+
listFiles(fullPath, rel);
|
|
41
|
+
} else {
|
|
42
|
+
console.log(` ${colors.gray}📄${colors.reset} ${rel}`);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
};
|
|
46
|
+
listFiles(GHOST_DIR);
|
|
47
|
+
}
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
52
|
+
fs.renameSync(ghostPath, file);
|
|
53
|
+
|
|
54
|
+
console.log(`${colors.green}✅ Restored: ${file}${colors.reset}`);
|
|
55
|
+
console.log(`\n${colors.blue}💡 To commit the restoration:${colors.reset}`);
|
|
56
|
+
console.log(` git add ${file}`);
|
|
57
|
+
console.log(` git commit -m "restore: recover ${file}"`);
|
|
58
|
+
console.log('');
|
|
59
|
+
};
|
package/lib/utils.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const glob = require('glob');
|
|
4
|
+
const crypto = require('crypto');
|
|
5
|
+
|
|
6
|
+
const IGNORE = ['node_modules/**', '.git/**', '.ghost/**'];
|
|
7
|
+
|
|
8
|
+
function findDuplicateFiles() {
|
|
9
|
+
const files = glob.sync('**/*.{js,css,html,json,md}', { ignore: IGNORE });
|
|
10
|
+
|
|
11
|
+
const contentMap = new Map();
|
|
12
|
+
const duplicates = [];
|
|
13
|
+
|
|
14
|
+
for (const file of files) {
|
|
15
|
+
try {
|
|
16
|
+
const content = fs.readFileSync(file, 'utf-8');
|
|
17
|
+
const hash = crypto.createHash('md5').update(content).digest('hex');
|
|
18
|
+
|
|
19
|
+
if (contentMap.has(hash)) {
|
|
20
|
+
duplicates.push({ file, duplicateOf: contentMap.get(hash) });
|
|
21
|
+
} else {
|
|
22
|
+
contentMap.set(hash, file);
|
|
23
|
+
}
|
|
24
|
+
} catch {}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return duplicates;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function findUnusedImages() {
|
|
31
|
+
const images = glob.sync('**/*.{png,jpg,jpeg,gif,svg,webp}', { ignore: IGNORE });
|
|
32
|
+
const sourceFiles = glob.sync('**/*.{html,css,js,jsx,ts,tsx,json,md}', { ignore: IGNORE });
|
|
33
|
+
|
|
34
|
+
return images.filter(img => {
|
|
35
|
+
const basename = path.basename(img);
|
|
36
|
+
return !sourceFiles.some(file => {
|
|
37
|
+
try {
|
|
38
|
+
const content = fs.readFileSync(file, 'utf-8');
|
|
39
|
+
return content.includes(basename) || content.includes(img);
|
|
40
|
+
} catch { return false; }
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function findUnusedDependencies() {
|
|
46
|
+
if (!fs.existsSync('package.json')) return [];
|
|
47
|
+
|
|
48
|
+
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf-8'));
|
|
49
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
50
|
+
const sourceFiles = glob.sync('**/*.{js,jsx,ts,tsx}', { ignore: IGNORE });
|
|
51
|
+
|
|
52
|
+
return Object.keys(deps).filter(dep =>
|
|
53
|
+
!sourceFiles.some(file => {
|
|
54
|
+
try {
|
|
55
|
+
const content = fs.readFileSync(file, 'utf-8');
|
|
56
|
+
return (
|
|
57
|
+
content.includes(`require('${dep}')`) ||
|
|
58
|
+
content.includes(`from '${dep}'`) ||
|
|
59
|
+
content.includes(`from "${dep}"`)
|
|
60
|
+
);
|
|
61
|
+
} catch { return false; }
|
|
62
|
+
})
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
module.exports = { findDuplicateFiles, findUnusedImages, findUnusedDependencies };
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@homelab.org/git-ghost",
|
|
3
|
+
"version": "1.0.7",
|
|
4
|
+
"description": "Detects ghost files, duplicates, and orphaned dependencies — and opens a PR to clean them up",
|
|
5
|
+
"bin": {
|
|
6
|
+
"git-ghost": "bin/git-ghost.js"
|
|
7
|
+
},
|
|
8
|
+
"keywords": [
|
|
9
|
+
"git",
|
|
10
|
+
"cleaner",
|
|
11
|
+
"pr",
|
|
12
|
+
"audit",
|
|
13
|
+
"duplicates",
|
|
14
|
+
"unused",
|
|
15
|
+
"dependencies"
|
|
16
|
+
],
|
|
17
|
+
"author": "MikeDMart",
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"glob": "^10.3.10"
|
|
21
|
+
},
|
|
22
|
+
"main": "index.js",
|
|
23
|
+
"directories": {
|
|
24
|
+
"lib": "lib"
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
28
|
+
},
|
|
29
|
+
"type": "commonjs"
|
|
30
|
+
}
|