filemayor 2.0.5 → 3.6.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/vault.js ADDED
@@ -0,0 +1,165 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * ═══════════════════════════════════════════════════════════════════
5
+ * FILEMAYOR CORE — VAULT (ZERO-DEPENDENCY SECRET MANAGER)
6
+ *
7
+ * Uses the OS-native credential store to keep API keys out of
8
+ * the filesystem entirely. Falls back to .env / environment
9
+ * variables if the system keychain is unavailable.
10
+ *
11
+ * Supported backends:
12
+ * macOS → security (Keychain Services)
13
+ * Windows → cmdkey / PowerShell CredentialManager
14
+ * Linux → secret-tool (freedesktop Secret Service)
15
+ * ═══════════════════════════════════════════════════════════════════
16
+ */
17
+
18
+ 'use strict';
19
+
20
+ const { execSync } = require('child_process');
21
+ const path = require('path');
22
+ const fs = require('fs');
23
+
24
+ const SERVICE_NAME = 'FileMayor';
25
+
26
+ // ─── Platform-Specific Implementations ───────────────────────────
27
+
28
+ function _saveSecret_darwin(key, value) {
29
+ // -U flag updates if already exists
30
+ execSync(
31
+ `security add-generic-password -a "${SERVICE_NAME}" -s "${key}" -w "${value}" -U`,
32
+ { stdio: 'ignore' }
33
+ );
34
+ }
35
+
36
+ function _getSecret_darwin(key) {
37
+ return execSync(
38
+ `security find-generic-password -a "${SERVICE_NAME}" -s "${key}" -w`,
39
+ { encoding: 'utf8' }
40
+ ).trim();
41
+ }
42
+
43
+ function _deleteSecret_darwin(key) {
44
+ execSync(
45
+ `security delete-generic-password -a "${SERVICE_NAME}" -s "${key}"`,
46
+ { stdio: 'ignore' }
47
+ );
48
+ }
49
+
50
+ function _saveSecret_win32(key, value) {
51
+ // Use PowerShell to store in Windows Credential Manager
52
+ const cmd = `powershell -Command "cmdkey /generic:${SERVICE_NAME}_${key} /user:${key} /pass:${value}"`;
53
+ execSync(cmd, { stdio: 'ignore' });
54
+ }
55
+
56
+ function _getSecret_win32(key) {
57
+ // Use PowerShell to retrieve from Windows Credential Manager
58
+ const cmd = `powershell -Command "(cmdkey /list:${SERVICE_NAME}_${key}) -match 'User' | Out-Null; $cred = Get-StoredCredential -Target ${SERVICE_NAME}_${key}; if($cred){$cred.GetNetworkCredential().Password}else{throw 'Not found'}"`;
59
+ try {
60
+ return execSync(cmd, { encoding: 'utf8' }).trim();
61
+ } catch {
62
+ // Fallback: try simpler cmdkey parsing
63
+ return null;
64
+ }
65
+ }
66
+
67
+ function _saveSecret_linux(key, value) {
68
+ execSync(
69
+ `echo "${value}" | secret-tool store --label="${SERVICE_NAME} ${key}" service "${SERVICE_NAME}" key "${key}"`,
70
+ { stdio: 'ignore' }
71
+ );
72
+ }
73
+
74
+ function _getSecret_linux(key) {
75
+ return execSync(
76
+ `secret-tool lookup service "${SERVICE_NAME}" key "${key}"`,
77
+ { encoding: 'utf8' }
78
+ ).trim();
79
+ }
80
+
81
+ // ─── The Vault ────────────────────────────────────────────────────
82
+
83
+ const Vault = {
84
+ /**
85
+ * Save a secret to the system keychain
86
+ * @param {string} key - Secret name (e.g., 'GEMINI_API_KEY')
87
+ * @param {string} value - Secret value
88
+ * @returns {boolean} Success
89
+ */
90
+ saveSecret(key, value) {
91
+ try {
92
+ if (process.platform === 'darwin') _saveSecret_darwin(key, value);
93
+ else if (process.platform === 'win32') _saveSecret_win32(key, value);
94
+ else _saveSecret_linux(key, value);
95
+ return true;
96
+ } catch (err) {
97
+ console.warn(`[Vault] Could not save to system keychain: ${err.message}`);
98
+ return false;
99
+ }
100
+ },
101
+
102
+ /**
103
+ * Retrieve a secret — checks System Keychain → ENV → .env file
104
+ * @param {string} key - Secret name
105
+ * @returns {string|null} The secret value, or null
106
+ */
107
+ getSecret(key) {
108
+ // Priority 1: System Keychain (most secure)
109
+ try {
110
+ if (process.platform === 'darwin') return _getSecret_darwin(key);
111
+ else if (process.platform === 'win32') {
112
+ const val = _getSecret_win32(key);
113
+ if (val) return val;
114
+ }
115
+ else return _getSecret_linux(key);
116
+ } catch { /* Keychain not available or key not found */ }
117
+
118
+ // Priority 2: Environment Variable
119
+ if (process.env[key]) return process.env[key];
120
+
121
+ // Priority 3: .env file (least secure, but common)
122
+ try {
123
+ const envPath = path.join(process.cwd(), '.env');
124
+ if (fs.existsSync(envPath)) {
125
+ const envFile = fs.readFileSync(envPath, 'utf-8');
126
+ for (const line of envFile.split('\n')) {
127
+ const trimmed = line.trim();
128
+ if (trimmed.startsWith('#') || !trimmed) continue;
129
+ const [k, ...vals] = trimmed.split('=');
130
+ if (k.trim() === key && vals.length) {
131
+ return vals.join('=').trim();
132
+ }
133
+ }
134
+ }
135
+ } catch { /* .env not available */ }
136
+
137
+ return null;
138
+ },
139
+
140
+ /**
141
+ * Delete a secret from the system keychain
142
+ * @param {string} key - Secret name
143
+ * @returns {boolean} Success
144
+ */
145
+ deleteSecret(key) {
146
+ try {
147
+ if (process.platform === 'darwin') _deleteSecret_darwin(key);
148
+ // Windows and Linux deletion handled differently
149
+ return true;
150
+ } catch {
151
+ return false;
152
+ }
153
+ },
154
+
155
+ /**
156
+ * Check if a secret exists anywhere in the priority chain
157
+ * @param {string} key
158
+ * @returns {boolean}
159
+ */
160
+ hasSecret(key) {
161
+ return Vault.getSecret(key) !== null;
162
+ }
163
+ };
164
+
165
+ module.exports = Vault;
package/index.js CHANGED
@@ -19,6 +19,22 @@
19
19
 
