@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.
Files changed (51) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +554 -0
  3. package/dist/commands/analyze.js +371 -0
  4. package/dist/commands/backup.js +257 -0
  5. package/dist/commands/clean.js +255 -0
  6. package/dist/commands/completion.js +714 -0
  7. package/dist/commands/config.js +474 -0
  8. package/dist/commands/doctor.js +280 -0
  9. package/dist/commands/duplicates.js +325 -0
  10. package/dist/commands/help.js +34 -0
  11. package/dist/commands/index.js +22 -0
  12. package/dist/commands/installer.js +266 -0
  13. package/dist/commands/optimize.js +270 -0
  14. package/dist/commands/purge.js +271 -0
  15. package/dist/commands/remove.js +184 -0
  16. package/dist/commands/reports.js +173 -0
  17. package/dist/commands/schedule.js +249 -0
  18. package/dist/commands/status.js +468 -0
  19. package/dist/commands/touchid.js +230 -0
  20. package/dist/commands/uninstall.js +336 -0
  21. package/dist/commands/update.js +182 -0
  22. package/dist/commands/watch.js +258 -0
  23. package/dist/index.js +131 -0
  24. package/dist/scanners/base.js +21 -0
  25. package/dist/scanners/browser-cache.js +111 -0
  26. package/dist/scanners/dev-cache.js +64 -0
  27. package/dist/scanners/docker.js +96 -0
  28. package/dist/scanners/downloads.js +66 -0
  29. package/dist/scanners/homebrew.js +82 -0
  30. package/dist/scanners/index.js +126 -0
  31. package/dist/scanners/installer.js +87 -0
  32. package/dist/scanners/ios-backups.js +82 -0
  33. package/dist/scanners/node-modules.js +75 -0
  34. package/dist/scanners/temp-files.js +65 -0
  35. package/dist/scanners/trash.js +90 -0
  36. package/dist/scanners/user-cache.js +62 -0
  37. package/dist/scanners/user-logs.js +53 -0
  38. package/dist/scanners/xcode.js +124 -0
  39. package/dist/types/index.js +23 -0
  40. package/dist/ui/index.js +5 -0
  41. package/dist/ui/monitors.js +345 -0
  42. package/dist/ui/output.js +304 -0
  43. package/dist/ui/prompts.js +270 -0
  44. package/dist/utils/config.js +133 -0
  45. package/dist/utils/debug.js +119 -0
  46. package/dist/utils/fs.js +283 -0
  47. package/dist/utils/help.js +265 -0
  48. package/dist/utils/index.js +6 -0
  49. package/dist/utils/paths.js +142 -0
  50. package/dist/utils/report.js +404 -0
  51. 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
+ }
@@ -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
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Utils module exports
3
+ */
4
+ export * from './fs.js';
5
+ export * from './paths.js';
6
+ export * from './config.js';