filemayor 2.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/core/categories.js +235 -0
- package/core/cleaner.js +527 -0
- package/core/config.js +562 -0
- package/core/index.js +79 -0
- package/core/organizer.js +528 -0
- package/core/reporter.js +572 -0
- package/core/scanner.js +436 -0
- package/core/security.js +317 -0
- package/core/sop-parser.js +565 -0
- package/core/watcher.js +478 -0
- package/index.js +536 -0
- package/package.json +55 -0
package/index.js
ADDED
|
@@ -0,0 +1,536 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ═══════════════════════════════════════════════════════════════════
|
|
5
|
+
*
|
|
6
|
+
* ███████╗██╗██╗ ███████╗███╗ ███╗ █████╗ ██╗ ██╗ ██████╗ ██████╗
|
|
7
|
+
* ██╔════╝██║██║ ██╔════╝████╗ ████║██╔══██╗╚██╗ ██╔╝██╔═══██╗██╔══██╗
|
|
8
|
+
* █████╗ ██║██║ █████╗ ██╔████╔██║███████║ ╚████╔╝ ██║ ██║██████╔╝
|
|
9
|
+
* ██╔══╝ ██║██║ ██╔══╝ ██║╚██╔╝██║██╔══██║ ╚██╔╝ ██║ ██║██╔══██╗
|
|
10
|
+
* ██║ ██║███████╗███████╗██║ ╚═╝ ██║██║ ██║ ██║ ╚██████╔╝██║ ██║
|
|
11
|
+
* ╚═╝ ╚═╝╚══════╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝
|
|
12
|
+
*
|
|
13
|
+
* Your Digital Life Organizer — CLI Edition
|
|
14
|
+
* Works everywhere: terminals, servers, data centers, CI/CD
|
|
15
|
+
*
|
|
16
|
+
* ═══════════════════════════════════════════════════════════════════
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
'use strict';
|
|
20
|
+
|
|
21
|
+
const path = require('path');
|
|
22
|
+
const os = require('os');
|
|
23
|
+
const fs = require('fs');
|
|
24
|
+
|
|
25
|
+
// Core modules
|
|
26
|
+
const { scan, scanByCategory, scanSummary } = require('./core/scanner');
|
|
27
|
+
const { organize, generatePlan, rollback, loadJournal } = require('./core/organizer');
|
|
28
|
+
const { findJunk, clean } = require('./core/cleaner');
|
|
29
|
+
const { FileWatcher } = require('./core/watcher');
|
|
30
|
+
const { loadConfig, createConfigFile } = require('./core/config');
|
|
31
|
+
const reporter = require('./core/reporter');
|
|
32
|
+
const { formatBytes } = require('./core/scanner');
|
|
33
|
+
const { getCategories } = require('./core/categories');
|
|
34
|
+
const { checkPermissions } = require('./core/security');
|
|
35
|
+
|
|
36
|
+
const { c, banner, Spinner, success, error, warn, info } = reporter;
|
|
37
|
+
|
|
38
|
+
// ─── Version ──────────────────────────────────────────────────────
|
|
39
|
+
const VERSION = '2.0.0';
|
|
40
|
+
|
|
41
|
+
// ─── Argument Parser ──────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
function parseArgs(argv) {
|
|
44
|
+
const args = {
|
|
45
|
+
command: null,
|
|
46
|
+
target: null,
|
|
47
|
+
flags: {},
|
|
48
|
+
positional: []
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const raw = argv.slice(2);
|
|
52
|
+
let i = 0;
|
|
53
|
+
|
|
54
|
+
while (i < raw.length) {
|
|
55
|
+
const arg = raw[i];
|
|
56
|
+
|
|
57
|
+
if (arg === '--help' || arg === '-h') {
|
|
58
|
+
args.flags.help = true;
|
|
59
|
+
} else if (arg === '--version' || arg === '-V') {
|
|
60
|
+
args.flags.version = true;
|
|
61
|
+
} else if (arg === '--dry-run') {
|
|
62
|
+
args.flags.dryRun = true;
|
|
63
|
+
} else if (arg === '--json') {
|
|
64
|
+
args.flags.format = 'json';
|
|
65
|
+
} else if (arg === '--csv') {
|
|
66
|
+
args.flags.format = 'csv';
|
|
67
|
+
} else if (arg === '--minimal') {
|
|
68
|
+
args.flags.format = 'minimal';
|
|
69
|
+
} else if (arg === '--yes' || arg === '-y') {
|
|
70
|
+
args.flags.yes = true;
|
|
71
|
+
} else if (arg === '--verbose' || arg === '-v') {
|
|
72
|
+
args.flags.verbose = true;
|
|
73
|
+
} else if (arg === '--quiet' || arg === '-q') {
|
|
74
|
+
args.flags.quiet = true;
|
|
75
|
+
} else if (arg === '--no-color') {
|
|
76
|
+
args.flags.noColor = true;
|
|
77
|
+
} else if (arg === '--recursive' || arg === '-r') {
|
|
78
|
+
args.flags.recursive = true;
|
|
79
|
+
} else if (arg === '--depth' && raw[i + 1]) {
|
|
80
|
+
args.flags.depth = parseInt(raw[++i], 10);
|
|
81
|
+
} else if (arg === '--naming' && raw[i + 1]) {
|
|
82
|
+
args.flags.naming = raw[++i];
|
|
83
|
+
} else if (arg === '--output' && raw[i + 1]) {
|
|
84
|
+
args.flags.outputDir = raw[++i];
|
|
85
|
+
} else if (arg === '--config' && raw[i + 1]) {
|
|
86
|
+
args.flags.config = raw[++i];
|
|
87
|
+
} else if (arg === '--duplicates' && raw[i + 1]) {
|
|
88
|
+
args.flags.duplicates = raw[++i];
|
|
89
|
+
} else if (arg === '--extensions' && raw[i + 1]) {
|
|
90
|
+
args.flags.extensions = raw[++i].split(',');
|
|
91
|
+
} else if (arg === '--category' && raw[i + 1]) {
|
|
92
|
+
args.flags.category = raw[++i].split(',');
|
|
93
|
+
} else if (arg === '--min-size' && raw[i + 1]) {
|
|
94
|
+
args.flags.minSize = parseSizeArg(raw[++i]);
|
|
95
|
+
} else if (arg === '--max-size' && raw[i + 1]) {
|
|
96
|
+
args.flags.maxSize = parseSizeArg(raw[++i]);
|
|
97
|
+
} else if (arg.startsWith('--')) {
|
|
98
|
+
// Unknown flag with potential value
|
|
99
|
+
const key = arg.slice(2).replace(/-([a-z])/g, (_, l) => l.toUpperCase());
|
|
100
|
+
if (raw[i + 1] && !raw[i + 1].startsWith('-')) {
|
|
101
|
+
args.flags[key] = raw[++i];
|
|
102
|
+
} else {
|
|
103
|
+
args.flags[key] = true;
|
|
104
|
+
}
|
|
105
|
+
} else if (arg.startsWith('-') && arg.length === 2) {
|
|
106
|
+
args.flags[arg.slice(1)] = true;
|
|
107
|
+
} else if (!args.command) {
|
|
108
|
+
args.command = arg;
|
|
109
|
+
} else if (!args.target) {
|
|
110
|
+
args.target = arg;
|
|
111
|
+
} else {
|
|
112
|
+
args.positional.push(arg);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
i++;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return args;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function parseSizeArg(str) {
|
|
122
|
+
const match = str.match(/^(\d+(?:\.\d+)?)\s*(b|kb|mb|gb|tb)?$/i);
|
|
123
|
+
if (!match) return 0;
|
|
124
|
+
const num = parseFloat(match[1]);
|
|
125
|
+
const unit = (match[2] || 'b').toLowerCase();
|
|
126
|
+
const multipliers = { b: 1, kb: 1024, mb: 1024 ** 2, gb: 1024 ** 3, tb: 1024 ** 4 };
|
|
127
|
+
return Math.floor(num * (multipliers[unit] || 1));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ─── Help Text ────────────────────────────────────────────────────
|
|
131
|
+
|
|
132
|
+
function printHelp() {
|
|
133
|
+
console.log(`
|
|
134
|
+
${banner()}
|
|
135
|
+
${c('bold', 'USAGE')}
|
|
136
|
+
${c('cyan', 'filemayor')} ${c('yellow', '<command>')} ${c('dim', '[path]')} ${c('dim', '[options]')}
|
|
137
|
+
|
|
138
|
+
${c('bold', 'COMMANDS')}
|
|
139
|
+
${c('yellow', 'scan')} ${c('dim', '<path>')} Scan directory and report contents
|
|
140
|
+
${c('yellow', 'organize')} ${c('dim', '<path>')} Organize files into categories
|
|
141
|
+
${c('yellow', 'clean')} ${c('dim', '<path>')} Find and remove junk files
|
|
142
|
+
${c('yellow', 'watch')} ${c('dim', '<path>')} Watch directory for changes (daemon)
|
|
143
|
+
${c('yellow', 'init')} Create .filemayor.yml config
|
|
144
|
+
${c('yellow', 'undo')} ${c('dim', '<path>')} Undo last organization
|
|
145
|
+
${c('yellow', 'info')} System info and version
|
|
146
|
+
|
|
147
|
+
${c('bold', 'OPTIONS')}
|
|
148
|
+
${c('dim', '--dry-run')} Preview changes without executing
|
|
149
|
+
${c('dim', '--json')} Output as JSON
|
|
150
|
+
${c('dim', '--csv')} Output as CSV
|
|
151
|
+
${c('dim', '--minimal')} Minimal output (paths only)
|
|
152
|
+
${c('dim', '--yes, -y')} Skip confirmation prompts
|
|
153
|
+
${c('dim', '--verbose, -v')} Detailed logging
|
|
154
|
+
${c('dim', '--quiet, -q')} Suppress output (exit code only)
|
|
155
|
+
${c('dim', '--no-color')} Disable colored output
|
|
156
|
+
${c('dim', '--config <path>')} Custom config file
|
|
157
|
+
${c('dim', '--depth <n>')} Max recursion depth
|
|
158
|
+
${c('dim', '--naming <type>')} Naming: original|category_prefix|date_prefix|clean
|
|
159
|
+
${c('dim', '--duplicates <s>')} Duplicates: rename|skip|overwrite
|
|
160
|
+
${c('dim', '--extensions <e>')} Filter by extensions (comma-separated)
|
|
161
|
+
${c('dim', '--output <path>')} Output directory for organized files
|
|
162
|
+
${c('dim', '--category <c>')} Filter by category (comma-separated)
|
|
163
|
+
${c('dim', '--min-size <s>')} Minimum file size (e.g., 1mb)
|
|
164
|
+
${c('dim', '--max-size <s>')} Maximum file size (e.g., 100mb)
|
|
165
|
+
${c('dim', '--help, -h')} Show this help
|
|
166
|
+
${c('dim', '--version, -V')} Show version
|
|
167
|
+
|
|
168
|
+
${c('bold', 'EXAMPLES')}
|
|
169
|
+
${c('dim', '$')} filemayor scan ~/Downloads
|
|
170
|
+
${c('dim', '$')} filemayor organize ~/Downloads --dry-run
|
|
171
|
+
${c('dim', '$')} filemayor organize ~/Downloads --naming category_prefix
|
|
172
|
+
${c('dim', '$')} filemayor clean /var/tmp --yes
|
|
173
|
+
${c('dim', '$')} filemayor scan . --json | jq '.files | length'
|
|
174
|
+
${c('dim', '$')} filemayor watch ~/Downloads
|
|
175
|
+
${c('dim', '$')} filemayor init
|
|
176
|
+
|
|
177
|
+
${c('bold', 'INSTALL')}
|
|
178
|
+
${c('dim', '$')} npm install -g filemayor
|
|
179
|
+
|
|
180
|
+
${c('dim', `FileMayor v${VERSION} — https://filemayor.app`)}
|
|
181
|
+
`);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ─── Commands ─────────────────────────────────────────────────────
|
|
185
|
+
|
|
186
|
+
async function cmdScan(target, flags, config) {
|
|
187
|
+
const targetPath = path.resolve(target || '.');
|
|
188
|
+
const format = flags.format || config.output.format;
|
|
189
|
+
|
|
190
|
+
const spinner = new Spinner(`Scanning ${c('cyan', targetPath)}...`);
|
|
191
|
+
if (format === 'table' && !flags.quiet) spinner.start();
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
const options = {
|
|
195
|
+
maxDepth: flags.depth || config.scanner.maxDepth,
|
|
196
|
+
includeHidden: config.scanner.includeHidden,
|
|
197
|
+
ignore: config.organize.ignore,
|
|
198
|
+
extensions: flags.extensions || null,
|
|
199
|
+
minSize: flags.minSize || config.scanner.minSize,
|
|
200
|
+
maxSize: flags.maxSize || config.scanner.maxSize || Infinity,
|
|
201
|
+
followSymlinks: config.scanner.followSymlinks,
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
const result = scan(targetPath, options);
|
|
205
|
+
|
|
206
|
+
spinner.stop();
|
|
207
|
+
|
|
208
|
+
if (flags.quiet) {
|
|
209
|
+
process.exit(result.files.length > 0 ? 0 : 1);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
console.log(reporter.formatScanReport(result, format));
|
|
214
|
+
} catch (err) {
|
|
215
|
+
spinner.fail(err.message);
|
|
216
|
+
process.exit(1);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async function cmdOrganize(target, flags, config) {
|
|
221
|
+
const targetPath = path.resolve(target || '.');
|
|
222
|
+
const format = flags.format || config.output.format;
|
|
223
|
+
const dryRun = flags.dryRun || false;
|
|
224
|
+
|
|
225
|
+
const spinner = new Spinner(`${dryRun ? 'Planning' : 'Organizing'} ${c('cyan', targetPath)}...`);
|
|
226
|
+
if (format === 'table' && !flags.quiet) spinner.start();
|
|
227
|
+
|
|
228
|
+
try {
|
|
229
|
+
const result = organize(targetPath, {
|
|
230
|
+
dryRun,
|
|
231
|
+
naming: flags.naming || config.organize.naming,
|
|
232
|
+
outputDir: flags.outputDir || null,
|
|
233
|
+
duplicateStrategy: flags.duplicates || config.organize.duplicates,
|
|
234
|
+
scanOptions: {
|
|
235
|
+
maxDepth: flags.depth || config.organize.maxDepth,
|
|
236
|
+
includeHidden: config.organize.includeHidden,
|
|
237
|
+
ignore: config.organize.ignore,
|
|
238
|
+
extensions: flags.extensions || null,
|
|
239
|
+
},
|
|
240
|
+
onProgress: format === 'table' ? (p) => {
|
|
241
|
+
spinner.update(`${dryRun ? 'Planning' : 'Organizing'} ${p.percent}% — ${p.file}`);
|
|
242
|
+
} : null,
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
spinner.stop();
|
|
246
|
+
|
|
247
|
+
if (flags.quiet) {
|
|
248
|
+
process.exit(0);
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
console.log(reporter.formatOrganizeReport(result, format));
|
|
253
|
+
|
|
254
|
+
if (!dryRun && result.execSummary) {
|
|
255
|
+
const exec = result.execSummary;
|
|
256
|
+
if (exec.failed > 0) process.exit(1);
|
|
257
|
+
}
|
|
258
|
+
} catch (err) {
|
|
259
|
+
spinner.fail(err.message);
|
|
260
|
+
process.exit(1);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
async function cmdClean(target, flags, config) {
|
|
265
|
+
const targetPath = path.resolve(target || '.');
|
|
266
|
+
const format = flags.format || config.output.format;
|
|
267
|
+
const dryRun = flags.dryRun || !flags.yes;
|
|
268
|
+
|
|
269
|
+
// If --yes not passed and not --dry-run, force dry run
|
|
270
|
+
if (!flags.yes && !flags.dryRun) {
|
|
271
|
+
console.log(warn('Running in dry-run mode. Use --yes to actually delete files.'));
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const spinner = new Spinner(`Scanning for junk in ${c('cyan', targetPath)}...`);
|
|
275
|
+
if (format === 'table' && !flags.quiet) spinner.start();
|
|
276
|
+
|
|
277
|
+
try {
|
|
278
|
+
const result = clean(targetPath, {
|
|
279
|
+
dryRun,
|
|
280
|
+
categories: flags.category || config.clean.categories,
|
|
281
|
+
maxDepth: flags.depth || config.clean.maxDepth,
|
|
282
|
+
includeDirectories: config.clean.includeDirectories,
|
|
283
|
+
onProgress: format === 'table' ? (p) => {
|
|
284
|
+
spinner.update(`${p.phase === 'scanning' ? 'Scanning' : 'Deleting'} ${p.current || ''}...`);
|
|
285
|
+
} : null,
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
spinner.stop();
|
|
289
|
+
|
|
290
|
+
if (flags.quiet) {
|
|
291
|
+
process.exit(result.junk.length > 0 ? 0 : 1);
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
console.log(reporter.formatCleanReport(result, format));
|
|
296
|
+
} catch (err) {
|
|
297
|
+
spinner.fail(err.message);
|
|
298
|
+
process.exit(1);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
async function cmdWatch(target, flags, config) {
|
|
303
|
+
const targetPath = path.resolve(target || '.');
|
|
304
|
+
const format = flags.format || config.output.format;
|
|
305
|
+
|
|
306
|
+
console.log(banner());
|
|
307
|
+
console.log(info(`Watching ${c('cyan', targetPath)} for changes...`));
|
|
308
|
+
console.log(c('dim', ' Press Ctrl+C to stop\n'));
|
|
309
|
+
|
|
310
|
+
const watcher = new FileWatcher({
|
|
311
|
+
directories: [targetPath, ...(config.watch.directories || [])],
|
|
312
|
+
rules: config.watch.rules || [],
|
|
313
|
+
debounceMs: config.watch.debounceMs || 500,
|
|
314
|
+
recursive: flags.recursive !== false,
|
|
315
|
+
autoOrganize: config.watch.autoOrganize || false,
|
|
316
|
+
organizeOptions: {
|
|
317
|
+
naming: config.organize.naming,
|
|
318
|
+
duplicateStrategy: config.organize.duplicates,
|
|
319
|
+
},
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
watcher.on('watching', (data) => {
|
|
323
|
+
console.log(success(`Watching: ${data.directory}`));
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
watcher.on('change', (event) => {
|
|
327
|
+
if (format === 'json') {
|
|
328
|
+
console.log(JSON.stringify(event));
|
|
329
|
+
} else {
|
|
330
|
+
const action = event.exists
|
|
331
|
+
? c('green', event.type === 'rename' ? 'NEW' : 'CHG')
|
|
332
|
+
: c('red', 'DEL');
|
|
333
|
+
const cat = event.category ? c('dim', ` [${event.category}]`) : '';
|
|
334
|
+
console.log(` ${action} ${event.filename}${cat} ${c('dim', event.sizeHuman)}`);
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
watcher.on('action', (data) => {
|
|
339
|
+
console.log(` ${c('yellow', '→')} ${data.action}: ${path.basename(data.source)} → ${data.destination || ''}`);
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
watcher.on('error', (err) => {
|
|
343
|
+
console.error(error(err.error || err.message));
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
watcher.start();
|
|
347
|
+
|
|
348
|
+
// Handle shutdown
|
|
349
|
+
process.on('SIGINT', () => {
|
|
350
|
+
console.log('\n');
|
|
351
|
+
watcher.stop();
|
|
352
|
+
const stats = watcher.getStats();
|
|
353
|
+
console.log(info(`Processed ${stats.eventsProcessed} events, ${stats.filesActioned} actions`));
|
|
354
|
+
process.exit(0);
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
async function cmdInit(flags) {
|
|
359
|
+
try {
|
|
360
|
+
const configPath = createConfigFile(process.cwd());
|
|
361
|
+
console.log(success(`Created ${c('cyan', configPath)}`));
|
|
362
|
+
console.log(c('dim', ' Edit this file to customize FileMayor settings'));
|
|
363
|
+
} catch (err) {
|
|
364
|
+
console.error(error(err.message));
|
|
365
|
+
process.exit(1);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
async function cmdUndo(target, flags) {
|
|
370
|
+
const targetPath = path.resolve(target || '.');
|
|
371
|
+
const journalPath = path.join(targetPath, '.filemayor-journal.json');
|
|
372
|
+
|
|
373
|
+
const journal = loadJournal(journalPath);
|
|
374
|
+
if (journal.length === 0) {
|
|
375
|
+
console.log(info('No operations to undo'));
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
console.log(info(`Found ${journal.length} operations to undo`));
|
|
380
|
+
|
|
381
|
+
const spinner = new Spinner('Undoing operations...');
|
|
382
|
+
spinner.start();
|
|
383
|
+
|
|
384
|
+
const result = rollback(journal, {
|
|
385
|
+
onProgress: (p) => {
|
|
386
|
+
spinner.update(`Undoing ${p.percent}% — ${p.file}`);
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
spinner.stop();
|
|
391
|
+
console.log(success(`Undone ${result.undone} operations`));
|
|
392
|
+
if (result.failed > 0) {
|
|
393
|
+
console.log(warn(`${result.failed} operations could not be undone`));
|
|
394
|
+
for (const err of result.errors) {
|
|
395
|
+
console.log(c('dim', ` ${err}`));
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Clear journal
|
|
400
|
+
try {
|
|
401
|
+
fs.writeFileSync(journalPath, '[]', 'utf8');
|
|
402
|
+
} catch { /* ignore */ }
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
async function cmdInfo(config) {
|
|
406
|
+
console.log(banner());
|
|
407
|
+
console.log(` ${c('bold', 'Version')} ${VERSION}`);
|
|
408
|
+
console.log(` ${c('bold', 'Node')} ${process.version}`);
|
|
409
|
+
console.log(` ${c('bold', 'Platform')} ${os.platform()} ${os.arch()}`);
|
|
410
|
+
console.log(` ${c('bold', 'OS')} ${os.type()} ${os.release()}`);
|
|
411
|
+
console.log(` ${c('bold', 'Home')} ${os.homedir()}`);
|
|
412
|
+
console.log(` ${c('bold', 'CWD')} ${process.cwd()}`);
|
|
413
|
+
|
|
414
|
+
// Config info
|
|
415
|
+
console.log(` ${c('bold', 'Config')} ${config._source || 'defaults'}`);
|
|
416
|
+
|
|
417
|
+
// Categories
|
|
418
|
+
const cats = getCategories();
|
|
419
|
+
const catCount = Object.keys(cats).length;
|
|
420
|
+
const extCount = Object.values(cats).reduce((sum, cat) => sum + cat.extensions.length, 0);
|
|
421
|
+
console.log(` ${c('bold', 'Categories')} ${catCount} categories, ${extCount} extensions`);
|
|
422
|
+
|
|
423
|
+
// Permissions
|
|
424
|
+
const perms = checkPermissions(process.cwd());
|
|
425
|
+
const permStr = `${perms.read ? c('green', 'R') : c('red', '-')}${perms.write ? c('green', 'W') : c('red', '-')}${perms.execute ? c('green', 'X') : c('red', '-')}`;
|
|
426
|
+
console.log(` ${c('bold', 'Permissions')} ${permStr} (current directory)`);
|
|
427
|
+
|
|
428
|
+
console.log('');
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// ─── Main Entry Point ─────────────────────────────────────────────
|
|
432
|
+
|
|
433
|
+
async function main() {
|
|
434
|
+
const args = parseArgs(process.argv);
|
|
435
|
+
|
|
436
|
+
// Version
|
|
437
|
+
if (args.flags.version) {
|
|
438
|
+
console.log(VERSION);
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Colors
|
|
443
|
+
if (args.flags.noColor || process.env.NO_COLOR) {
|
|
444
|
+
reporter.setColors(false);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Load config
|
|
448
|
+
let config;
|
|
449
|
+
try {
|
|
450
|
+
const configResult = loadConfig({
|
|
451
|
+
configPath: args.flags.config || null,
|
|
452
|
+
cliOverrides: {},
|
|
453
|
+
});
|
|
454
|
+
config = configResult.config;
|
|
455
|
+
config._source = configResult.source;
|
|
456
|
+
|
|
457
|
+
// Show validation warnings
|
|
458
|
+
if (configResult.validation.warnings.length > 0 && args.flags.verbose) {
|
|
459
|
+
for (const w of configResult.validation.warnings) {
|
|
460
|
+
console.error(warn(`Config: ${w}`));
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
if (!configResult.validation.valid) {
|
|
464
|
+
for (const e of configResult.validation.errors) {
|
|
465
|
+
console.error(error(`Config: ${e}`));
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
} catch (err) {
|
|
469
|
+
console.error(error(`Config error: ${err.message}`));
|
|
470
|
+
process.exit(1);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// No command — show help
|
|
474
|
+
if (!args.command || args.flags.help) {
|
|
475
|
+
printHelp();
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// Route commands
|
|
480
|
+
switch (args.command) {
|
|
481
|
+
case 'scan':
|
|
482
|
+
case 's':
|
|
483
|
+
await cmdScan(args.target, args.flags, config);
|
|
484
|
+
break;
|
|
485
|
+
|
|
486
|
+
case 'organize':
|
|
487
|
+
case 'org':
|
|
488
|
+
case 'o':
|
|
489
|
+
await cmdOrganize(args.target, args.flags, config);
|
|
490
|
+
break;
|
|
491
|
+
|
|
492
|
+
case 'clean':
|
|
493
|
+
case 'cl':
|
|
494
|
+
case 'c':
|
|
495
|
+
await cmdClean(args.target, args.flags, config);
|
|
496
|
+
break;
|
|
497
|
+
|
|
498
|
+
case 'watch':
|
|
499
|
+
case 'w':
|
|
500
|
+
await cmdWatch(args.target, args.flags, config);
|
|
501
|
+
break;
|
|
502
|
+
|
|
503
|
+
case 'init':
|
|
504
|
+
case 'i':
|
|
505
|
+
await cmdInit(args.flags);
|
|
506
|
+
break;
|
|
507
|
+
|
|
508
|
+
case 'undo':
|
|
509
|
+
case 'u':
|
|
510
|
+
await cmdUndo(args.target, args.flags);
|
|
511
|
+
break;
|
|
512
|
+
|
|
513
|
+
case 'info':
|
|
514
|
+
await cmdInfo(config);
|
|
515
|
+
break;
|
|
516
|
+
|
|
517
|
+
case 'help':
|
|
518
|
+
printHelp();
|
|
519
|
+
break;
|
|
520
|
+
|
|
521
|
+
default:
|
|
522
|
+
console.error(error(`Unknown command: "${args.command}"`));
|
|
523
|
+
console.log(c('dim', 'Run "filemayor --help" for usage'));
|
|
524
|
+
process.exit(1);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// ─── Run ──────────────────────────────────────────────────────────
|
|
529
|
+
|
|
530
|
+
main().catch(err => {
|
|
531
|
+
console.error(error(err.message));
|
|
532
|
+
if (process.env.DEBUG) {
|
|
533
|
+
console.error(err.stack);
|
|
534
|
+
}
|
|
535
|
+
process.exit(1);
|
|
536
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "filemayor",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "Enterprise file management engine — scan, organize, clean, and watch your filesystem from any terminal",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"filemayor": "./index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "node --test ../tests/core.test.js"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"file-manager",
|
|
14
|
+
"file-organizer",
|
|
15
|
+
"directory-scanner",
|
|
16
|
+
"cleanup",
|
|
17
|
+
"filesystem",
|
|
18
|
+
"cli",
|
|
19
|
+
"terminal",
|
|
20
|
+
"productivity",
|
|
21
|
+
"devtools",
|
|
22
|
+
"sysadmin",
|
|
23
|
+
"data-center",
|
|
24
|
+
"sop",
|
|
25
|
+
"automation"
|
|
26
|
+
],
|
|
27
|
+
"author": {
|
|
28
|
+
"name": "FileMayor",
|
|
29
|
+
"url": "https://github.com/Hrypopo/FileMayor"
|
|
30
|
+
},
|
|
31
|
+
"license": "PROPRIETARY",
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "git+https://github.com/Hrypopo/FileMayor.git"
|
|
35
|
+
},
|
|
36
|
+
"bugs": {
|
|
37
|
+
"url": "https://github.com/Hrypopo/FileMayor/issues"
|
|
38
|
+
},
|
|
39
|
+
"homepage": "https://github.com/Hrypopo/FileMayor#readme",
|
|
40
|
+
"engines": {
|
|
41
|
+
"node": ">=18.0.0"
|
|
42
|
+
},
|
|
43
|
+
"os": [
|
|
44
|
+
"win32",
|
|
45
|
+
"darwin",
|
|
46
|
+
"linux"
|
|
47
|
+
],
|
|
48
|
+
"files": [
|
|
49
|
+
"index.js",
|
|
50
|
+
"core/",
|
|
51
|
+
"package.json",
|
|
52
|
+
"../LICENSE",
|
|
53
|
+
"../README.md"
|
|
54
|
+
]
|
|
55
|
+
}
|