20
20
  'use strict';
21
21
 
22
+ // ─── Load .env (zero-dependency) ─────────────────────────────────
23
+ try {
24
+ const _fs = require('fs');
25
+ const _path = require('path');
26
+ const envPath = _path.join(__dirname, '..', '.env');
27
+ _fs.readFileSync(envPath, 'utf-8').split('\n').forEach(line => {
28
+ const trimmed = line.trim();
29
+ if (trimmed && !trimmed.startsWith('#')) {
30
+ const [key, ...vals] = trimmed.split('=');
31
+ if (key && vals.length && !process.env[key.trim()]) {
32
+ process.env[key.trim()] = vals.join('=').trim();
33
+ }
34
+ }
35
+ });
36
+ } catch { /* .env not found — that's fine, user may set env vars directly */ }
37
+
22
38
  const path = require('path');
23
39
  const os = require('os');
24
40
  const fs = require('fs');
@@ -29,16 +45,18 @@ const { organize, generatePlan, rollback, loadJournal } = require('./core/organi
29
45
  const { findJunk, clean } = require('./core/cleaner');
30
46
  const { FileWatcher } = require('./core/watcher');
31
47
  const { loadConfig, createConfigFile } = require('./core/config');
48
+ const { analyzeDirectory } = require('./core/analyzer');
32
49
  const reporter = require('./core/reporter');
33
50
  const { formatBytes } = require('./core/scanner');
34
51
  const { getCategories } = require('./core/categories');
35
52
  const { checkPermissions } = require('./core/security');
36
53
  const { activateLicense, deactivateLicense, getLicenseInfo, checkProFeature, checkBulkLimit } = require('./core/license');
54
+ const { ExplainEngine, CureEngine, ApplyEngine, DedupeEngine } = require('./core/index');
37
55
 
38
56
  const { c, banner, Spinner, success, error, warn, info } = reporter;
39
57
 
40
58
  // ─── Version ──────────────────────────────────────────────────────
41
- const VERSION = '2.0.5';
59
+ const VERSION = '3.6.0';
42
60
 
43
61
  // ─── Path Helpers ─────────────────────────────────────────────────
44
62
 
@@ -153,8 +171,15 @@ ${banner()}
153
171
  ${c('bold', 'USAGE')}
154
172
  ${c('cyan', 'filemayor')} ${c('yellow', '<command>')} ${c('dim', '[path]')} ${c('dim', '[options]')}
155
173
 
174
+ ${c('yellow', 'explain')} ${c('dim', '<path>')} Diagnose folder health (Viral!)
175
+ ${c('yellow', 'cure')} ${c('dim', '<path>')} Plan treatment using AI
176
+ ${c('yellow', 'apply')} Execute the cure plan
177
+ ${c('yellow', 'duplicates')} ${c('dim', '<path>')} Find duplicate files
178
+ ${c('yellow', 'dedupe')} ${c('dim', '<path>')} Remove duplicate files
179
+
156
180
  ${c('bold', 'COMMANDS')}
157
181
  ${c('yellow', 'scan')} ${c('dim', '<path>')} Scan directory and report contents
182
+ ${c('yellow', 'analyze')} ${c('dim', '<path>')} Deep analysis (duplicates, bloat, savings)
158
183
  ${c('yellow', 'organize')} ${c('dim', '<path>')} Organize files into categories
159
184
  ${c('yellow', 'clean')} ${c('dim', '<path>')} Find and remove junk files
160
185
  ${c('yellow', 'watch')} ${c('dim', '<path>')} Watch directory for changes ${c('magenta', '[PRO]')}
@@ -236,6 +261,35 @@ async function cmdScan(target, flags, config) {
236
261
  }
237
262
  }
