@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.
@@ -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
+ &nbsp;·&nbsp;
315
+ <a href="https://github.com/MikeDMart/GitGhost/issues">🐛 Report a bug</a>
316
+ &nbsp;·&nbsp;
317
+ <a href="https://github.com/MikeDMart/GitGhost/issues">💡 Request a feature</a>
318
+ </p>
@@ -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
+ }