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/core/config.js
ADDED
|
@@ -0,0 +1,562 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ═══════════════════════════════════════════════════════════════════
|
|
5
|
+
* FILEMAYOR CORE — CONFIG
|
|
6
|
+
* YAML/JSON config parser with schema validation, merge strategy,
|
|
7
|
+
* environment variable expansion, and defaults.
|
|
8
|
+
* ═══════════════════════════════════════════════════════════════════
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
'use strict';
|
|
12
|
+
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const os = require('os');
|
|
16
|
+
|
|
17
|
+
// ─── Default Configuration ────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
const DEFAULT_CONFIG = {
|
|
20
|
+
version: 1,
|
|
21
|
+
organize: {
|
|
22
|
+
naming: 'original', // original | category_prefix | date_prefix | clean
|
|
23
|
+
duplicates: 'rename', // rename | skip | overwrite
|
|
24
|
+
maxDepth: 20,
|
|
25
|
+
includeHidden: false,
|
|
26
|
+
ignore: [
|
|
27
|
+
'node_modules', '.git', '__pycache__', '.venv',
|
|
28
|
+
'dist', 'build', '.next', '.cache'
|
|
29
|
+
],
|
|
30
|
+
categories: {} // Custom category overrides
|
|
31
|
+
},
|
|
32
|
+
clean: {
|
|
33
|
+
categories: ['temp', 'cache', 'system', 'logs'],
|
|
34
|
+
maxDepth: 10,
|
|
35
|
+
includeDirectories: true,
|
|
36
|
+
customPatterns: {}
|
|
37
|
+
},
|
|
38
|
+
watch: {
|
|
39
|
+
directories: [],
|
|
40
|
+
debounceMs: 500,
|
|
41
|
+
recursive: true,
|
|
42
|
+
autoOrganize: false,
|
|
43
|
+
rules: [],
|
|
44
|
+
logPath: null
|
|
45
|
+
},
|
|
46
|
+
scanner: {
|
|
47
|
+
maxDepth: 20,
|
|
48
|
+
followSymlinks: false,
|
|
49
|
+
includeHidden: false,
|
|
50
|
+
ignore: [
|
|
51
|
+
'node_modules', '.git', '__pycache__', '.venv',
|
|
52
|
+
'dist', 'build'
|
|
53
|
+
],
|
|
54
|
+
minSize: 0,
|
|
55
|
+
maxSize: null
|
|
56
|
+
},
|
|
57
|
+
output: {
|
|
58
|
+
format: 'table', // table | json | csv | minimal
|
|
59
|
+
colors: true,
|
|
60
|
+
verbose: false,
|
|
61
|
+
quiet: false
|
|
62
|
+
},
|
|
63
|
+
security: {
|
|
64
|
+
maxOperationsPerMinute: 1000,
|
|
65
|
+
confirmDestructive: true,
|
|
66
|
+
journalEnabled: true,
|
|
67
|
+
journalPath: '.filemayor-journal.json'
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// ─── Config File Discovery ────────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
const CONFIG_FILENAMES = [
|
|
74
|
+
'.filemayor.yml',
|
|
75
|
+
'.filemayor.yaml',
|
|
76
|
+
'.filemayor.json',
|
|
77
|
+
'filemayor.config.js',
|
|
78
|
+
'filemayor.config.json',
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Search for a config file starting from a directory and walking up
|
|
83
|
+
* @param {string} startDir - Directory to start searching from
|
|
84
|
+
* @returns {string|null} Path to found config file, or null
|
|
85
|
+
*/
|
|
86
|
+
function findConfigFile(startDir = process.cwd()) {
|
|
87
|
+
let dir = path.resolve(startDir);
|
|
88
|
+
const root = path.parse(dir).root;
|
|
89
|
+
|
|
90
|
+
while (dir !== root) {
|
|
91
|
+
for (const filename of CONFIG_FILENAMES) {
|
|
92
|
+
const configPath = path.join(dir, filename);
|
|
93
|
+
if (fs.existsSync(configPath)) {
|
|
94
|
+
return configPath;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
dir = path.dirname(dir);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Check home directory
|
|
101
|
+
for (const filename of CONFIG_FILENAMES) {
|
|
102
|
+
const homePath = path.join(os.homedir(), filename);
|
|
103
|
+
if (fs.existsSync(homePath)) {
|
|
104
|
+
return homePath;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ─── Config Parsing ───────────────────────────────────────────────
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Simple YAML parser for FileMayor config files
|
|
115
|
+
* Supports: strings, numbers, booleans, arrays, nested objects, comments
|
|
116
|
+
* Does NOT support: anchors, aliases, multi-line strings, flow style
|
|
117
|
+
* @param {string} yamlContent - YAML string
|
|
118
|
+
* @returns {Object} Parsed object
|
|
119
|
+
*/
|
|
120
|
+
function parseSimpleYaml(yamlContent) {
|
|
121
|
+
const lines = yamlContent.split('\n');
|
|
122
|
+
const result = {};
|
|
123
|
+
const stack = [{ indent: -1, obj: result }];
|
|
124
|
+
|
|
125
|
+
for (let i = 0; i < lines.length; i++) {
|
|
126
|
+
let line = lines[i];
|
|
127
|
+
|
|
128
|
+
// Remove comments (but not # inside quotes)
|
|
129
|
+
const commentIdx = line.search(/(?<!['"])\s*#/);
|
|
130
|
+
if (commentIdx >= 0) {
|
|
131
|
+
line = line.substring(0, commentIdx);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Skip empty lines
|
|
135
|
+
const trimmed = line.trimEnd();
|
|
136
|
+
if (trimmed.length === 0) continue;
|
|
137
|
+
|
|
138
|
+
// Calculate indent
|
|
139
|
+
const indent = line.length - line.trimStart().length;
|
|
140
|
+
const content = trimmed.trim();
|
|
141
|
+
|
|
142
|
+
// Handle array items
|
|
143
|
+
if (content.startsWith('- ')) {
|
|
144
|
+
const parent = stack[stack.length - 1];
|
|
145
|
+
const arrayItemValue = content.slice(2).trim();
|
|
146
|
+
|
|
147
|
+
// Find the last key that should be an array
|
|
148
|
+
const parentObj = parent.obj;
|
|
149
|
+
const keys = Object.keys(parentObj);
|
|
150
|
+
const lastKey = keys[keys.length - 1];
|
|
151
|
+
|
|
152
|
+
if (lastKey && Array.isArray(parentObj[lastKey])) {
|
|
153
|
+
parentObj[lastKey].push(parseYamlValue(arrayItemValue));
|
|
154
|
+
} else if (lastKey && parentObj[lastKey] === null) {
|
|
155
|
+
parentObj[lastKey] = [parseYamlValue(arrayItemValue)];
|
|
156
|
+
}
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Handle key: value
|
|
161
|
+
const colonIdx = content.indexOf(':');
|
|
162
|
+
if (colonIdx === -1) continue;
|
|
163
|
+
|
|
164
|
+
const key = content.substring(0, colonIdx).trim();
|
|
165
|
+
const rawValue = content.substring(colonIdx + 1).trim();
|
|
166
|
+
|
|
167
|
+
// Pop stack to correct level
|
|
168
|
+
while (stack.length > 1 && stack[stack.length - 1].indent >= indent) {
|
|
169
|
+
stack.pop();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const currentObj = stack[stack.length - 1].obj;
|
|
173
|
+
|
|
174
|
+
if (rawValue === '' || rawValue === null) {
|
|
175
|
+
// Nested object or array will follow
|
|
176
|
+
currentObj[key] = {};
|
|
177
|
+
stack.push({ indent, obj: currentObj[key] });
|
|
178
|
+
} else if (rawValue.startsWith('[') && rawValue.endsWith(']')) {
|
|
179
|
+
// Inline array: [item1, item2]
|
|
180
|
+
const items = rawValue.slice(1, -1).split(',').map(s => parseYamlValue(s.trim()));
|
|
181
|
+
currentObj[key] = items;
|
|
182
|
+
} else {
|
|
183
|
+
currentObj[key] = parseYamlValue(rawValue);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return result;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Parse a YAML value to its JS type
|
|
192
|
+
*/
|
|
193
|
+
function parseYamlValue(str) {
|
|
194
|
+
if (str === 'true') return true;
|
|
195
|
+
if (str === 'false') return false;
|
|
196
|
+
if (str === 'null' || str === '~') return null;
|
|
197
|
+
|
|
198
|
+
// Remove quotes
|
|
199
|
+
if ((str.startsWith('"') && str.endsWith('"')) ||
|
|
200
|
+
(str.startsWith("'") && str.endsWith("'"))) {
|
|
201
|
+
return str.slice(1, -1);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Numbers
|
|
205
|
+
if (/^-?\d+(\.\d+)?$/.test(str)) {
|
|
206
|
+
return parseFloat(str);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return str;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Load and parse a config file
|
|
214
|
+
* @param {string} configPath - Path to config file
|
|
215
|
+
* @returns {Object} Parsed configuration
|
|
216
|
+
*/
|
|
217
|
+
function loadConfigFile(configPath) {
|
|
218
|
+
const resolved = path.resolve(configPath);
|
|
219
|
+
|
|
220
|
+
if (!fs.existsSync(resolved)) {
|
|
221
|
+
throw new Error(`Config file not found: ${resolved}`);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const content = fs.readFileSync(resolved, 'utf8');
|
|
225
|
+
const ext = path.extname(resolved).toLowerCase();
|
|
226
|
+
|
|
227
|
+
if (ext === '.json') {
|
|
228
|
+
return JSON.parse(content);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (ext === '.js') {
|
|
232
|
+
return require(resolved);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// YAML
|
|
236
|
+
return parseSimpleYaml(content);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ─── Environment Variable Expansion ───────────────────────────────
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Expand environment variables and special paths in config values
|
|
243
|
+
* Supports: $HOME, $USER, ~, ${VAR_NAME}, $VAR_NAME
|
|
244
|
+
* @param {any} value - Config value (string, array, or object)
|
|
245
|
+
* @returns {any} Expanded value
|
|
246
|
+
*/
|
|
247
|
+
function expandVariables(value) {
|
|
248
|
+
if (typeof value === 'string') {
|
|
249
|
+
return value
|
|
250
|
+
.replace(/^~(?=[/\\]|$)/, os.homedir())
|
|
251
|
+
.replace(/\$HOME/g, os.homedir())
|
|
252
|
+
.replace(/\$USER/g, os.userInfo().username)
|
|
253
|
+
.replace(/\$\{(\w+)\}/g, (_, name) => process.env[name] || '')
|
|
254
|
+
.replace(/\$(\w+)/g, (match, name) => {
|
|
255
|
+
// Don't replace if it looks like a YAML reference
|
|
256
|
+
if (['HOME', 'USER'].includes(name)) return process.env[name] || '';
|
|
257
|
+
return process.env[name] !== undefined ? process.env[name] : match;
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (Array.isArray(value)) {
|
|
262
|
+
return value.map(v => expandVariables(v));
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (value && typeof value === 'object') {
|
|
266
|
+
const result = {};
|
|
267
|
+
for (const [k, v] of Object.entries(value)) {
|
|
268
|
+
result[k] = expandVariables(v);
|
|
269
|
+
}
|
|
270
|
+
return result;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return value;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// ─── Deep Merge ───────────────────────────────────────────────────
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Deep merge two objects (source overwrites target)
|
|
280
|
+
* @param {Object} target - Base object
|
|
281
|
+
* @param {Object} source - Override object
|
|
282
|
+
* @returns {Object} Merged result
|
|
283
|
+
*/
|
|
284
|
+
function deepMerge(target, source) {
|
|
285
|
+
const result = { ...target };
|
|
286
|
+
|
|
287
|
+
for (const [key, value] of Object.entries(source)) {
|
|
288
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
289
|
+
result[key] = deepMerge(result[key] || {}, value);
|
|
290
|
+
} else {
|
|
291
|
+
result[key] = value;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return result;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// ─── Config Schema Validation ─────────────────────────────────────
|
|
299
|
+
|
|
300
|
+
const VALID_NAMING = ['original', 'category_prefix', 'date_prefix', 'clean'];
|
|
301
|
+
const VALID_DUPLICATES = ['rename', 'skip', 'overwrite'];
|
|
302
|
+
const VALID_FORMATS = ['table', 'json', 'csv', 'minimal'];
|
|
303
|
+
const VALID_CLEAN_CATEGORIES = ['temp', 'cache', 'system', 'logs', 'build', 'dependencies', 'ide'];
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Validate a config object and return errors
|
|
307
|
+
* @param {Object} config - Config to validate
|
|
308
|
+
* @returns {{ valid: boolean, errors: string[], warnings: string[] }}
|
|
309
|
+
*/
|
|
310
|
+
function validateConfig(config) {
|
|
311
|
+
const errors = [];
|
|
312
|
+
const warnings = [];
|
|
313
|
+
|
|
314
|
+
// Organize
|
|
315
|
+
if (config.organize) {
|
|
316
|
+
if (config.organize.naming && !VALID_NAMING.includes(config.organize.naming)) {
|
|
317
|
+
errors.push(`organize.naming: invalid value "${config.organize.naming}". Valid: ${VALID_NAMING.join(', ')}`);
|
|
318
|
+
}
|
|
319
|
+
if (config.organize.duplicates && !VALID_DUPLICATES.includes(config.organize.duplicates)) {
|
|
320
|
+
errors.push(`organize.duplicates: invalid value "${config.organize.duplicates}". Valid: ${VALID_DUPLICATES.join(', ')}`);
|
|
321
|
+
}
|
|
322
|
+
if (config.organize.maxDepth && (typeof config.organize.maxDepth !== 'number' || config.organize.maxDepth < 1)) {
|
|
323
|
+
errors.push('organize.maxDepth: must be a positive number');
|
|
324
|
+
}
|
|
325
|
+
if (config.organize.maxDepth > 50) {
|
|
326
|
+
warnings.push('organize.maxDepth: values above 50 may cause performance issues');
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Clean
|
|
331
|
+
if (config.clean) {
|
|
332
|
+
if (config.clean.categories) {
|
|
333
|
+
for (const cat of config.clean.categories) {
|
|
334
|
+
if (!VALID_CLEAN_CATEGORIES.includes(cat)) {
|
|
335
|
+
warnings.push(`clean.categories: unknown category "${cat}"`);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Output
|
|
342
|
+
if (config.output) {
|
|
343
|
+
if (config.output.format && !VALID_FORMATS.includes(config.output.format)) {
|
|
344
|
+
errors.push(`output.format: invalid value "${config.output.format}". Valid: ${VALID_FORMATS.join(', ')}`);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Watch
|
|
349
|
+
if (config.watch && config.watch.rules) {
|
|
350
|
+
for (let i = 0; i < config.watch.rules.length; i++) {
|
|
351
|
+
const rule = config.watch.rules[i];
|
|
352
|
+
if (!rule.match) {
|
|
353
|
+
errors.push(`watch.rules[${i}]: missing "match" pattern`);
|
|
354
|
+
}
|
|
355
|
+
if (!rule.action) {
|
|
356
|
+
errors.push(`watch.rules[${i}]: missing "action"`);
|
|
357
|
+
}
|
|
358
|
+
if (['move', 'copy'].includes(rule.action) && !rule.dest) {
|
|
359
|
+
errors.push(`watch.rules[${i}]: action "${rule.action}" requires "dest"`);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return {
|
|
365
|
+
valid: errors.length === 0,
|
|
366
|
+
errors,
|
|
367
|
+
warnings
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// ─── Main Config Loader ──────────────────────────────────────────
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Load full configuration with merge hierarchy:
|
|
375
|
+
* defaults → user config (~/.filemayor.yml) → project config → CLI flags
|
|
376
|
+
* @param {Object} options
|
|
377
|
+
* @returns {{ config: Object, source: string, validation: Object }}
|
|
378
|
+
*/
|
|
379
|
+
function loadConfig(options = {}) {
|
|
380
|
+
const {
|
|
381
|
+
configPath = null, // Explicit config file path
|
|
382
|
+
cliOverrides = {}, // CLI flag overrides
|
|
383
|
+
searchDir = process.cwd(), // Where to search for config
|
|
384
|
+
validate = true // Run validation
|
|
385
|
+
} = options;
|
|
386
|
+
|
|
387
|
+
let config = { ...DEFAULT_CONFIG };
|
|
388
|
+
let source = 'defaults';
|
|
389
|
+
|
|
390
|
+
// 1. Try to load user-level config from home dir
|
|
391
|
+
const homeConfig = path.join(os.homedir(), '.filemayor.yml');
|
|
392
|
+
if (fs.existsSync(homeConfig)) {
|
|
393
|
+
try {
|
|
394
|
+
const userConfig = loadConfigFile(homeConfig);
|
|
395
|
+
config = deepMerge(config, expandVariables(userConfig));
|
|
396
|
+
source = homeConfig;
|
|
397
|
+
} catch { /* ignore user config errors */ }
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// 2. Try to load project-level config
|
|
401
|
+
const projectConfig = configPath || findConfigFile(searchDir);
|
|
402
|
+
if (projectConfig && projectConfig !== homeConfig) {
|
|
403
|
+
try {
|
|
404
|
+
const projConfig = loadConfigFile(projectConfig);
|
|
405
|
+
config = deepMerge(config, expandVariables(projConfig));
|
|
406
|
+
source = projectConfig;
|
|
407
|
+
} catch (err) {
|
|
408
|
+
if (configPath) {
|
|
409
|
+
// Explicit path — throw
|
|
410
|
+
throw new Error(`Failed to load config: ${err.message}`);
|
|
411
|
+
}
|
|
412
|
+
// Auto-discovered — ignore
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// 3. Apply CLI overrides
|
|
417
|
+
if (Object.keys(cliOverrides).length > 0) {
|
|
418
|
+
config = deepMerge(config, cliOverrides);
|
|
419
|
+
source = `${source} + CLI flags`;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// 4. Validate
|
|
423
|
+
let validation = { valid: true, errors: [], warnings: [] };
|
|
424
|
+
if (validate) {
|
|
425
|
+
validation = validateConfig(config);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
return { config, source, validation };
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// ─── Config Template Generation ──────────────────────────────────
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Generate a default .filemayor.yml template
|
|
435
|
+
* @returns {string} YAML config template
|
|
436
|
+
*/
|
|
437
|
+
function generateTemplate() {
|
|
438
|
+
return `# ═══════════════════════════════════════════════════════════
|
|
439
|
+
# FileMayor Configuration
|
|
440
|
+
# Place this file in your project root or home directory
|
|
441
|
+
# ═══════════════════════════════════════════════════════════
|
|
442
|
+
|
|
443
|
+
version: 1
|
|
444
|
+
|
|
445
|
+
# ─── File Organization ─────────────────────────────────────
|
|
446
|
+
organize:
|
|
447
|
+
# Naming convention: original | category_prefix | date_prefix | clean
|
|
448
|
+
naming: original
|
|
449
|
+
|
|
450
|
+
# Duplicate handling: rename | skip | overwrite
|
|
451
|
+
duplicates: rename
|
|
452
|
+
|
|
453
|
+
# Max directory depth to scan
|
|
454
|
+
maxDepth: 20
|
|
455
|
+
|
|
456
|
+
# Include hidden files (starting with .)
|
|
457
|
+
includeHidden: false
|
|
458
|
+
|
|
459
|
+
# Directories to ignore
|
|
460
|
+
ignore: [node_modules, .git, __pycache__, .venv, dist, build, .next, .cache]
|
|
461
|
+
|
|
462
|
+
# Custom category overrides (merge with defaults)
|
|
463
|
+
# categories:
|
|
464
|
+
# reports:
|
|
465
|
+
# label: Reports
|
|
466
|
+
# extensions: [.report, .summary]
|
|
467
|
+
# color: "#ff6b6b"
|
|
468
|
+
|
|
469
|
+
# ─── System Cleaner ────────────────────────────────────────
|
|
470
|
+
clean:
|
|
471
|
+
# Junk categories to scan: temp, cache, system, logs, build, dependencies, ide
|
|
472
|
+
categories: [temp, cache, system, logs]
|
|
473
|
+
|
|
474
|
+
# Max depth for junk scanning
|
|
475
|
+
maxDepth: 10
|
|
476
|
+
|
|
477
|
+
# Include junk directories (node_modules, .cache, etc.)
|
|
478
|
+
includeDirectories: true
|
|
479
|
+
|
|
480
|
+
# ─── File Watcher (Daemon Mode) ────────────────────────────
|
|
481
|
+
watch:
|
|
482
|
+
# Directories to watch
|
|
483
|
+
directories: []
|
|
484
|
+
|
|
485
|
+
# Debounce delay for rapid changes (ms)
|
|
486
|
+
debounceMs: 500
|
|
487
|
+
|
|
488
|
+
# Watch subdirectories
|
|
489
|
+
recursive: true
|
|
490
|
+
|
|
491
|
+
# Auto-organize on new files
|
|
492
|
+
autoOrganize: false
|
|
493
|
+
|
|
494
|
+
# Rules engine
|
|
495
|
+
# rules:
|
|
496
|
+
# - match: "*.pdf"
|
|
497
|
+
# action: move
|
|
498
|
+
# dest: ~/Documents/PDFs
|
|
499
|
+
# - match: "*.{jpg,png,gif}"
|
|
500
|
+
# action: move
|
|
501
|
+
# dest: ~/Pictures
|
|
502
|
+
# - match: "@code"
|
|
503
|
+
# action: log
|
|
504
|
+
|
|
505
|
+
# ─── Output Formatting ─────────────────────────────────────
|
|
506
|
+
output:
|
|
507
|
+
# Format: table | json | csv | minimal
|
|
508
|
+
format: table
|
|
509
|
+
|
|
510
|
+
# Enable colors
|
|
511
|
+
colors: true
|
|
512
|
+
|
|
513
|
+
# Verbose logging
|
|
514
|
+
verbose: false
|
|
515
|
+
|
|
516
|
+
# Suppress all output (exit code only)
|
|
517
|
+
quiet: false
|
|
518
|
+
|
|
519
|
+
# ─── Security ──────────────────────────────────────────────
|
|
520
|
+
security:
|
|
521
|
+
# Max file operations per minute
|
|
522
|
+
maxOperationsPerMinute: 1000
|
|
523
|
+
|
|
524
|
+
# Require confirmation for destructive operations
|
|
525
|
+
confirmDestructive: true
|
|
526
|
+
|
|
527
|
+
# Enable operation journal for undo
|
|
528
|
+
journalEnabled: true
|
|
529
|
+
|
|
530
|
+
# Journal file path (relative to working directory)
|
|
531
|
+
journalPath: .filemayor-journal.json
|
|
532
|
+
`;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* Create a config file in the specified directory
|
|
537
|
+
* @param {string} dir - Directory to create config in
|
|
538
|
+
* @param {string} filename - Config filename
|
|
539
|
+
* @returns {string} Path to created file
|
|
540
|
+
*/
|
|
541
|
+
function createConfigFile(dir = process.cwd(), filename = '.filemayor.yml') {
|
|
542
|
+
const configPath = path.join(dir, filename);
|
|
543
|
+
if (fs.existsSync(configPath)) {
|
|
544
|
+
throw new Error(`Config file already exists: ${configPath}`);
|
|
545
|
+
}
|
|
546
|
+
fs.writeFileSync(configPath, generateTemplate(), 'utf8');
|
|
547
|
+
return configPath;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
module.exports = {
|
|
551
|
+
DEFAULT_CONFIG,
|
|
552
|
+
CONFIG_FILENAMES,
|
|
553
|
+
findConfigFile,
|
|
554
|
+
loadConfigFile,
|
|
555
|
+
loadConfig,
|
|
556
|
+
validateConfig,
|
|
557
|
+
deepMerge,
|
|
558
|
+
expandVariables,
|
|
559
|
+
parseSimpleYaml,
|
|
560
|
+
generateTemplate,
|
|
561
|
+
createConfigFile
|
|
562
|
+
};
|
package/core/index.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ═══════════════════════════════════════════════════════════════════
|
|
5
|
+
* FILEMAYOR CORE — INDEX (Barrel Export)
|
|
6
|
+
* Unified entry point for all core modules
|
|
7
|
+
* ═══════════════════════════════════════════════════════════════════
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
'use strict';
|
|
11
|
+
|
|
12
|
+
const scanner = require('./scanner');
|
|
13
|
+
const organizer = require('./organizer');
|
|
14
|
+
const cleaner = require('./cleaner');
|
|
15
|
+
const watcher = require('./watcher');
|
|
16
|
+
const config = require('./config');
|
|
17
|
+
const reporter = require('./reporter');
|
|
18
|
+
const categories = require('./categories');
|
|
19
|
+
const security = require('./security');
|
|
20
|
+
const sopParser = require('./sop-parser');
|
|
21
|
+
|
|
22
|
+
module.exports = {
|
|
23
|
+
// Scanner
|
|
24
|
+
Scanner: scanner.Scanner,
|
|
25
|
+
scan: scanner.scan,
|
|
26
|
+
scanByCategory: scanner.scanByCategory,
|
|
27
|
+
scanSummary: scanner.scanSummary,
|
|
28
|
+
formatBytes: scanner.formatBytes,
|
|
29
|
+
|
|
30
|
+
// Organizer
|
|
31
|
+
organize: organizer.organize,
|
|
32
|
+
generatePlan: organizer.generatePlan,
|
|
33
|
+
executePlan: organizer.executePlan,
|
|
34
|
+
rollback: organizer.rollback,
|
|
35
|
+
loadJournal: organizer.loadJournal,
|
|
36
|
+
NAMING_CONVENTIONS: organizer.NAMING_CONVENTIONS,
|
|
37
|
+
|
|
38
|
+
// Cleaner
|
|
39
|
+
Cleaner: cleaner.Cleaner,
|
|
40
|
+
findJunk: cleaner.findJunk,
|
|
41
|
+
deleteJunk: cleaner.deleteJunk,
|
|
42
|
+
clean: cleaner.clean,
|
|
43
|
+
JUNK_CATEGORIES: cleaner.JUNK_CATEGORIES,
|
|
44
|
+
|
|
45
|
+
// Watcher
|
|
46
|
+
FileWatcher: watcher.FileWatcher,
|
|
47
|
+
|
|
48
|
+
// Config
|
|
49
|
+
loadConfig: config.loadConfig,
|
|
50
|
+
findConfigFile: config.findConfigFile,
|
|
51
|
+
createConfigFile: config.createConfigFile,
|
|
52
|
+
generateTemplate: config.generateTemplate,
|
|
53
|
+
validateConfig: config.validateConfig,
|
|
54
|
+
|
|
55
|
+
// Reporter
|
|
56
|
+
reporter,
|
|
57
|
+
|
|
58
|
+
// Categories
|
|
59
|
+
categorize: categories.categorize,
|
|
60
|
+
getCategories: categories.getCategories,
|
|
61
|
+
mergeCategories: categories.mergeCategories,
|
|
62
|
+
DEFAULT_CATEGORIES: categories.DEFAULT_CATEGORIES,
|
|
63
|
+
|
|
64
|
+
// Security
|
|
65
|
+
validatePath: security.validatePath,
|
|
66
|
+
isFileSafe: security.isFileSafe,
|
|
67
|
+
isDirSafe: security.isDirSafe,
|
|
68
|
+
sanitizeFilename: security.sanitizeFilename,
|
|
69
|
+
checkPermissions: security.checkPermissions,
|
|
70
|
+
|
|
71
|
+
// SOP Parser
|
|
72
|
+
parseSOP: sopParser.parseSOP,
|
|
73
|
+
parseRuleBased: sopParser.parseRuleBased,
|
|
74
|
+
rulesToConfig: sopParser.rulesToConfig,
|
|
75
|
+
FILE_TYPE_ALIASES: sopParser.FILE_TYPE_ALIASES,
|
|
76
|
+
|
|
77
|
+
// Version
|
|
78
|
+
VERSION: require('../package.json').version || '2.0.0'
|
|
79
|
+
};
|