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