@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,266 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Installer command - Find and remove installer files
|
|
3
|
+
*/
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import { Command } from 'commander';
|
|
6
|
+
import { readdir, stat, rm } from 'fs/promises';
|
|
7
|
+
import { join, basename, extname } from 'path';
|
|
8
|
+
import fg from 'fast-glob';
|
|
9
|
+
import { enhanceCommandHelp } from '../utils/help.js';
|
|
10
|
+
import { exists, getSize, formatSize, expandPath } from '../utils/fs.js';
|
|
11
|
+
import { printHeader, success, warning, error, info, separator, createSpinner, succeedSpinner, failSpinner, } from '../ui/output.js';
|
|
12
|
+
import { confirmAction, selectFiles } from '../ui/prompts.js';
|
|
13
|
+
/**
|
|
14
|
+
* Installer file extensions
|
|
15
|
+
*/
|
|
16
|
+
const INSTALLER_EXTENSIONS = ['.dmg', '.pkg', '.zip', '.app', '.tar.gz', '.tgz'];
|
|
17
|
+
/**
|
|
18
|
+
* Directories to search for installers
|
|
19
|
+
*/
|
|
20
|
+
const SEARCH_DIRS = [
|
|
21
|
+
'~/Downloads',
|
|
22
|
+
'~/Desktop',
|
|
23
|
+
'~/Library/Caches/Homebrew/downloads',
|
|
24
|
+
'~/Library/Mobile Documents/com~apple~CloudDocs/Downloads',
|
|
25
|
+
'~/Library/Application Support/Steam/steamapps',
|
|
26
|
+
];
|
|
27
|
+
/**
|
|
28
|
+
* Get source label from path
|
|
29
|
+
*/
|
|
30
|
+
function getSourceLabel(filePath) {
|
|
31
|
+
const home = expandPath('~');
|
|
32
|
+
const path = filePath.toLowerCase();
|
|
33
|
+
if (path.includes('/downloads')) {
|
|
34
|
+
return 'Downloads';
|
|
35
|
+
}
|
|
36
|
+
if (path.includes('/desktop')) {
|
|
37
|
+
return 'Desktop';
|
|
38
|
+
}
|
|
39
|
+
if (path.includes('/homebrew')) {
|
|
40
|
+
return 'Homebrew';
|
|
41
|
+
}
|
|
42
|
+
if (path.includes('/clouddocs') || path.includes('/icloud')) {
|
|
43
|
+
return 'iCloud';
|
|
44
|
+
}
|
|
45
|
+
if (path.includes('/steam')) {
|
|
46
|
+
return 'Steam';
|
|
47
|
+
}
|
|
48
|
+
if (path.includes('/mail')) {
|
|
49
|
+
return 'Mail';
|
|
50
|
+
}
|
|
51
|
+
return 'Other';
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Check if file is an installer
|
|
55
|
+
*/
|
|
56
|
+
function isInstaller(filename) {
|
|
57
|
+
const ext = extname(filename).toLowerCase();
|
|
58
|
+
const name = filename.toLowerCase();
|
|
59
|
+
// Check extension
|
|
60
|
+
if (INSTALLER_EXTENSIONS.includes(ext)) {
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
// Check for .tar.gz
|
|
64
|
+
if (name.endsWith('.tar.gz') || name.endsWith('.tgz')) {
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Scan for installer files
|
|
71
|
+
*/
|
|
72
|
+
async function scanInstallers() {
|
|
73
|
+
const installers = [];
|
|
74
|
+
for (const searchDir of SEARCH_DIRS) {
|
|
75
|
+
const dir = expandPath(searchDir);
|
|
76
|
+
if (!exists(dir)) {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
const entries = await readdir(dir);
|
|
81
|
+
for (const entry of entries) {
|
|
82
|
+
if (!isInstaller(entry)) {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
const filePath = join(dir, entry);
|
|
86
|
+
try {
|
|
87
|
+
const stats = await stat(filePath);
|
|
88
|
+
const size = stats.isDirectory() ? await getSize(filePath) : stats.size;
|
|
89
|
+
// Only include files larger than 10MB
|
|
90
|
+
if (size < 10 * 1024 * 1024) {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
installers.push({
|
|
94
|
+
path: filePath,
|
|
95
|
+
name: entry,
|
|
96
|
+
size,
|
|
97
|
+
isDirectory: stats.isDirectory(),
|
|
98
|
+
modifiedAt: stats.mtime,
|
|
99
|
+
source: getSourceLabel(filePath),
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
// Skip if cannot access
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
// Skip if cannot access directory
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// Also search recursively in Downloads for nested installers
|
|
112
|
+
const downloadsDir = expandPath('~/Downloads');
|
|
113
|
+
if (exists(downloadsDir)) {
|
|
114
|
+
try {
|
|
115
|
+
const patterns = INSTALLER_EXTENSIONS.map((ext) => `**/*${ext}`);
|
|
116
|
+
const matches = await fg(patterns, {
|
|
117
|
+
cwd: downloadsDir,
|
|
118
|
+
absolute: true,
|
|
119
|
+
deep: 3, // Max depth
|
|
120
|
+
ignore: ['**/node_modules/**'],
|
|
121
|
+
});
|
|
122
|
+
for (const match of matches) {
|
|
123
|
+
// Skip if already added
|
|
124
|
+
if (installers.some((i) => i.path === match)) {
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
try {
|
|
128
|
+
const stats = await stat(match);
|
|
129
|
+
const size = stats.isDirectory() ? await getSize(match) : stats.size;
|
|
130
|
+
if (size < 10 * 1024 * 1024) {
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
installers.push({
|
|
134
|
+
path: match,
|
|
135
|
+
name: basename(match),
|
|
136
|
+
size,
|
|
137
|
+
isDirectory: stats.isDirectory(),
|
|
138
|
+
modifiedAt: stats.mtime,
|
|
139
|
+
source: 'Downloads',
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
// Skip
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
// Skip
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
// Sort by size descending
|
|
152
|
+
installers.sort((a, b) => b.size - a.size);
|
|
153
|
+
return installers;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Execute installer command
|
|
157
|
+
*/
|
|
158
|
+
export async function installerCommand(options) {
|
|
159
|
+
const isDryRun = options.dryRun || false;
|
|
160
|
+
printHeader(isDryRun ? '💿 Installer Files (Dry Run)' : '💿 Installer Files');
|
|
161
|
+
// Scan for installers
|
|
162
|
+
const spinner = createSpinner('Scanning for installer files...');
|
|
163
|
+
const installers = await scanInstallers();
|
|
164
|
+
succeedSpinner(spinner, `Found ${installers.length} installer files`);
|
|
165
|
+
if (installers.length === 0) {
|
|
166
|
+
console.log();
|
|
167
|
+
success('No installer files found!');
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
// Calculate total size
|
|
171
|
+
const totalSize = installers.reduce((sum, i) => sum + i.size, 0);
|
|
172
|
+
// Show found installers
|
|
173
|
+
console.log();
|
|
174
|
+
console.log(chalk.bold(`Found ${installers.length} installer files (${formatSize(totalSize)}):`));
|
|
175
|
+
console.log();
|
|
176
|
+
for (const installer of installers.slice(0, 15)) {
|
|
177
|
+
const sizeStr = formatSize(installer.size).padStart(10);
|
|
178
|
+
const sourceStr = chalk.dim(`| ${installer.source}`);
|
|
179
|
+
console.log(` ${chalk.cyan('●')} ${installer.name.slice(0, 30).padEnd(32)} ${chalk.yellow(sizeStr)} ${sourceStr}`);
|
|
180
|
+
}
|
|
181
|
+
if (installers.length > 15) {
|
|
182
|
+
console.log(chalk.dim(` ... and ${installers.length - 15} more`));
|
|
183
|
+
}
|
|
184
|
+
// Select files to remove
|
|
185
|
+
console.log();
|
|
186
|
+
let selectedInstallers;
|
|
187
|
+
if (options.yes) {
|
|
188
|
+
selectedInstallers = installers;
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
info('Select installers to remove:');
|
|
192
|
+
selectedInstallers = (await selectFiles(installers));
|
|
193
|
+
}
|
|
194
|
+
if (selectedInstallers.length === 0) {
|
|
195
|
+
warning('No installers selected');
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
const selectedSize = selectedInstallers.reduce((sum, i) => sum + i.size, 0);
|
|
199
|
+
// Confirm
|
|
200
|
+
if (!options.yes) {
|
|
201
|
+
console.log();
|
|
202
|
+
const confirmed = await confirmAction(`${isDryRun ? 'Simulate removing' : 'Remove'} ${selectedInstallers.length} installers (${formatSize(selectedSize)})?`, false);
|
|
203
|
+
if (!confirmed) {
|
|
204
|
+
warning('Operation cancelled');
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
// Execute removal
|
|
209
|
+
console.log();
|
|
210
|
+
const removeSpinner = createSpinner(isDryRun ? 'Simulating removal...' : 'Removing installers...');
|
|
211
|
+
let removedCount = 0;
|
|
212
|
+
let removedSize = 0;
|
|
213
|
+
const errors = [];
|
|
214
|
+
for (const installer of selectedInstallers) {
|
|
215
|
+
if (!isDryRun) {
|
|
216
|
+
try {
|
|
217
|
+
await rm(installer.path, { recursive: true, force: true });
|
|
218
|
+
removedCount++;
|
|
219
|
+
removedSize += installer.size;
|
|
220
|
+
}
|
|
221
|
+
catch (err) {
|
|
222
|
+
errors.push(`${installer.name}: ${err.message}`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
removedCount++;
|
|
227
|
+
removedSize += installer.size;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
if (errors.length === 0) {
|
|
231
|
+
succeedSpinner(removeSpinner, isDryRun ? 'Simulation complete' : 'Removal complete');
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
failSpinner(removeSpinner, 'Completed with errors');
|
|
235
|
+
}
|
|
236
|
+
// Summary
|
|
237
|
+
console.log();
|
|
238
|
+
separator('═');
|
|
239
|
+
console.log(chalk.bold(isDryRun ? 'Dry Run Complete' : 'Removal Complete'));
|
|
240
|
+
separator('─');
|
|
241
|
+
console.log(` Installers ${isDryRun ? 'would be ' : ''}removed: ${chalk.green(removedCount)}`);
|
|
242
|
+
console.log(` Space ${isDryRun ? 'would be ' : ''}freed: ${chalk.green(formatSize(removedSize))}`);
|
|
243
|
+
separator('═');
|
|
244
|
+
if (errors.length > 0) {
|
|
245
|
+
console.log();
|
|
246
|
+
console.log(chalk.bold.red('Errors:'));
|
|
247
|
+
errors.forEach((err) => error(err));
|
|
248
|
+
}
|
|
249
|
+
if (isDryRun) {
|
|
250
|
+
console.log();
|
|
251
|
+
info('This was a dry run. No files were deleted.');
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Create installer command
|
|
256
|
+
*/
|
|
257
|
+
export function createInstallerCommand() {
|
|
258
|
+
const cmd = new Command('installer')
|
|
259
|
+
.description('Find and remove installer files (dmg, pkg, zip)')
|
|
260
|
+
.option('-n, --dry-run', 'Preview only, no deletions')
|
|
261
|
+
.option('-y, --yes', 'Skip confirmation prompts')
|
|
262
|
+
.action(async (options) => {
|
|
263
|
+
await installerCommand(options);
|
|
264
|
+
});
|
|
265
|
+
return enhanceCommandHelp(cmd);
|
|
266
|
+
}
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Optimize command - System maintenance and optimization
|
|
3
|
+
*/
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import { Command } from 'commander';
|
|
6
|
+
import { exec } from 'child_process';
|
|
7
|
+
import { promisify } from 'util';
|
|
8
|
+
import { enhanceCommandHelp } from '../utils/help.js';
|
|
9
|
+
import { printHeader, warning, info, separator, createSpinner, succeedSpinner, failSpinner, } from '../ui/output.js';
|
|
10
|
+
import { confirmAction, selectItems } from '../ui/prompts.js';
|
|
11
|
+
import { debug, debugSection, debugObj } from '../utils/debug.js';
|
|
12
|
+
const execAsync = promisify(exec);
|
|
13
|
+
/**
|
|
14
|
+
* Available optimization tasks
|
|
15
|
+
*/
|
|
16
|
+
const tasks = [
|
|
17
|
+
{
|
|
18
|
+
id: 'flush-dns',
|
|
19
|
+
name: 'Flush DNS Cache',
|
|
20
|
+
description: 'Clear DNS resolver cache',
|
|
21
|
+
requiresSudo: true,
|
|
22
|
+
action: async () => {
|
|
23
|
+
await execAsync('sudo dscacheutil -flushcache');
|
|
24
|
+
await execAsync('sudo killall -HUP mDNSResponder');
|
|
25
|
+
return 'DNS cache flushed';
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
id: 'rebuild-spotlight',
|
|
30
|
+
name: 'Rebuild Spotlight Index',
|
|
31
|
+
description: 'Rebuild the Spotlight search index (may take a while)',
|
|
32
|
+
requiresSudo: true,
|
|
33
|
+
action: async () => {
|
|
34
|
+
await execAsync('sudo mdutil -E /');
|
|
35
|
+
return 'Spotlight reindexing started';
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
id: 'rebuild-launch-services',
|
|
40
|
+
name: 'Rebuild Launch Services',
|
|
41
|
+
description: 'Rebuild the Launch Services database',
|
|
42
|
+
requiresSudo: false,
|
|
43
|
+
action: async () => {
|
|
44
|
+
await execAsync('/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister -kill -r -domain local -domain system -domain user');
|
|
45
|
+
return 'Launch Services database rebuilt';
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
id: 'purge-memory',
|
|
50
|
+
name: 'Purge Memory',
|
|
51
|
+
description: 'Release inactive memory',
|
|
52
|
+
requiresSudo: true,
|
|
53
|
+
action: async () => {
|
|
54
|
+
await execAsync('sudo purge');
|
|
55
|
+
return 'Memory purged';
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
id: 'verify-disk',
|
|
60
|
+
name: 'Verify Disk',
|
|
61
|
+
description: 'Verify the boot disk',
|
|
62
|
+
requiresSudo: false,
|
|
63
|
+
action: async () => {
|
|
64
|
+
const { stdout } = await execAsync('diskutil verifyVolume /');
|
|
65
|
+
return stdout.trim() || 'Disk verification completed';
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
id: 'clear-font-cache',
|
|
70
|
+
name: 'Clear Font Cache',
|
|
71
|
+
description: 'Clear system font caches',
|
|
72
|
+
requiresSudo: true,
|
|
73
|
+
action: async () => {
|
|
74
|
+
await execAsync('sudo atsutil databases -remove');
|
|
75
|
+
return 'Font caches cleared (restart may be required)';
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
id: 'rebuild-mail-index',
|
|
80
|
+
name: 'Rebuild Mail Index',
|
|
81
|
+
description: 'Rebuild Mail.app envelope index',
|
|
82
|
+
requiresSudo: false,
|
|
83
|
+
action: async () => {
|
|
84
|
+
const mailEnvelope = '~/Library/Mail/V*/MailData/Envelope Index';
|
|
85
|
+
await execAsync(`rm -rf ${mailEnvelope} 2>/dev/null || true`);
|
|
86
|
+
return 'Mail index will be rebuilt on next launch';
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
id: 'clear-quicklook',
|
|
91
|
+
name: 'Clear QuickLook Cache',
|
|
92
|
+
description: 'Reset QuickLook server',
|
|
93
|
+
requiresSudo: false,
|
|
94
|
+
action: async () => {
|
|
95
|
+
await execAsync('qlmanage -r cache');
|
|
96
|
+
await execAsync('qlmanage -r');
|
|
97
|
+
return 'QuickLook cache cleared';
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
id: 'reset-bluetooth',
|
|
102
|
+
name: 'Reset Bluetooth Module',
|
|
103
|
+
description: 'Reset the Bluetooth controller',
|
|
104
|
+
requiresSudo: true,
|
|
105
|
+
action: async () => {
|
|
106
|
+
await execAsync('sudo pkill -9 bluetoothd || true');
|
|
107
|
+
return 'Bluetooth module reset';
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
id: 'flush-network',
|
|
112
|
+
name: 'Flush Network Settings',
|
|
113
|
+
description: 'Reset network interfaces',
|
|
114
|
+
requiresSudo: true,
|
|
115
|
+
action: async () => {
|
|
116
|
+
await execAsync('sudo ifconfig en0 down && sudo ifconfig en0 up');
|
|
117
|
+
return 'Network interface reset';
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
id: 'clear-asl-logs',
|
|
122
|
+
name: 'Clear ASL Logs',
|
|
123
|
+
description: 'Clear Apple System Logs',
|
|
124
|
+
requiresSudo: true,
|
|
125
|
+
action: async () => {
|
|
126
|
+
await execAsync('sudo rm -rf /private/var/log/asl/*.asl');
|
|
127
|
+
return 'ASL logs cleared';
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
id: 'enable-trim',
|
|
132
|
+
name: 'Enable TRIM (SSD)',
|
|
133
|
+
description: 'Enable TRIM for non-Apple SSDs',
|
|
134
|
+
requiresSudo: true,
|
|
135
|
+
action: async () => {
|
|
136
|
+
await execAsync('sudo trimforce enable');
|
|
137
|
+
return 'TRIM enabled';
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
];
|
|
141
|
+
/**
|
|
142
|
+
* Execute optimize command
|
|
143
|
+
*/
|
|
144
|
+
export async function optimizeCommand(options) {
|
|
145
|
+
const isDryRun = options.dryRun || false;
|
|
146
|
+
debugSection('Optimize Command');
|
|
147
|
+
debugObj('Options', options);
|
|
148
|
+
debug(`Available tasks: ${tasks.length}`);
|
|
149
|
+
printHeader(isDryRun ? '⚡ System Optimization (Dry Run)' : '⚡ System Optimization');
|
|
150
|
+
// Show available tasks
|
|
151
|
+
console.log(chalk.bold('Available optimization tasks:'));
|
|
152
|
+
console.log();
|
|
153
|
+
tasks.forEach((task, index) => {
|
|
154
|
+
const sudo = task.requiresSudo ? chalk.yellow('[sudo]') : '';
|
|
155
|
+
console.log(` ${chalk.cyan(`${index + 1}.`)} ${chalk.bold(task.name)} ${sudo}`);
|
|
156
|
+
console.log(` ${chalk.dim(task.description)}`);
|
|
157
|
+
});
|
|
158
|
+
console.log();
|
|
159
|
+
// Select tasks
|
|
160
|
+
let selectedTasks;
|
|
161
|
+
if (options.all) {
|
|
162
|
+
selectedTasks = tasks;
|
|
163
|
+
info(`Running all ${tasks.length} optimization tasks`);
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
const taskChoices = tasks.map((task) => ({
|
|
167
|
+
name: `${task.name}${task.requiresSudo ? ' [sudo]' : ''}`,
|
|
168
|
+
value: task.id,
|
|
169
|
+
description: task.description,
|
|
170
|
+
}));
|
|
171
|
+
const selectedIds = await selectItems('Select optimization tasks to run:', taskChoices);
|
|
172
|
+
if (selectedIds.length === 0) {
|
|
173
|
+
warning('No tasks selected');
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
selectedTasks = tasks.filter((t) => selectedIds.includes(t.id));
|
|
177
|
+
}
|
|
178
|
+
// Check for sudo tasks
|
|
179
|
+
const sudoTasks = selectedTasks.filter((t) => t.requiresSudo);
|
|
180
|
+
if (sudoTasks.length > 0 && !isDryRun) {
|
|
181
|
+
console.log();
|
|
182
|
+
warning(`${sudoTasks.length} task(s) require administrator privileges`);
|
|
183
|
+
if (!options.yes) {
|
|
184
|
+
const confirmed = await confirmAction('Continue with sudo tasks?', true);
|
|
185
|
+
if (!confirmed) {
|
|
186
|
+
// Filter out sudo tasks
|
|
187
|
+
selectedTasks = selectedTasks.filter((t) => !t.requiresSudo);
|
|
188
|
+
if (selectedTasks.length === 0) {
|
|
189
|
+
warning('No tasks remaining');
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
// Confirm execution
|
|
196
|
+
if (!options.yes && !isDryRun) {
|
|
197
|
+
console.log();
|
|
198
|
+
const confirmed = await confirmAction(`Run ${selectedTasks.length} optimization task(s)?`, true);
|
|
199
|
+
if (!confirmed) {
|
|
200
|
+
warning('Optimization cancelled');
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
// Execute tasks
|
|
205
|
+
console.log();
|
|
206
|
+
separator();
|
|
207
|
+
let successCount = 0;
|
|
208
|
+
let failCount = 0;
|
|
209
|
+
const results = [];
|
|
210
|
+
for (const task of selectedTasks) {
|
|
211
|
+
debug(`Executing task: ${task.id} - ${task.name}`);
|
|
212
|
+
debug(`Requires sudo: ${task.requiresSudo}`);
|
|
213
|
+
const spinner = createSpinner(`${task.name}...`);
|
|
214
|
+
if (isDryRun) {
|
|
215
|
+
debug(`Task ${task.id} skipped (dry run)`);
|
|
216
|
+
succeedSpinner(spinner, `${task.name} ${chalk.dim('(dry run)')}`);
|
|
217
|
+
results.push({ task, success: true, message: 'Dry run - skipped' });
|
|
218
|
+
successCount++;
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
try {
|
|
222
|
+
const message = await task.action();
|
|
223
|
+
debug(`Task ${task.id} completed: ${message}`);
|
|
224
|
+
succeedSpinner(spinner, `${task.name}: ${chalk.green(message)}`);
|
|
225
|
+
results.push({ task, success: true, message });
|
|
226
|
+
successCount++;
|
|
227
|
+
}
|
|
228
|
+
catch (err) {
|
|
229
|
+
const errorMsg = err.message || 'Unknown error';
|
|
230
|
+
debug(`Task ${task.id} failed: ${errorMsg}`);
|
|
231
|
+
failSpinner(spinner, `${task.name}: ${chalk.red(errorMsg)}`);
|
|
232
|
+
results.push({ task, success: false, message: errorMsg });
|
|
233
|
+
failCount++;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
// Print summary
|
|
237
|
+
console.log();
|
|
238
|
+
separator();
|
|
239
|
+
console.log();
|
|
240
|
+
console.log(chalk.bold('Optimization Summary'));
|
|
241
|
+
console.log();
|
|
242
|
+
console.log(` ${chalk.green('✓')} Successful: ${chalk.green(successCount)}`);
|
|
243
|
+
if (failCount > 0) {
|
|
244
|
+
console.log(` ${chalk.red('✗')} Failed: ${chalk.red(failCount)}`);
|
|
245
|
+
}
|
|
246
|
+
if (isDryRun) {
|
|
247
|
+
console.log();
|
|
248
|
+
info('This was a dry run. No changes were made.');
|
|
249
|
+
}
|
|
250
|
+
// Show recommendations
|
|
251
|
+
console.log();
|
|
252
|
+
console.log(chalk.bold('💡 Recommendations:'));
|
|
253
|
+
console.log(chalk.dim(' - Run optimization monthly for best performance'));
|
|
254
|
+
console.log(chalk.dim(' - Some changes may require a restart to take effect'));
|
|
255
|
+
console.log(chalk.dim(' - Use "broom clean" to free up disk space'));
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Create optimize command
|
|
259
|
+
*/
|
|
260
|
+
export function createOptimizeCommand() {
|
|
261
|
+
const cmd = new Command('optimize')
|
|
262
|
+
.description('System maintenance and optimization')
|
|
263
|
+
.option('-n, --dry-run', 'Preview only, no changes')
|
|
264
|
+
.option('-y, --yes', 'Skip confirmation prompts')
|
|
265
|
+
.option('-a, --all', 'Run all optimization tasks')
|
|
266
|
+
.action(async (options) => {
|
|
267
|
+
await optimizeCommand(options);
|
|
268
|
+
});
|
|
269
|
+
return enhanceCommandHelp(cmd);
|
|
270
|
+
}
|