@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,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Debug utilities for Broom CLI
|
|
3
|
+
*/
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
let debugMode = false;
|
|
6
|
+
/**
|
|
7
|
+
* Enable debug mode
|
|
8
|
+
*/
|
|
9
|
+
export function enableDebug() {
|
|
10
|
+
debugMode = true;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Disable debug mode
|
|
14
|
+
*/
|
|
15
|
+
export function disableDebug() {
|
|
16
|
+
debugMode = false;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Check if debug mode is enabled
|
|
20
|
+
*/
|
|
21
|
+
export function isDebugEnabled() {
|
|
22
|
+
return debugMode;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Log debug message
|
|
26
|
+
*/
|
|
27
|
+
export function debug(message, ...args) {
|
|
28
|
+
if (!debugMode) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const timestamp = new Date().toISOString().split('T')[1].slice(0, 12);
|
|
32
|
+
console.log(chalk.dim(`[${timestamp}] `) + chalk.magenta('[DEBUG] ') + message, ...args);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Log debug object
|
|
36
|
+
*/
|
|
37
|
+
export function debugObj(label, obj) {
|
|
38
|
+
if (!debugMode) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const timestamp = new Date().toISOString().split('T')[1].slice(0, 12);
|
|
42
|
+
console.log(chalk.dim(`[${timestamp}] `) + chalk.magenta('[DEBUG] ') + chalk.cyan(label + ':'));
|
|
43
|
+
console.log(JSON.stringify(obj, null, 2));
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Log debug section header
|
|
47
|
+
*/
|
|
48
|
+
export function debugSection(title) {
|
|
49
|
+
if (!debugMode) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
console.log();
|
|
53
|
+
console.log(chalk.magenta('━'.repeat(60)));
|
|
54
|
+
console.log(chalk.magenta.bold(`[DEBUG] ${title}`));
|
|
55
|
+
console.log(chalk.magenta('━'.repeat(60)));
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Log debug timing
|
|
59
|
+
*/
|
|
60
|
+
export function debugTime(label) {
|
|
61
|
+
if (!debugMode) {
|
|
62
|
+
return () => { };
|
|
63
|
+
}
|
|
64
|
+
const start = performance.now();
|
|
65
|
+
debug(`Starting: ${label}`);
|
|
66
|
+
return () => {
|
|
67
|
+
const duration = performance.now() - start;
|
|
68
|
+
debug(`Completed: ${label} (${duration.toFixed(2)}ms)`);
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Debug wrapper for async functions
|
|
73
|
+
*/
|
|
74
|
+
export async function debugAsync(label, fn) {
|
|
75
|
+
const end = debugTime(label);
|
|
76
|
+
try {
|
|
77
|
+
const result = await fn();
|
|
78
|
+
end();
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
debug(`Error in ${label}: ${error}`);
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Log file operation
|
|
88
|
+
*/
|
|
89
|
+
export function debugFile(operation, path, details) {
|
|
90
|
+
if (!debugMode) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const opColors = {
|
|
94
|
+
scan: chalk.blue,
|
|
95
|
+
read: chalk.cyan,
|
|
96
|
+
write: chalk.yellow,
|
|
97
|
+
delete: chalk.red,
|
|
98
|
+
skip: chalk.gray,
|
|
99
|
+
};
|
|
100
|
+
const colorFn = opColors[operation.toLowerCase()] || chalk.white;
|
|
101
|
+
const detailsStr = details ? chalk.dim(` (${details})`) : '';
|
|
102
|
+
debug(`${colorFn(operation.toUpperCase().padEnd(6))} ${path}${detailsStr}`);
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Log risk level info
|
|
106
|
+
*/
|
|
107
|
+
export function debugRisk(path, level, reason) {
|
|
108
|
+
if (!debugMode) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const colors = {
|
|
112
|
+
safe: chalk.green,
|
|
113
|
+
moderate: chalk.yellow,
|
|
114
|
+
risky: chalk.red,
|
|
115
|
+
};
|
|
116
|
+
const colorFn = colors[level];
|
|
117
|
+
const reasonStr = reason ? chalk.dim(` - ${reason}`) : '';
|
|
118
|
+
debug(`Risk: ${colorFn(level.toUpperCase())} ${path}${reasonStr}`);
|
|
119
|
+
}
|
package/dist/utils/fs.js
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File system utilities for Broom CLI
|
|
3
|
+
*/
|
|
4
|
+
import { stat, readdir, rm, access, lstat, unlink } from 'fs/promises';
|
|
5
|
+
import { existsSync } from 'fs';
|
|
6
|
+
import { join, resolve } from 'path';
|
|
7
|
+
import { homedir } from 'os';
|
|
8
|
+
import { exec } from 'child_process';
|
|
9
|
+
import { promisify } from 'util';
|
|
10
|
+
const execAsync = promisify(exec);
|
|
11
|
+
// Protected system paths that should NEVER be deleted
|
|
12
|
+
const PROTECTED_PATHS = [
|
|
13
|
+
'/System',
|
|
14
|
+
'/usr',
|
|
15
|
+
'/bin',
|
|
16
|
+
'/sbin',
|
|
17
|
+
'/etc',
|
|
18
|
+
'/var/log',
|
|
19
|
+
'/var/db',
|
|
20
|
+
'/var/root',
|
|
21
|
+
'/private/var/db',
|
|
22
|
+
'/private/var/root',
|
|
23
|
+
'/Library/Apple',
|
|
24
|
+
'/Applications/Utilities',
|
|
25
|
+
];
|
|
26
|
+
// Excluded paths (iCloud Drive, etc.)
|
|
27
|
+
const EXCLUDED_PATHS = ['iCloud Drive', 'Mobile Documents', '.Trash'];
|
|
28
|
+
// Allowed cache/temp paths
|
|
29
|
+
const ALLOWED_PATHS = [
|
|
30
|
+
'/tmp',
|
|
31
|
+
'/private/tmp',
|
|
32
|
+
'/var/tmp',
|
|
33
|
+
'/private/var/tmp',
|
|
34
|
+
'/var/folders',
|
|
35
|
+
'/private/var/folders',
|
|
36
|
+
];
|
|
37
|
+
/**
|
|
38
|
+
* Check if a path should be excluded from scanning
|
|
39
|
+
*/
|
|
40
|
+
export function isExcludedPath(path) {
|
|
41
|
+
const normalizedPath = path.toLowerCase();
|
|
42
|
+
for (const excluded of EXCLUDED_PATHS) {
|
|
43
|
+
if (normalizedPath.includes(excluded.toLowerCase())) {
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Check if a path is a protected system path
|
|
51
|
+
*/
|
|
52
|
+
export function isProtectedPath(path) {
|
|
53
|
+
const normalizedPath = resolve(path);
|
|
54
|
+
// Check allowed paths first
|
|
55
|
+
for (const allowed of ALLOWED_PATHS) {
|
|
56
|
+
if (normalizedPath.startsWith(allowed)) {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// Check protected paths
|
|
61
|
+
for (const protected_ of PROTECTED_PATHS) {
|
|
62
|
+
if (normalizedPath === protected_ || normalizedPath.startsWith(protected_ + '/')) {
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Check if a path exists
|
|
70
|
+
*/
|
|
71
|
+
export function exists(path) {
|
|
72
|
+
return existsSync(path);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Check if a path exists (async)
|
|
76
|
+
*/
|
|
77
|
+
export async function existsAsync(path) {
|
|
78
|
+
try {
|
|
79
|
+
await access(path);
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Get file/directory size recursively
|
|
88
|
+
* Uses 'du' command for efficiency on large directories
|
|
89
|
+
*/
|
|
90
|
+
export async function getSize(path) {
|
|
91
|
+
try {
|
|
92
|
+
const stats = await stat(path);
|
|
93
|
+
if (stats.isFile()) {
|
|
94
|
+
return stats.size;
|
|
95
|
+
}
|
|
96
|
+
if (stats.isDirectory()) {
|
|
97
|
+
// Use du command for efficient directory size calculation
|
|
98
|
+
try {
|
|
99
|
+
const { stdout } = await execAsync(`du -sk "${path}" 2>/dev/null || echo "0"`);
|
|
100
|
+
const sizeKb = parseInt(stdout.split('\t')[0], 10);
|
|
101
|
+
return isNaN(sizeKb) ? 0 : sizeKb * 1024;
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
// Fallback to recursive calculation for small directories
|
|
105
|
+
return getSizeRecursive(path);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return 0;
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return 0;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Recursive size calculation (fallback)
|
|
116
|
+
*/
|
|
117
|
+
async function getSizeRecursive(path, depth = 0, maxDepth = 5) {
|
|
118
|
+
if (depth > maxDepth) {
|
|
119
|
+
return 0; // Limit recursion depth
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
const stats = await stat(path);
|
|
123
|
+
if (stats.isFile()) {
|
|
124
|
+
return stats.size;
|
|
125
|
+
}
|
|
126
|
+
if (stats.isDirectory()) {
|
|
127
|
+
const files = await readdir(path);
|
|
128
|
+
let total = 0;
|
|
129
|
+
// Process in batches to avoid memory issues
|
|
130
|
+
for (const file of files) {
|
|
131
|
+
total += await getSizeRecursive(join(path, file), depth + 1, maxDepth);
|
|
132
|
+
}
|
|
133
|
+
return total;
|
|
134
|
+
}
|
|
135
|
+
return 0;
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
return 0;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Format bytes to human-readable string
|
|
143
|
+
*/
|
|
144
|
+
export function formatSize(bytes) {
|
|
145
|
+
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
146
|
+
let size = bytes;
|
|
147
|
+
let unitIndex = 0;
|
|
148
|
+
while (size >= 1024 && unitIndex < units.length - 1) {
|
|
149
|
+
size /= 1024;
|
|
150
|
+
unitIndex++;
|
|
151
|
+
}
|
|
152
|
+
return `${size.toFixed(2)} ${units[unitIndex]}`;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Get all files in a directory (non-recursive)
|
|
156
|
+
*/
|
|
157
|
+
export async function getFilesInDirectory(path) {
|
|
158
|
+
try {
|
|
159
|
+
if (!exists(path)) {
|
|
160
|
+
return [];
|
|
161
|
+
}
|
|
162
|
+
const stats = await stat(path);
|
|
163
|
+
if (!stats.isDirectory()) {
|
|
164
|
+
return [];
|
|
165
|
+
}
|
|
166
|
+
const files = await readdir(path);
|
|
167
|
+
return files.map((file) => join(path, file));
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
return [];
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Check if path is a directory
|
|
175
|
+
*/
|
|
176
|
+
export async function isDirectory(path) {
|
|
177
|
+
try {
|
|
178
|
+
const stats = await stat(path);
|
|
179
|
+
return stats.isDirectory();
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Remove a file or directory
|
|
187
|
+
*/
|
|
188
|
+
export async function remove(path) {
|
|
189
|
+
if (isProtectedPath(path)) {
|
|
190
|
+
throw new Error(`Cannot remove protected path: ${path}`);
|
|
191
|
+
}
|
|
192
|
+
try {
|
|
193
|
+
await rm(path, { recursive: true, force: true });
|
|
194
|
+
}
|
|
195
|
+
catch (error) {
|
|
196
|
+
throw new Error(`Failed to remove ${path}: ${error.message}`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Remove a single item
|
|
201
|
+
*/
|
|
202
|
+
export async function removeItem(path, dryRun = false) {
|
|
203
|
+
if (isProtectedPath(path)) {
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
if (dryRun) {
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
try {
|
|
210
|
+
const stats = await lstat(path);
|
|
211
|
+
if (stats.isSymbolicLink()) {
|
|
212
|
+
await unlink(path);
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
await rm(path, { recursive: true, force: true });
|
|
216
|
+
}
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Remove multiple items
|
|
225
|
+
*/
|
|
226
|
+
export async function removeItems(items, dryRun = false, onProgress) {
|
|
227
|
+
let success = 0;
|
|
228
|
+
let failed = 0;
|
|
229
|
+
let freedSpace = 0;
|
|
230
|
+
for (let i = 0; i < items.length; i++) {
|
|
231
|
+
const item = items[i];
|
|
232
|
+
if (onProgress) {
|
|
233
|
+
onProgress(i + 1, items.length, item);
|
|
234
|
+
}
|
|
235
|
+
const removed = await removeItem(item.path, dryRun);
|
|
236
|
+
if (removed) {
|
|
237
|
+
success++;
|
|
238
|
+
freedSpace += item.size;
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
failed++;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return { success, failed, freedSpace };
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Create a cleanable item from a path
|
|
248
|
+
*/
|
|
249
|
+
export async function createCleanableItem(path) {
|
|
250
|
+
try {
|
|
251
|
+
if (!exists(path)) {
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
const stats = await stat(path);
|
|
255
|
+
const size = await getSize(path);
|
|
256
|
+
const name = path.split('/').pop() || path;
|
|
257
|
+
return {
|
|
258
|
+
path,
|
|
259
|
+
size,
|
|
260
|
+
name,
|
|
261
|
+
isDirectory: stats.isDirectory(),
|
|
262
|
+
modifiedAt: stats.mtime,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
catch {
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Get home directory
|
|
271
|
+
*/
|
|
272
|
+
export function getHomeDir() {
|
|
273
|
+
return homedir();
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Expand ~ to home directory
|
|
277
|
+
*/
|
|
278
|
+
export function expandPath(path) {
|
|
279
|
+
if (path.startsWith('~')) {
|
|
280
|
+
return path.replace('~', homedir());
|
|
281
|
+
}
|
|
282
|
+
return path;
|
|
283
|
+
}
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom help utilities for enhanced command option display
|
|
3
|
+
*/
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
/**
|
|
6
|
+
* Analyze option to determine its type and properties
|
|
7
|
+
*/
|
|
8
|
+
function analyzeOption(option) {
|
|
9
|
+
const flags = option.flags;
|
|
10
|
+
const description = option.description || '';
|
|
11
|
+
let type = 'boolean';
|
|
12
|
+
let required = false;
|
|
13
|
+
if (flags.includes('<') && flags.includes('>')) {
|
|
14
|
+
required = true;
|
|
15
|
+
const argMatch = flags.match(/<([^>]+)>/);
|
|
16
|
+
const argName = argMatch ? argMatch[1] : '';
|
|
17
|
+
if (argName.toLowerCase().includes('number') ||
|
|
18
|
+
argName.toLowerCase().includes('count') ||
|
|
19
|
+
argName.toLowerCase().includes('seconds') ||
|
|
20
|
+
argName.toLowerCase().includes('interval') ||
|
|
21
|
+
argName.toLowerCase().includes('depth') ||
|
|
22
|
+
argName.toLowerCase().includes('limit')) {
|
|
23
|
+
type = 'number';
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
type = 'string';
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
else if (flags.includes('[') && flags.includes(']')) {
|
|
30
|
+
required = false;
|
|
31
|
+
const argMatch = flags.match(/\[([^\]]+)\]/);
|
|
32
|
+
const argName = argMatch ? argMatch[1] : '';
|
|
33
|
+
if (argName.toLowerCase().includes('number') ||
|
|
34
|
+
argName.toLowerCase().includes('count') ||
|
|
35
|
+
argName.toLowerCase().includes('seconds') ||
|
|
36
|
+
argName.toLowerCase().includes('interval') ||
|
|
37
|
+
argName.toLowerCase().includes('depth') ||
|
|
38
|
+
argName.toLowerCase().includes('limit')) {
|
|
39
|
+
type = 'number';
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
type = 'string';
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
flags,
|
|
47
|
+
description,
|
|
48
|
+
type,
|
|
49
|
+
required,
|
|
50
|
+
defaultValue: option.defaultValue?.toString(),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Strip ANSI codes to get actual visual length
|
|
55
|
+
*/
|
|
56
|
+
function getVisualLength(str) {
|
|
57
|
+
return str.replace(/\x1b\[[0-9;]*m/g, '').length;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Generate formatted options table with perfect alignment
|
|
61
|
+
*/
|
|
62
|
+
export function generateOptionsTable(command) {
|
|
63
|
+
const options = command.options;
|
|
64
|
+
if (options.length === 0) {
|
|
65
|
+
return chalk.dim(' No options available.');
|
|
66
|
+
}
|
|
67
|
+
const optionInfos = options.map(analyzeOption);
|
|
68
|
+
const flagWidth = 24;
|
|
69
|
+
const typeWidth = 10;
|
|
70
|
+
const reqWidth = 10;
|
|
71
|
+
const descWidth = 49;
|
|
72
|
+
let output = '\n';
|
|
73
|
+
// Calculate border length
|
|
74
|
+
const borderLength = flagWidth + typeWidth + reqWidth + descWidth + 3;
|
|
75
|
+
// Top border
|
|
76
|
+
output += ' ' + '─'.repeat(borderLength) + '\n';
|
|
77
|
+
// Header
|
|
78
|
+
output +=
|
|
79
|
+
' ' +
|
|
80
|
+
'FLAG'.padEnd(flagWidth) +
|
|
81
|
+
'TYPE'.padEnd(typeWidth) +
|
|
82
|
+
'REQUIRED'.padEnd(reqWidth) +
|
|
83
|
+
'DESCRIPTION'.padEnd(descWidth) +
|
|
84
|
+
'\n';
|
|
85
|
+
// Header separator
|
|
86
|
+
output += ' ' + '─'.repeat(borderLength) + '\n';
|
|
87
|
+
// Data rows
|
|
88
|
+
for (const info of optionInfos) {
|
|
89
|
+
const desc = info.description + (info.defaultValue ? ` (default: ${info.defaultValue})` : '');
|
|
90
|
+
const truncDesc = desc.length > descWidth - 2 ? desc.substring(0, descWidth - 5) + '...' : desc;
|
|
91
|
+
const flagCell = chalk.cyan(info.flags);
|
|
92
|
+
const typeCell = getTypeColor(info.type);
|
|
93
|
+
const reqCell = info.required ? chalk.red('Yes') : chalk.green('No');
|
|
94
|
+
const descCell = truncDesc;
|
|
95
|
+
const flagPad = flagWidth - getVisualLength(flagCell);
|
|
96
|
+
const typePad = typeWidth - getVisualLength(typeCell);
|
|
97
|
+
const reqPad = reqWidth - getVisualLength(reqCell);
|
|
98
|
+
const descPad = descWidth - getVisualLength(descCell);
|
|
99
|
+
output +=
|
|
100
|
+
' ' +
|
|
101
|
+
flagCell +
|
|
102
|
+
' '.repeat(Math.max(0, flagPad)) +
|
|
103
|
+
typeCell +
|
|
104
|
+
' '.repeat(Math.max(0, typePad)) +
|
|
105
|
+
reqCell +
|
|
106
|
+
' '.repeat(Math.max(0, reqPad)) +
|
|
107
|
+
descCell +
|
|
108
|
+
' '.repeat(Math.max(0, descPad)) +
|
|
109
|
+
'\n';
|
|
110
|
+
}
|
|
111
|
+
// Bottom border
|
|
112
|
+
output += ' ' + '─'.repeat(borderLength) + '\n';
|
|
113
|
+
return output;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Get colored type string
|
|
117
|
+
*/
|
|
118
|
+
function getTypeColor(type) {
|
|
119
|
+
switch (type) {
|
|
120
|
+
case 'boolean':
|
|
121
|
+
return chalk.green('boolean');
|
|
122
|
+
case 'string':
|
|
123
|
+
return chalk.blue('string');
|
|
124
|
+
case 'number':
|
|
125
|
+
return chalk.yellow('number');
|
|
126
|
+
default:
|
|
127
|
+
return chalk.white('unknown');
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Enhanced help formatter that includes options table
|
|
132
|
+
*/
|
|
133
|
+
export function formatCommandHelp(command) {
|
|
134
|
+
const name = command.name();
|
|
135
|
+
const description = command.description();
|
|
136
|
+
const usage = command.usage();
|
|
137
|
+
const args = command.args;
|
|
138
|
+
let help = '';
|
|
139
|
+
help += chalk.bold('Usage: ') + `${name} ${usage || '[options]'}\n\n`;
|
|
140
|
+
if (description) {
|
|
141
|
+
help += description + '\n\n';
|
|
142
|
+
}
|
|
143
|
+
if (args.length > 0) {
|
|
144
|
+
help += chalk.bold('Arguments:\n');
|
|
145
|
+
for (const arg of args) {
|
|
146
|
+
const argName = arg.name || arg;
|
|
147
|
+
const argDesc = arg.description || '';
|
|
148
|
+
const required = `[${argName}]`;
|
|
149
|
+
help += ` ${required.padEnd(20)} ${argDesc}\n`;
|
|
150
|
+
}
|
|
151
|
+
help += '\n';
|
|
152
|
+
}
|
|
153
|
+
help += generateOptionsTable(command);
|
|
154
|
+
const examples = getExamplesFromDescription(description);
|
|
155
|
+
if (examples.length > 0) {
|
|
156
|
+
help += '\n' + chalk.bold('Examples:\n');
|
|
157
|
+
examples.forEach((example) => {
|
|
158
|
+
help += ` ${chalk.dim('$')} ${chalk.cyan(example)}\n`;
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
return help;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Extract examples from command description
|
|
165
|
+
*/
|
|
166
|
+
function getExamplesFromDescription(description) {
|
|
167
|
+
if (!description) {
|
|
168
|
+
return [];
|
|
169
|
+
}
|
|
170
|
+
const examples = [];
|
|
171
|
+
if (description.includes('cleanup') || description.includes('clean')) {
|
|
172
|
+
examples.push('broom clean --dry-run', 'broom clean --all', 'broom clean --debug');
|
|
173
|
+
}
|
|
174
|
+
if (description.includes('uninstall') || description.includes('remove')) {
|
|
175
|
+
examples.push('broom uninstall --dry-run', 'broom uninstall --debug');
|
|
176
|
+
}
|
|
177
|
+
if (description.includes('optimize') || description.includes('maintenance')) {
|
|
178
|
+
examples.push('broom optimize --all', 'broom optimize --dry-run');
|
|
179
|
+
}
|
|
180
|
+
if (description.includes('analyze')) {
|
|
181
|
+
examples.push('broom analyze ~/Downloads', 'broom analyze --depth 2');
|
|
182
|
+
}
|
|
183
|
+
if (description.includes('status') || description.includes('monitor')) {
|
|
184
|
+
examples.push('broom status', 'broom status --interval 5');
|
|
185
|
+
}
|
|
186
|
+
return examples;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Apply custom help formatting to a command
|
|
190
|
+
*/
|
|
191
|
+
export function enhanceCommandHelp(command) {
|
|
192
|
+
command.configureHelp({
|
|
193
|
+
formatHelp: () => formatCommandHelp(command),
|
|
194
|
+
});
|
|
195
|
+
return command;
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Global options information for main help
|
|
199
|
+
*/
|
|
200
|
+
export function getGlobalOptionsTable() {
|
|
201
|
+
const globalOptions = [
|
|
202
|
+
{
|
|
203
|
+
flags: '-v, --version',
|
|
204
|
+
description: 'Output the current version',
|
|
205
|
+
type: 'boolean',
|
|
206
|
+
required: false,
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
flags: '--debug',
|
|
210
|
+
description: 'Enable debug mode with detailed logs',
|
|
211
|
+
type: 'boolean',
|
|
212
|
+
required: false,
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
flags: '-h, --help',
|
|
216
|
+
description: 'Display help for command',
|
|
217
|
+
type: 'boolean',
|
|
218
|
+
required: false,
|
|
219
|
+
},
|
|
220
|
+
];
|
|
221
|
+
const flagWidth = 24;
|
|
222
|
+
const typeWidth = 10;
|
|
223
|
+
const reqWidth = 10;
|
|
224
|
+
const descWidth = 49;
|
|
225
|
+
let output = '\n';
|
|
226
|
+
// Calculate border length
|
|
227
|
+
const borderLength = flagWidth + typeWidth + reqWidth + descWidth + 3;
|
|
228
|
+
// Top border
|
|
229
|
+
output += ' ' + '─'.repeat(borderLength) + '\n';
|
|
230
|
+
// Header
|
|
231
|
+
output +=
|
|
232
|
+
' ' +
|
|
233
|
+
'FLAG'.padEnd(flagWidth) +
|
|
234
|
+
'TYPE'.padEnd(typeWidth) +
|
|
235
|
+
'REQUIRED'.padEnd(reqWidth) +
|
|
236
|
+
'DESCRIPTION'.padEnd(descWidth) +
|
|
237
|
+
'\n';
|
|
238
|
+
// Header separator
|
|
239
|
+
output += ' ' + '─'.repeat(borderLength) + '\n';
|
|
240
|
+
// Data rows
|
|
241
|
+
for (const option of globalOptions) {
|
|
242
|
+
const flagCell = chalk.cyan(option.flags);
|
|
243
|
+
const typeCell = getTypeColor(option.type);
|
|
244
|
+
const reqCell = option.required ? chalk.red('Yes') : chalk.green('No');
|
|
245
|
+
const descCell = option.description;
|
|
246
|
+
const flagPad = flagWidth - getVisualLength(flagCell);
|
|
247
|
+
const typePad = typeWidth - getVisualLength(typeCell);
|
|
248
|
+
const reqPad = reqWidth - getVisualLength(reqCell);
|
|
249
|
+
const descPad = descWidth - getVisualLength(descCell);
|
|
250
|
+
output +=
|
|
251
|
+
' ' +
|
|
252
|
+
flagCell +
|
|
253
|
+
' '.repeat(Math.max(0, flagPad)) +
|
|
254
|
+
typeCell +
|
|
255
|
+
' '.repeat(Math.max(0, typePad)) +
|
|
256
|
+
reqCell +
|
|
257
|
+
' '.repeat(Math.max(0, reqPad)) +
|
|
258
|
+
descCell +
|
|
259
|
+
' '.repeat(Math.max(0, descPad)) +
|
|
260
|
+
'\n';
|
|
261
|
+
}
|
|
262
|
+
// Bottom border
|
|
263
|
+
output += ' ' + '─'.repeat(borderLength) + '\n';
|
|
264
|
+
return output;
|
|
265
|
+
}
|