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/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
+ }