filemayor 2.1.0 → 4.0.5

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.
@@ -0,0 +1,186 @@
1
+ /**
2
+ * ═══════════════════════════════════════════════════════════════════
3
+ * FILEMAYOR AGENTIC CREW — INTENT STRATEGIST
4
+ *
5
+ * Universal domain-aware intent classification.
6
+ * Detects folder "archetypes" from filenames, extensions, and
7
+ * directory names — then maps to the optimal organization strategy.
8
+ *
9
+ * Supports: Music Production, Civics/Law, Agri-Tech, Business,
10
+ * Technical/Code, Library, and General Archive.
11
+ * ═══════════════════════════════════════════════════════════════════
12
+ */
13
+
14
+ 'use strict';
15
+
16
+ const path = require('path');
17
+
18
+ // ─── Domain Archetype Definitions ─────────────────────────────────
19
+
20
+ const ARCHETYPES = {
21
+ production: {
22
+ label: 'Music Production',
23
+ extSignals: ['.wav', '.mp3', '.flac', '.als', '.flp', '.mid', '.aif', '.ogg', '.aac', '.stem'],
24
+ nameSignals: ['_v1', '_v2', '_final', '_master', '_mix', 'stem', 'beat', 'vocal', 'inst'],
25
+ strategy: 'atomic_bundle_preservation',
26
+ hierarchy: 'Project Name → Stems / Masters / Project Files'
27
+ },
28
+ civics: {
29
+ label: 'Civics & Law (RSA)',
30
+ extSignals: ['.pdf', '.docx', '.doc', '.odt', '.jpg', '.png'],
31
+ nameSignals: ['sars', 'gazette', 'court', 'affidavit', 'invoice', 'contract', 'act_',
32
+ 'regulation', 'compliance', 'permit', 'certificate', 'agsa', 'pfma', 'sca',
33
+ 'judgment', 'bill', 'irp5', 'it3a', 'green_paper', 'white_paper', 'audit_report',
34
+ 'fica', 'cipc', 'bbbee', 'labour_', 'municipal'],
35
+ strategy: 'entity_year_doctype',
36
+ hierarchy: 'Authority → Year → Document Type',
37
+ folders: ['01_Legislation', '02_Compliance', '03_Oversight', '04_Judiciary']
38
+ },
39
+ agritech: {
40
+ label: 'Agri-Tech (Farm)',
41
+ extSignals: ['.csv', '.xlsx', '.xls', '.tsv'],
42
+ nameSignals: ['tag_', 'vet_', 'herd', 'cattle', 'livestock', 'feed', 'breed',
43
+ 'pasture', 'dip_', 'auction', 'kraal', 'bovine', 'vaccination', 'fmd',
44
+ 'brucella', 'deworming', 'weaner', 'heifer', 'bull_', 'calf_', 'stud_',
45
+ 'transport_permit', 'movement_permit', 'feedlot'],
46
+ strategy: 'category_datestamp',
47
+ hierarchy: 'Livestock Category → Date-Stamped Logs',
48
+ folders: ['Inventory', 'Health', 'Logistics']
49
+ },
50
+ business: {
51
+ label: 'Business',
52
+ extSignals: ['.pdf', '.png', '.jpg', '.xlsx'],
53
+ nameSignals: ['invoice_', 'receipt', 'quotation', 'po_', 'statement', 'expense', 'tax_', 'vat', 'payslip', 'order_'],
54
+ strategy: 'quarter_vendor_status',
55
+ hierarchy: 'Quarter → Vendor → Status'
56
+ },
57
+ technical: {
58
+ label: 'Technical / Code',
59
+ extSignals: ['.js', '.ts', '.py', '.go', '.rs', '.java', '.cpp', '.c', '.h', '.json', '.yaml', '.yml', '.toml', '.env', '.sh'],
60
+ nameSignals: ['config', 'test_', '_spec', 'build', 'deploy', 'docker', 'readme', 'license', 'makefile'],
61
+ strategy: 'substrate_preservation',
62
+ hierarchy: 'Project Root → src / tests / config / docs'
63
+ },
64
+ library: {
65
+ label: 'Library / Reference',
66
+ extSignals: ['.pdf', '.epub', '.mobi', '.djvu'],
67
+ nameSignals: ['book', 'chapter', 'isbn', 'edition', 'vol_', 'author'],
68
+ strategy: 'ancestry_matching',
69
+ hierarchy: 'Genre / Subject → Author → Title'
70
+ }
71
+ };
72
+
73
+ class IntentStrategist {
74
+ /**
75
+ * Classify user intent from a prompt string.
76
+ * Uses keyword matching for fast, local classification.
77
+ */
78
+ classify(prompt, context = {}) {
79
+ const p = prompt.toLowerCase();
80
+ let strategy = 'clustering';
81
+ let intent = 'organize';
82
+ let archetype = null;
83
+
84
+ const isRefine = p.includes('refine') || p.includes('structure') || p.includes('within');
85
+
86
+ if (p.includes('clean') || p.includes('space') || p.includes('junk')) {
87
+ intent = 'cleanup';
88
+ strategy = 'retention';
89
+ } else if (p.includes('tax') || p.includes('invoice') || p.includes('receipt')) {
90
+ intent = 'financial_sorting';
91
+ strategy = 'deep_scan';
92
+ archetype = 'business';
93
+ } else if (p.includes('music') || p.includes('stem') || p.includes('production') || p.includes('beat')) {
94
+ intent = 'music_production';
95
+ strategy = 'atomic_bundle_preservation';
96
+ archetype = 'production';
97
+ } else if (p.includes('book') || p.includes('library') || p.includes('read')) {
98
+ intent = 'library_curation';
99
+ strategy = 'ancestry_matching';
100
+ archetype = 'library';
101
+ } else if (p.includes('farm') || p.includes('cattle') || p.includes('herd') || p.includes('livestock')) {
102
+ intent = 'agritech_management';
103
+ strategy = 'category_datestamp';
104
+ archetype = 'agritech';
105
+ } else if (p.includes('law') || p.includes('legal') || p.includes('court') || p.includes('compliance')) {
106
+ intent = 'civics_filing';
107
+ strategy = 'entity_year_doctype';
108
+ archetype = 'civics';
109
+ } else if (p.includes('code') || p.includes('project') || p.includes('repo')) {
110
+ intent = 'codebase_org';
111
+ strategy = 'substrate_preservation';
112
+ archetype = 'technical';
113
+ }
114
+
115
+ return {
116
+ intent,
117
+ strategy,
118
+ isRefine,
119
+ archetype: archetype ? ARCHETYPES[archetype] : null,
120
+ rules: {
121
+ lockBundles: true,
122
+ respectAncestry: true,
123
+ depthLimit: 3
124
+ }
125
+ };
126
+ }
127
+
128
+ /**
129
+ * Detect the folder archetype from file metadata (zero-content).
130
+ * Scores each archetype by counting matching extension and name signals.
131
+ * Includes "Parental Hint" — the directory name itself influences scoring.
132
+ *
133
+ * @param {Array} files - Array of { name, ext?, size? }
134
+ * @param {string} [folderName] - Name of the parent directory (parental hint)
135
+ * @returns {{ archetype: string, label: string, confidence: number, strategy: string }}
136
+ */
137
+ detectArchetype(files, folderName = '') {
138
+ const scores = {};
139
+ const folderHint = (folderName || '').toLowerCase();
140
+
141
+ for (const [key, arch] of Object.entries(ARCHETYPES)) {
142
+ let score = 0;
143
+
144
+ for (const file of files) {
145
+ const name = (file.name || '').toLowerCase();
146
+ const ext = (file.ext || path.extname(name)).toLowerCase();
147
+
148
+ // Extension match
149
+ if (arch.extSignals.includes(ext)) score += 2;
150
+
151
+ // Name signal match
152
+ for (const signal of arch.nameSignals) {
153
+ if (name.includes(signal)) score += 3;
154
+ }
155
+ }
156
+
157
+ // Parental hint bonus (directory name contains archetype keywords)
158
+ for (const signal of arch.nameSignals) {
159
+ if (folderHint.includes(signal)) score += 5;
160
+ }
161
+
162
+ scores[key] = score;
163
+ }
164
+
165
+ // Find the winner
166
+ const sorted = Object.entries(scores).sort((a, b) => b[1] - a[1]);
167
+ const [topKey, topScore] = sorted[0];
168
+ const totalSignals = Object.values(scores).reduce((a, b) => a + b, 0);
169
+ const confidence = totalSignals > 0 ? Math.round((topScore / totalSignals) * 100) : 0;
170
+
171
+ if (topScore === 0) {
172
+ return { archetype: 'general', label: 'General / Mixed', confidence: 0, strategy: 'clustering' };
173
+ }
174
+
175
+ const arch = ARCHETYPES[topKey];
176
+ return {
177
+ archetype: topKey,
178
+ label: arch.label,
179
+ confidence,
180
+ strategy: arch.strategy,
181
+ hierarchy: arch.hierarchy
182
+ };
183
+ }
184
+ }
185
+
186
+ module.exports = IntentStrategist;
@@ -0,0 +1,116 @@
1
+ /**
2
+ * ═══════════════════════════════════════════════════════════════════
3
+ * FILEMAYOR AGENTIC CREW — SECURITY ARCHITECT
4
+ * Deterministic validator. Never trusts AI output.
5
+ * ═══════════════════════════════════════════════════════════════════
6
+ */
7
+
8
+ 'use strict';
9
+
10
+ const { isFileSafe, validatePath } = require('../security');
11
+ const os = require('os');
12
+
13
+ class SecurityArchitect {
14
+ constructor() {
15
+ // Dynamically resolve home directory parts for path normalization
16
+ const homeParts = os.homedir().toLowerCase().replace(/\\/g, '/').split('/').filter(x => x.length > 0);
17
+ this._homePrefixParts = new Set(homeParts); // e.g. Set{'c:', 'users', 'hloni'}
18
+ }
19
+
20
+ /**
21
+ * Final validation of an AI-generated plan
22
+ * @param {Object} plan - The Curative Plan from Planner
23
+ * @param {Object} sentryData - The ground-truth telemetry
24
+ * @returns {Object} Validated/Sanitized Plan
25
+ */
26
+ validate(plan, sentryData) {
27
+ if (!plan || !Array.isArray(plan.plan)) {
28
+ return { ...plan, plan: [], status: 'invalid' };
29
+ }
30
+
31
+ const domainTriggerFolders = ['books', 'library', 'projects', 'music', 'work', 'downloads'];
32
+ const clusters = sentryData?.clusters || {};
33
+ const allSamples = Object.values(clusters).flatMap(c => c.samples || []);
34
+ const homeParts = this._homePrefixParts;
35
+
36
+ // Robust Normalizer: strips drive letter and home dir parts, leaving only domain-relevant segments
37
+ const normalize = (p) => {
38
+ if (!p) return [];
39
+ return p.toLowerCase()
40
+ .replace(/\\/g, '/')
41
+ .split('/')
42
+ .filter(x => x.length > 0 && !homeParts.has(x));
43
+ };
44
+
45
+ const safeSteps = plan.plan.filter(step => {
46
+ try {
47
+ if (!step.source || !step.destination) return false;
48
+
49
+ const srcCheck = isFileSafe(step.source);
50
+ if (!srcCheck.safe) return false;
51
+
52
+ // 1. Domain Preservation (Rule of Ancestry)
53
+ const srcParts = normalize(step.source);
54
+ const destParts = normalize(step.destination);
55
+
56
+ const triggerFound = srcParts.find(p => domainTriggerFolders.includes(p));
57
+
58
+ if (triggerFound) {
59
+ // A move is valid if the EXACT trigger folder is present in the destination path
60
+ const preserved = destParts.includes(triggerFound);
61
+
62
+ if (!preserved && !step.reason.includes('COLLECTION_MOVE')) {
63
+ console.warn(`[ARCHITECTURE] Blocked domain scattering for ${step.source} -> ${step.destination}`);
64
+ return false;
65
+ }
66
+ }
67
+
68
+ // 2. Atomic Bundle Lock
69
+ const sourceMeta = allSamples.find(s => s.path === step.source);
70
+ if (sourceMeta && sourceMeta.bundleId) {
71
+ const bundleId = sourceMeta.bundleId;
72
+ const bundleMembers = allSamples.filter(s => s.bundleId === bundleId);
73
+ const planMembers = plan.plan.filter(s => {
74
+ const meta = allSamples.find(m => m.path === s.source);
75
+ return meta && meta.bundleId === bundleId;
76
+ });
77
+
78
+ // If not all members are moving, it's a split.
79
+ if (planMembers.length < bundleMembers.length) {
80
+ console.warn(`[ARCHITECTURE] Blocked bundle split for ${step.source} (Bundle: ${bundleId})`);
81
+ return false;
82
+ }
83
+
84
+ // Ensure all members are going to the same parent domain
85
+ const firstDestParts = normalize(planMembers[0].destination);
86
+ const bundleDestRoot = firstDestParts.slice(0, -1).join('/');
87
+
88
+ const matches = planMembers.every(s => {
89
+ const dParts = normalize(s.destination);
90
+ const dRoot = dParts.slice(0, -1).join('/');
91
+ return dRoot === bundleDestRoot;
92
+ });
93
+
94
+ if (!matches) {
95
+ console.warn(`[ARCHITECTURE] Blocked bundle scattering for ${step.source}`);
96
+ return false;
97
+ }
98
+ }
99
+
100
+ return true;
101
+ } catch (err) {
102
+ console.error('[ARCHITECTURE] Validation Error:', err);
103
+ return false;
104
+ }
105
+ });
106
+
107
+ return {
108
+ ...plan,
109
+ plan: safeSteps,
110
+ validatedCount: safeSteps.length,
111
+ status: safeSteps.length > 0 ? 'safe' : 'blocked'
112
+ };
113
+ }
114
+ }
115
+
116
+ module.exports = SecurityArchitect;
package/core/analyzer.js CHANGED
@@ -1,152 +1,163 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * ═══════════════════════════════════════════════════════════════════
5
- * FILEMAYOR CORE — ANALYZER
6
- * Deep filesystem insights: duplicate detection, bloat mapping,
7
- * and potential savings calculation.
8
- * ═══════════════════════════════════════════════════════════════════
9
- */
10
-
11
- 'use strict';
12
-
13
- const fs = require('fs');
14
- const path = require('path');
15
- const crypto = require('crypto');
16
- const { scan } = require('./scanner');
17
- const { findJunk } = require('./cleaner');
18
- const { formatBytes } = require('./scanner');
19
-
20
- /**
21
- * Analyze a directory for duplicates, bloat, and junk
22
- * @param {string} dirPath - Root directory to analyze
23
- * @param {Object} options - Analysis options
24
- * @returns {Object} Deep analysis results
25
- */
26
- function analyzeDirectory(dirPath, options = {}) {
27
- const {
28
- maxDepth = 10,
29
- minSize = 1024, // Ignore files smaller than 1KB for duplicate analysis
30
- } = options;
31
-
32
- // 1. Initial Scan
33
- const scanResult = scan(dirPath, { maxDepth, includeDirectories: true });
34
- const { files, stats } = scanResult;
35
-
36
- // 2. Duplicate Detection (Size-first hashing)
37
- const sizeMap = new Map();
38
- const duplicates = [];
39
-
40
- // Group by size first (fast)
41
- for (const file of files) {
42
- if (file.size < minSize) continue;
43
- if (!sizeMap.has(file.size)) {
44
- sizeMap.set(file.size, []);
45
- }
46
- sizeMap.get(file.size).push(file);
47
- }
48
-
49
- // Hash only those with identical sizes
50
- for (const [size, candidateFiles] of sizeMap.entries()) {
51
- if (candidateFiles.length < 2) continue;
52
-
53
- const hashMap = new Map();
54
- for (const file of candidateFiles) {
55
- try {
56
- // Partial hash (first 16KB) for efficiency
57
- const hash = getFileHash(file.path, 16384);
58
- if (!hashMap.has(hash)) {
59
- hashMap.set(hash, []);
60
- }
61
- hashMap.get(hash).push(file);
62
- } catch (err) {
63
- // Skip files we can't read
64
- }
65
- }
66
-
67
- for (const [hash, dupeFiles] of hashMap.entries()) {
68
- if (dupeFiles.length > 1) {
69
- duplicates.push({
70
- size,
71
- sizeHuman: formatBytes(size),
72
- files: dupeFiles.map(f => ({ name: f.name, path: f.path, relativePath: f.relativePath })),
73
- wastedSpace: size * (dupeFiles.length - 1),
74
- wastedSpaceHuman: formatBytes(size * (dupeFiles.length - 1))
75
- });
76
- }
77
- }
78
- }
79
-
80
- // 3. Bloat Mapping (Top Directories)
81
- const dirMap = new Map();
82
- for (const file of files) {
83
- const parts = file.relativePath.split(path.sep);
84
- let currentPath = '';
85
- // Only track top-level and second-level folders for bloat mapping
86
- for (let i = 0; i < Math.min(parts.length - 1, 2); i++) {
87
- currentPath = currentPath ? path.join(currentPath, parts[i]) : parts[i];
88
- if (!dirMap.has(currentPath)) {
89
- dirMap.set(currentPath, { size: 0, count: 0 });
90
- }
91
- const info = dirMap.get(currentPath);
92
- info.size += file.size;
93
- info.count += 1;
94
- }
95
- }
96
-
97
- const largestDirs = Array.from(dirMap.entries())
98
- .map(([name, info]) => ({ name, ...info, sizeHuman: formatBytes(info.size) }))
99
- .sort((a, b) => b.size - a.size)
100
- .slice(0, 5);
101
-
102
- // 4. Junk Detection Integration
103
- const junkResult = findJunk(dirPath, { maxDepth });
104
- const totalJunkSize = junkResult.stats.totalSize;
105
- const totalDuplicateWasted = duplicates.reduce((sum, d) => sum + d.wastedSpace, 0);
106
-
107
- return {
108
- root: dirPath,
109
- timestamp: Date.now(),
110
- summary: {
111
- totalFiles: stats.filesFound,
112
- totalSize: stats.totalSize,
113
- totalSizeHuman: formatBytes(stats.totalSize),
114
- potentialSavings: totalJunkSize + totalDuplicateWasted,
115
- potentialSavingsHuman: formatBytes(totalJunkSize + totalDuplicateWasted)
116
- },
117
- duplicates: {
118
- sets: duplicates.length,
119
- totalWasted: totalDuplicateWasted,
120
- totalWastedHuman: formatBytes(totalDuplicateWasted),
121
- details: duplicates.sort((a, b) => b.wastedSpace - a.wastedSpace).slice(0, 10)
122
- },
123
- largestDirs,
124
- junk: {
125
- count: junkResult.junk.length,
126
- totalSize: totalJunkSize,
127
- totalSizeHuman: formatBytes(totalJunkSize),
128
- categories: junkResult.stats.byCategory
129
- }
130
- };
131
- }
132
-
133
- /**
134
- * Get hash of file content (partial or full)
135
- */
136
- function getFileHash(filePath, limit = null) {
137
- const buffer = limit
138
- ? Buffer.alloc(limit)
139
- : fs.readFileSync(filePath);
140
-
141
- if (limit) {
142
- const fd = fs.openSync(filePath, 'r');
143
- fs.readSync(fd, buffer, 0, limit, 0);
144
- fs.closeSync(fd);
145
- }
146
-
147
- return crypto.createHash('md5').update(buffer).digest('hex');
148
- }
149
-
150
- module.exports = {
151
- analyzeDirectory
152
- };
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * ═══════════════════════════════════════════════════════════════════
5
+ * FILEMAYOR CORE — ANALYZER
6
+ * Deep filesystem insights: duplicate detection, bloat mapping,
7
+ * and potential savings calculation.
8
+ * ═══════════════════════════════════════════════════════════════════
9
+ */
10
+
11
+ 'use strict';
12
+
13
+ const fs = require('fs');
14
+ const path = require('path');
15
+ const crypto = require('crypto');
16
+ const { scan } = require('./scanner');
17
+ const { findJunk } = require('./cleaner');
18
+ const { formatBytes } = require('./scanner');
19
+
20
+ /**
21
+ * Analyze a directory for duplicates, bloat, and junk
22
+ * @param {string} dirPath - Root directory to analyze
23
+ * @param {Object} options - Analysis options
24
+ * @returns {Object} Deep analysis results
25
+ */
26
+ function analyzeDirectory(dirPath, options = {}) {
27
+ const {
28
+ maxDepth = 10,
29
+ minSize = 1024, // Ignore files smaller than 1KB for duplicate analysis
30
+ } = options;
31
+
32
+ // 1. Initial Scan
33
+ const scanResult = scan(dirPath, { maxDepth, includeDirectories: true });
34
+ const { files, stats } = scanResult;
35
+
36
+ // 2. Duplicate Detection (Size-first hashing)
37
+ const sizeMap = new Map();
38
+ const duplicates = [];
39
+
40
+ // Group by size first (fast)
41
+ for (const file of files) {
42
+ if (file.size < minSize) continue;
43
+ if (!sizeMap.has(file.size)) {
44
+ sizeMap.set(file.size, []);
45
+ }
46
+ sizeMap.get(file.size).push(file);
47
+ }
48
+
49
+ // Hash only those with identical sizes
50
+ for (const [size, candidateFiles] of sizeMap.entries()) {
51
+ if (candidateFiles.length < 2) continue;
52
+
53
+ const hashMap = new Map();
54
+ for (const file of candidateFiles) {
55
+ try {
56
+ // Partial hash (first 16KB) for efficiency
57
+ const hash = getFileHash(file.path, 16384);
58
+ if (!hashMap.has(hash)) {
59
+ hashMap.set(hash, []);
60
+ }
61
+ hashMap.get(hash).push(file);
62
+ } catch (err) {
63
+ // Skip files we can't read
64
+ }
65
+ }
66
+
67
+ for (const [hash, dupeFiles] of hashMap.entries()) {
68
+ if (dupeFiles.length > 1) {
69
+ duplicates.push({
70
+ size,
71
+ sizeHuman: formatBytes(size),
72
+ files: dupeFiles.map(f => ({ name: f.name, path: f.path, relativePath: f.relativePath })),
73
+ wastedSpace: size * (dupeFiles.length - 1),
74
+ wastedSpaceHuman: formatBytes(size * (dupeFiles.length - 1))
75
+ });
76
+ }
77
+ }
78
+ }
79
+ // 3. Bloat Mapping (Top Directories) & Progress Reporting
80
+ const dirMap = new Map();
81
+ for (let i = 0; i < files.length; i++) {
82
+ const file = files[i];
83
+
84
+ // Progress callback
85
+ if (options.onProgress) {
86
+ options.onProgress({
87
+ current: i + 1,
88
+ total: files.length,
89
+ percent: Math.round(((i + 1) / files.length) * 100),
90
+ file: file.name
91
+ });
92
+ }
93
+
94
+ const parts = file.relativePath.split(path.sep);
95
+ let currentPath = '';
96
+ // Only track top-level and second-level folders for bloat mapping
97
+ for (let j = 0; j < Math.min(parts.length - 1, 2); j++) {
98
+ currentPath = currentPath ? path.join(currentPath, parts[j]) : parts[j];
99
+ if (!dirMap.has(currentPath)) {
100
+ dirMap.set(currentPath, { size: 0, count: 0 });
101
+ }
102
+ const info = dirMap.get(currentPath);
103
+ info.size += file.size;
104
+ info.count += 1;
105
+ }
106
+ }
107
+
108
+ const largestDirs = Array.from(dirMap.entries())
109
+ .map(([name, info]) => ({ name, ...info, sizeHuman: formatBytes(info.size) }))
110
+ .sort((a, b) => b.size - a.size)
111
+ .slice(0, 5);
112
+
113
+ // 4. Junk Detection Integration
114
+ const junkResult = findJunk(dirPath, { maxDepth });
115
+ const totalJunkSize = junkResult.stats.totalSize;
116
+ const totalDuplicateWasted = duplicates.reduce((sum, d) => sum + d.wastedSpace, 0);
117
+
118
+ return {
119
+ root: dirPath,
120
+ timestamp: Date.now(),
121
+ summary: {
122
+ totalFiles: stats.filesFound,
123
+ totalSize: stats.totalSize,
124
+ totalSizeHuman: formatBytes(stats.totalSize),
125
+ potentialSavings: totalJunkSize + totalDuplicateWasted,
126
+ potentialSavingsHuman: formatBytes(totalJunkSize + totalDuplicateWasted)
127
+ },
128
+ duplicates: {
129
+ sets: duplicates.length,
130
+ totalWasted: totalDuplicateWasted,
131
+ totalWastedHuman: formatBytes(totalDuplicateWasted),
132
+ details: duplicates.sort((a, b) => b.wastedSpace - a.wastedSpace).slice(0, 10)
133
+ },
134
+ largestDirs,
135
+ junk: {
136
+ count: junkResult.junk.length,
137
+ totalSize: totalJunkSize,
138
+ totalSizeHuman: formatBytes(totalJunkSize),
139
+ categories: junkResult.stats.byCategory
140
+ }
141
+ };
142
+ }
143
+
144
+ /**
145
+ * Get hash of file content (partial or full)
146
+ */
147
+ function getFileHash(filePath, limit = null) {
148
+ const buffer = limit
149
+ ? Buffer.alloc(limit)
150
+ : fs.readFileSync(filePath);
151
+
152
+ if (limit) {
153
+ const fd = fs.openSync(filePath, 'r');
154
+ fs.readSync(fd, buffer, 0, limit, 0);
155
+ fs.closeSync(fd);
156
+ }
157
+
158
+ return crypto.createHash('md5').update(buffer).digest('hex');
159
+ }
160
+
161
+ module.exports = {
162
+ analyzeDirectory
163
+ };