Skip to content

Commit e12b76c

Browse files
committed
Initial commit
0 parents  commit e12b76c

File tree

11 files changed

+5845
-0
lines changed

11 files changed

+5845
-0
lines changed

.github/workflows/ci.yml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [ "main" ]
6+
pull_request:
7+
branches: [ "main" ]
8+
9+
jobs:
10+
build:
11+
runs-on: ubuntu-latest
12+
13+
steps:
14+
- uses: actions/checkout@v4
15+
16+
- name: Use Node.js
17+
uses: actions/setup-node@v4
18+
with:
19+
node-version: '20'
20+
cache: 'npm'
21+
22+
- name: Install dependencies
23+
run: npm ci
24+
25+
- name: Build
26+
run: npm run build -- --linux --dir
27+
# Building for linux directory checks if build config is valid without full packaging overhead

.github/workflows/release.yml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
name: Release
2+
3+
on:
4+
push:
5+
tags:
6+
- 'v*'
7+
8+
jobs:
9+
release:
10+
runs-on: macos-latest
11+
permissions:
12+
contents: write
13+
14+
steps:
15+
- name: Checkout
16+
uses: actions/checkout@v4
17+
18+
- name: Use Node.js
19+
uses: actions/setup-node@v4
20+
with:
21+
node-version: '20'
22+
cache: 'npm'
23+
24+
- name: Install dependencies
25+
run: npm ci
26+
27+
- name: Build & Release
28+
env:
29+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
30+
run: npm run build -- --publish always

.gitignore

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
2+
# Dependencies
3+
node_modules
4+
.pnpm-store
5+
6+
# Production
7+
dist
8+
build
9+
10+
# Misc
11+
.DS_Store
12+
.env
13+
.npmrc
14+
npm-debug.log*
15+
yarn-debug.log*
16+
yarn-error.log*

index.html

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
4+
<head>
5+
<meta charset="UTF-8">
6+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
7+
<title>Node Modules Nuke</title>
8+
<!-- Content Security Policy to secure the app -->
9+
<meta http-equiv="Content-Security-Policy" content="script-src 'self' 'unsafe-inline';">
10+
<link rel="stylesheet" href="styles.css">
11+
</head>
12+
13+
<body>
14+
<div class="app-container">
15+
<header class="app-header">
16+
<div class="title-bar">Node Modules Nuke</div>
17+
<div class="control-panel">
18+
<button id="select-dir-btn" class="btn primary">Select Directory</button>
19+
<div class="stats">
20+
<span id="total-size">0 MB</span> found
21+
</div>
22+
</div>
23+
</header>
24+
25+
<main class="content-area">
26+
<div id="scan-status" class="status-message hidden">
27+
<div class="nuclear-spinner">☢️</div>
28+
<div>Scanning sector...</div>
29+
</div>
30+
<div class="list-header hidden" id="list-header">
31+
<label class="checkbox-container">
32+
<input type="checkbox" id="select-all">
33+
<span class="checkmark"></span>
34+
</label>
35+
<span class="col-path sortable" data-sort="path">Path <span></span></span>
36+
<span class="col-size sortable" data-sort="sizeBytes">Size <span></span></span>
37+
</div>
38+
<ul id="modules-list" class="modules-list">
39+
<!-- List items will be injected here -->
40+
</ul>
41+
</main>
42+
43+
<footer class="app-footer">
44+
<button id="nuke-btn" class="btn danger" disabled>NUKE SELECTED</button>
45+
</footer>
46+
</div>
47+
<script src="renderer.js"></script>
48+
</body>
49+
50+
</html>

main.js

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
const { app, BrowserWindow, ipcMain, dialog } = require('electron');
2+
const path = require('path');
3+
const fs = require('fs/promises');
4+
// Additional modules will be required for scanning logic
5+
6+
function createWindow() {
7+
const win = new BrowserWindow({
8+
width: 1000,
9+
height: 700,
10+
webPreferences: {
11+
nodeIntegration: false,
12+
contextIsolation: true,
13+
preload: path.join(__dirname, 'preload.js')
14+
},
15+
backgroundColor: '#121212', // Dark mode background
16+
titleBarStyle: 'hiddenInset' // Premium feel for macOS
17+
});
18+
19+
win.loadFile('index.html');
20+
}
21+
22+
app.whenReady().then(() => {
23+
createWindow();
24+
25+
app.on('activate', () => {
26+
if (BrowserWindow.getAllWindows().length === 0) {
27+
createWindow();
28+
}
29+
});
30+
});
31+
32+
app.on('window-all-closed', () => {
33+
if (process.platform !== 'darwin') {
34+
app.quit();
35+
}
36+
});
37+
38+
// IPC Handlers
39+
ipcMain.handle('select-directory', async () => {
40+
const result = await dialog.showOpenDialog({
41+
properties: ['openDirectory']
42+
});
43+
return result.filePaths[0];
44+
});
45+
46+
ipcMain.handle('scan-directory', async (event, rootPath) => {
47+
// Basic validation
48+
if (!rootPath) return [];
49+
50+
// Helper to format size
51+
const formatSize = (bytes) => {
52+
if (bytes === 0) return '0 B';
53+
const k = 1024;
54+
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
55+
const i = Math.floor(Math.log(bytes) / Math.log(k));
56+
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
57+
};
58+
59+
const modules = [];
60+
61+
// Recursive scanner
62+
async function scan(dir) {
63+
let entries;
64+
try {
65+
entries = await fs.readdir(dir, { withFileTypes: true });
66+
} catch (e) {
67+
// Permission denied or other error, skip
68+
return;
69+
}
70+
71+
for (const entry of entries) {
72+
const fullPath = path.join(dir, entry.name);
73+
if (entry.isDirectory()) {
74+
if (entry.name === 'node_modules') {
75+
// Found one! Calculate size and add to list.
76+
// Do not recurse INSIDE node_modules
77+
try {
78+
const size = await getDirSize(fullPath);
79+
modules.push({
80+
path: fullPath,
81+
sizeBytes: size,
82+
sizeFormatted: formatSize(size)
83+
});
84+
} catch (e) {
85+
console.error(`Error sizing ${fullPath}:`, e);
86+
}
87+
} else {
88+
// Recurse
89+
if (!entry.name.startsWith('.')) { // Skip hidden folders like .git
90+
await scan(fullPath);
91+
}
92+
}
93+
}
94+
}
95+
}
96+
97+
// Directory size calculator
98+
async function getDirSize(dir) {
99+
let size = 0;
100+
let entries;
101+
try {
102+
entries = await fs.readdir(dir, { withFileTypes: true });
103+
} catch (e) { return 0; }
104+
105+
for (const entry of entries) {
106+
const fullPath = path.join(dir, entry.name);
107+
if (entry.isDirectory()) {
108+
size += await getDirSize(fullPath);
109+
} else {
110+
try {
111+
const stats = await fs.stat(fullPath);
112+
size += stats.size;
113+
} catch (e) { }
114+
}
115+
}
116+
return size;
117+
}
118+
119+
await scan(rootPath);
120+
return modules;
121+
});
122+
123+
ipcMain.handle('delete-directories', async (event, paths) => {
124+
for (const p of paths) {
125+
try {
126+
await fs.rm(p, { recursive: true, force: true });
127+
} catch (error) {
128+
console.error(`Failed to delete ${p}:`, error);
129+
// Could return errors to UI, but for now simple log
130+
}
131+
}
132+
return true;
133+
});

0 commit comments

Comments
 (0)