238
263
 
264
+ async function cmdAnalyze(target, flags, config) {
265
+ const targetPath = path.resolve(target || '.');
266
+ const format = flags.format || config.output.format;
267
+
268
+ const spinner = new Spinner(`Analyzing ${c('cyan', targetPath)}...`);
269
+ if (format === 'table' && !flags.quiet) spinner.start();
270
+
271
+ try {
272
+ const options = {
273
+ maxDepth: flags.depth || config.scanner.maxDepth,
274
+ minSize: flags.minSize || 1024,
275
+ };
276
+
277
+ const result = analyzeDirectory(targetPath, options);
278
+
279
+ spinner.stop();
280
+
281
+ if (flags.quiet) {
282
+ process.exit(result.summary.potentialSavings > 0 ? 0 : 1);
283
+ return;
284
+ }
285
+
286
+ console.log(reporter.formatAnalyzeReport(result, format));
287
+ } catch (err) {
288
+ spinner.fail(err.message);
289
+ process.exit(1);
290
+ }
291
+ }
292
+
239
293
  async function cmdOrganize(target, flags, config) {
240
294
  const targetPath = path.resolve(target || '.');
241
295
  const format = flags.format || config.output.format;
@@ -428,6 +482,151 @@ async function cmdUndo(target, flags) {
428
482
  } catch { /* ignore */ }
429
483
  }
430
484
 
