@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,271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Purge command - Clean project-specific artifacts
|
|
3
|
+
*/
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import { Command } from 'commander';
|
|
6
|
+
import { stat, rm } from 'fs/promises';
|
|
7
|
+
import { basename } from 'path';
|
|
8
|
+
import fg from 'fast-glob';
|
|
9
|
+
import { enhanceCommandHelp } from '../utils/help.js';
|
|
10
|
+
import { getSize, formatSize, expandPath } from '../utils/fs.js';
|
|
11
|
+
import { printHeader, success, warning, error, info, separator, createSpinner, succeedSpinner, printProgressBar, } from '../ui/output.js';
|
|
12
|
+
import { confirmAction, selectItems } from '../ui/prompts.js';
|
|
13
|
+
import { debug, debugSection, debugObj } from '../utils/debug.js';
|
|
14
|
+
/**
|
|
15
|
+
* Project artifact targets
|
|
16
|
+
*/
|
|
17
|
+
const purgeTargets = [
|
|
18
|
+
{
|
|
19
|
+
id: 'node_modules',
|
|
20
|
+
name: 'Node.js (node_modules)',
|
|
21
|
+
patterns: ['**/node_modules'],
|
|
22
|
+
description: 'npm/yarn/pnpm packages',
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
id: 'build',
|
|
26
|
+
name: 'Build outputs (dist, build)',
|
|
27
|
+
patterns: ['**/dist', '**/build', '**/out', '**/.next', '**/.nuxt'],
|
|
28
|
+
description: 'Compiled/bundled files',
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
id: 'cache',
|
|
32
|
+
name: 'Project caches',
|
|
33
|
+
patterns: ['**/.cache', '**/.parcel-cache', '**/.turbo', '**/.eslintcache'],
|
|
34
|
+
description: 'Build and tool caches',
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
id: 'coverage',
|
|
38
|
+
name: 'Test coverage',
|
|
39
|
+
patterns: ['**/coverage', '**/.nyc_output'],
|
|
40
|
+
description: 'Code coverage reports',
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
id: 'python',
|
|
44
|
+
name: 'Python artifacts',
|
|
45
|
+
patterns: [
|
|
46
|
+
'**/__pycache__',
|
|
47
|
+
'**/*.pyc',
|
|
48
|
+
'**/.pytest_cache',
|
|
49
|
+
'**/.mypy_cache',
|
|
50
|
+
'**/venv',
|
|
51
|
+
'**/.venv',
|
|
52
|
+
],
|
|
53
|
+
description: 'Python caches and venvs',
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
id: 'rust',
|
|
57
|
+
name: 'Rust (target)',
|
|
58
|
+
patterns: ['**/target'],
|
|
59
|
+
description: 'Cargo build artifacts',
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
id: 'java',
|
|
63
|
+
name: 'Java/Gradle/Maven',
|
|
64
|
+
patterns: ['**/.gradle', '**/build', '**/target'],
|
|
65
|
+
description: 'Java build artifacts',
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
id: 'ios',
|
|
69
|
+
name: 'iOS/Xcode',
|
|
70
|
+
patterns: ['**/DerivedData', '**/Pods', '**/.build'],
|
|
71
|
+
description: 'Xcode build data and CocoaPods',
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
id: 'logs',
|
|
75
|
+
name: 'Log files',
|
|
76
|
+
patterns: ['**/*.log', '**/logs', '**/.logs'],
|
|
77
|
+
description: 'Application logs',
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
id: 'temp',
|
|
81
|
+
name: 'Temporary files',
|
|
82
|
+
patterns: ['**/.tmp', '**/tmp', '**/*.tmp', '**/*.temp'],
|
|
83
|
+
description: 'Temporary files',
|
|
84
|
+
},
|
|
85
|
+
];
|
|
86
|
+
/**
|
|
87
|
+
* Find matching items for a target
|
|
88
|
+
*/
|
|
89
|
+
async function findTargetItems(basePath, target, recursive) {
|
|
90
|
+
const items = [];
|
|
91
|
+
for (const pattern of target.patterns) {
|
|
92
|
+
const searchPattern = recursive ? pattern : pattern.replace('**/', '');
|
|
93
|
+
try {
|
|
94
|
+
const matches = await fg(searchPattern, {
|
|
95
|
+
cwd: basePath,
|
|
96
|
+
absolute: true,
|
|
97
|
+
onlyDirectories: !pattern.includes('*.*'),
|
|
98
|
+
dot: true,
|
|
99
|
+
ignore: ['**/node_modules/**/node_modules'], // Avoid nested node_modules
|
|
100
|
+
});
|
|
101
|
+
for (const match of matches) {
|
|
102
|
+
try {
|
|
103
|
+
const stats = await stat(match);
|
|
104
|
+
const size = await getSize(match);
|
|
105
|
+
items.push({
|
|
106
|
+
path: match,
|
|
107
|
+
name: basename(match),
|
|
108
|
+
size,
|
|
109
|
+
isDirectory: stats.isDirectory(),
|
|
110
|
+
modifiedAt: stats.mtime,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
// Skip if cannot access
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
// Pattern match error
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
// Deduplicate by path
|
|
123
|
+
const seen = new Set();
|
|
124
|
+
return items.filter((item) => {
|
|
125
|
+
if (seen.has(item.path)) {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
seen.add(item.path);
|
|
129
|
+
return true;
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Execute purge command
|
|
134
|
+
*/
|
|
135
|
+
export async function purgeCommand(options) {
|
|
136
|
+
const isDryRun = options.dryRun || false;
|
|
137
|
+
const basePath = options.path ? expandPath(options.path) : process.cwd();
|
|
138
|
+
const recursive = options.recursive ?? true;
|
|
139
|
+
debugSection('Purge Command');
|
|
140
|
+
debugObj('Options', options);
|
|
141
|
+
debug(`Base path: ${basePath}`);
|
|
142
|
+
debug(`Recursive: ${recursive}`);
|
|
143
|
+
printHeader(isDryRun ? '🧹 Purge Project Artifacts (Dry Run)' : '🧹 Purge Project Artifacts');
|
|
144
|
+
console.log(`Scanning: ${chalk.cyan(basePath)}`);
|
|
145
|
+
console.log(`Mode: ${recursive ? 'Recursive' : 'Current directory only'}`);
|
|
146
|
+
console.log();
|
|
147
|
+
// Select targets
|
|
148
|
+
const targetChoices = purgeTargets.map((t) => ({
|
|
149
|
+
name: t.name,
|
|
150
|
+
value: t.id,
|
|
151
|
+
description: t.description,
|
|
152
|
+
}));
|
|
153
|
+
const selectedIds = await selectItems('Select artifact types to purge:', targetChoices);
|
|
154
|
+
if (selectedIds.length === 0) {
|
|
155
|
+
warning('No targets selected');
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
const selectedTargets = purgeTargets.filter((t) => selectedIds.includes(t.id));
|
|
159
|
+
// Scan for items
|
|
160
|
+
console.log();
|
|
161
|
+
const spinner = createSpinner('Scanning for artifacts...');
|
|
162
|
+
const allItems = new Map();
|
|
163
|
+
let totalSize = 0;
|
|
164
|
+
let totalCount = 0;
|
|
165
|
+
for (const target of selectedTargets) {
|
|
166
|
+
const items = await findTargetItems(basePath, target, recursive);
|
|
167
|
+
if (items.length > 0) {
|
|
168
|
+
allItems.set(target.id, items);
|
|
169
|
+
totalCount += items.length;
|
|
170
|
+
totalSize += items.reduce((sum, i) => sum + i.size, 0);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
succeedSpinner(spinner, `Found ${totalCount} items (${formatSize(totalSize)})`);
|
|
174
|
+
if (totalCount === 0) {
|
|
175
|
+
console.log();
|
|
176
|
+
success('No artifacts found to purge!');
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
// Show found items
|
|
180
|
+
console.log();
|
|
181
|
+
console.log(chalk.bold('Found artifacts:'));
|
|
182
|
+
console.log();
|
|
183
|
+
for (const [targetId, items] of allItems) {
|
|
184
|
+
const target = purgeTargets.find((t) => t.id === targetId);
|
|
185
|
+
const targetSize = items.reduce((sum, i) => sum + i.size, 0);
|
|
186
|
+
console.log(chalk.bold(` ${target.name}`));
|
|
187
|
+
console.log(` Items: ${items.length} Size: ${chalk.yellow(formatSize(targetSize))}`);
|
|
188
|
+
// Show first few items
|
|
189
|
+
for (const item of items.slice(0, 3)) {
|
|
190
|
+
const relativePath = item.path.replace(basePath, '.');
|
|
191
|
+
console.log(` ${chalk.dim(relativePath)} ${chalk.dim(`(${formatSize(item.size)})`)}`);
|
|
192
|
+
}
|
|
193
|
+
if (items.length > 3) {
|
|
194
|
+
console.log(chalk.dim(` ... and ${items.length - 3} more`));
|
|
195
|
+
}
|
|
196
|
+
console.log();
|
|
197
|
+
}
|
|
198
|
+
// Confirm
|
|
199
|
+
if (!options.yes) {
|
|
200
|
+
console.log();
|
|
201
|
+
const confirmed = await confirmAction(`${isDryRun ? 'Simulate' : 'Purge'} ${totalCount} items (${formatSize(totalSize)})?`, false);
|
|
202
|
+
if (!confirmed) {
|
|
203
|
+
warning('Purge cancelled');
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
// Execute purge
|
|
208
|
+
console.log();
|
|
209
|
+
separator();
|
|
210
|
+
let removedCount = 0;
|
|
211
|
+
let removedSize = 0;
|
|
212
|
+
const errors = [];
|
|
213
|
+
const allItemsFlat = Array.from(allItems.values()).flat();
|
|
214
|
+
for (let i = 0; i < allItemsFlat.length; i++) {
|
|
215
|
+
const item = allItemsFlat[i];
|
|
216
|
+
const progress = ((i + 1) / allItemsFlat.length) * 100;
|
|
217
|
+
printProgressBar(progress, 30, `Purging: ${basename(item.path)}`);
|
|
218
|
+
if (!isDryRun) {
|
|
219
|
+
try {
|
|
220
|
+
await rm(item.path, { recursive: true, force: true });
|
|
221
|
+
removedCount++;
|
|
222
|
+
removedSize += item.size;
|
|
223
|
+
}
|
|
224
|
+
catch (err) {
|
|
225
|
+
errors.push(`${item.path}: ${err.message}`);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
removedCount++;
|
|
230
|
+
removedSize += item.size;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
// Clear progress line
|
|
234
|
+
process.stdout.write('\r' + ' '.repeat(80) + '\r');
|
|
235
|
+
// Summary
|
|
236
|
+
console.log();
|
|
237
|
+
separator();
|
|
238
|
+
console.log();
|
|
239
|
+
console.log(chalk.bold(isDryRun ? '🧪 Dry Run Complete' : '✨ Purge Complete'));
|
|
240
|
+
console.log();
|
|
241
|
+
console.log(` Items ${isDryRun ? 'would be ' : ''}removed: ${chalk.green(removedCount)}`);
|
|
242
|
+
console.log(` Space ${isDryRun ? 'would be ' : ''}freed: ${chalk.green(formatSize(removedSize))}`);
|
|
243
|
+
if (errors.length > 0) {
|
|
244
|
+
console.log(` ${chalk.red(`Errors: ${errors.length}`)}`);
|
|
245
|
+
console.log();
|
|
246
|
+
errors.slice(0, 5).forEach((err) => error(err));
|
|
247
|
+
if (errors.length > 5) {
|
|
248
|
+
console.log(chalk.dim(` ... and ${errors.length - 5} more errors`));
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
if (isDryRun) {
|
|
252
|
+
console.log();
|
|
253
|
+
info('This was a dry run. No files were deleted.');
|
|
254
|
+
info('Run without --dry-run to actually purge artifacts.');
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Create purge command
|
|
259
|
+
*/
|
|
260
|
+
export function createPurgeCommand() {
|
|
261
|
+
const cmd = new Command('purge')
|
|
262
|
+
.description('Clean project-specific build artifacts')
|
|
263
|
+
.option('-n, --dry-run', 'Preview only, no deletions')
|
|
264
|
+
.option('-y, --yes', 'Skip confirmation prompts')
|
|
265
|
+
.option('-p, --path <path>', 'Path to scan (default: current directory)')
|
|
266
|
+
.option('--no-recursive', 'Only scan current directory')
|
|
267
|
+
.action(async (options) => {
|
|
268
|
+
await purgeCommand(options);
|
|
269
|
+
});
|
|
270
|
+
return enhanceCommandHelp(cmd);
|
|
271
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* remove command - Uninstall broom from the system
|
|
3
|
+
*/
|
|
4
|
+
import { Command } from 'commander';
|
|
5
|
+
import { execSync } from 'child_process';
|
|
6
|
+
import { existsSync, rmSync } from 'fs';
|
|
7
|
+
import { homedir } from 'os';
|
|
8
|
+
import { join } from 'path';
|
|
9
|
+
import chalk from 'chalk';
|
|
10
|
+
import ora from 'ora';
|
|
11
|
+
import { confirm } from '@inquirer/prompts';
|
|
12
|
+
import { enhanceCommandHelp } from '../utils/help.js';
|
|
13
|
+
const CONFIG_DIR = join(homedir(), '.config', 'broom');
|
|
14
|
+
const CACHE_DIR = join(homedir(), '.cache', 'broom');
|
|
15
|
+
const DATA_DIR = join(homedir(), '.local', 'share', 'broom');
|
|
16
|
+
/**
|
|
17
|
+
* Detect how broom was installed
|
|
18
|
+
*/
|
|
19
|
+
function detectInstallMethod() {
|
|
20
|
+
try {
|
|
21
|
+
// Check if installed globally via npm/yarn/pnpm
|
|
22
|
+
const npmList = execSync('npm list -g --depth=0 2>/dev/null', { encoding: 'utf-8' });
|
|
23
|
+
if (npmList.includes('broom')) {
|
|
24
|
+
return 'npm';
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
catch { }
|
|
28
|
+
try {
|
|
29
|
+
const yarnList = execSync('yarn global list 2>/dev/null', { encoding: 'utf-8' });
|
|
30
|
+
if (yarnList.includes('broom')) {
|
|
31
|
+
return 'yarn';
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
catch { }
|
|
35
|
+
try {
|
|
36
|
+
const pnpmList = execSync('pnpm list -g 2>/dev/null', { encoding: 'utf-8' });
|
|
37
|
+
if (pnpmList.includes('broom')) {
|
|
38
|
+
return 'pnpm';
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
catch { }
|
|
42
|
+
try {
|
|
43
|
+
const bunList = execSync('bun pm ls -g 2>/dev/null', { encoding: 'utf-8' });
|
|
44
|
+
if (bunList.includes('broom')) {
|
|
45
|
+
return 'bun';
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
catch { }
|
|
49
|
+
// Check if running from local development
|
|
50
|
+
const scriptPath = process.argv[1];
|
|
51
|
+
if (scriptPath && scriptPath.includes('/dist/index.js')) {
|
|
52
|
+
return 'local';
|
|
53
|
+
}
|
|
54
|
+
return 'unknown';
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Get files/directories that would be removed
|
|
58
|
+
*/
|
|
59
|
+
function getRemovableItems(keepConfig) {
|
|
60
|
+
const items = [
|
|
61
|
+
{ path: CACHE_DIR, exists: existsSync(CACHE_DIR), description: 'Cache directory' },
|
|
62
|
+
{ path: DATA_DIR, exists: existsSync(DATA_DIR), description: 'Data directory' },
|
|
63
|
+
];
|
|
64
|
+
if (!keepConfig) {
|
|
65
|
+
items.push({
|
|
66
|
+
path: CONFIG_DIR,
|
|
67
|
+
exists: existsSync(CONFIG_DIR),
|
|
68
|
+
description: 'Config directory',
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
return items;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Remove user data directories
|
|
75
|
+
*/
|
|
76
|
+
function removeUserData(keepConfig) {
|
|
77
|
+
let removed = 0;
|
|
78
|
+
if (existsSync(CACHE_DIR)) {
|
|
79
|
+
rmSync(CACHE_DIR, { recursive: true, force: true });
|
|
80
|
+
removed++;
|
|
81
|
+
}
|
|
82
|
+
if (existsSync(DATA_DIR)) {
|
|
83
|
+
rmSync(DATA_DIR, { recursive: true, force: true });
|
|
84
|
+
removed++;
|
|
85
|
+
}
|
|
86
|
+
if (!keepConfig && existsSync(CONFIG_DIR)) {
|
|
87
|
+
rmSync(CONFIG_DIR, { recursive: true, force: true });
|
|
88
|
+
removed++;
|
|
89
|
+
}
|
|
90
|
+
return removed;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Uninstall broom
|
|
94
|
+
*/
|
|
95
|
+
async function uninstallBroom(opts) {
|
|
96
|
+
console.log(chalk.bold('\n🗑️ Uninstall broom\n'));
|
|
97
|
+
const installMethod = detectInstallMethod();
|
|
98
|
+
const items = getRemovableItems(opts.keepConfig);
|
|
99
|
+
const existingItems = items.filter((i) => i.exists);
|
|
100
|
+
console.log(chalk.dim('Installation method: ') + installMethod);
|
|
101
|
+
console.log();
|
|
102
|
+
if (installMethod === 'local') {
|
|
103
|
+
console.log(chalk.yellow('broom appears to be running from a local development directory.'));
|
|
104
|
+
console.log(chalk.dim('To remove, simply delete the project directory.\n'));
|
|
105
|
+
}
|
|
106
|
+
if (existingItems.length > 0) {
|
|
107
|
+
console.log(chalk.bold('User data to be removed:'));
|
|
108
|
+
for (const item of existingItems) {
|
|
109
|
+
console.log(` ${chalk.red('•')} ${item.description}: ${chalk.dim(item.path)}`);
|
|
110
|
+
}
|
|
111
|
+
console.log();
|
|
112
|
+
}
|
|
113
|
+
if (opts.keepConfig) {
|
|
114
|
+
console.log(chalk.dim('Configuration will be preserved.\n'));
|
|
115
|
+
}
|
|
116
|
+
if (!opts.yes) {
|
|
117
|
+
const confirmed = await confirm({
|
|
118
|
+
message: 'Are you sure you want to uninstall broom?',
|
|
119
|
+
default: false,
|
|
120
|
+
});
|
|
121
|
+
if (!confirmed) {
|
|
122
|
+
console.log(chalk.yellow('Uninstall cancelled'));
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
// Remove user data first
|
|
127
|
+
if (existingItems.length > 0) {
|
|
128
|
+
const dataSpinner = ora('Removing user data...').start();
|
|
129
|
+
const removed = removeUserData(opts.keepConfig);
|
|
130
|
+
dataSpinner.succeed(`Removed ${removed} data ${removed === 1 ? 'directory' : 'directories'}`);
|
|
131
|
+
}
|
|
132
|
+
// Uninstall package
|
|
133
|
+
if (installMethod !== 'local' && installMethod !== 'unknown') {
|
|
134
|
+
const uninstallSpinner = ora(`Uninstalling broom via ${installMethod}...`).start();
|
|
135
|
+
try {
|
|
136
|
+
let cmd;
|
|
137
|
+
switch (installMethod) {
|
|
138
|
+
case 'npm':
|
|
139
|
+
cmd = 'npm uninstall -g broom';
|
|
140
|
+
break;
|
|
141
|
+
case 'yarn':
|
|
142
|
+
cmd = 'yarn global remove broom';
|
|
143
|
+
break;
|
|
144
|
+
case 'pnpm':
|
|
145
|
+
cmd = 'pnpm remove -g broom';
|
|
146
|
+
break;
|
|
147
|
+
case 'bun':
|
|
148
|
+
cmd = 'bun remove -g broom';
|
|
149
|
+
break;
|
|
150
|
+
default:
|
|
151
|
+
cmd = 'npm uninstall -g broom';
|
|
152
|
+
}
|
|
153
|
+
execSync(cmd, { stdio: 'pipe' });
|
|
154
|
+
uninstallSpinner.succeed('broom uninstalled successfully');
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
uninstallSpinner.fail('Failed to uninstall broom package');
|
|
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 uninstall -g broom'));
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
console.log(chalk.green('\n👋 broom has been removed. Thanks for using broom!\n'));
|
|
166
|
+
// Remove shell completion
|
|
167
|
+
console.log(chalk.dim('Remember to remove any shell completion scripts you may have added:'));
|
|
168
|
+
console.log(chalk.dim(' - ~/.bashrc or ~/.bash_profile'));
|
|
169
|
+
console.log(chalk.dim(' - ~/.zshrc or ~/.zsh/completions/_broom'));
|
|
170
|
+
console.log(chalk.dim(' - ~/.config/fish/completions/broom.fish\n'));
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Create remove command
|
|
174
|
+
*/
|
|
175
|
+
export function createRemoveCommand() {
|
|
176
|
+
const cmd = new Command('remove')
|
|
177
|
+
.description('Uninstall broom from the system')
|
|
178
|
+
.option('-y, --yes', 'Skip confirmation prompts')
|
|
179
|
+
.option('-k, --keep-config', 'Keep configuration files')
|
|
180
|
+
.action(async (opts) => {
|
|
181
|
+
await uninstallBroom(opts);
|
|
182
|
+
});
|
|
183
|
+
return enhanceCommandHelp(cmd);
|
|
184
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reports command - Manage cleanup reports
|
|
3
|
+
*/
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import { Command } from 'commander';
|
|
6
|
+
import { enhanceCommandHelp } from '../utils/help.js';
|
|
7
|
+
import { readdir, stat, unlink } from 'fs/promises';
|
|
8
|
+
import { join } from 'path';
|
|
9
|
+
import { homedir } from 'os';
|
|
10
|
+
import { exists, formatSize } from '../utils/fs.js';
|
|
11
|
+
import { printHeader, success, warning, info, separator, createSpinner, succeedSpinner, } from '../ui/output.js';
|
|
12
|
+
import { confirm } from '../ui/prompts.js';
|
|
13
|
+
/**
|
|
14
|
+
* Get all report files
|
|
15
|
+
*/
|
|
16
|
+
async function getReportFiles() {
|
|
17
|
+
const reportsDir = join(homedir(), '.broom', 'reports');
|
|
18
|
+
if (!exists(reportsDir)) {
|
|
19
|
+
return [];
|
|
20
|
+
}
|
|
21
|
+
const files = [];
|
|
22
|
+
try {
|
|
23
|
+
const entries = await readdir(reportsDir);
|
|
24
|
+
for (const entry of entries) {
|
|
25
|
+
if (!entry.endsWith('.html'))
|
|
26
|
+
continue;
|
|
27
|
+
const filePath = join(reportsDir, entry);
|
|
28
|
+
const stats = await stat(filePath);
|
|
29
|
+
files.push({
|
|
30
|
+
path: filePath,
|
|
31
|
+
name: entry,
|
|
32
|
+
size: stats.size,
|
|
33
|
+
date: stats.mtime,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
// Sort by date descending (newest first)
|
|
37
|
+
files.sort((a, b) => b.date.getTime() - a.date.getTime());
|
|
38
|
+
return files;
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return [];
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* List all reports
|
|
46
|
+
*/
|
|
47
|
+
async function listReports() {
|
|
48
|
+
printHeader('📊 Cleanup Reports');
|
|
49
|
+
const files = await getReportFiles();
|
|
50
|
+
if (files.length === 0) {
|
|
51
|
+
info('No reports found');
|
|
52
|
+
console.log();
|
|
53
|
+
console.log(chalk.dim('Reports are created with: broom clean --report'));
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
console.log(chalk.bold(`Found ${files.length} report(s):`));
|
|
57
|
+
console.log();
|
|
58
|
+
const totalSize = files.reduce((sum, file) => sum + file.size, 0);
|
|
59
|
+
for (let i = 0; i < files.length; i++) {
|
|
60
|
+
const file = files[i];
|
|
61
|
+
const dateStr = file.date.toLocaleString('ja-JP');
|
|
62
|
+
const sizeStr = formatSize(file.size);
|
|
63
|
+
console.log(` ${i + 1}. ${chalk.cyan(file.name)}`);
|
|
64
|
+
console.log(` ${chalk.dim(`Date: ${dateStr} | Size: ${sizeStr}`)}`);
|
|
65
|
+
}
|
|
66
|
+
console.log();
|
|
67
|
+
separator();
|
|
68
|
+
console.log();
|
|
69
|
+
console.log(`Total size: ${chalk.yellow(formatSize(totalSize))}`);
|
|
70
|
+
console.log();
|
|
71
|
+
console.log(chalk.dim('To delete reports: broom reports clean'));
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Clean all reports
|
|
75
|
+
*/
|
|
76
|
+
async function cleanReports(options) {
|
|
77
|
+
printHeader('🗑️ Clean Reports');
|
|
78
|
+
const files = await getReportFiles();
|
|
79
|
+
if (files.length === 0) {
|
|
80
|
+
info('No reports to delete');
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const totalSize = files.reduce((sum, file) => sum + file.size, 0);
|
|
84
|
+
console.log(chalk.bold(`Found ${files.length} report(s) to delete`));
|
|
85
|
+
console.log(`Total size: ${chalk.yellow(formatSize(totalSize))}`);
|
|
86
|
+
console.log();
|
|
87
|
+
// Confirm deletion
|
|
88
|
+
if (!options.yes) {
|
|
89
|
+
const shouldDelete = await confirm({
|
|
90
|
+
message: `Delete all ${files.length} report file(s)?`,
|
|
91
|
+
default: false,
|
|
92
|
+
});
|
|
93
|
+
if (!shouldDelete) {
|
|
94
|
+
info('Cancelled');
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// Delete files
|
|
99
|
+
const spinner = createSpinner('Deleting reports...');
|
|
100
|
+
let deleted = 0;
|
|
101
|
+
let failed = 0;
|
|
102
|
+
for (const file of files) {
|
|
103
|
+
try {
|
|
104
|
+
await unlink(file.path);
|
|
105
|
+
deleted++;
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
failed++;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
succeedSpinner(spinner, 'Deletion complete');
|
|
112
|
+
console.log();
|
|
113
|
+
success(`Deleted ${deleted} report(s)`);
|
|
114
|
+
console.log(`Freed space: ${chalk.green(formatSize(totalSize))}`);
|
|
115
|
+
if (failed > 0) {
|
|
116
|
+
warning(`Failed to delete ${failed} file(s)`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Open latest report
|
|
121
|
+
*/
|
|
122
|
+
async function openLatestReport() {
|
|
123
|
+
const files = await getReportFiles();
|
|
124
|
+
if (files.length === 0) {
|
|
125
|
+
warning('No reports found');
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const latest = files[0];
|
|
129
|
+
console.log(chalk.dim(`Opening: ${latest.name}`));
|
|
130
|
+
const { exec } = await import('child_process');
|
|
131
|
+
const { promisify } = await import('util');
|
|
132
|
+
const execAsync = promisify(exec);
|
|
133
|
+
try {
|
|
134
|
+
await execAsync(`open "${latest.path}"`);
|
|
135
|
+
success('Report opened in browser');
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
warning('Failed to open report');
|
|
139
|
+
console.log(chalk.dim(`Path: ${latest.path}`));
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Execute reports command
|
|
144
|
+
*/
|
|
145
|
+
async function reportsCommand(subcommand, options) {
|
|
146
|
+
switch (subcommand) {
|
|
147
|
+
case 'list':
|
|
148
|
+
await listReports();
|
|
149
|
+
break;
|
|
150
|
+
case 'clean':
|
|
151
|
+
await cleanReports(options);
|
|
152
|
+
break;
|
|
153
|
+
case 'open':
|
|
154
|
+
await openLatestReport();
|
|
155
|
+
break;
|
|
156
|
+
default:
|
|
157
|
+
await listReports();
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Create reports command
|
|
163
|
+
*/
|
|
164
|
+
export function createReportsCommand() {
|
|
165
|
+
const cmd = new Command('reports')
|
|
166
|
+
.description('Manage cleanup reports')
|
|
167
|
+
.argument('[subcommand]', 'Subcommand: list, clean, open', 'list')
|
|
168
|
+
.option('-y, --yes', 'Skip confirmation prompts')
|
|
169
|
+
.action(async (subcommand, options) => {
|
|
170
|
+
await reportsCommand(subcommand, options);
|
|
171
|
+
});
|
|
172
|
+
return enhanceCommandHelp(cmd);
|
|
173
|
+
}
|