@tukuyomil032/broom 1.0.0 → 1.0.1

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.
@@ -225,50 +225,46 @@ async function interactiveConfigEdit() {
225
225
  console.log(chalk.bold('⚙️ Configuration Editor'));
226
226
  console.log(chalk.dim('Select an option to configure:'));
227
227
  console.log();
228
- console.log(` ${chalk.cyan('1')} Monitor Preset (Current: ${config.monitorPreset})`);
229
- console.log(` ${chalk.cyan('2')} Safety Level (Current: ${config.safetyLevel})`);
230
- console.log(` ${chalk.cyan('3')} Auto Confirm (Current: ${config.autoConfirm ? 'Yes' : 'No'})`);
231
- console.log(` ${chalk.cyan('4')} Dry Run (Current: ${config.dryRun ? 'Yes' : 'No'})`);
232
- console.log(` ${chalk.cyan('5')} Verbose (Current: ${config.verbose ? 'Yes' : 'No'})`);
233
- console.log(` ${chalk.cyan('6')} Manage Whitelist (${config.whitelist.length} items)`);
234
- console.log(` ${chalk.cyan('7')} Manage Blacklist (${config.blacklist.length} items)`);
235
- console.log(` ${chalk.cyan('8')} View All Settings`);
236
- console.log(` ${chalk.cyan('9')} Reset to Defaults`);
228
+ console.log(` ${chalk.cyan('1')} Safety Level (Current: ${config.safetyLevel})`);
229
+ console.log(` ${chalk.cyan('2')} Auto Confirm (Current: ${config.autoConfirm ? 'Yes' : 'No'})`);
230
+ console.log(` ${chalk.cyan('3')} Dry Run (Current: ${config.dryRun ? 'Yes' : 'No'})`);
231
+ console.log(` ${chalk.cyan('4')} Verbose (Current: ${config.verbose ? 'Yes' : 'No'})`);
232
+ console.log(` ${chalk.cyan('5')} Manage Whitelist (${config.whitelist.length} items)`);
233
+ console.log(` ${chalk.cyan('6')} Manage Blacklist (${config.blacklist.length} items)`);
234
+ console.log(` ${chalk.cyan('7')} View All Settings`);
235
+ console.log(` ${chalk.cyan('8')} Reset to Defaults`);
237
236
  console.log(` ${chalk.cyan('0')} Exit`);
238
237
  console.log();
239
- const choice = await inputPrompt('Choose an option (0-9):');
238
+ const choice = await inputPrompt('Choose an option (0-8):');
240
239
  switch (choice) {
241
240
  case '1':
242
- await configureMonitorPreset(config);
243
- break;
244
- case '2':
245
241
  await configureSafetyLevel(config);
246
242
  break;
247
- case '3':
243
+ case '2':
248
244
  config.autoConfirm = !config.autoConfirm;
249
245
  await saveConfig(config);
250
246
  success(`Auto Confirm set to: ${config.autoConfirm ? 'Yes' : 'No'}`);
251
247
  break;
252
- case '4':
248
+ case '3':
253
249
  config.dryRun = !config.dryRun;
254
250
  await saveConfig(config);
255
251
  success(`Dry Run set to: ${config.dryRun ? 'Yes' : 'No'}`);
256
252
  break;
257
- case '5':
253
+ case '4':
258
254
  config.verbose = !config.verbose;
259
255
  await saveConfig(config);
260
256
  success(`Verbose set to: ${config.verbose ? 'Yes' : 'No'}`);
261
257
  break;
262
- case '6':
258
+ case '5':
263
259
  await manageWhitelist(config);
264
260
  break;
265
- case '7':
261
+ case '6':
266
262
  await manageBlacklist(config);
267
263
  break;
268
- case '8':
264
+ case '7':
269
265
  await listConfig();
270
266
  break;
271
- case '9':
267
+ case '8':
272
268
  const confirmed = await confirmAction('Reset configuration to defaults?', false);
273
269
  if (confirmed) {
274
270
  await saveConfig({ ...DEFAULT_CONFIG });
@@ -288,32 +284,6 @@ async function interactiveConfigEdit() {
288
284
  }
289
285
  }
290
286
  }
291
- /**
292
- * Configure monitor preset
293
- */
294
- async function configureMonitorPreset(config) {
295
- console.log();
296
- console.log(chalk.bold('Monitor Presets:'));
297
- console.log(` ${chalk.cyan('1')} Classic Grid Layout (CPU, Memory, Disk, Network in grid)`);
298
- console.log(` ${chalk.cyan('2')} Minimal Compact (Simple single-panel layout)`);
299
- console.log(` ${chalk.cyan('3')} Detailed Information (Comprehensive system info)`);
300
- console.log(` ${chalk.cyan('4')} Linux-style Dashboard (Like htop/top)`);
301
- console.log(` ${chalk.cyan('5')} Modern Colorful Dashboard (Color-rich modern design)`);
302
- console.log();
303
- const choice = await inputPrompt('Select preset (1-5):');
304
- if (!choice) {
305
- return;
306
- }
307
- const preset = parseInt(choice);
308
- if (preset >= 1 && preset <= 5) {
309
- config.monitorPreset = preset;
310
- await saveConfig(config);
311
- success(`Monitor preset set to: ${preset}`);
312
- }
313
- else {
314
- error('Invalid preset selection');
315
- }
316
- }
317
287
  /**
318
288
  * Configure safety level
319
289
  */
@@ -211,42 +211,61 @@ export async function uninstallCommand(options) {
211
211
  warning('No applications found');
212
212
  return;
213
213
  }
214
- // Select app
214
+ // Select apps (multiple selection via space)
215
215
  console.log();
216
- const selectedApp = await selectApp(apps);
217
- if (!selectedApp) {
216
+ const selectedApps = await selectApp(apps);
217
+ if (!selectedApps || selectedApps.length === 0) {
218
218
  warning('No application selected');
219
219
  return;
220
220
  }
221
- // Find related files
221
+ // Find related files for all selected apps
222
222
  console.log();
223
- const searchSpinner = createSpinner(`Searching for ${selectedApp.name} related files...`);
224
- const relatedFiles = await findAppFiles(selectedApp);
223
+ const searchSpinner = createSpinner(`Searching for related files...`);
224
+ const relatedFilesAll = new Map();
225
+ for (const app of selectedApps) {
226
+ const files = await findAppFiles(app);
227
+ for (const f of files) {
228
+ relatedFilesAll.set(f.path, f);
229
+ }
230
+ }
231
+ const relatedFiles = Array.from(relatedFilesAll.values());
225
232
  succeedSpinner(searchSpinner, `Found ${relatedFiles.length} related files`);
226
- // Show app info
233
+ // Show selected apps info
227
234
  console.log();
228
- console.log(chalk.bold(`Application: ${selectedApp.name}`));
229
- console.log(` Path: ${chalk.dim(selectedApp.path)}`);
230
- console.log(` Size: ${chalk.yellow(formatSize(selectedApp.size))}`);
231
- if (selectedApp.bundleId) {
232
- console.log(` Bundle ID: ${chalk.dim(selectedApp.bundleId)}`);
235
+ console.log(chalk.bold('Selected Applications:'));
236
+ let totalAppSize = 0;
237
+ for (const app of selectedApps) {
238
+ totalAppSize += app.size;
239
+ console.log(` - ${chalk.bold(app.name)} ${chalk.dim(app.path)} ${chalk.yellow(formatSize(app.size))}`);
240
+ if (app.bundleId) {
241
+ console.log(` Bundle ID: ${chalk.dim(app.bundleId)}`);
242
+ }
233
243
  }
244
+ // Ensure one blank line between Selected Applications and Related Files
245
+ console.log();
234
246
  // Show related files
235
247
  if (relatedFiles.length > 0) {
236
248
  const totalRelatedSize = relatedFiles.reduce((sum, f) => sum + f.size, 0);
237
- console.log();
238
249
  console.log(chalk.bold(`Related Files (${formatSize(totalRelatedSize)}):`));
239
- for (const file of relatedFiles.slice(0, 10)) {
240
- console.log(` ${file.isDirectory ? '📁' : '📄'} ${chalk.dim(file.path)} ${chalk.yellow(formatSize(file.size))}`);
250
+ // Determine column widths for neat alignment
251
+ const filesToShow = relatedFiles.slice(0, 10);
252
+ const maxPath = Math.min(80, Math.max(...filesToShow.map((f) => f.path.length), 20));
253
+ const sizeWidth = 12;
254
+ for (const file of filesToShow) {
255
+ const icon = file.isDirectory ? '📁' : '📄';
256
+ const pathDisp = chalk.dim(file.path.padEnd(maxPath));
257
+ const sizeDisp = chalk.yellow(formatSize(file.size).padStart(sizeWidth));
258
+ console.log(` ${icon} ${pathDisp} ${sizeDisp}`);
241
259
  }
242
- if (relatedFiles.length > 10) {
243
- console.log(chalk.dim(` ... and ${relatedFiles.length - 10} more`));
260
+ if (relatedFiles.length > filesToShow.length) {
261
+ console.log(chalk.dim(` ... and ${relatedFiles.length - filesToShow.length} more`));
244
262
  }
245
263
  }
246
264
  // Confirm uninstall
247
265
  console.log();
248
266
  if (!options.yes) {
249
- const confirmed = await confirmAction(`Uninstall ${selectedApp.name} and remove related files?`, false);
267
+ const names = selectedApps.map((a) => a.name).join(', ');
268
+ const confirmed = await confirmAction(`Uninstall ${selectedApps.length} app(s): ${names} and remove related files?`, false);
250
269
  if (!confirmed) {
251
270
  warning('Uninstall cancelled');
252
271
  return;
@@ -267,13 +286,17 @@ export async function uninstallCommand(options) {
267
286
  let freedSpace = 0;
268
287
  const errors = [];
269
288
  if (!isDryRun) {
270
- // Remove app
271
- appRemoved = await removeAppToTrash(selectedApp.path);
272
- if (appRemoved) {
273
- freedSpace += selectedApp.size;
274
- }
275
- else {
276
- errors.push(`Failed to remove ${selectedApp.name}`);
289
+ // Remove each selected app
290
+ let appsRemovedCount = 0;
291
+ for (const app of selectedApps) {
292
+ const removed = await removeAppToTrash(app.path);
293
+ if (removed) {
294
+ appsRemovedCount++;
295
+ freedSpace += app.size;
296
+ }
297
+ else {
298
+ errors.push(`Failed to remove ${app.name}`);
299
+ }
277
300
  }
278
301
  // Remove related files
279
302
  for (const file of filesToRemove) {
@@ -293,8 +316,8 @@ export async function uninstallCommand(options) {
293
316
  }
294
317
  else {
295
318
  // Dry run - just count
296
- appRemoved = true;
297
- freedSpace = selectedApp.size;
319
+ const appsRemovedCount = selectedApps.length;
320
+ freedSpace = selectedApps.reduce((s, a) => s + a.size, 0);
298
321
  filesRemoved = filesToRemove.length;
299
322
  freedSpace += filesToRemove.reduce((sum, f) => sum + f.size, 0);
300
323
  }
@@ -307,7 +330,7 @@ export async function uninstallCommand(options) {
307
330
  // Print summary
308
331
  const summaryHeading = isDryRun ? 'Dry Run Complete' : 'Uninstall Complete';
309
332
  const summaryDetails = [
310
- `Application: ${appRemoved ? chalk.green('Removed') : chalk.red('Failed')}`,
333
+ `Applications removed: ${chalk.green(String(selectedApps.length - errors.filter((e) => e.startsWith('Failed to remove')).length))}`,
311
334
  `Related files removed: ${filesRemoved}`,
312
335
  `Space freed: ${chalk.green(formatSize(freedSpace))}`,
313
336
  ];
package/dist/index.js CHANGED
@@ -10,7 +10,7 @@ import chalk from 'chalk';
10
10
  import { createCleanCommand, createUninstallCommand, createOptimizeCommand, createAnalyzeCommand, createStatusCommand, createPurgeCommand, createInstallerCommand, createTouchIdCommand, createCompletionCommand, createUpdateCommand, createRemoveCommand, createConfigCommand, createDoctorCommand, createBackupCommand, createRestoreCommand, createDuplicatesCommand, createScheduleCommand, createWatchCommand, createReportsCommand, createHelpCommand, setCommandsList, } from './commands/index.js';
11
11
  import { enableDebug, debug } from './utils/debug.js';
12
12
  import { getGlobalOptionsTable } from './utils/help.js';
13
- const VERSION = '1.0.0';
13
+ const VERSION = '1.0.1';
14
14
  // ASCII art logo
15
15
  const logo = chalk.cyan(`
16
16
  ██████╗ ██████╗ ██████╗ ██████╗ ███╗ ███╗
@@ -8,7 +8,6 @@ export const DEFAULT_CONFIG = {
8
8
  blacklist: [],
9
9
  autoConfirm: false,
10
10
  safetyLevel: 'moderate',
11
- monitorPreset: 1,
12
11
  scanLocations: {
13
12
  userCache: true,
14
13
  systemCache: true,
@@ -75,25 +75,35 @@ export async function selectFiles(items) {
75
75
  return [];
76
76
  }
77
77
  }
78
+ // No app icon rendering here — removed per request. Formatting below aligns columns.
78
79
  /**
79
- * Select an application
80
+ * Select an application with custom navigation (no looping)
80
81
  */
81
82
  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
- }));
83
+ if (apps.length === 0)
84
+ return [];
85
+ // Align columns: [Name padded] [Size right-aligned] [BundleId dimmed]
86
+ const maxName = Math.min(36, Math.max(...apps.map((a) => a.name.length), 10));
87
+ const sizeWidth = 10;
88
+ const choices = apps.map((app) => {
89
+ const namePad = app.name.padEnd(maxName);
90
+ const sizeStr = formatSize(app.size).padStart(sizeWidth);
91
+ const bundle = app.bundleId ? chalk.dim(` ${app.bundleId}`) : '';
92
+ return {
93
+ name: `${namePad} ${chalk.yellow(sizeStr)}${bundle}`,
94
+ value: app,
95
+ checked: false,
96
+ };
97
+ });
89
98
  try {
90
- return await select({
91
- message: 'Select an application:',
99
+ return await checkbox({
100
+ message: 'Select applications to uninstall (space to toggle, Enter to confirm):',
92
101
  choices,
102
+ loop: false,
93
103
  });
94
104
  }
95
105
  catch {
96
- return null;
106
+ return [];
97
107
  }
98
108
  }
99
109
  /**
@@ -0,0 +1,67 @@
1
+ import { promises as fs } from 'fs';
2
+ import { tmpdir } from 'os';
3
+ import { join, basename, extname } from 'path';
4
+ import { execFile } from 'child_process';
5
+ import fg from 'fast-glob';
6
+ function execFileAsync(cmd, args) {
7
+ return new Promise((resolve, reject) => {
8
+ execFile(cmd, args, (err, stdout, stderr) => {
9
+ if (err)
10
+ return reject(err);
11
+ resolve({ stdout, stderr });
12
+ });
13
+ });
14
+ }
15
+ export async function findIcnsFiles(appPath) {
16
+ const patterns = [
17
+ `${appPath}/Contents/Resources/*.icns`,
18
+ `${appPath}/Contents/Resources/*.*icon*`,
19
+ ];
20
+ const results = await fg(patterns, { onlyFiles: true, unique: true });
21
+ return results;
22
+ }
23
+ export async function convertIcnsToPng(icnsPath, size, outPath) {
24
+ // Use sips to convert and resize
25
+ // sips -s format png <in> --out <out>
26
+ // then resize with sips -z <height> <width> <out>
27
+ await execFileAsync('sips', ['-s', 'format', 'png', icnsPath, '--out', outPath]);
28
+ await execFileAsync('sips', ['-z', String(size), String(size), outPath]);
29
+ }
30
+ export function supportsKitty() {
31
+ return Boolean(process.env.KITTY_WINDOW_ID);
32
+ }
33
+ export function supportsIterm() {
34
+ return Boolean(process.env.ITERM_SESSION_ID);
35
+ }
36
+ export function supportsWezTerm() {
37
+ return Boolean(process.env.WEZTERM_WINDOW || process.env.WEZTERM_EXECUTABLE || process.env.TERM_PROGRAM === 'WezTerm');
38
+ }
39
+ export async function getAppIconInline(appPath, size = 24) {
40
+ try {
41
+ const icns = await findIcnsFiles(appPath);
42
+ if (icns.length === 0)
43
+ return '';
44
+ const icnsPath = icns[0];
45
+ const tmpName = `broom-icon-${basename(icnsPath, extname(icnsPath))}-${size}.png`;
46
+ const outPath = join(tmpdir(), tmpName);
47
+ await convertIcnsToPng(icnsPath, size, outPath);
48
+ const buf = await fs.readFile(outPath);
49
+ const b64 = buf.toString('base64');
50
+ // iTerm2 / WezTerm protocol (OSC 1337)
51
+ if (supportsIterm() || supportsWezTerm()) {
52
+ // ESC ] 1337;File=...:<base64> BEL
53
+ return `\u001b]1337;File=inline=1;width=${size}px;height=${size}px;preserveAspectRatio=1;:${b64}\u0007 `;
54
+ }
55
+ // Kitty protocol
56
+ if (supportsKitty()) {
57
+ // ESC _ G ... base64 ESC \\
58
+ // Use minimal header; many kitty-compatible terminals accept this
59
+ return `\u001b_Gf=100,a=T;inline=1;width=${size}px;height=${size}px;${b64}\u001b\\ `;
60
+ }
61
+ // No supported inline image protocol
62
+ return '';
63
+ }
64
+ catch (e) {
65
+ return '';
66
+ }
67
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tukuyomil032/broom",
3
3
  "private": false,
4
- "version": "1.0.0",
4
+ "version": "1.0.1",
5
5
  "description": "🧹 macOS Disk Cleanup CLI - Clean up caches, logs, trash, browser data, dev artifacts, and more",
6
6
  "type": "module",
7
7
  "main": "./dist/index.js",