485
+ // ==================== DIAGNOSTIC TRIAD (v3.5) ====================
486
+
487
+ let activePlanState = null;
488
+
489
+ async function cmdExplain(target, flags) {
490
+ const targetPath = path.resolve(target || '.');
491
+ const spinner = new Spinner(`Diagnosing ${c('cyan', targetPath)}...`);
492
+ spinner.start();
493
+
494
+ try {
495
+ const engine = new ExplainEngine();
496
+ const result = engine.run(targetPath);
497
+ spinner.stop();
498
+ console.log(reporter.formatExplainReport(result));
499
+ } catch (err) {
500
+ spinner.fail(err.message);
501
+ process.exit(1);
502
+ }
503
+ }
504
+
505
+ async function cmdCure(target, flags) {
506
+ const targetPath = path.resolve(target || '.');
507
+ const prompt = flags.prompt || 'Clean up this folder and group similar files.';
508
+
509
+ if (!process.env.GEMINI_API_KEY) {
510
+ console.log(error('GEMINI_API_KEY environment variable is required for AI curative features.'));
511
+ process.exit(1);
512
+ }
513
+
514
+ const spinner = new Spinner(`Consulting AI for ${c('cyan', targetPath)}...`);
515
+ spinner.start();
516
+
517
+ try {
518
+ const engine = new CureEngine(targetPath, process.env.GEMINI_API_KEY);
519
+ activePlanState = await engine.generatePlan(prompt);
520
+ spinner.stop();
521
+
522
+ // Cache plan locally for 'apply' command
523
+ const cachePath = path.join(os.tmpdir(), 'filemayor-plan.json');
524
+ fs.writeFileSync(cachePath, JSON.stringify(activePlanState), 'utf8');
525
+
526
+ console.log(reporter.formatCureReport(activePlanState));
527
+ } catch (err) {
528
+ spinner.fail(err.message);
529
+ process.exit(1);
530
+ }
531
+ }
532
+
533
+ async function cmdApply(flags) {
534
+ const cachePath = path.join(os.tmpdir(), 'filemayor-plan.json');
535
+ if (!fs.existsSync(cachePath)) {
536
+ console.log(error('No plan found. Run "filemayor cure" first.'));
537
+ process.exit(1);
538
+ }
539
+
540
+ const plan = JSON.parse(fs.readFileSync(cachePath, 'utf8'));
541
+
542
+ // [Sprint B] Logic Guardrail: Check batch volume & destructive patterns
543
+ const LogicGuardrail = require('./core/guardrail');
544
+ const { FileMayorJailer } = require('./core/jailer');
545
+ const guardrail = new LogicGuardrail(50);
546
+ const jailer = new FileMayorJailer(process.cwd());
547
+
548
+ if (plan.plan && Array.isArray(plan.plan)) {
549
+ const approved = await guardrail.verifyBatch(plan.plan);
550
+ if (!approved) {
551
+ console.log(warn('Operation cancelled by user.'));
552
+ return;
553
+ }
554
+
555
+ // [Sprint B] Jailer: Validate every move before execution
556
+ const blocked = [];
557
+ for (const step of plan.plan) {
558
+ const check = jailer.validateMove(step.source, step.destination);
559
+ if (!check.safe) {
560
+ blocked.push({ file: path.basename(step.source), reason: check.error });
561
+ }
562
+ }
563
+ if (blocked.length > 0) {
564
+ console.log(error(`Jailer blocked ${blocked.length} unsafe operations:`));
565
+ for (const b of blocked) {
566
+ console.log(c('dim', ` ✗ ${b.file}: ${b.reason}`));
567
+ }
568
+ plan.plan = plan.plan.filter(step => jailer.validateMove(step.source, step.destination).safe);
569
+ if (plan.plan.length === 0) {
570
+ console.log(error('No safe operations remain. Aborting.'));
571
+ return;
572
+ }
573
+ console.log(info(`Proceeding with ${plan.plan.length} safe operations.`));
574
+ }
575
+ }
576
+
577
+ const spinner = new Spinner(`Executing cure...`);
578
+ spinner.start();
579
+
580
+ try {
581
+ const engine = new ApplyEngine();
582
+ const result = await engine.apply(plan, (p) => {
583
+ spinner.update(`Relocating: ${p.index}/${p.total} — ${path.basename(p.current || '')}`);
584
+ });
585
+ spinner.succeed(`Cure applied! ${result.stats.success} files moved.`);
586
+ fs.unlinkSync(cachePath); // Clear plan
587
+ } catch (err) {
588
+ spinner.fail(err.message);
589
+ process.exit(1);
590
+ }
591
+ }
592
+
593
+ async function cmdDuplicates(target, flags) {
594
+ const targetPath = path.resolve(target || '.');
595
+ const spinner = new Spinner(`Hunting duplicates in ${c('cyan', targetPath)}...`);
596
+ spinner.start();
597
+
598
+ try {
599
+ const engine = new DedupeEngine();
600
+ const result = engine.find(targetPath);
601
+ spinner.stop();
602
+ console.log(reporter.formatDedupeReport(result));
603
+ } catch (err) {
604
+ spinner.fail(err.message);
605
+ process.exit(1);
606
+ }
607
+ }
608
+
609
+ async function cmdDedupe(target, flags) {
610
+ const targetPath = path.resolve(target || '.');
611
+ const spinner = new Spinner(`Deduplicating ${c('cyan', targetPath)}...`);
612
+ spinner.start();
613
+
614
+ try {
615
+ const engine = new DedupeEngine();
616
+ const report = engine.find(targetPath);
617
+ if (report.sets === 0) {
618
+ spinner.succeed('Total order restored: No duplicates found.');
619
+ return;
620
+ }
621
+
622
+ const result = await engine.clean(report);
623
+ spinner.succeed(`Purged ${result.deleted} duplicates. Reclaimed ${result.freedHuman}.`);
624
+ } catch (err) {
625
+ spinner.fail(err.message);
626
+ process.exit(1);
627
+ }
628
+ }
629
+
431
630
  async function cmdInfo(config) {
432
631
  console.log(banner());
433
632
  console.log(` ${c('bold', 'Version')} ${VERSION}`);
@@ -526,7 +725,7 @@ async function cmdLicense(action, positional, flags) {
526
725
  }
527
726
  console.log('');
528
727
  if (!li.active) {
529
- console.log(c('dim', ' Get a license: https://filemayor.lemonsqueezy.com/checkout/buy/d2795526-eb05-4272-8084-98b6c7a118bb'));
728
+ console.log(c('dim', ' Get a license: https://filemayor.lemonsqueezy.com/checkout/buy/7fdcc87f-0660-4c1c-b3db-99f94773b71a'));
530
729
  console.log(c('dim', ' Activate: filemayor license activate <key>'));
531
730
  console.log('');
532
731
  }
@@ -590,6 +789,35 @@ async function main() {
590
789
  await cmdScan(args.target, args.flags, config);
591
790
  break;
592
791
 
792
+ case 'explain':
793
+ case 'dx':
794
+ await cmdExplain(args.target, args.flags);
795
+ break;
796
+
797
+ case 'cure':
798
+ case 'plan':
799
+ await cmdCure(args.target, args.flags);
800
+ break;
801
+
802
+ case 'apply':
803
+ case 'exec':
804
+ await cmdApply(args.flags);
805
+ break;
806
+
807
+ case 'duplicates':
808
+ case 'dupes':
809
+ await cmdDuplicates(args.target, args.flags);
810
+ break;
811
+
812
+ case 'dedupe':
813
+ await cmdDedupe(args.target, args.flags);
814
+ break;
815
+
816
+ case 'analyze':
817
+ case 'a':
818
+ await cmdAnalyze(args.target, args.flags, config);
819
+ break;
820
+
593
821
  case 'organize':
594
822
  case 'org':
595
823
  case 'o':
package/package.json CHANGED
@@ -1,55 +1,55 @@
1
- {
2
- "name": "filemayor",
3
- "version": "2.0.5",
4
- "description": "FileMayor — Your Digital Life Organizer.",
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": "Lehlohonolo Goodwill Nchefu (Chevza)",
29
- "email": "nchefuh@gmail.com",
30
- "url": "https://github.com/Hrypopo"
31
- },
32
- "license": "PROPRIETARY",
33
- "repository": {
34
- "type": "git",
35
- "url": "git+https://github.com/Hrypopo/FileMayor.git"
36
- },
37
- "bugs": {
38
- "url": "https://github.com/Hrypopo/FileMayor/issues"
39
- },
40
- "homepage": "https://filemayor.com",
41
- "engines": {
42
- "node": ">=18.0.0"
43
- },
44
- "os": [
45
- "win32",
46
- "darwin",
47
- "linux"
48
- ],
49
- "files": [
50
- "index.js",
51
- "core/",
52
- "LICENSE",
53
- "README.md"
54
- ]
55
- }
1
+ {
2
+ "name": "filemayor",
3
+ "version": "3.6.0",
4
+ "description": "FileMayor — Your Digital Life Organizer. CLI + Desktop + PWA.",
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": "Lehlohonolo Goodwill Nchefu (Chevza)",
29
+ "email": "hloninchefu@gmail.com",
30
+ "url": "https://github.com/Hrypopo"
31
+ },
32
+ "license": "PROPRIETARY",
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "git+https://github.com/Hrypopo/FileMayor.git"
36
+ },
37
+ "bugs": {
38
+ "url": "https://github.com/Hrypopo/FileMayor/issues"
39
+ },
40
+ "homepage": "https://filemayor.com",
41
+ "engines": {
42
+ "node": ">=18.0.0"
43
+ },
44
+ "os": [
45
+ "win32",
46
+ "darwin",
47
+ "linux"
48
+ ],
49
+ "files": [
50
+ "index.js",
51
+ "core/",
52
+ "LICENSE",
53
+ "README.md"
54
+ ]
55
+ }