@tukuyomil032/broom 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +554 -0
- package/dist/commands/analyze.js +371 -0
- package/dist/commands/backup.js +257 -0
- package/dist/commands/clean.js +255 -0
- package/dist/commands/completion.js +714 -0
- package/dist/commands/config.js +474 -0
- package/dist/commands/doctor.js +280 -0
- package/dist/commands/duplicates.js +325 -0
- package/dist/commands/help.js +34 -0
- package/dist/commands/index.js +22 -0
- package/dist/commands/installer.js +266 -0
- package/dist/commands/optimize.js +270 -0
- package/dist/commands/purge.js +271 -0
- package/dist/commands/remove.js +184 -0
- package/dist/commands/reports.js +173 -0
- package/dist/commands/schedule.js +249 -0
- package/dist/commands/status.js +468 -0
- package/dist/commands/touchid.js +230 -0
- package/dist/commands/uninstall.js +336 -0
- package/dist/commands/update.js +182 -0
- package/dist/commands/watch.js +258 -0
- package/dist/index.js +131 -0
- package/dist/scanners/base.js +21 -0
- package/dist/scanners/browser-cache.js +111 -0
- package/dist/scanners/dev-cache.js +64 -0
- package/dist/scanners/docker.js +96 -0
- package/dist/scanners/downloads.js +66 -0
- package/dist/scanners/homebrew.js +82 -0
- package/dist/scanners/index.js +126 -0
- package/dist/scanners/installer.js +87 -0
- package/dist/scanners/ios-backups.js +82 -0
- package/dist/scanners/node-modules.js +75 -0
- package/dist/scanners/temp-files.js +65 -0
- package/dist/scanners/trash.js +90 -0
- package/dist/scanners/user-cache.js +62 -0
- package/dist/scanners/user-logs.js +53 -0
- package/dist/scanners/xcode.js +124 -0
- package/dist/types/index.js +23 -0
- package/dist/ui/index.js +5 -0
- package/dist/ui/monitors.js +345 -0
- package/dist/ui/output.js +304 -0
- package/dist/ui/prompts.js +270 -0
- package/dist/utils/config.js +133 -0
- package/dist/utils/debug.js +119 -0
- package/dist/utils/fs.js +283 -0
- package/dist/utils/help.js +265 -0
- package/dist/utils/index.js +6 -0
- package/dist/utils/paths.js +142 -0
- package/dist/utils/report.js +404 -0
- package/package.json +87 -0
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Status command - Real-time system monitoring with animated broom character
|
|
3
|
+
*/
|
|
4
|
+
import blessed from 'blessed';
|
|
5
|
+
import { Command } from 'commander';
|
|
6
|
+
import * as si from 'systeminformation';
|
|
7
|
+
import { exec } from 'child_process';
|
|
8
|
+
import { promisify } from 'util';
|
|
9
|
+
import { enhanceCommandHelp } from '../utils/help.js';
|
|
10
|
+
const execAsync = promisify(exec);
|
|
11
|
+
// Broom character ASCII art animation frames (like mole's cat)
|
|
12
|
+
const BROOM_FRAMES = [
|
|
13
|
+
// Frame 0: Idle
|
|
14
|
+
`
|
|
15
|
+
╭─────────────────╮
|
|
16
|
+
│ ╭───────╮ │
|
|
17
|
+
│ │ ◕ ◕ │ │
|
|
18
|
+
│ │ ─── │ │
|
|
19
|
+
│ ╰───────╯ │
|
|
20
|
+
│ ╭───────╮ │
|
|
21
|
+
│ /│ BROOM │\\ │
|
|
22
|
+
│ / ╰───────╯ \\ │
|
|
23
|
+
│ / ||||||| \\ │
|
|
24
|
+
│ ||||||| │
|
|
25
|
+
╰─────────────────╯
|
|
26
|
+
`,
|
|
27
|
+
// Frame 1: Sweeping right
|
|
28
|
+
`
|
|
29
|
+
╭─────────────────╮
|
|
30
|
+
│ ╭───────╮ │
|
|
31
|
+
│ │ ◕ ◕ │ │
|
|
32
|
+
│ │ ─── │░░░ │
|
|
33
|
+
│ ╰───────╯░░░ │
|
|
34
|
+
│ ╭───────╮░░ │
|
|
35
|
+
│ /│ BROOM │\\ │
|
|
36
|
+
│ / ╰───────╯ \\ │
|
|
37
|
+
│ /|||||||\\ │
|
|
38
|
+
│ ||||||| │
|
|
39
|
+
╰─────────────────╯
|
|
40
|
+
`,
|
|
41
|
+
// Frame 2: Sweeping
|
|
42
|
+
`
|
|
43
|
+
╭─────────────────╮
|
|
44
|
+
│ ╭───────╮ │
|
|
45
|
+
│ │ ◕ᴗ◕ │ │
|
|
46
|
+
│ ░░ │ ~~~ │ │
|
|
47
|
+
│░░░ ╰───────╯ │
|
|
48
|
+
│░░ ╭───────╮ │
|
|
49
|
+
│ /│ BROOM │\\ │
|
|
50
|
+
│ / ╰───────╯ \\ │
|
|
51
|
+
│ / ||||||| \\ │
|
|
52
|
+
│ ||||||| │
|
|
53
|
+
╰─────────────────╯
|
|
54
|
+
`,
|
|
55
|
+
// Frame 3: Dust cloud
|
|
56
|
+
`
|
|
57
|
+
╭─────────────────╮
|
|
58
|
+
│ ╭───────╮ · │
|
|
59
|
+
│ · │ ^ ^ │ · │
|
|
60
|
+
│ · │ ◡◡ │ · │
|
|
61
|
+
│ · ·╰───────╯· · │
|
|
62
|
+
│ · ╭───────╮ · │
|
|
63
|
+
│ /│ BROOM │\\ │
|
|
64
|
+
│ / ╰───────╯ \\ │
|
|
65
|
+
│ /|||||||\\ │
|
|
66
|
+
│ ||||||| │
|
|
67
|
+
╰─────────────────╯
|
|
68
|
+
`,
|
|
69
|
+
// Frame 4: Sparkle clean
|
|
70
|
+
`
|
|
71
|
+
╭─────────────────╮
|
|
72
|
+
│ ╭───────╮ ✨ │
|
|
73
|
+
│ ✨ │ ✧ ✧ │ │
|
|
74
|
+
│ │ ◡◡◡ │ ✨ │
|
|
75
|
+
│ ╰───────╯ │
|
|
76
|
+
│ ✨ ╭───────╮ │
|
|
77
|
+
│ /│ CLEAN │\\ │
|
|
78
|
+
│ / ╰───────╯ \\ ✨│
|
|
79
|
+
│ /|||||||\\ │
|
|
80
|
+
│ ||||||| │
|
|
81
|
+
╰─────────────────╯
|
|
82
|
+
`,
|
|
83
|
+
];
|
|
84
|
+
// Simpler frames for better compatibility
|
|
85
|
+
const BROOM_SIMPLE_FRAMES = [
|
|
86
|
+
// Frame 0: Idle
|
|
87
|
+
[
|
|
88
|
+
' ╭─────────╮ ',
|
|
89
|
+
' │ ◕ ◕ │ ',
|
|
90
|
+
' │ ── │ ',
|
|
91
|
+
' ╰────┬───╯ ',
|
|
92
|
+
' ╭────┴───╮ ',
|
|
93
|
+
' │ BROOM │ ',
|
|
94
|
+
' ╰────────╯ ',
|
|
95
|
+
' ▓▓ ',
|
|
96
|
+
' ▓▓▓▓ ',
|
|
97
|
+
' ▓▓▓▓▓▓ ',
|
|
98
|
+
],
|
|
99
|
+
// Frame 1: Sweeping right
|
|
100
|
+
[
|
|
101
|
+
' ╭─────────╮ ',
|
|
102
|
+
' │ ◕ ◕ │ ░ ',
|
|
103
|
+
' │ ── │░░░',
|
|
104
|
+
' ╰────┬───╯ ░░',
|
|
105
|
+
' ╭────┴───╮ ░ ',
|
|
106
|
+
' │ BROOM │ ',
|
|
107
|
+
' ╰────────╯ ',
|
|
108
|
+
' ▓▓ ',
|
|
109
|
+
' ▓▓▓▓\\ ',
|
|
110
|
+
' ▓▓▓▓▓▓\\ ',
|
|
111
|
+
],
|
|
112
|
+
// Frame 2: Sweeping left
|
|
113
|
+
[
|
|
114
|
+
' ╭─────────╮ ',
|
|
115
|
+
' ░ │ ◕ᴗ◕ │ ',
|
|
116
|
+
'░░░│ ~~ │ ',
|
|
117
|
+
'░░ ╰────┬───╯ ',
|
|
118
|
+
' ░ ╭────┴───╮ ',
|
|
119
|
+
' │ BROOM │ ',
|
|
120
|
+
' ╰────────╯ ',
|
|
121
|
+
' ▓▓ ',
|
|
122
|
+
' /▓▓▓▓ ',
|
|
123
|
+
' /▓▓▓▓▓▓ ',
|
|
124
|
+
],
|
|
125
|
+
// Frame 3: Dust cloud
|
|
126
|
+
[
|
|
127
|
+
' ╭─────────╮ · ',
|
|
128
|
+
' · │ ^ ^ │ · ',
|
|
129
|
+
' ·│ ◡◡ │· ',
|
|
130
|
+
' · ╰────┬───╯ · ',
|
|
131
|
+
' ·╭────┴───╮· ',
|
|
132
|
+
' │ BROOM │ ',
|
|
133
|
+
' ╰────────╯ ',
|
|
134
|
+
' ▓▓ ',
|
|
135
|
+
' ▓▓▓▓ ',
|
|
136
|
+
' ▓▓▓▓▓▓ ',
|
|
137
|
+
],
|
|
138
|
+
// Frame 4: Sparkle
|
|
139
|
+
[
|
|
140
|
+
' ╭─────────╮ ✨',
|
|
141
|
+
' ✨│ ✧ ✧ │ ',
|
|
142
|
+
' │ ◡◡◡ │ ✨',
|
|
143
|
+
' ╰────┬───╯ ',
|
|
144
|
+
' ✨╭────┴───╮ ',
|
|
145
|
+
' │ CLEAN! │ ✨',
|
|
146
|
+
' ╰────────╯ ',
|
|
147
|
+
' ▓▓ ',
|
|
148
|
+
' ▓▓▓▓ ',
|
|
149
|
+
' ▓▓▓▓▓▓ ',
|
|
150
|
+
],
|
|
151
|
+
];
|
|
152
|
+
/**
|
|
153
|
+
* Format bytes to human readable
|
|
154
|
+
*/
|
|
155
|
+
function formatBytes(bytes, decimals = 1) {
|
|
156
|
+
if (bytes === 0)
|
|
157
|
+
return '0 B';
|
|
158
|
+
const k = 1024;
|
|
159
|
+
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
160
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
161
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(decimals)) + ' ' + sizes[i];
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Format speed (bytes per second)
|
|
165
|
+
*/
|
|
166
|
+
function formatSpeed(bytesPerSec) {
|
|
167
|
+
if (bytesPerSec < 1024)
|
|
168
|
+
return bytesPerSec.toFixed(1) + ' B/s';
|
|
169
|
+
if (bytesPerSec < 1024 * 1024)
|
|
170
|
+
return (bytesPerSec / 1024).toFixed(1) + ' KB/s';
|
|
171
|
+
return (bytesPerSec / 1024 / 1024).toFixed(1) + ' MB/s';
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Create colored bar with border
|
|
175
|
+
*/
|
|
176
|
+
function createColoredBar(percent, width) {
|
|
177
|
+
const filled = Math.round((percent / 100) * width);
|
|
178
|
+
let bar = '';
|
|
179
|
+
for (let i = 0; i < filled; i++) {
|
|
180
|
+
const ratio = i / width;
|
|
181
|
+
if (ratio < 0.5) {
|
|
182
|
+
bar += '{green-fg}█{/green-fg}';
|
|
183
|
+
}
|
|
184
|
+
else if (ratio < 0.75) {
|
|
185
|
+
bar += '{yellow-fg}█{/yellow-fg}';
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
bar += '{red-fg}█{/red-fg}';
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
bar += '{black-fg}' + '░'.repeat(width - filled) + '{/black-fg}';
|
|
192
|
+
return bar;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Get local IP address
|
|
196
|
+
*/
|
|
197
|
+
async function getLocalIP() {
|
|
198
|
+
try {
|
|
199
|
+
const netInterfaces = await si.networkInterfaces();
|
|
200
|
+
const active = netInterfaces.find((n) => n.ip4 && !n.internal && n.operstate === 'up');
|
|
201
|
+
return active?.ip4 || 'N/A';
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
return 'N/A';
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Get top processes by CPU
|
|
209
|
+
*/
|
|
210
|
+
async function getTopProcesses() {
|
|
211
|
+
try {
|
|
212
|
+
const { stdout } = await execAsync('ps -Ao comm,pcpu -r | head -11 | tail -10');
|
|
213
|
+
return stdout
|
|
214
|
+
.trim()
|
|
215
|
+
.split('\n')
|
|
216
|
+
.map((line) => {
|
|
217
|
+
const parts = line.trim().split(/\s+/);
|
|
218
|
+
const cpu = parseFloat(parts.pop() || '0');
|
|
219
|
+
const name = parts.join(' ').replace(/^.*\//, '').slice(0, 20);
|
|
220
|
+
return { name, cpu };
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
return [];
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Calculate system health score (0-100)
|
|
229
|
+
*/
|
|
230
|
+
function calculateHealth(cpuUsage, memUsage, diskUsage, batteryPercent) {
|
|
231
|
+
const cpuScore = Math.max(0, 100 - cpuUsage);
|
|
232
|
+
const memScore = Math.max(0, 100 - memUsage);
|
|
233
|
+
const diskScore = Math.max(0, 100 - diskUsage);
|
|
234
|
+
const batteryScore = batteryPercent;
|
|
235
|
+
return Math.round(cpuScore * 0.3 + memScore * 0.3 + diskScore * 0.2 + batteryScore * 0.2);
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Run status TUI
|
|
239
|
+
*/
|
|
240
|
+
export async function statusCommand(options) {
|
|
241
|
+
const interval = (options.interval ?? 2) * 1000;
|
|
242
|
+
const showBroom = options.broom !== false;
|
|
243
|
+
let broomFrame = 0;
|
|
244
|
+
// Create blessed screen
|
|
245
|
+
const screen = blessed.screen({
|
|
246
|
+
smartCSR: true,
|
|
247
|
+
title: 'Broom System Status',
|
|
248
|
+
fullUnicode: true,
|
|
249
|
+
});
|
|
250
|
+
// Animation box at top
|
|
251
|
+
const animBox = blessed.box({
|
|
252
|
+
parent: screen,
|
|
253
|
+
top: 0,
|
|
254
|
+
left: 'center',
|
|
255
|
+
width: 25,
|
|
256
|
+
height: showBroom ? 12 : 0,
|
|
257
|
+
tags: true,
|
|
258
|
+
style: { fg: 'cyan' },
|
|
259
|
+
});
|
|
260
|
+
// Header
|
|
261
|
+
const headerBox = blessed.box({
|
|
262
|
+
parent: screen,
|
|
263
|
+
top: showBroom ? 12 : 0,
|
|
264
|
+
left: 0,
|
|
265
|
+
width: '100%',
|
|
266
|
+
height: 3,
|
|
267
|
+
tags: true,
|
|
268
|
+
border: { type: 'line' },
|
|
269
|
+
style: { border: { fg: 'cyan' } },
|
|
270
|
+
label: ' {bold}🧹 Broom System Status{/bold} ',
|
|
271
|
+
});
|
|
272
|
+
// CPU Box
|
|
273
|
+
const cpuBox = blessed.box({
|
|
274
|
+
parent: screen,
|
|
275
|
+
top: showBroom ? 15 : 3,
|
|
276
|
+
left: 0,
|
|
277
|
+
width: '50%-1',
|
|
278
|
+
height: 10,
|
|
279
|
+
label: ' {yellow-fg}●{/yellow-fg} CPU ',
|
|
280
|
+
tags: true,
|
|
281
|
+
border: { type: 'line' },
|
|
282
|
+
style: { border: { fg: 'cyan' } },
|
|
283
|
+
});
|
|
284
|
+
// Memory Box
|
|
285
|
+
const memBox = blessed.box({
|
|
286
|
+
parent: screen,
|
|
287
|
+
top: showBroom ? 15 : 3,
|
|
288
|
+
left: '50%',
|
|
289
|
+
width: '50%',
|
|
290
|
+
height: 10,
|
|
291
|
+
label: ' {red-fg}▣{/red-fg} Memory ',
|
|
292
|
+
tags: true,
|
|
293
|
+
border: { type: 'line' },
|
|
294
|
+
style: { border: { fg: 'cyan' } },
|
|
295
|
+
});
|
|
296
|
+
// Disk Box
|
|
297
|
+
const diskBox = blessed.box({
|
|
298
|
+
parent: screen,
|
|
299
|
+
top: showBroom ? 25 : 13,
|
|
300
|
+
left: 0,
|
|
301
|
+
width: '50%-1',
|
|
302
|
+
height: 6,
|
|
303
|
+
label: ' {blue-fg}▣{/blue-fg} Disk ',
|
|
304
|
+
tags: true,
|
|
305
|
+
border: { type: 'line' },
|
|
306
|
+
style: { border: { fg: 'cyan' } },
|
|
307
|
+
});
|
|
308
|
+
// Network Box
|
|
309
|
+
const netBox = blessed.box({
|
|
310
|
+
parent: screen,
|
|
311
|
+
top: showBroom ? 25 : 13,
|
|
312
|
+
left: '50%',
|
|
313
|
+
width: '50%',
|
|
314
|
+
height: 6,
|
|
315
|
+
label: ' {cyan-fg}↕{/cyan-fg} Network ',
|
|
316
|
+
tags: true,
|
|
317
|
+
border: { type: 'line' },
|
|
318
|
+
style: { border: { fg: 'cyan' } },
|
|
319
|
+
});
|
|
320
|
+
// Processes Box
|
|
321
|
+
const procBox = blessed.box({
|
|
322
|
+
parent: screen,
|
|
323
|
+
top: showBroom ? 31 : 19,
|
|
324
|
+
left: 0,
|
|
325
|
+
width: '100%',
|
|
326
|
+
height: 9,
|
|
327
|
+
label: ' {magenta-fg}●{/magenta-fg} Top Processes ',
|
|
328
|
+
tags: true,
|
|
329
|
+
border: { type: 'line' },
|
|
330
|
+
style: { border: { fg: 'cyan' } },
|
|
331
|
+
});
|
|
332
|
+
// Footer
|
|
333
|
+
blessed.box({
|
|
334
|
+
parent: screen,
|
|
335
|
+
bottom: 0,
|
|
336
|
+
left: 0,
|
|
337
|
+
width: '100%',
|
|
338
|
+
height: 1,
|
|
339
|
+
tags: true,
|
|
340
|
+
content: '{gray-fg}Press q or Ctrl+C to exit{/gray-fg}',
|
|
341
|
+
style: { fg: 'gray' },
|
|
342
|
+
});
|
|
343
|
+
// Exit keys
|
|
344
|
+
screen.key(['escape', 'q', 'C-c'], () => {
|
|
345
|
+
process.exit(0);
|
|
346
|
+
});
|
|
347
|
+
/**
|
|
348
|
+
* Update animation
|
|
349
|
+
*/
|
|
350
|
+
function updateAnimation() {
|
|
351
|
+
if (!showBroom)
|
|
352
|
+
return;
|
|
353
|
+
const frame = BROOM_SIMPLE_FRAMES[broomFrame % BROOM_SIMPLE_FRAMES.length];
|
|
354
|
+
animBox.setContent('{cyan-fg}' + frame.join('\n') + '{/cyan-fg}');
|
|
355
|
+
broomFrame++;
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Update all data
|
|
359
|
+
*/
|
|
360
|
+
async function update() {
|
|
361
|
+
try {
|
|
362
|
+
// Gather all data in parallel
|
|
363
|
+
const [cpuLoad, cpuInfo, cpuTemp, mem, disk, battery, netStats, osInfo, graphics] = await Promise.all([
|
|
364
|
+
si.currentLoad(),
|
|
365
|
+
si.cpu(),
|
|
366
|
+
si.cpuTemperature(),
|
|
367
|
+
si.mem(),
|
|
368
|
+
si.fsSize(),
|
|
369
|
+
si.battery(),
|
|
370
|
+
si.networkStats(),
|
|
371
|
+
si.osInfo(),
|
|
372
|
+
si.graphics(),
|
|
373
|
+
]);
|
|
374
|
+
const localIP = await getLocalIP();
|
|
375
|
+
const topProcs = await getTopProcesses();
|
|
376
|
+
const mainDisk = disk.find((d) => d.mount === '/') || disk[0];
|
|
377
|
+
const activeNet = netStats.find((n) => n.operstate === 'up') || netStats[0];
|
|
378
|
+
// Calculate health
|
|
379
|
+
const memUsage = (mem.used / mem.total) * 100;
|
|
380
|
+
const diskUsage = mainDisk?.use ?? 0;
|
|
381
|
+
const batteryPercent = battery.hasBattery ? battery.percent : 100;
|
|
382
|
+
const health = calculateHealth(cpuLoad.currentLoad, memUsage, diskUsage, batteryPercent);
|
|
383
|
+
// Header content
|
|
384
|
+
const gpu = graphics.controllers?.[0];
|
|
385
|
+
const gpuName = gpu?.model ? `(${gpu.model.replace('Apple ', '').split(' ')[0]})` : '';
|
|
386
|
+
const headerContent = ` {bold}Health:{/bold} {${health > 70 ? 'green' : health > 40 ? 'yellow' : 'red'}-fg}${health}%{/} | ` +
|
|
387
|
+
`{bold}Host:{/bold} ${osInfo.hostname.split('.')[0]} | ` +
|
|
388
|
+
`{bold}CPU:{/bold} ${cpuInfo.manufacturer} ${cpuInfo.brand} ${gpuName}`;
|
|
389
|
+
headerBox.setContent(headerContent);
|
|
390
|
+
// CPU content
|
|
391
|
+
const cpuCores = cpuLoad.cpus || [];
|
|
392
|
+
let cpuContent = '';
|
|
393
|
+
const coresToShow = cpuCores.slice(0, 6);
|
|
394
|
+
coresToShow.forEach((core, i) => {
|
|
395
|
+
const bar = createColoredBar(core.load, 15);
|
|
396
|
+
cpuContent += ` Core${(i + 1).toString().padStart(2)} ${bar} ${core.load.toFixed(1).padStart(5)}%\n`;
|
|
397
|
+
});
|
|
398
|
+
const temp = cpuTemp.main > 0 ? ` @ ${cpuTemp.main.toFixed(0)}°C` : '';
|
|
399
|
+
cpuContent += `\n {gray-fg}Load: ${cpuLoad.currentLoad.toFixed(1)}%${temp}{/gray-fg}`;
|
|
400
|
+
cpuBox.setContent(cpuContent);
|
|
401
|
+
// Memory content
|
|
402
|
+
const memUsedPercent = (mem.used / mem.total) * 100;
|
|
403
|
+
const memFreePercent = (mem.free / mem.total) * 100;
|
|
404
|
+
const swapUsedPercent = mem.swaptotal > 0 ? (mem.swapused / mem.swaptotal) * 100 : 0;
|
|
405
|
+
let memContent = '';
|
|
406
|
+
memContent += ` Used ${createColoredBar(memUsedPercent, 15)} ${memUsedPercent.toFixed(1).padStart(5)}%\n`;
|
|
407
|
+
memContent += ` Free ${createColoredBar(100 - memFreePercent, 15)} ${memFreePercent.toFixed(1).padStart(5)}%\n`;
|
|
408
|
+
memContent += ` Swap ${createColoredBar(swapUsedPercent, 15)} ${swapUsedPercent.toFixed(1).padStart(5)}%\n`;
|
|
409
|
+
memContent += `\n {gray-fg}Total: ${formatBytes(mem.total)} | Avail: ${formatBytes(mem.available)}{/gray-fg}`;
|
|
410
|
+
memBox.setContent(memContent);
|
|
411
|
+
// Disk content
|
|
412
|
+
const diskBar = createColoredBar(diskUsage, 20);
|
|
413
|
+
let diskContent = '';
|
|
414
|
+
diskContent += ` Usage ${diskBar} ${diskUsage.toFixed(1).padStart(5)}%\n`;
|
|
415
|
+
diskContent += ` {gray-fg}Used: ${formatBytes(mainDisk?.used ?? 0)} / ${formatBytes(mainDisk?.size ?? 0)}{/gray-fg}\n`;
|
|
416
|
+
diskContent += ` {gray-fg}Free: ${formatBytes((mainDisk?.size ?? 0) - (mainDisk?.used ?? 0))}{/gray-fg}`;
|
|
417
|
+
diskBox.setContent(diskContent);
|
|
418
|
+
// Network content
|
|
419
|
+
const rxSpeed = activeNet?.rx_sec ?? 0;
|
|
420
|
+
const txSpeed = activeNet?.tx_sec ?? 0;
|
|
421
|
+
let netContent = '';
|
|
422
|
+
netContent += ` {green-fg}↓{/green-fg} Download: ${formatSpeed(rxSpeed).padStart(12)}\n`;
|
|
423
|
+
netContent += ` {red-fg}↑{/red-fg} Upload: ${formatSpeed(txSpeed).padStart(12)}\n`;
|
|
424
|
+
netContent += ` {gray-fg}IP: ${localIP}{/gray-fg}`;
|
|
425
|
+
netBox.setContent(netContent);
|
|
426
|
+
// Processes content
|
|
427
|
+
let procContent = '';
|
|
428
|
+
topProcs.slice(0, 6).forEach((proc, i) => {
|
|
429
|
+
const bar = createColoredBar(Math.min(proc.cpu, 100), 10);
|
|
430
|
+
procContent += ` ${(i + 1).toString().padStart(2)}. ${proc.name.padEnd(20)} ${bar} ${proc.cpu.toFixed(1).padStart(5)}%\n`;
|
|
431
|
+
});
|
|
432
|
+
procBox.setContent(procContent);
|
|
433
|
+
screen.render();
|
|
434
|
+
}
|
|
435
|
+
catch (err) {
|
|
436
|
+
// Ignore errors and try again
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
// Initial update
|
|
440
|
+
updateAnimation();
|
|
441
|
+
await update();
|
|
442
|
+
// Set interval for updates
|
|
443
|
+
const updateInterval = setInterval(update, interval);
|
|
444
|
+
// Animation interval (faster for smoother animation)
|
|
445
|
+
const animInterval = setInterval(() => {
|
|
446
|
+
updateAnimation();
|
|
447
|
+
screen.render();
|
|
448
|
+
}, 500);
|
|
449
|
+
// Cleanup on exit
|
|
450
|
+
screen.on('destroy', () => {
|
|
451
|
+
clearInterval(updateInterval);
|
|
452
|
+
clearInterval(animInterval);
|
|
453
|
+
});
|
|
454
|
+
screen.render();
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* Create status command
|
|
458
|
+
*/
|
|
459
|
+
export function createStatusCommand() {
|
|
460
|
+
const cmd = new Command('status')
|
|
461
|
+
.description('Real-time system monitoring dashboard')
|
|
462
|
+
.option('-i, --interval <seconds>', 'Update interval in seconds (default: 2)', parseInt)
|
|
463
|
+
.option('--no-broom', 'Disable broom animation')
|
|
464
|
+
.action(async (options) => {
|
|
465
|
+
await statusCommand(options);
|
|
466
|
+
});
|
|
467
|
+
return enhanceCommandHelp(cmd);
|
|
468
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* touchid command - Configure Touch ID for sudo authentication
|
|
3
|
+
*/
|
|
4
|
+
import { Command } from 'commander';
|
|
5
|
+
import { execSync } from 'child_process';
|
|
6
|
+
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
|
7
|
+
import chalk from 'chalk';
|
|
8
|
+
import ora from 'ora';
|
|
9
|
+
import { confirm } from '@inquirer/prompts';
|
|
10
|
+
import { enhanceCommandHelp } from '../utils/help.js';
|
|
11
|
+
const PAM_SUDO_PATH = '/etc/pam.d/sudo';
|
|
12
|
+
const PAM_SUDO_LOCAL_PATH = '/etc/pam.d/sudo_local';
|
|
13
|
+
const PAM_SUDO_TEMPLATE_PATH = '/etc/pam.d/sudo_local.template';
|
|
14
|
+
const TOUCHID_LINE = 'auth sufficient pam_tid.so';
|
|
15
|
+
/**
|
|
16
|
+
* Check current Touch ID sudo status
|
|
17
|
+
*/
|
|
18
|
+
function checkTouchIdStatus() {
|
|
19
|
+
// Check sudo_local first (preferred method for macOS Sonoma+)
|
|
20
|
+
if (existsSync(PAM_SUDO_LOCAL_PATH)) {
|
|
21
|
+
try {
|
|
22
|
+
const content = readFileSync(PAM_SUDO_LOCAL_PATH, 'utf-8');
|
|
23
|
+
if (content.includes('pam_tid.so') && !content.includes('#auth')) {
|
|
24
|
+
return {
|
|
25
|
+
enabled: true,
|
|
26
|
+
method: 'sudo_local',
|
|
27
|
+
hasTemplate: existsSync(PAM_SUDO_TEMPLATE_PATH),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
// Permission denied, need sudo
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
// Check sudo file (legacy method)
|
|
36
|
+
if (existsSync(PAM_SUDO_PATH)) {
|
|
37
|
+
try {
|
|
38
|
+
const content = readFileSync(PAM_SUDO_PATH, 'utf-8');
|
|
39
|
+
const lines = content.split('\n');
|
|
40
|
+
for (const line of lines) {
|
|
41
|
+
if (line.includes('pam_tid.so') && !line.trim().startsWith('#')) {
|
|
42
|
+
return { enabled: true, method: 'sudo', hasTemplate: existsSync(PAM_SUDO_TEMPLATE_PATH) };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
// Permission denied, need sudo
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return { enabled: false, method: 'none', hasTemplate: existsSync(PAM_SUDO_TEMPLATE_PATH) };
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Enable Touch ID for sudo
|
|
54
|
+
*/
|
|
55
|
+
async function enableTouchId(skipConfirm) {
|
|
56
|
+
const status = checkTouchIdStatus();
|
|
57
|
+
if (status.enabled) {
|
|
58
|
+
console.log(chalk.green('✓ Touch ID for sudo is already enabled'));
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
console.log(chalk.bold('\n🔐 Touch ID for sudo Configuration\n'));
|
|
62
|
+
console.log('This will configure Touch ID authentication for sudo commands.');
|
|
63
|
+
console.log('You can use your fingerprint instead of typing your password.\n');
|
|
64
|
+
if (!skipConfirm) {
|
|
65
|
+
const confirmed = await confirm({
|
|
66
|
+
message: 'Enable Touch ID for sudo?',
|
|
67
|
+
default: true,
|
|
68
|
+
});
|
|
69
|
+
if (!confirmed) {
|
|
70
|
+
console.log(chalk.yellow('Cancelled'));
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const spinner = ora('Enabling Touch ID for sudo...').start();
|
|
75
|
+
try {
|
|
76
|
+
// Use sudo_local method for macOS Sonoma+ (preferred)
|
|
77
|
+
if (status.hasTemplate) {
|
|
78
|
+
// Copy template to sudo_local
|
|
79
|
+
const command = `sudo cp "${PAM_SUDO_TEMPLATE_PATH}" "${PAM_SUDO_LOCAL_PATH}" && sudo sed -i '' 's/#auth/auth/' "${PAM_SUDO_LOCAL_PATH}"`;
|
|
80
|
+
execSync(command, { stdio: 'pipe' });
|
|
81
|
+
spinner.succeed('Touch ID for sudo enabled via sudo_local');
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
// Legacy method: modify /etc/pam.d/sudo directly
|
|
85
|
+
// First backup
|
|
86
|
+
const backupPath = `${PAM_SUDO_PATH}.bak`;
|
|
87
|
+
if (!existsSync(backupPath)) {
|
|
88
|
+
execSync(`sudo cp "${PAM_SUDO_PATH}" "${backupPath}"`, { stdio: 'pipe' });
|
|
89
|
+
}
|
|
90
|
+
// Read current content
|
|
91
|
+
const content = execSync(`sudo cat "${PAM_SUDO_PATH}"`, { encoding: 'utf-8' });
|
|
92
|
+
const lines = content.split('\n');
|
|
93
|
+
// Check if pam_tid.so is already there but commented
|
|
94
|
+
let modified = false;
|
|
95
|
+
const newLines = lines.map((line) => {
|
|
96
|
+
if (line.includes('pam_tid.so')) {
|
|
97
|
+
modified = true;
|
|
98
|
+
return TOUCHID_LINE;
|
|
99
|
+
}
|
|
100
|
+
return line;
|
|
101
|
+
});
|
|
102
|
+
// If not found, add after the first auth line
|
|
103
|
+
if (!modified) {
|
|
104
|
+
for (let i = 0; i < newLines.length; i++) {
|
|
105
|
+
if (newLines[i].trim().startsWith('auth')) {
|
|
106
|
+
newLines.splice(i, 0, TOUCHID_LINE);
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// Write back
|
|
112
|
+
const tempFile = '/tmp/broom_pam_sudo';
|
|
113
|
+
writeFileSync(tempFile, newLines.join('\n'));
|
|
114
|
+
execSync(`sudo cp "${tempFile}" "${PAM_SUDO_PATH}"`, { stdio: 'pipe' });
|
|
115
|
+
spinner.succeed('Touch ID for sudo enabled via /etc/pam.d/sudo');
|
|
116
|
+
}
|
|
117
|
+
console.log(chalk.dim('\nNote: Touch ID for sudo may not work in some Terminal apps.'));
|
|
118
|
+
console.log(chalk.dim('Works best in Terminal.app and iTerm2.'));
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
spinner.fail('Failed to enable Touch ID for sudo');
|
|
122
|
+
if (error instanceof Error) {
|
|
123
|
+
console.error(chalk.red(error.message));
|
|
124
|
+
}
|
|
125
|
+
console.log(chalk.dim('\nTip: Make sure you have admin privileges.'));
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Disable Touch ID for sudo
|
|
130
|
+
*/
|
|
131
|
+
async function disableTouchId(skipConfirm) {
|
|
132
|
+
const status = checkTouchIdStatus();
|
|
133
|
+
if (!status.enabled) {
|
|
134
|
+
console.log(chalk.yellow('Touch ID for sudo is not enabled'));
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (!skipConfirm) {
|
|
138
|
+
const confirmed = await confirm({
|
|
139
|
+
message: 'Disable Touch ID for sudo?',
|
|
140
|
+
default: false,
|
|
141
|
+
});
|
|
142
|
+
if (!confirmed) {
|
|
143
|
+
console.log(chalk.yellow('Cancelled'));
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
const spinner = ora('Disabling Touch ID for sudo...').start();
|
|
148
|
+
try {
|
|
149
|
+
if (status.method === 'sudo_local') {
|
|
150
|
+
// Remove sudo_local file
|
|
151
|
+
execSync(`sudo rm -f "${PAM_SUDO_LOCAL_PATH}"`, { stdio: 'pipe' });
|
|
152
|
+
spinner.succeed('Touch ID for sudo disabled (removed sudo_local)');
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
// Comment out pam_tid.so in /etc/pam.d/sudo
|
|
156
|
+
const content = execSync(`sudo cat "${PAM_SUDO_PATH}"`, { encoding: 'utf-8' });
|
|
157
|
+
const lines = content.split('\n');
|
|
158
|
+
const newLines = lines.map((line) => {
|
|
159
|
+
if (line.includes('pam_tid.so') && !line.trim().startsWith('#')) {
|
|
160
|
+
return '#' + line;
|
|
161
|
+
}
|
|
162
|
+
return line;
|
|
163
|
+
});
|
|
164
|
+
const tempFile = '/tmp/broom_pam_sudo';
|
|
165
|
+
writeFileSync(tempFile, newLines.join('\n'));
|
|
166
|
+
execSync(`sudo cp "${tempFile}" "${PAM_SUDO_PATH}"`, { stdio: 'pipe' });
|
|
167
|
+
spinner.succeed('Touch ID for sudo disabled');
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
catch (error) {
|
|
171
|
+
spinner.fail('Failed to disable Touch ID for sudo');
|
|
172
|
+
if (error instanceof Error) {
|
|
173
|
+
console.error(chalk.red(error.message));
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Show Touch ID sudo status
|
|
179
|
+
*/
|
|
180
|
+
function showStatus() {
|
|
181
|
+
const status = checkTouchIdStatus();
|
|
182
|
+
console.log(chalk.bold('\n🔐 Touch ID for sudo Status\n'));
|
|
183
|
+
if (status.enabled) {
|
|
184
|
+
console.log(chalk.green(' Status: ') + chalk.bold.green('Enabled'));
|
|
185
|
+
console.log(chalk.dim(' Method: ') + status.method);
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
console.log(chalk.yellow(' Status: ') + chalk.bold.yellow('Disabled'));
|
|
189
|
+
}
|
|
190
|
+
console.log(chalk.dim(' Template: ') + (status.hasTemplate ? 'Available' : 'Not found'));
|
|
191
|
+
console.log();
|
|
192
|
+
if (!status.enabled) {
|
|
193
|
+
console.log(chalk.dim('Run `broom touchid enable` to enable Touch ID for sudo.\n'));
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Create touchid command
|
|
198
|
+
*/
|
|
199
|
+
export function createTouchIdCommand() {
|
|
200
|
+
const cmd = new Command('touchid')
|
|
201
|
+
.description('Configure Touch ID for sudo authentication')
|
|
202
|
+
.option('-y, --yes', 'Skip confirmation prompts');
|
|
203
|
+
cmd
|
|
204
|
+
.command('enable')
|
|
205
|
+
.description('Enable Touch ID for sudo')
|
|
206
|
+
.option('-y, --yes', 'Skip confirmation prompts')
|
|
207
|
+
.action(async (opts) => {
|
|
208
|
+
const parentOpts = cmd.opts();
|
|
209
|
+
await enableTouchId(opts.yes || parentOpts.yes);
|
|
210
|
+
});
|
|
211
|
+
cmd
|
|
212
|
+
.command('disable')
|
|
213
|
+
.description('Disable Touch ID for sudo')
|
|
214
|
+
.option('-y, --yes', 'Skip confirmation prompts')
|
|
215
|
+
.action(async (opts) => {
|
|
216
|
+
const parentOpts = cmd.opts();
|
|
217
|
+
await disableTouchId(opts.yes || parentOpts.yes);
|
|
218
|
+
});
|
|
219
|
+
cmd
|
|
220
|
+
.command('status')
|
|
221
|
+
.description('Show Touch ID sudo status')
|
|
222
|
+
.action(() => {
|
|
223
|
+
showStatus();
|
|
224
|
+
});
|
|
225
|
+
// Default action - show status
|
|
226
|
+
cmd.action(() => {
|
|
227
|
+
showStatus();
|
|
228
|
+
});
|
|
229
|
+
return enhanceCommandHelp(cmd);
|
|
230
|
+
}
|