@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,304 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal output helpers for Broom CLI
|
|
3
|
+
*/
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import ora from 'ora';
|
|
6
|
+
import { formatSize } from '../utils/fs.js';
|
|
7
|
+
// Icons
|
|
8
|
+
export const ICONS = {
|
|
9
|
+
success: '✓',
|
|
10
|
+
error: '✗',
|
|
11
|
+
warning: '⚠',
|
|
12
|
+
info: 'ℹ',
|
|
13
|
+
arrow: '→',
|
|
14
|
+
folder: '📁',
|
|
15
|
+
file: '📄',
|
|
16
|
+
trash: '🗑',
|
|
17
|
+
clean: '🧹',
|
|
18
|
+
disk: '💾',
|
|
19
|
+
cpu: '🖥',
|
|
20
|
+
memory: '🧠',
|
|
21
|
+
network: '🌐',
|
|
22
|
+
dryRun: '👁',
|
|
23
|
+
admin: '🔐',
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Print success message
|
|
27
|
+
*/
|
|
28
|
+
export function success(message) {
|
|
29
|
+
console.log(chalk.green(ICONS.success), message);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Print error message
|
|
33
|
+
*/
|
|
34
|
+
export function error(message) {
|
|
35
|
+
console.log(chalk.red(ICONS.error), message);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Print warning message
|
|
39
|
+
*/
|
|
40
|
+
export function warning(message) {
|
|
41
|
+
console.log(chalk.yellow(ICONS.warning), message);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Print info message
|
|
45
|
+
*/
|
|
46
|
+
export function info(message) {
|
|
47
|
+
console.log(chalk.blue(ICONS.info), message);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Print separator line
|
|
51
|
+
*/
|
|
52
|
+
export function separator(char = '─', length = 60) {
|
|
53
|
+
console.log(chalk.gray(char.repeat(length)));
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Print header
|
|
57
|
+
*/
|
|
58
|
+
export function printHeader(title) {
|
|
59
|
+
console.log();
|
|
60
|
+
console.log(chalk.bold.magenta(title));
|
|
61
|
+
separator();
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Print welcome message
|
|
65
|
+
*/
|
|
66
|
+
export function printWelcome() {
|
|
67
|
+
console.log();
|
|
68
|
+
console.log(chalk.bold.cyan('🧹 Broom'));
|
|
69
|
+
console.log(chalk.dim('macOS Disk Cleanup Tool'));
|
|
70
|
+
separator();
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Print file with details
|
|
74
|
+
*/
|
|
75
|
+
export function printFile(item, index) {
|
|
76
|
+
const prefix = index !== undefined ? `${String(index + 1).padStart(2, ' ')}.` : ' ';
|
|
77
|
+
const icon = item.isDirectory ? ICONS.folder : ICONS.file;
|
|
78
|
+
const size = chalk.yellow(formatSize(item.size).padStart(10));
|
|
79
|
+
const path = chalk.dim(item.path);
|
|
80
|
+
console.log(`${prefix} ${icon} ${size} ${path}`);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Print file list
|
|
84
|
+
*/
|
|
85
|
+
export function printFiles(items) {
|
|
86
|
+
items.forEach((item, index) => printFile(item, index));
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Print scan result
|
|
90
|
+
*/
|
|
91
|
+
export function printScanResult(result) {
|
|
92
|
+
const { category, items, totalSize } = result;
|
|
93
|
+
const sizeStr = formatSize(totalSize);
|
|
94
|
+
const countStr = `${items.length} items`;
|
|
95
|
+
if (items.length > 0) {
|
|
96
|
+
console.log(` ${chalk.green(ICONS.success)} ${category.name.padEnd(35)} ${chalk.yellow(sizeStr.padStart(12))} ${chalk.dim(`(${countStr})`)}`);
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
console.log(` ${chalk.gray(ICONS.info)} ${category.name.padEnd(35)} ${chalk.gray('Empty')}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Print removal summary
|
|
104
|
+
*/
|
|
105
|
+
export function printRemovalSummary(summary) {
|
|
106
|
+
separator();
|
|
107
|
+
console.log(chalk.bold('Removal Summary:'));
|
|
108
|
+
console.log(`Total files: ${chalk.cyan(summary.totalFiles)}`);
|
|
109
|
+
console.log(`Successfully removed: ${chalk.green(summary.successCount)}`);
|
|
110
|
+
if (summary.failureCount > 0) {
|
|
111
|
+
console.log(`Failed: ${chalk.red(summary.failureCount)}`);
|
|
112
|
+
}
|
|
113
|
+
console.log(`Space freed: ${chalk.yellow(formatSize(summary.totalSizeFreed))}`);
|
|
114
|
+
separator();
|
|
115
|
+
if (summary.failureCount > 0) {
|
|
116
|
+
console.log(chalk.bold('\nFailed removals:'));
|
|
117
|
+
summary.results
|
|
118
|
+
.filter((r) => !r.success)
|
|
119
|
+
.forEach((r) => {
|
|
120
|
+
error(`${r.path}: ${r.error || 'Unknown error'}`);
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Print summary block
|
|
126
|
+
*/
|
|
127
|
+
export function printSummaryBlock(heading, details) {
|
|
128
|
+
console.log();
|
|
129
|
+
separator('═');
|
|
130
|
+
console.log(chalk.bold(heading));
|
|
131
|
+
separator('─');
|
|
132
|
+
details.forEach((detail) => console.log(` ${detail}`));
|
|
133
|
+
separator('═');
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Create spinner
|
|
137
|
+
*/
|
|
138
|
+
export function createSpinner(text) {
|
|
139
|
+
return ora({
|
|
140
|
+
text,
|
|
141
|
+
spinner: 'dots',
|
|
142
|
+
}).start();
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Start spinner
|
|
146
|
+
*/
|
|
147
|
+
export function startSpinner(text) {
|
|
148
|
+
return ora(text).start();
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Succeed spinner
|
|
152
|
+
*/
|
|
153
|
+
export function succeedSpinner(spinner, text) {
|
|
154
|
+
if (text) {
|
|
155
|
+
spinner.succeed(text);
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
spinner.succeed();
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Fail spinner
|
|
163
|
+
*/
|
|
164
|
+
export function failSpinner(spinner, text) {
|
|
165
|
+
if (text) {
|
|
166
|
+
spinner.fail(text);
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
spinner.fail();
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Update spinner text
|
|
174
|
+
*/
|
|
175
|
+
export function updateSpinner(spinner, text) {
|
|
176
|
+
spinner.text = text;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Print progress bar inline with improved design
|
|
180
|
+
*/
|
|
181
|
+
export function printProgressBar(percent, width = 30, label = '') {
|
|
182
|
+
const filled = Math.round((percent / 100) * width);
|
|
183
|
+
// Build bar without gridlines for progress bars
|
|
184
|
+
let bar = '';
|
|
185
|
+
for (let i = 0; i < width; i++) {
|
|
186
|
+
if (i < filled) {
|
|
187
|
+
const ratio = i / width;
|
|
188
|
+
if (ratio < 0.5) {
|
|
189
|
+
bar += chalk.green('█');
|
|
190
|
+
}
|
|
191
|
+
else if (ratio < 0.75) {
|
|
192
|
+
bar += chalk.yellow('█');
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
bar += chalk.red('█');
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
bar += chalk.gray('░');
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
const percentStr = `${percent.toFixed(0)}%`.padStart(4);
|
|
203
|
+
const line = `\r${chalk.gray('│')}${bar}${chalk.gray('│')} ${percentStr}${label ? ` ${chalk.dim(label)}` : ''}`;
|
|
204
|
+
process.stdout.write(line);
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Print styled progress bar with message
|
|
208
|
+
*/
|
|
209
|
+
export function printStyledProgressBar(percent, width = 35, message = '', showPercent = true) {
|
|
210
|
+
const filled = Math.round((percent / 100) * width);
|
|
211
|
+
// Build bar without gridlines for styled progress bars
|
|
212
|
+
let bar = chalk.gray('╔');
|
|
213
|
+
for (let i = 0; i < width; i++) {
|
|
214
|
+
if (i < filled) {
|
|
215
|
+
const ratio = i / width;
|
|
216
|
+
if (ratio < 0.5) {
|
|
217
|
+
bar += chalk.green('█');
|
|
218
|
+
}
|
|
219
|
+
else if (ratio < 0.75) {
|
|
220
|
+
bar += chalk.yellow('█');
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
bar += chalk.red('█');
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
bar += chalk.gray('░');
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
bar += chalk.gray('╗');
|
|
231
|
+
const percentStr = showPercent ? ` ${percent.toFixed(0).padStart(3)}%` : '';
|
|
232
|
+
return `${bar}${percentStr}${message ? ` ${chalk.dim(message)}` : ''}`;
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Create progress tracker
|
|
236
|
+
*/
|
|
237
|
+
export function createProgress(total, taskMessage = 'Processing...') {
|
|
238
|
+
const spinner = ora(taskMessage).start();
|
|
239
|
+
return {
|
|
240
|
+
update: (current, message) => {
|
|
241
|
+
const percent = Math.round((current / total) * 100);
|
|
242
|
+
const progressBar = createProgressBar(percent);
|
|
243
|
+
spinner.text = `${progressBar} ${percent}%${message ? ` - ${message}` : ''}`;
|
|
244
|
+
},
|
|
245
|
+
finish: (message) => {
|
|
246
|
+
spinner.succeed(message || 'Complete');
|
|
247
|
+
},
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Create ASCII progress bar
|
|
252
|
+
*/
|
|
253
|
+
function createProgressBar(percent, width = 20) {
|
|
254
|
+
const filled = Math.round((percent / 100) * width);
|
|
255
|
+
let bar = '';
|
|
256
|
+
for (let i = 0; i < width; i++) {
|
|
257
|
+
const isGridline = i > 0 && i % (width / 5) === 0;
|
|
258
|
+
if (i < filled) {
|
|
259
|
+
bar += isGridline ? '│' : '█';
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
bar += isGridline ? '┊' : ' ';
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return `[${bar}]`;
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Clear terminal
|
|
269
|
+
*/
|
|
270
|
+
export function clearTerminal() {
|
|
271
|
+
if (process.stdout.isTTY) {
|
|
272
|
+
process.stdout.write('\x1B[2J\x1B[H');
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Print table
|
|
277
|
+
*/
|
|
278
|
+
export function printTable(headers, rows, columnWidths) {
|
|
279
|
+
const widths = columnWidths ||
|
|
280
|
+
headers.map((h, i) => {
|
|
281
|
+
const maxRow = Math.max(...rows.map((r) => (r[i] || '').length));
|
|
282
|
+
return Math.max(h.length, maxRow);
|
|
283
|
+
});
|
|
284
|
+
// Header
|
|
285
|
+
const headerRow = headers.map((h, i) => h.padEnd(widths[i])).join(' │ ');
|
|
286
|
+
console.log(chalk.bold(headerRow));
|
|
287
|
+
// Separator
|
|
288
|
+
const sep = widths.map((w) => '─'.repeat(w)).join('─┼─');
|
|
289
|
+
console.log(chalk.gray(sep));
|
|
290
|
+
// Rows
|
|
291
|
+
rows.forEach((row) => {
|
|
292
|
+
const line = row.map((cell, i) => cell.padEnd(widths[i])).join(' │ ');
|
|
293
|
+
console.log(line);
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Show hide cursor
|
|
298
|
+
*/
|
|
299
|
+
export function hideCursor() {
|
|
300
|
+
process.stdout.write('\x1B[?25l');
|
|
301
|
+
}
|
|
302
|
+
export function showCursor() {
|
|
303
|
+
process.stdout.write('\x1B[?25h');
|
|
304
|
+
}
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactive prompts for Broom CLI
|
|
3
|
+
*/
|
|
4
|
+
import { select, confirm, checkbox, input } from '@inquirer/prompts';
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
import { formatSize } from '../utils/fs.js';
|
|
7
|
+
// Re-export inquirer functions for direct use
|
|
8
|
+
export { select, confirm, checkbox, input };
|
|
9
|
+
/**
|
|
10
|
+
* Select a category to clean
|
|
11
|
+
*/
|
|
12
|
+
export async function selectCategory(results) {
|
|
13
|
+
const choices = results
|
|
14
|
+
.filter((r) => r.items.length > 0)
|
|
15
|
+
.map((r) => ({
|
|
16
|
+
name: `${r.category.name} (${formatSize(r.totalSize)} - ${r.items.length} items)`,
|
|
17
|
+
value: r,
|
|
18
|
+
}));
|
|
19
|
+
if (choices.length === 0) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
return await select({
|
|
24
|
+
message: 'Select a category to clean:',
|
|
25
|
+
choices,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Select multiple categories to clean
|
|
34
|
+
*/
|
|
35
|
+
export async function selectCategories(results) {
|
|
36
|
+
const choices = results
|
|
37
|
+
.filter((r) => r.items.length > 0)
|
|
38
|
+
.map((r) => ({
|
|
39
|
+
name: `${r.category.name} (${formatSize(r.totalSize)} - ${r.items.length} items)`,
|
|
40
|
+
value: r,
|
|
41
|
+
checked: r.category.safetyLevel === 'safe',
|
|
42
|
+
}));
|
|
43
|
+
if (choices.length === 0) {
|
|
44
|
+
return [];
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
return await checkbox({
|
|
48
|
+
message: 'Select categories to clean (space to toggle):',
|
|
49
|
+
choices,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Select files to clean
|
|
58
|
+
*/
|
|
59
|
+
export async function selectFiles(items) {
|
|
60
|
+
if (items.length === 0) {
|
|
61
|
+
return [];
|
|
62
|
+
}
|
|
63
|
+
const choices = items.map((item) => ({
|
|
64
|
+
name: `${item.isDirectory ? '📁' : '📄'} ${item.name} (${formatSize(item.size)}) - ${chalk.dim(item.path)}`,
|
|
65
|
+
value: item,
|
|
66
|
+
checked: true,
|
|
67
|
+
}));
|
|
68
|
+
try {
|
|
69
|
+
return await checkbox({
|
|
70
|
+
message: 'Select files to remove:',
|
|
71
|
+
choices,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return [];
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Select an application
|
|
80
|
+
*/
|
|
81
|
+
export async function selectApp(apps) {
|
|
82
|
+
if (apps.length === 0) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
const choices = apps.map((app) => ({
|
|
86
|
+
name: `${app.name} (${formatSize(app.size)})${app.bundleId ? chalk.dim(` - ${app.bundleId}`) : ''}`,
|
|
87
|
+
value: app,
|
|
88
|
+
}));
|
|
89
|
+
try {
|
|
90
|
+
return await select({
|
|
91
|
+
message: 'Select an application:',
|
|
92
|
+
choices,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Confirm action
|
|
101
|
+
*/
|
|
102
|
+
export async function confirmAction(message, defaultValue = false) {
|
|
103
|
+
try {
|
|
104
|
+
return await confirm({
|
|
105
|
+
message,
|
|
106
|
+
default: defaultValue,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Confirm removal
|
|
115
|
+
*/
|
|
116
|
+
export async function confirmRemoval(itemCount, totalSize) {
|
|
117
|
+
try {
|
|
118
|
+
return await confirm({
|
|
119
|
+
message: `Remove ${itemCount} item(s) (${formatSize(totalSize)})?`,
|
|
120
|
+
default: false,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Prompt for text input
|
|
129
|
+
*/
|
|
130
|
+
export async function promptInput(message, defaultValue) {
|
|
131
|
+
try {
|
|
132
|
+
return await input({
|
|
133
|
+
message,
|
|
134
|
+
default: defaultValue,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Text input prompt (alias for promptInput)
|
|
143
|
+
*/
|
|
144
|
+
export async function inputPrompt(message, defaultValue) {
|
|
145
|
+
return promptInput(message, defaultValue);
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Select from generic items
|
|
149
|
+
*/
|
|
150
|
+
export async function selectItems(message, choices) {
|
|
151
|
+
if (choices.length === 0) {
|
|
152
|
+
return [];
|
|
153
|
+
}
|
|
154
|
+
try {
|
|
155
|
+
return await checkbox({
|
|
156
|
+
message,
|
|
157
|
+
choices: choices.map((c) => ({
|
|
158
|
+
name: c.description ? `${c.name} - ${chalk.dim(c.description)}` : c.name,
|
|
159
|
+
value: c.value,
|
|
160
|
+
})),
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
return [];
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Select a path (directory browser)
|
|
169
|
+
*/
|
|
170
|
+
export async function selectPath(message, choices) {
|
|
171
|
+
if (choices.length === 0) {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
try {
|
|
175
|
+
return await select({
|
|
176
|
+
message,
|
|
177
|
+
choices,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Select main menu action
|
|
186
|
+
*/
|
|
187
|
+
export async function selectMainAction() {
|
|
188
|
+
try {
|
|
189
|
+
return await select({
|
|
190
|
+
message: 'What would you like to do?',
|
|
191
|
+
choices: [
|
|
192
|
+
{ name: '🧹 Clean - Deep system cleanup', value: 'clean' },
|
|
193
|
+
{ name: '🗑️ Uninstall - Remove apps completely', value: 'uninstall' },
|
|
194
|
+
{ name: '🔧 Optimize - Check and maintain system', value: 'optimize' },
|
|
195
|
+
{ name: '📊 Analyze - Explore disk usage', value: 'analyze' },
|
|
196
|
+
{ name: '📈 Status - Monitor system health', value: 'status' },
|
|
197
|
+
{ name: '📦 Purge - Clean project artifacts', value: 'purge' },
|
|
198
|
+
{ name: '💿 Installer - Remove installer files', value: 'installer' },
|
|
199
|
+
{ name: '⚙️ Config - Manage settings', value: 'config' },
|
|
200
|
+
{ name: '❌ Exit', value: 'exit' },
|
|
201
|
+
],
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Select config option
|
|
210
|
+
*/
|
|
211
|
+
export async function selectConfigOption() {
|
|
212
|
+
try {
|
|
213
|
+
return await select({
|
|
214
|
+
message: 'Select a configuration option:',
|
|
215
|
+
choices: [
|
|
216
|
+
{ name: 'Toggle scan locations', value: 'locations' },
|
|
217
|
+
{ name: 'Manage whitelist', value: 'whitelist' },
|
|
218
|
+
{ name: 'Toggle dry run mode', value: 'dryrun' },
|
|
219
|
+
{ name: 'Reset to defaults', value: 'reset' },
|
|
220
|
+
{ name: 'Exit', value: 'exit' },
|
|
221
|
+
],
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Select scan locations to toggle
|
|
230
|
+
*/
|
|
231
|
+
export async function selectScanLocations(currentLocations) {
|
|
232
|
+
const choices = [
|
|
233
|
+
{ name: 'User Cache', value: 'userCache', checked: currentLocations.userCache },
|
|
234
|
+
{ name: 'System Cache', value: 'systemCache', checked: currentLocations.systemCache },
|
|
235
|
+
{ name: 'System Logs', value: 'systemLogs', checked: currentLocations.systemLogs },
|
|
236
|
+
{ name: 'User Logs', value: 'userLogs', checked: currentLocations.userLogs },
|
|
237
|
+
{ name: 'Trash', value: 'trash', checked: currentLocations.trash },
|
|
238
|
+
{ name: 'Downloads', value: 'downloads', checked: currentLocations.downloads },
|
|
239
|
+
{ name: 'Browser Cache', value: 'browserCache', checked: currentLocations.browserCache },
|
|
240
|
+
{ name: 'Development Cache', value: 'devCache', checked: currentLocations.devCache },
|
|
241
|
+
{ name: 'Xcode Cache', value: 'xcodeCache', checked: currentLocations.xcodeCache },
|
|
242
|
+
];
|
|
243
|
+
try {
|
|
244
|
+
return await checkbox({
|
|
245
|
+
message: 'Select locations to scan:',
|
|
246
|
+
choices,
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
catch {
|
|
250
|
+
return [];
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Select cleanup action
|
|
255
|
+
*/
|
|
256
|
+
export async function selectCleanupAction() {
|
|
257
|
+
try {
|
|
258
|
+
return await select({
|
|
259
|
+
message: 'What would you like to do?',
|
|
260
|
+
choices: [
|
|
261
|
+
{ name: 'Remove selected files', value: 'remove' },
|
|
262
|
+
{ name: 'Back to category selection', value: 'back' },
|
|
263
|
+
{ name: 'Cancel', value: 'cancel' },
|
|
264
|
+
],
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
catch {
|
|
268
|
+
return 'cancel';
|
|
269
|
+
}
|
|
270
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration management for Broom CLI
|
|
3
|
+
*/
|
|
4
|
+
import { readFile, writeFile, mkdir } from 'fs/promises';
|
|
5
|
+
import { join } from 'path';
|
|
6
|
+
import { homedir } from 'os';
|
|
7
|
+
import { existsSync } from 'fs';
|
|
8
|
+
import { DEFAULT_CONFIG } from '../types/index.js';
|
|
9
|
+
const CONFIG_DIR = join(homedir(), '.config', 'broom');
|
|
10
|
+
const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
|
|
11
|
+
const WHITELIST_FILE = join(CONFIG_DIR, 'whitelist');
|
|
12
|
+
let configCache = null;
|
|
13
|
+
/**
|
|
14
|
+
* Ensure config directory exists
|
|
15
|
+
*/
|
|
16
|
+
async function ensureConfigDir() {
|
|
17
|
+
if (!existsSync(CONFIG_DIR)) {
|
|
18
|
+
await mkdir(CONFIG_DIR, { recursive: true });
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Load configuration
|
|
23
|
+
*/
|
|
24
|
+
export async function loadConfig() {
|
|
25
|
+
if (configCache) {
|
|
26
|
+
return configCache;
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
await ensureConfigDir();
|
|
30
|
+
if (!existsSync(CONFIG_FILE)) {
|
|
31
|
+
await saveConfig(DEFAULT_CONFIG);
|
|
32
|
+
configCache = DEFAULT_CONFIG;
|
|
33
|
+
return DEFAULT_CONFIG;
|
|
34
|
+
}
|
|
35
|
+
const content = await readFile(CONFIG_FILE, 'utf-8');
|
|
36
|
+
const config = { ...DEFAULT_CONFIG, ...JSON.parse(content) };
|
|
37
|
+
// Load whitelist
|
|
38
|
+
if (existsSync(WHITELIST_FILE)) {
|
|
39
|
+
const whitelistContent = await readFile(WHITELIST_FILE, 'utf-8');
|
|
40
|
+
config.whitelist = whitelistContent
|
|
41
|
+
.split('\n')
|
|
42
|
+
.map((line) => line.trim())
|
|
43
|
+
.filter((line) => line && !line.startsWith('#'));
|
|
44
|
+
}
|
|
45
|
+
configCache = config;
|
|
46
|
+
return config;
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
configCache = DEFAULT_CONFIG;
|
|
50
|
+
return DEFAULT_CONFIG;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Save configuration
|
|
55
|
+
*/
|
|
56
|
+
export async function saveConfig(config) {
|
|
57
|
+
await ensureConfigDir();
|
|
58
|
+
const { whitelist, ...configWithoutWhitelist } = config;
|
|
59
|
+
await writeFile(CONFIG_FILE, JSON.stringify(configWithoutWhitelist, null, 2), 'utf-8');
|
|
60
|
+
// Save whitelist separately
|
|
61
|
+
if (whitelist && whitelist.length > 0) {
|
|
62
|
+
const whitelistContent = `# Broom Whitelist\n# One path per line\n\n${whitelist.join('\n')}`;
|
|
63
|
+
await writeFile(WHITELIST_FILE, whitelistContent, 'utf-8');
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
// Delete whitelist file if empty
|
|
67
|
+
if (existsSync(WHITELIST_FILE)) {
|
|
68
|
+
const { unlink } = await import('fs/promises');
|
|
69
|
+
await unlink(WHITELIST_FILE);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
configCache = config;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Reset configuration to defaults
|
|
76
|
+
*/
|
|
77
|
+
export async function resetConfig() {
|
|
78
|
+
await saveConfig(DEFAULT_CONFIG);
|
|
79
|
+
configCache = DEFAULT_CONFIG;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Get config file path
|
|
83
|
+
*/
|
|
84
|
+
export function getConfigPath() {
|
|
85
|
+
return CONFIG_FILE;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Get whitelist file path
|
|
89
|
+
*/
|
|
90
|
+
export function getWhitelistPath() {
|
|
91
|
+
return WHITELIST_FILE;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Check if path is whitelisted
|
|
95
|
+
*/
|
|
96
|
+
export function isWhitelisted(path, whitelist) {
|
|
97
|
+
const normalizedPath = path.replace(/\/+$/, '');
|
|
98
|
+
for (const pattern of whitelist) {
|
|
99
|
+
const normalizedPattern = pattern.replace(/\/+$/, '').replace('~', homedir());
|
|
100
|
+
if (normalizedPath === normalizedPattern) {
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
if (normalizedPath.startsWith(normalizedPattern + '/')) {
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Add path to whitelist
|
|
111
|
+
*/
|
|
112
|
+
export async function addToWhitelist(path) {
|
|
113
|
+
const config = await loadConfig();
|
|
114
|
+
if (!config.whitelist.includes(path)) {
|
|
115
|
+
config.whitelist.push(path);
|
|
116
|
+
await saveConfig(config);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Remove path from whitelist
|
|
121
|
+
*/
|
|
122
|
+
export async function removeFromWhitelist(path) {
|
|
123
|
+
const config = await loadConfig();
|
|
124
|
+
config.whitelist = config.whitelist.filter((p) => p !== path);
|
|
125
|
+
await saveConfig(config);
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Clear config cache
|
|
129
|
+
*/
|
|
130
|
+
export function clearConfigCache() {
|
|
131
|
+
configCache = null;
|
|
132
|
+
}
|
|
133
|
+
export { CONFIG_DIR, CONFIG_FILE, WHITELIST_FILE };
|