@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,336 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Uninstall command - Remove applications and their leftovers
|
|
3
|
+
*/
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import { Command } from 'commander';
|
|
6
|
+
import { readdir, stat, rm } from 'fs/promises';
|
|
7
|
+
import { join } from 'path';
|
|
8
|
+
import { exec } from 'child_process';
|
|
9
|
+
import { promisify } from 'util';
|
|
10
|
+
import { enhanceCommandHelp } from '../utils/help.js';
|
|
11
|
+
import { paths } from '../utils/paths.js';
|
|
12
|
+
import { exists, getSize, formatSize, isProtectedPath, expandPath } from '../utils/fs.js';
|
|
13
|
+
import { printHeader, warning, error, info, createSpinner, succeedSpinner, failSpinner, printSummaryBlock, } from '../ui/output.js';
|
|
14
|
+
import { selectApp, selectFiles, confirmAction } from '../ui/prompts.js';
|
|
15
|
+
const execAsync = promisify(exec);
|
|
16
|
+
/**
|
|
17
|
+
* Get bundle ID from app's Info.plist
|
|
18
|
+
*/
|
|
19
|
+
async function getBundleId(appPath) {
|
|
20
|
+
try {
|
|
21
|
+
const plistPath = join(appPath, 'Contents', 'Info.plist');
|
|
22
|
+
if (!exists(plistPath)) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
// Use plutil to convert plist to JSON
|
|
26
|
+
const { stdout } = await execAsync(`plutil -convert json -o - "${plistPath}"`);
|
|
27
|
+
const plist = JSON.parse(stdout);
|
|
28
|
+
return plist.CFBundleIdentifier || null;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Scan for installed applications
|
|
36
|
+
*/
|
|
37
|
+
async function scanApplications() {
|
|
38
|
+
const apps = [];
|
|
39
|
+
const appDirs = [paths.applications, paths.userApplications];
|
|
40
|
+
for (const appDir of appDirs) {
|
|
41
|
+
try {
|
|
42
|
+
if (!exists(appDir)) {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
const entries = await readdir(appDir);
|
|
46
|
+
for (const entry of entries) {
|
|
47
|
+
if (!entry.endsWith('.app')) {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
const appPath = join(appDir, entry);
|
|
51
|
+
try {
|
|
52
|
+
const stats = await stat(appPath);
|
|
53
|
+
if (!stats.isDirectory()) {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
const size = await getSize(appPath);
|
|
57
|
+
const bundleId = await getBundleId(appPath);
|
|
58
|
+
const name = entry.replace('.app', '');
|
|
59
|
+
apps.push({
|
|
60
|
+
name,
|
|
61
|
+
path: appPath,
|
|
62
|
+
bundleId: bundleId || undefined,
|
|
63
|
+
size,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
// Skip if cannot access
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
// Skip if cannot access directory
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// Sort by name
|
|
76
|
+
apps.sort((a, b) => a.name.localeCompare(b.name));
|
|
77
|
+
return apps;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Find app-related files
|
|
81
|
+
*/
|
|
82
|
+
async function findAppFiles(app) {
|
|
83
|
+
const items = [];
|
|
84
|
+
const searchTerms = [];
|
|
85
|
+
// Build search terms from app name and bundle ID
|
|
86
|
+
if (app.bundleId) {
|
|
87
|
+
searchTerms.push(app.bundleId);
|
|
88
|
+
// Also search for variations (e.g., com.company.app -> company.app)
|
|
89
|
+
const parts = app.bundleId.split('.');
|
|
90
|
+
if (parts.length >= 2) {
|
|
91
|
+
searchTerms.push(parts.slice(-2).join('.'));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// Add app name variations
|
|
95
|
+
searchTerms.push(app.name);
|
|
96
|
+
searchTerms.push(app.name.toLowerCase());
|
|
97
|
+
searchTerms.push(app.name.replace(/\s+/g, ''));
|
|
98
|
+
// Directories to search
|
|
99
|
+
const searchDirs = [
|
|
100
|
+
paths.userCache,
|
|
101
|
+
paths.applicationSupport,
|
|
102
|
+
paths.preferences,
|
|
103
|
+
paths.savedState,
|
|
104
|
+
paths.userLogs,
|
|
105
|
+
join(paths.applicationSupport, '..', 'Containers'),
|
|
106
|
+
join(paths.applicationSupport, '..', 'Group Containers'),
|
|
107
|
+
];
|
|
108
|
+
for (const searchDir of searchDirs) {
|
|
109
|
+
try {
|
|
110
|
+
if (!exists(searchDir)) {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
const entries = await readdir(searchDir);
|
|
114
|
+
for (const entry of entries) {
|
|
115
|
+
const entryLower = entry.toLowerCase();
|
|
116
|
+
// Check if entry matches any search term
|
|
117
|
+
const matches = searchTerms.some((term) => entryLower.includes(term.toLowerCase()));
|
|
118
|
+
if (!matches) {
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
const entryPath = join(searchDir, entry);
|
|
122
|
+
try {
|
|
123
|
+
const stats = await stat(entryPath);
|
|
124
|
+
const size = await getSize(entryPath);
|
|
125
|
+
items.push({
|
|
126
|
+
path: entryPath,
|
|
127
|
+
size,
|
|
128
|
+
name: entry,
|
|
129
|
+
isDirectory: stats.isDirectory(),
|
|
130
|
+
modifiedAt: stats.mtime,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
// Skip if cannot access
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
// Skip if cannot access directory
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
// Also check for launch agents/daemons
|
|
143
|
+
const launchDirs = [
|
|
144
|
+
join(expandPath('~'), 'Library', 'LaunchAgents'),
|
|
145
|
+
'/Library/LaunchAgents',
|
|
146
|
+
'/Library/LaunchDaemons',
|
|
147
|
+
];
|
|
148
|
+
for (const launchDir of launchDirs) {
|
|
149
|
+
try {
|
|
150
|
+
if (!exists(launchDir)) {
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
const entries = await readdir(launchDir);
|
|
154
|
+
for (const entry of entries) {
|
|
155
|
+
if (!entry.endsWith('.plist')) {
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
const entryLower = entry.toLowerCase();
|
|
159
|
+
const matches = searchTerms.some((term) => entryLower.includes(term.toLowerCase()));
|
|
160
|
+
if (!matches) {
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
const entryPath = join(launchDir, entry);
|
|
164
|
+
try {
|
|
165
|
+
const stats = await stat(entryPath);
|
|
166
|
+
items.push({
|
|
167
|
+
path: entryPath,
|
|
168
|
+
size: stats.size,
|
|
169
|
+
name: entry,
|
|
170
|
+
isDirectory: false,
|
|
171
|
+
modifiedAt: stats.mtime,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
// Skip if cannot access
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
// Skip if cannot access directory
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
// Sort by size descending
|
|
184
|
+
items.sort((a, b) => b.size - a.size);
|
|
185
|
+
return items;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Remove app using Finder (moves to trash)
|
|
189
|
+
*/
|
|
190
|
+
async function removeAppToTrash(appPath) {
|
|
191
|
+
try {
|
|
192
|
+
const script = `tell application "Finder" to delete POSIX file "${appPath}"`;
|
|
193
|
+
await execAsync(`osascript -e '${script}'`);
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Execute uninstall command
|
|
202
|
+
*/
|
|
203
|
+
export async function uninstallCommand(options) {
|
|
204
|
+
const isDryRun = options.dryRun || false;
|
|
205
|
+
printHeader(isDryRun ? '🗑️ Uninstall Apps (Dry Run)' : '🗑️ Uninstall Apps');
|
|
206
|
+
// Scan for apps
|
|
207
|
+
const spinner = createSpinner('Scanning applications...');
|
|
208
|
+
const apps = await scanApplications();
|
|
209
|
+
succeedSpinner(spinner, `Found ${apps.length} applications`);
|
|
210
|
+
if (apps.length === 0) {
|
|
211
|
+
warning('No applications found');
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
// Select app
|
|
215
|
+
console.log();
|
|
216
|
+
const selectedApp = await selectApp(apps);
|
|
217
|
+
if (!selectedApp) {
|
|
218
|
+
warning('No application selected');
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
// Find related files
|
|
222
|
+
console.log();
|
|
223
|
+
const searchSpinner = createSpinner(`Searching for ${selectedApp.name} related files...`);
|
|
224
|
+
const relatedFiles = await findAppFiles(selectedApp);
|
|
225
|
+
succeedSpinner(searchSpinner, `Found ${relatedFiles.length} related files`);
|
|
226
|
+
// Show app info
|
|
227
|
+
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)}`);
|
|
233
|
+
}
|
|
234
|
+
// Show related files
|
|
235
|
+
if (relatedFiles.length > 0) {
|
|
236
|
+
const totalRelatedSize = relatedFiles.reduce((sum, f) => sum + f.size, 0);
|
|
237
|
+
console.log();
|
|
238
|
+
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))}`);
|
|
241
|
+
}
|
|
242
|
+
if (relatedFiles.length > 10) {
|
|
243
|
+
console.log(chalk.dim(` ... and ${relatedFiles.length - 10} more`));
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
// Confirm uninstall
|
|
247
|
+
console.log();
|
|
248
|
+
if (!options.yes) {
|
|
249
|
+
const confirmed = await confirmAction(`Uninstall ${selectedApp.name} and remove related files?`, false);
|
|
250
|
+
if (!confirmed) {
|
|
251
|
+
warning('Uninstall cancelled');
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
// Select files to remove
|
|
256
|
+
let filesToRemove = relatedFiles;
|
|
257
|
+
if (!options.yes && relatedFiles.length > 0) {
|
|
258
|
+
console.log();
|
|
259
|
+
info('Select which related files to remove:');
|
|
260
|
+
filesToRemove = await selectFiles(relatedFiles);
|
|
261
|
+
}
|
|
262
|
+
// Execute uninstall
|
|
263
|
+
console.log();
|
|
264
|
+
const uninstallSpinner = createSpinner(isDryRun ? 'Simulating uninstall...' : 'Uninstalling...');
|
|
265
|
+
let appRemoved = false;
|
|
266
|
+
let filesRemoved = 0;
|
|
267
|
+
let freedSpace = 0;
|
|
268
|
+
const errors = [];
|
|
269
|
+
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}`);
|
|
277
|
+
}
|
|
278
|
+
// Remove related files
|
|
279
|
+
for (const file of filesToRemove) {
|
|
280
|
+
if (isProtectedPath(file.path)) {
|
|
281
|
+
errors.push(`Skipped protected path: ${file.path}`);
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
try {
|
|
285
|
+
await rm(file.path, { recursive: true, force: true });
|
|
286
|
+
filesRemoved++;
|
|
287
|
+
freedSpace += file.size;
|
|
288
|
+
}
|
|
289
|
+
catch (err) {
|
|
290
|
+
errors.push(`Failed to remove ${file.path}: ${err.message}`);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
else {
|
|
295
|
+
// Dry run - just count
|
|
296
|
+
appRemoved = true;
|
|
297
|
+
freedSpace = selectedApp.size;
|
|
298
|
+
filesRemoved = filesToRemove.length;
|
|
299
|
+
freedSpace += filesToRemove.reduce((sum, f) => sum + f.size, 0);
|
|
300
|
+
}
|
|
301
|
+
if (errors.length === 0) {
|
|
302
|
+
succeedSpinner(uninstallSpinner, isDryRun ? 'Simulation complete' : 'Uninstall complete');
|
|
303
|
+
}
|
|
304
|
+
else {
|
|
305
|
+
failSpinner(uninstallSpinner, 'Uninstall completed with errors');
|
|
306
|
+
}
|
|
307
|
+
// Print summary
|
|
308
|
+
const summaryHeading = isDryRun ? 'Dry Run Complete' : 'Uninstall Complete';
|
|
309
|
+
const summaryDetails = [
|
|
310
|
+
`Application: ${appRemoved ? chalk.green('Removed') : chalk.red('Failed')}`,
|
|
311
|
+
`Related files removed: ${filesRemoved}`,
|
|
312
|
+
`Space freed: ${chalk.green(formatSize(freedSpace))}`,
|
|
313
|
+
];
|
|
314
|
+
if (errors.length > 0) {
|
|
315
|
+
summaryDetails.push(`${chalk.red(`Errors: ${errors.length}`)}`);
|
|
316
|
+
}
|
|
317
|
+
printSummaryBlock(summaryHeading, summaryDetails);
|
|
318
|
+
if (errors.length > 0) {
|
|
319
|
+
console.log();
|
|
320
|
+
console.log(chalk.bold.red('Errors:'));
|
|
321
|
+
errors.forEach((err) => error(err));
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Create uninstall command
|
|
326
|
+
*/
|
|
327
|
+
export function createUninstallCommand() {
|
|
328
|
+
const cmd = new Command('uninstall')
|
|
329
|
+
.description('Remove apps and their leftovers')
|
|
330
|
+
.option('-n, --dry-run', 'Preview only, no deletions')
|
|
331
|
+
.option('-y, --yes', 'Skip confirmation prompts')
|
|
332
|
+
.action(async (options) => {
|
|
333
|
+
await uninstallCommand(options);
|
|
334
|
+
});
|
|
335
|
+
return enhanceCommandHelp(cmd);
|
|
336
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* update command - Self-update broom to the latest version
|
|
3
|
+
*/
|
|
4
|
+
import { Command } from 'commander';
|
|
5
|
+
import { execSync } from 'child_process';
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
import ora from 'ora';
|
|
8
|
+
import { confirm } from '@inquirer/prompts';
|
|
9
|
+
import { enhanceCommandHelp } from '../utils/help.js';
|
|
10
|
+
const PACKAGE_NAME = 'broom-cli';
|
|
11
|
+
/**
|
|
12
|
+
* Get current installed version
|
|
13
|
+
*/
|
|
14
|
+
function getCurrentVersion() {
|
|
15
|
+
try {
|
|
16
|
+
const packageJson = require('../../package.json');
|
|
17
|
+
return packageJson.version;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return 'unknown';
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Get latest version from npm
|
|
25
|
+
*/
|
|
26
|
+
function getLatestVersion() {
|
|
27
|
+
try {
|
|
28
|
+
// Try npm
|
|
29
|
+
const result = execSync(`npm view ${PACKAGE_NAME} version 2>/dev/null`, {
|
|
30
|
+
encoding: 'utf-8',
|
|
31
|
+
timeout: 10000,
|
|
32
|
+
});
|
|
33
|
+
return result.trim();
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Compare semver versions
|
|
41
|
+
*/
|
|
42
|
+
function compareVersions(current, latest) {
|
|
43
|
+
const parseParts = (v) => v.split('.').map((n) => parseInt(n, 10) || 0);
|
|
44
|
+
const currentParts = parseParts(current);
|
|
45
|
+
const latestParts = parseParts(latest);
|
|
46
|
+
for (let i = 0; i < 3; i++) {
|
|
47
|
+
const c = currentParts[i] || 0;
|
|
48
|
+
const l = latestParts[i] || 0;
|
|
49
|
+
if (l > c) {
|
|
50
|
+
return 1; // newer
|
|
51
|
+
}
|
|
52
|
+
if (l < c) {
|
|
53
|
+
return -1; // older
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return 0; // same
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Check for updates
|
|
60
|
+
*/
|
|
61
|
+
async function checkForUpdates() {
|
|
62
|
+
const spinner = ora('Checking for updates...').start();
|
|
63
|
+
const current = getCurrentVersion();
|
|
64
|
+
const latest = getLatestVersion();
|
|
65
|
+
if (!latest) {
|
|
66
|
+
spinner.warn('Could not fetch latest version');
|
|
67
|
+
console.log(chalk.dim('\nMake sure you have internet connection.'));
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
spinner.stop();
|
|
71
|
+
console.log(chalk.bold('\n📦 broom Version Info\n'));
|
|
72
|
+
console.log(` Current version: ${chalk.cyan(current)}`);
|
|
73
|
+
console.log(` Latest version: ${chalk.green(latest)}`);
|
|
74
|
+
const cmp = compareVersions(current, latest);
|
|
75
|
+
if (cmp > 0) {
|
|
76
|
+
console.log(chalk.yellow('\n Update available!'));
|
|
77
|
+
console.log(chalk.dim(` Run 'broom update' to update.\n`));
|
|
78
|
+
}
|
|
79
|
+
else if (cmp < 0) {
|
|
80
|
+
console.log(chalk.cyan('\n You are running a newer version.\n'));
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
console.log(chalk.green('\n You are up to date!\n'));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Detect package manager
|
|
88
|
+
*/
|
|
89
|
+
function detectPackageManager() {
|
|
90
|
+
const managers = ['bun', 'pnpm', 'yarn', 'npm'];
|
|
91
|
+
for (const manager of managers) {
|
|
92
|
+
try {
|
|
93
|
+
execSync(`which ${manager}`, { stdio: 'pipe' });
|
|
94
|
+
return manager;
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Perform update
|
|
104
|
+
*/
|
|
105
|
+
async function performUpdate(skipConfirm) {
|
|
106
|
+
const current = getCurrentVersion();
|
|
107
|
+
const spinner = ora('Checking for updates...').start();
|
|
108
|
+
const latest = getLatestVersion();
|
|
109
|
+
if (!latest) {
|
|
110
|
+
spinner.fail('Could not fetch latest version');
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const cmp = compareVersions(current, latest);
|
|
114
|
+
if (cmp <= 0) {
|
|
115
|
+
spinner.succeed(`Already up to date (v${current})`);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
spinner.info(`Update available: ${current} → ${latest}`);
|
|
119
|
+
if (!skipConfirm) {
|
|
120
|
+
const confirmed = await confirm({
|
|
121
|
+
message: `Update broom to v${latest}?`,
|
|
122
|
+
default: true,
|
|
123
|
+
});
|
|
124
|
+
if (!confirmed) {
|
|
125
|
+
console.log(chalk.yellow('Update cancelled'));
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const pm = detectPackageManager();
|
|
130
|
+
if (!pm) {
|
|
131
|
+
console.error(chalk.red('No package manager found'));
|
|
132
|
+
console.log(chalk.dim('Install npm, yarn, pnpm, or bun first.'));
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const updateSpinner = ora(`Updating broom using ${pm}...`).start();
|
|
136
|
+
try {
|
|
137
|
+
let cmd;
|
|
138
|
+
switch (pm) {
|
|
139
|
+
case 'npm':
|
|
140
|
+
cmd = `npm update -g ${PACKAGE_NAME}`;
|
|
141
|
+
break;
|
|
142
|
+
case 'yarn':
|
|
143
|
+
cmd = `yarn global upgrade ${PACKAGE_NAME}`;
|
|
144
|
+
break;
|
|
145
|
+
case 'pnpm':
|
|
146
|
+
cmd = `pnpm update -g ${PACKAGE_NAME}`;
|
|
147
|
+
break;
|
|
148
|
+
case 'bun':
|
|
149
|
+
cmd = `bun update -g ${PACKAGE_NAME}`;
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
execSync(cmd, { stdio: 'pipe' });
|
|
153
|
+
updateSpinner.succeed(`Successfully updated to v${latest}`);
|
|
154
|
+
console.log(chalk.dim('\nChangelog: https://github.com/your-username/broom/releases'));
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
updateSpinner.fail('Update failed');
|
|
158
|
+
if (error instanceof Error) {
|
|
159
|
+
console.error(chalk.red(error.message));
|
|
160
|
+
}
|
|
161
|
+
console.log(chalk.dim('\nTry running manually:'));
|
|
162
|
+
console.log(chalk.dim(` npm install -g ${PACKAGE_NAME}@latest`));
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Create update command
|
|
167
|
+
*/
|
|
168
|
+
export function createUpdateCommand() {
|
|
169
|
+
const cmd = new Command('update')
|
|
170
|
+
.description('Self-update broom to the latest version')
|
|
171
|
+
.option('-c, --check', 'Check for updates only (no install)')
|
|
172
|
+
.option('-y, --yes', 'Skip confirmation prompts')
|
|
173
|
+
.action(async (opts) => {
|
|
174
|
+
if (opts.check) {
|
|
175
|
+
await checkForUpdates();
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
await performUpdate(opts.yes);
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
return enhanceCommandHelp(cmd);
|
|
182
|
+
}
|