filemayor 3.6.0 → 4.0.7

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.
@@ -23,7 +23,7 @@ class FileMayorFS {
23
23
  useJournal: true,
24
24
  dryRun: false,
25
25
  trashPath: path.join(os.tmpdir(), 'filemayor_trash'),
26
- journalPath: path.join(os.homedir(), '.filemayor-master-journal.json'),
26
+ journalPath: path.join(os.homedir(), '.filemayor-master-journal.ndjson'),
27
27
  ...options
28
28
  };
29
29
  this.sessionJournal = [];
@@ -77,6 +77,10 @@ class FileMayorFS {
77
77
  const destDir = path.dirname(destination);
78
78
  await fs.mkdir(destDir, { recursive: true });
79
79
 
80
+ // WAL: write pending entry BEFORE the operation
81
+ const opId = crypto.randomUUID();
82
+ await this._journalPending(opId, source, destination, 'move', snapshot);
83
+
80
84
  try {
81
85
  await fs.rename(source, destination);
82
86
  } catch (err) {
@@ -86,11 +90,13 @@ class FileMayorFS {
86
90
  } else throw err;
87
91
  }
88
92
 
93
+ // WAL: record completion AFTER the operation
94
+ await this._journalDone(opId);
95
+
89
96
  result.status = 'success';
90
97
  this.stats.opsSucceeded++;
91
98
  this.stats.filesMoved++;
92
99
  this.stats.totalBytesProcessed += snapshot.size || 0;
93
- await this._logToJournal(source, destination, 'move', snapshot);
94
100
 
95
101
  } catch (err) {
96
102
  result.status = 'error';
@@ -126,61 +132,79 @@ class FileMayorFS {
126
132
  }
127
133
 
128
134
  /** ------------------------
129
- * Journaling
135
+ * Journaling (WAL)
130
136
  * -------------------------*/
131
- async _logToJournal(source, destination, action, snapshot) {
137
+ async _journalWriteLine(entry) {
132
138
  if (!this.options.useJournal) return;
139
+ try {
140
+ const line = JSON.stringify(entry) + '\n';
141
+ fssync.appendFileSync(this.options.journalPath, line, 'utf8');
142
+ } catch (err) {
143
+ console.error(`[FAIL] Journal write failed: ${err.message}`);
144
+ }
145
+ }
146
+
147
+ async _logToJournal(source, destination, action, snapshot) {
148
+ // WAL: record intent BEFORE operation (called pre-move),
149
+ // then update to 'done' AFTER (called post-move).
150
+ // This is handled by _journalPending / _journalDone.
151
+ }
133
152
 
153
+ async _journalPending(id, source, destination, action, snapshot) {
134
154
  const entry = {
135
- id: crypto.randomUUID(),
155
+ id,
136
156
  timestamp: new Date().toISOString(),
137
157
  action,
138
158
  source,
139
159
  destination,
140
- snapshot
160
+ snapshot,
161
+ status: 'pending',
141
162
  };
142
-
143
163
  this.sessionJournal.push(entry);
164
+ await this._journalWriteLine(entry);
165
+ return entry;
166
+ }
144
167
 
145
- // Persistent Journaling (v3.5)
146
- try {
147
- let diskJournal = [];
148
- if (fssync.existsSync(this.options.journalPath)) {
149
- diskJournal = JSON.parse(fssync.readFileSync(this.options.journalPath, 'utf8'));
150
- }
151
- diskJournal.push(entry);
152
- // Save last 1000 ops globally
153
- fssync.writeFileSync(this.options.journalPath, JSON.stringify(diskJournal.slice(-1000), null, 2));
154
- } catch (err) {
155
- console.error(`[FAIL] Disk Journal write failed: ${err.message}`);
156
- }
168
+ async _journalDone(id) {
169
+ await this._journalWriteLine({ id, status: 'done', completedAt: new Date().toISOString() });
157
170
  }
158
171
 
159
172
  /** ------------------------
160
173
  * Rollback
161
174
  * -------------------------*/
162
175
  async rollback() {
163
- // Use session journal if available, otherwise try to load from disk
164
- let reversed = [...this.sessionJournal].reverse();
165
-
166
- if (reversed.length === 0 && fssync.existsSync(this.options.journalPath)) {
167
- console.log(`[SYS] Session journal empty. Attempting disk rollback...`);
168
- const diskJournal = JSON.parse(fssync.readFileSync(this.options.journalPath, 'utf8'));
169
- reversed = diskJournal.reverse();
176
+ const entries = new Map();
177
+ const done = new Set();
178
+
179
+ if (fssync.existsSync(this.options.journalPath)) {
180
+ const lines = fssync.readFileSync(this.options.journalPath, 'utf8').split('\n');
181
+ for (const line of lines) {
182
+ if (!line.trim()) continue;
183
+ try {
184
+ const rec = JSON.parse(line);
185
+ if (rec.status === 'pending') entries.set(rec.id, rec);
186
+ if (rec.status === 'done') done.add(rec.id);
187
+ } catch { /* skip malformed lines */ }
188
+ }
189
+ }
190
+
191
+ // Add session-only pending entries not yet on disk
192
+ for (const e of this.sessionJournal) {
193
+ if (!entries.has(e.id)) entries.set(e.id, e);
170
194
  }
171
195
 
172
- console.log(`[SYS] Rolling back ${reversed.length} operations...`);
196
+ // Reverse-order rollback of moves that never completed
197
+ const toRollback = [...entries.values()]
198
+ .filter(e => !done.has(e.id))
199
+ .sort((a, b) => b.timestamp.localeCompare(a.timestamp));
200
+
201
+ console.log(`[SYS] Rolling back ${toRollback.length} incomplete operations...`);
173
202
 
174
- for (const entry of reversed) {
203
+ for (const entry of toRollback) {
175
204
  try {
176
- if (entry.action === 'move') {
177
- if (fssync.existsSync(entry.destination)) {
178
- await fs.rename(entry.destination, entry.source);
179
- }
180
- } else if (entry.action === 'delete') {
181
- if (fssync.existsSync(entry.destination)) {
182
- await fs.rename(entry.destination, entry.source);
183
- }
205
+ if (fssync.existsSync(entry.destination)) {
206
+ await fs.rename(entry.destination, entry.source);
207
+ await this._journalWriteLine({ id: entry.id, status: 'rolled-back', rolledBackAt: new Date().toISOString() });
184
208
  }
185
209
  } catch (err) {
186
210
  console.error(`[FAIL] Rollback failed for ${entry.source}: ${err.message}`);
@@ -188,7 +212,7 @@ class FileMayorFS {
188
212
  }
189
213
 
190
214
  this.sessionJournal = [];
191
- console.log(`[SYS] Rollback complete.`);
215
+ console.log('[SYS] Rollback complete.');
192
216
  }
193
217
 
194
218
  getJournal() {
@@ -196,4 +220,52 @@ class FileMayorFS {
196
220
  }
197
221
  }
198
222
 
199
- module.exports = FileMayorFS;
223
+ const JOURNAL_PATH = path.join(os.homedir(), '.filemayor-master-journal.ndjson');
224
+
225
+ /**
226
+ * Standalone rollback function that reads the master journal on disk
227
+ * and reverses all completed operations (status === 'done').
228
+ * Returns { reversed: Array<entry>, count: number }.
229
+ */
230
+ async function rollback(journalPath) {
231
+ const entries = new Map();
232
+ const done = new Set();
233
+
234
+ if (fssync.existsSync(journalPath)) {
235
+ const lines = fssync.readFileSync(journalPath, 'utf8').split('\n');
236
+ for (const line of lines) {
237
+ if (!line.trim()) continue;
238
+ try {
239
+ const rec = JSON.parse(line);
240
+ if (rec.status === 'pending') entries.set(rec.id, rec);
241
+ if (rec.status === 'done') done.add(rec.id);
242
+ } catch { /* skip malformed lines */ }
243
+ }
244
+ }
245
+
246
+ // Rollback moves that completed successfully (status === 'done')
247
+ const toRollback = [...entries.values()]
248
+ .filter(e => done.has(e.id))
249
+ .sort((a, b) => b.timestamp.localeCompare(a.timestamp));
250
+
251
+ const reversed = [];
252
+ for (const entry of toRollback) {
253
+ try {
254
+ if (fssync.existsSync(entry.destination)) {
255
+ await fs.rename(entry.destination, entry.source);
256
+ fssync.appendFileSync(journalPath,
257
+ JSON.stringify({ id: entry.id, status: 'rolled-back', rolledBackAt: new Date().toISOString() }) + '\n',
258
+ 'utf8');
259
+ reversed.push(entry);
260
+ }
261
+ } catch (err) {
262
+ console.error(`[FAIL] Rollback failed for ${entry.source}: ${err.message}`);
263
+ }
264
+ }
265
+
266
+ return { reversed, count: reversed.length };
267
+ }
268
+
269
+ module.exports = FileMayorFS;
270
+ module.exports.JOURNAL_PATH = JOURNAL_PATH;
271
+ module.exports.rollback = rollback;
package/core/index.js CHANGED
@@ -1,135 +1,135 @@
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 IntentInterpreter = require('./intent-interpreter');
15
- const FileMayorFS = require('./fs-abstraction');
16
- const cleaner = require('./cleaner');
17
- const watcher = require('./watcher');
18
- const config = require('./config');
19
- const reporter = require('./reporter');
20
- const categories = require('./categories');
21
- const security = require('./security');
22
- const sopParser = require('./sop-parser');
23
- const analyzer = require('./analyzer');
24
- const license = require('./license');
25
- const ExplainEngine = require('./engine/explain-engine');
26
- const CureEngine = require('./engine/cure-engine');
27
- const ApplyEngine = require('./engine/apply-engine');
28
- const PreviewEngine = require('./engine/preview-engine');
29
- const DedupeEngine = require('./engine/dedupe-engine');
30
- const MetadataStore = require('./metadata-store');
31
- const strategist = require('./ai/strategist');
32
- const sentry = require('./ai/sentry');
33
- const planner = require('./ai/planner');
34
- const validator = require('./ai/validator');
35
- const { FileMayorJailer, enforceUserSpace } = require('./jailer');
36
- const Vault = require('./vault');
37
- const LogicGuardrail = require('./guardrail');
38
- const { initEmergencyHalt, updateJournalRef, clearEmergencyHalt } = require('./emergency-halt');
39
-
40
- module.exports = {
41
- // Scanner
42
- Scanner: scanner.Scanner,
43
- scan: scanner.scan,
44
- scanByCategory: scanner.scanByCategory,
45
- scanSummary: scanner.scanSummary,
46
- formatBytes: scanner.formatBytes,
47
- analyzeDirectory: analyzer.analyzeDirectory,
48
-
49
- // Organizer
50
- organize: organizer.organize,
51
- generatePlan: organizer.generatePlan,
52
- executePlan: organizer.executePlan,
53
- rollback: organizer.rollback,
54
- loadJournal: organizer.loadJournal,
55
- NAMING_CONVENTIONS: organizer.NAMING_CONVENTIONS,
56
-
57
- // Intent Interpreter
58
- IntentInterpreter: IntentInterpreter,
59
-
60
- // FS Abstraction
61
- FileMayorFS: FileMayorFS,
62
-
63
- // Cleaner
64
- Cleaner: cleaner.Cleaner,
65
- findJunk: cleaner.findJunk,
66
- deleteJunk: cleaner.deleteJunk,
67
- clean: cleaner.clean,
68
- JUNK_CATEGORIES: cleaner.JUNK_CATEGORIES,
69
-
70
- // Watcher
71
- FileWatcher: watcher.FileWatcher,
72
-
73
- // Config
74
- loadConfig: config.loadConfig,
75
- findConfigFile: config.findConfigFile,
76
- createConfigFile: config.createConfigFile,
77
- generateTemplate: config.generateTemplate,
78
- validateConfig: config.validateConfig,
79
-
80
- // Reporter
81
- reporter,
82
-
83
- // Categories
84
- categorize: categories.categorize,
85
- getCategories: categories.getCategories,
86
- mergeCategories: categories.mergeCategories,
87
- DEFAULT_CATEGORIES: categories.DEFAULT_CATEGORIES,
88
-
89
- // Security
90
- validatePath: security.validatePath,
91
- isFileSafe: security.isFileSafe,
92
- isDirSafe: security.isDirSafe,
93
- sanitizeFilename: security.sanitizeFilename,
94
- checkPermissions: security.checkPermissions,
95
-
96
- // SOP Parser
97
- parseSOP: sopParser.parseSOP,
98
- parseRuleBased: sopParser.parseRuleBased,
99
- rulesToConfig: sopParser.rulesToConfig,
100
- FILE_TYPE_ALIASES: sopParser.FILE_TYPE_ALIASES,
101
-
102
- // License
103
- activateLicense: license.activateLicense,
104
- deactivateLicense: license.deactivateLicense,
105
- getLicenseInfo: license.getLicenseInfo,
106
- checkProFeature: license.checkProFeature,
107
- checkBulkLimit: license.checkBulkLimit,
108
- hasFeature: license.hasFeature,
109
-
110
- // Intent Agents
111
- IntentStrategist: strategist,
112
- MetadataSentry: sentry,
113
- CurativePlanner: planner,
114
- SecurityArchitect: validator,
115
-
116
- // Engines
117
- ExplainEngine,
118
- CureEngine,
119
- ApplyEngine,
120
- PreviewEngine,
121
- DedupeEngine,
122
- MetadataStore,
123
-
124
- // Hardened Runtime
125
- FileMayorJailer,
126
- enforceUserSpace,
127
- Vault,
128
- LogicGuardrail,
129
- initEmergencyHalt,
130
- updateJournalRef,
131
- clearEmergencyHalt,
132
-
133
- // Version
134
- VERSION: require('../package.json').version || '2.0.0'
135
- };
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 IntentInterpreter = require('./intent-interpreter');
15
+ const FileMayorFS = require('./fs-abstraction');
16
+ const cleaner = require('./cleaner');
17
+ const watcher = require('./watcher');
18
+ const config = require('./config');
19
+ const reporter = require('./reporter');
20
+ const categories = require('./categories');
21
+ const security = require('./security');
22
+ const sopParser = require('./sop-parser');
23
+ const analyzer = require('./analyzer');
24
+ const license = require('./license');
25
+ const ExplainEngine = require('./engine/explain-engine');
26
+ const CureEngine = require('./engine/cure-engine');
27
+ const ApplyEngine = require('./engine/apply-engine');
28
+ const PreviewEngine = require('./engine/preview-engine');
29
+ const DedupeEngine = require('./engine/dedupe-engine');
30
+ const MetadataStore = require('./metadata-store');
31
+ const strategist = require('./ai/strategist');
32
+ const sentry = require('./ai/sentry');
33
+ const planner = require('./ai/planner');
34
+ const validator = require('./ai/validator');
35
+ const { FileMayorJailer, enforceUserSpace } = require('./jailer');
36
+ const Vault = require('./vault');
37
+ const LogicGuardrail = require('./guardrail');
38
+ const { initEmergencyHalt, updateJournalRef, clearEmergencyHalt } = require('./emergency-halt');
39
+
40
+ module.exports = {
41
+ // Scanner
42
+ Scanner: scanner.Scanner,
43
+ scan: scanner.scan,
44
+ scanByCategory: scanner.scanByCategory,
45
+ scanSummary: scanner.scanSummary,
46
+ formatBytes: scanner.formatBytes,
47
+ analyzeDirectory: analyzer.analyzeDirectory,
48
+
49
+ // Organizer
50
+ organize: organizer.organize,
51
+ generatePlan: organizer.generatePlan,
52
+ executePlan: organizer.executePlan,
53
+ rollback: organizer.rollback,
54
+ loadJournal: organizer.loadJournal,
55
+ NAMING_CONVENTIONS: organizer.NAMING_CONVENTIONS,
56
+
57
+ // Intent Interpreter
58
+ IntentInterpreter: IntentInterpreter,
59
+
60
+ // FS Abstraction
61
+ FileMayorFS: FileMayorFS,
62
+
63
+ // Cleaner
64
+ Cleaner: cleaner.Cleaner,
65
+ findJunk: cleaner.findJunk,
66
+ deleteJunk: cleaner.deleteJunk,
67
+ clean: cleaner.clean,
68
+ JUNK_CATEGORIES: cleaner.JUNK_CATEGORIES,
69
+
70
+ // Watcher
71
+ FileWatcher: watcher.FileWatcher,
72
+
73
+ // Config
74
+ loadConfig: config.loadConfig,
75
+ findConfigFile: config.findConfigFile,
76
+ createConfigFile: config.createConfigFile,
77
+ generateTemplate: config.generateTemplate,
78
+ validateConfig: config.validateConfig,
79
+
80
+ // Reporter
81
+ reporter,
82
+
83
+ // Categories
84
+ categorize: categories.categorize,
85
+ getCategories: categories.getCategories,
86
+ mergeCategories: categories.mergeCategories,
87
+ DEFAULT_CATEGORIES: categories.DEFAULT_CATEGORIES,
88
+
89
+ // Security
90
+ validatePath: security.validatePath,
91
+ isFileSafe: security.isFileSafe,
92
+ isDirSafe: security.isDirSafe,
93
+ sanitizeFilename: security.sanitizeFilename,
94
+ checkPermissions: security.checkPermissions,
95
+
96
+ // SOP Parser
97
+ parseSOP: sopParser.parseSOP,
98
+ parseRuleBased: sopParser.parseRuleBased,
99
+ rulesToConfig: sopParser.rulesToConfig,
100
+ FILE_TYPE_ALIASES: sopParser.FILE_TYPE_ALIASES,
101
+
102
+ // License
103
+ activateLicense: license.activateLicense,
104
+ deactivateLicense: license.deactivateLicense,
105
+ getLicenseInfo: license.getLicenseInfo,
106
+ checkProFeature: license.checkProFeature,
107
+ checkBulkLimit: license.checkBulkLimit,
108
+ hasFeature: license.hasFeature,
109
+
110
+ // Intent Agents
111
+ IntentStrategist: strategist,
112
+ MetadataSentry: sentry,
113
+ CurativePlanner: planner,
114
+ SecurityArchitect: validator,
115
+
116
+ // Engines
117
+ ExplainEngine,
118
+ CureEngine,
119
+ ApplyEngine,
120
+ PreviewEngine,
121
+ DedupeEngine,
122
+ MetadataStore,
123
+
124
+ // Hardened Runtime
125
+ FileMayorJailer,
126
+ enforceUserSpace,
127
+ Vault,
128
+ LogicGuardrail,
129
+ initEmergencyHalt,
130
+ updateJournalRef,
131
+ clearEmergencyHalt,
132
+
133
+ // Version
134
+ VERSION: require('../package.json').version || '2.0.0'
135
+ };