@magentrix-corp/magentrix-cli 1.1.4 → 1.2.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.
@@ -0,0 +1,96 @@
1
+ import { walkFiles } from '../cacher.js';
2
+ import Config from '../config.js';
3
+ import path from 'path';
4
+
5
+ const config = new Config();
6
+ const EXPORT_ROOT = 'src';
7
+
8
+ // Load cache
9
+ const hits = await config.searchObject({}, { filename: "base.json", global: false });
10
+ const cachedResults = hits?.[0]?.value || {};
11
+
12
+ const cachedFiles = Object.values(cachedResults).map((c) => ({
13
+ ...c,
14
+ tag: c.recordId,
15
+ filePath: c.filePath || c.lastKnownPath,
16
+ }));
17
+
18
+ console.log('Total cached files:', cachedFiles.length);
19
+ console.log('Cached assets (type=File):', cachedFiles.filter(cf => cf.type === 'File').length);
20
+ console.log('Cached folders (type=Folder):', cachedFiles.filter(cf => cf.type === 'Folder').length);
21
+ console.log('');
22
+
23
+ // Build Set like in publish
24
+ const cachedAssetPaths = new Set();
25
+ cachedFiles
26
+ .filter(cf => cf.type === 'File' || cf.type === 'Folder')
27
+ .forEach(cf => {
28
+ if (cf.lastKnownActualPath) {
29
+ cachedAssetPaths.add(path.normalize(path.resolve(cf.lastKnownActualPath)).toLowerCase());
30
+ }
31
+ if (cf.filePath) {
32
+ cachedAssetPaths.add(path.normalize(path.resolve(cf.filePath)).toLowerCase());
33
+ }
34
+ if (cf.lastKnownPath) {
35
+ cachedAssetPaths.add(path.normalize(path.resolve(cf.lastKnownPath)).toLowerCase());
36
+ }
37
+ });
38
+
39
+ console.log('Cached asset paths in Set:', cachedAssetPaths.size);
40
+ console.log('');
41
+
42
+ // Get local assets
43
+ const assetPaths = await walkFiles(path.join(EXPORT_ROOT, 'Assets'));
44
+ console.log('Local asset files found:', assetPaths.length);
45
+ console.log('');
46
+
47
+ // Check first few
48
+ let matched = 0;
49
+ let notMatched = 0;
50
+
51
+ for (let i = 0; i < Math.min(10, assetPaths.length); i++) {
52
+ const assetPath = assetPaths[i];
53
+ const normalizedAssetPath = path.normalize(path.resolve(assetPath)).toLowerCase();
54
+ const inCache = cachedAssetPaths.has(normalizedAssetPath);
55
+
56
+ if (inCache) {
57
+ matched++;
58
+ } else {
59
+ notMatched++;
60
+ console.log('NOT IN CACHE:', assetPath);
61
+ console.log(' Normalized:', normalizedAssetPath);
62
+ console.log('');
63
+ }
64
+ }
65
+
66
+ console.log(`First 10 files: ${matched} matched, ${notMatched} not matched`);
67
+ console.log('');
68
+
69
+ // Check if the Set actually contains the sample path
70
+ const samplePath = assetPaths[0];
71
+ const sampleNormalized = path.normalize(path.resolve(samplePath)).toLowerCase();
72
+ console.log('Sample path:', samplePath);
73
+ console.log('Normalized:', sampleNormalized);
74
+ console.log('In Set:', cachedAssetPaths.has(sampleNormalized));
75
+ console.log('');
76
+
77
+ // Check what's actually in the Set for this file
78
+ const sampleInBase = Object.values(cachedResults).find(b =>
79
+ b.filePath === 'Assets/Acronis/Banners/1.png' ||
80
+ b.lastKnownActualPath === 'src/Assets/Acronis/Banners/1.png'
81
+ );
82
+
83
+ if (sampleInBase) {
84
+ console.log('Found in base.json:');
85
+ console.log(' type:', sampleInBase.type);
86
+ console.log(' filePath:', sampleInBase.filePath);
87
+ console.log(' lastKnownActualPath:', sampleInBase.lastKnownActualPath);
88
+ console.log(' lastKnownPath:', sampleInBase.lastKnownPath);
89
+
90
+ // Check what gets added to the Set
91
+ if (sampleInBase.lastKnownActualPath) {
92
+ const normalized = path.normalize(path.resolve(sampleInBase.lastKnownActualPath)).toLowerCase();
93
+ console.log(' Normalized lastKnownActualPath:', normalized);
94
+ console.log(' Match:', normalized === sampleNormalized);
95
+ }
96
+ }
@@ -6,6 +6,11 @@ import { v4 as uuidv4 } from 'uuid';
6
6
  import fspath from 'path';
7
7
  import { setFileTag } from "./filetag.js";
8
8
  import { toLocalPath } from "./assetPaths.js";
9
+ import chalk from 'chalk';
10
+ import Config from "./config.js";
11
+ import { sha256 } from "./hash.js";
12
+
13
+ const config = new Config();
9
14
 
10
15
  export const walkAssets = async (instanceUrl, token, assetPath) => {
11
16
  const assetResults = await listAssets(instanceUrl, token, assetPath);
@@ -35,46 +40,252 @@ export const walkAssets = async (instanceUrl, token, assetPath) => {
35
40
  return walkedAssets;
36
41
  }
37
42
 
38
- export const downloadAssets = async (instanceUrl, token, path) => {
43
+ export const downloadAssets = async (instanceUrl, token, path, progressCallback = null, logger = null) => {
39
44
  const allAssets = await walkAssets(instanceUrl, token, path);
40
45
 
46
+ // Count total files for progress tracking
47
+ let totalFiles = 0;
48
+ let downloadedFiles = 0;
49
+
50
+ const countFiles = (assets) => {
51
+ for (const asset of assets) {
52
+ if (asset.Type === 'File') {
53
+ totalFiles++;
54
+ }
55
+ if (asset.Type === 'Folder' && asset.Children) {
56
+ countFiles(asset.Children);
57
+ }
58
+ }
59
+ };
60
+ countFiles(allAssets);
61
+
62
+ // Collect all base updates to write at the end (batch operation)
63
+ const baseUpdates = {};
64
+
41
65
  const iterateDownload = async (assets) => {
66
+ // Handle empty assets array
67
+ if (!assets || assets.length === 0) {
68
+ return;
69
+ }
70
+
42
71
  const parentApiPath = assets?.[0]?.ParentApiPath; // Use API path for API calls
43
72
  const parentLocalFolder = assets?.[0]?.ParentFolder; // Use local path for file system
44
73
  const folders = assets.filter(asset => asset.Type === 'Folder');
45
74
  const files = assets.filter(asset => asset.Type === 'File');
46
75
 
47
76
  for (const folder of folders) {
48
- fs.mkdirSync(fspath.join(EXPORT_ROOT, folder.Path), { recursive: true });
77
+ const folderPath = fspath.join(EXPORT_ROOT, folder.Path);
78
+ fs.mkdirSync(folderPath, { recursive: true });
79
+
80
+ // Collect base update for folder
81
+ if (fs.existsSync(folderPath)) {
82
+ const folderStats = fs.statSync(folderPath);
83
+ baseUpdates[folder.Path] = {
84
+ lastModified: folderStats.mtimeMs,
85
+ contentHash: '',
86
+ compressedContent: '',
87
+ recordId: folder.Path,
88
+ type: folder.Type,
89
+ filePath: folder.Path,
90
+ lastKnownActualPath: folderPath,
91
+ lastKnownPath: fspath.resolve(folder.Path)
92
+ };
93
+ }
94
+
49
95
  await iterateDownload(folder?.Children || []);
50
96
  }
51
97
 
52
98
  if (files.length > 0) {
53
- const savedAs = await downloadAssetsZip({
54
- baseUrl: instanceUrl,
55
- token: token, // "Bearer" prefix added in code
56
- path: parentApiPath, // Use API path for API call
57
- names: files.map(file => file.Name),
58
- outFile: fspath.join(EXPORT_ROOT, parentLocalFolder, 'assets.zip'), // Use local path for file system
59
- });
99
+ // Batch files dynamically to avoid URL length limits
100
+ // Testing shows the API fails at ~2150 chars (likely 2048 limit on the server)
101
+ // We use 2000 as a safe limit with buffer
102
+ const MAX_URL_LENGTH = 2000;
103
+ const fileBatches = [];
60
104
 
61
- await extract(savedAs, {
62
- dir: fspath.resolve(fspath.join(EXPORT_ROOT, parentLocalFolder)) // Use local path for extraction
63
- });
105
+ // Calculate base URL length once (everything except the file names)
106
+ const baseUrl = new URL("/api/3.0/staticassets/download", instanceUrl);
107
+ baseUrl.searchParams.set("path", parentApiPath);
108
+ baseUrl.searchParams.set('download-format', 'zip');
109
+ // The names will be added as: &names=encoded_comma_separated_list
110
+ // Calculate the base without names parameter
111
+ const baseUrlStr = baseUrl.toString();
112
+ const baseLength = baseUrlStr.length + '&names='.length;
113
+
114
+ let currentBatch = [];
115
+ let currentNames = '';
116
+
117
+ for (const file of files) {
118
+ // Calculate what the names parameter would be if we add this file
119
+ const testNames = currentNames
120
+ ? `${currentNames},${file.Name}`
121
+ : file.Name;
122
+
123
+ // Calculate the URL length with this names string encoded
124
+ const encodedTestNames = encodeURIComponent(testNames);
125
+ const testLength = baseLength + encodedTestNames.length;
126
+
127
+ // Check if adding this file would exceed the URL limit
128
+ if (testLength > MAX_URL_LENGTH && currentBatch.length > 0) {
129
+ // Start a new batch - current batch is full
130
+ fileBatches.push([...currentBatch]);
131
+ currentBatch = [file];
132
+ currentNames = file.Name;
133
+ } else {
134
+ // Add to current batch
135
+ currentBatch.push(file);
136
+ currentNames = testNames;
137
+ }
138
+ }
139
+
140
+ // Add the last batch if it has files
141
+ if (currentBatch.length > 0) {
142
+ fileBatches.push(currentBatch);
143
+ }
144
+
145
+ for (let batchIndex = 0; batchIndex < fileBatches.length; batchIndex++) {
146
+ const batch = fileBatches[batchIndex];
147
+
148
+ // Skip empty batches
149
+ if (!batch || batch.length === 0) {
150
+ continue;
151
+ }
152
+
153
+ const batchZipName = fileBatches.length > 1
154
+ ? `assets-batch-${batchIndex + 1}.zip`
155
+ : 'assets.zip';
64
156
 
65
- // for (const file of files) {
66
- // // Ensure the file was created properly
67
- // // TODO: Create some error handling incase it wasn't
68
- // const downloadedPath = fspath.join(EXPORT_ROOT, file.Path);
69
- // if (!fs.existsSync(downloadedPath)) continue;
70
- // }
157
+ try {
158
+ // Validate that we have the required data
159
+ if (!parentApiPath) {
160
+ console.warn(`Warning: Skipping batch because parentApiPath is undefined. Files: ${batch.map(f => f.Name).join(', ')}`);
161
+ continue;
162
+ }
71
163
 
72
- fs.rmSync(savedAs);
164
+ if (!parentLocalFolder) {
165
+ console.warn(`Warning: Skipping batch because parentLocalFolder is undefined. Path: ${parentApiPath}`);
166
+ continue;
167
+ }
168
+
169
+ // Debug logging for problematic paths
170
+ if (logger && parentApiPath.includes('@')) {
171
+ logger.info('Downloading batch with special characters', {
172
+ path: parentApiPath,
173
+ fileCount: batch.length,
174
+ firstFile: batch[0]?.Name
175
+ });
176
+ }
177
+
178
+ const savedAs = await downloadAssetsZip({
179
+ baseUrl: instanceUrl,
180
+ token: token, // "Bearer" prefix added in code
181
+ path: parentApiPath, // Use API path for API call
182
+ names: batch.map(file => file.Name),
183
+ outFile: fspath.join(EXPORT_ROOT, parentLocalFolder, batchZipName), // Use local path for file system
184
+ });
185
+
186
+ await extract(savedAs, {
187
+ dir: fspath.resolve(fspath.join(EXPORT_ROOT, parentLocalFolder)) // Use local path for extraction
188
+ });
189
+
190
+ fs.rmSync(savedAs);
191
+
192
+ // Collect base updates for all files in this batch
193
+ // We'll write them all at once at the end for performance
194
+ for (const file of batch) {
195
+ try {
196
+ const filePath = fspath.join(EXPORT_ROOT, file.Path);
197
+ if (fs.existsSync(filePath)) {
198
+ const fileStats = fs.statSync(filePath);
199
+ const fileContent = fs.readFileSync(filePath, "utf-8");
200
+ const contentHash = sha256(fileContent);
201
+
202
+ baseUpdates[file.Path] = {
203
+ lastModified: fileStats.mtimeMs,
204
+ contentHash,
205
+ compressedContent: '', // Assets don't store content
206
+ recordId: file.Path,
207
+ type: file.Type,
208
+ filePath: file.Path,
209
+ lastKnownActualPath: filePath,
210
+ lastKnownPath: fspath.resolve(file.Path)
211
+ };
212
+ }
213
+ } catch (err) {
214
+ // Log but don't fail the download
215
+ if (logger) {
216
+ logger.warning(`Failed to prepare base update for ${file.Path}`, { error: err.message });
217
+ }
218
+ }
219
+ }
220
+
221
+ // Update progress
222
+ downloadedFiles += batch.length;
223
+ if (progressCallback) {
224
+ progressCallback(downloadedFiles, totalFiles, `Downloaded ${downloadedFiles}/${totalFiles} files`);
225
+ }
226
+ } catch (error) {
227
+ // Check if this is a 404 error (files don't exist on server)
228
+ const is404 = error.message && error.message.includes('404');
229
+
230
+ if (is404) {
231
+ // Files don't exist on server - log info but continue
232
+ // This is expected behavior for files that were deleted on the server
233
+ const fileNames = batch.map(f => f.Name).join(', ');
234
+ const infoMessage = `Skipped ${batch.length} missing files from ${parentApiPath || '(undefined path)'}`;
235
+
236
+ console.warn(chalk.gray(`\n ℹ️ ${infoMessage}`));
237
+ console.warn(chalk.gray(` These files don't exist on the server anymore.`));
238
+
239
+ // Log to file if logger is available (as INFO, not WARNING)
240
+ if (logger) {
241
+ logger.info(infoMessage, {
242
+ path: parentApiPath,
243
+ fileCount: batch.length,
244
+ firstFewFiles: fileNames.substring(0, 200) + (fileNames.length > 200 ? '...' : '')
245
+ });
246
+ }
247
+
248
+ // Still update progress as if we "downloaded" them (they don't exist)
249
+ downloadedFiles += batch.length;
250
+ if (progressCallback) {
251
+ progressCallback(downloadedFiles, totalFiles, `Skipped ${batch.length} missing files`);
252
+ }
253
+ } else {
254
+ // Other errors should still fail
255
+ const fileNames = batch.map(f => f.Name).join(', ');
256
+ console.error(`\nFailed to download batch from ${parentApiPath || '(undefined path)'}`);
257
+ console.error(`Files: ${fileNames}`);
258
+ console.error(`Error: ${error.message}\n`);
259
+ throw error;
260
+ }
261
+ }
262
+ }
73
263
  }
74
264
  }
75
265
 
76
266
  await iterateDownload(allAssets);
77
267
 
268
+ // Write all base updates in ONE operation at the end (super fast!)
269
+ if (Object.keys(baseUpdates).length > 0) {
270
+ if (logger) {
271
+ logger.info(`Writing ${Object.keys(baseUpdates).length} asset base updates to base.json`);
272
+ }
273
+
274
+ // Read current base.json once (pass null as key to get entire config)
275
+ const baseConfig = config.read(null, { filename: "base.json" }) || {};
276
+
277
+ // Merge all updates
278
+ Object.assign(baseConfig, baseUpdates);
279
+
280
+ // Write once
281
+ const baseJsonPath = fspath.join(fspath.dirname(config.projectConfigPath), "base.json");
282
+ fs.writeFileSync(baseJsonPath, JSON.stringify(baseConfig, null, 2), { mode: 0o600 });
283
+
284
+ if (logger) {
285
+ logger.info(`Successfully wrote all asset base updates`);
286
+ }
287
+ }
288
+
78
289
  return {
79
290
  tree: allAssets
80
291
  };
@@ -0,0 +1,283 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import chalk from 'chalk';
4
+ import Config from './config.js';
5
+ import inquirer from 'inquirer';
6
+
7
+ const config = new Config();
8
+
9
+ /**
10
+ * Logger utility for tracking errors, warnings, and operations during pull/publish.
11
+ * Logs are stored in .magentrix/logs/ directory.
12
+ */
13
+
14
+ export class Logger {
15
+ constructor(operationName = 'operation') {
16
+ this.operationName = operationName;
17
+ this.logDir = '.magentrix/logs';
18
+ this.timestamp = new Date().toISOString().replace(/[:.]/g, '-').split('.')[0];
19
+ this.logFile = path.join(this.logDir, `${operationName}-${this.timestamp}.log`);
20
+ this.errors = [];
21
+ this.warnings = [];
22
+ this.infos = [];
23
+ this.loggingEnabled = null; // Will be determined on first write
24
+
25
+ // Check if logging is enabled in config
26
+ this.checkLoggingPreference();
27
+ }
28
+
29
+ /**
30
+ * Check user's logging preference from config
31
+ */
32
+ checkLoggingPreference() {
33
+ const preference = config.read('saveLogs', { global: true });
34
+
35
+ if (preference !== undefined && preference !== null) {
36
+ this.loggingEnabled = preference === true || preference === 'true';
37
+ }
38
+ // If undefined, we'll prompt on first write
39
+ }
40
+
41
+ /**
42
+ * Prompt user about logging preference (only once)
43
+ */
44
+ async promptLoggingPreference() {
45
+ console.log('');
46
+ console.log(chalk.cyan('📋 Log File Settings'));
47
+ console.log(chalk.gray('Magentrix CLI can save detailed operation logs for debugging.'));
48
+ console.log('');
49
+
50
+ const answer = await inquirer.prompt([
51
+ {
52
+ type: 'confirm',
53
+ name: 'saveLogs',
54
+ message: 'Would you like to save operation logs to files?',
55
+ default: true
56
+ }
57
+ ]);
58
+
59
+ this.loggingEnabled = answer.saveLogs;
60
+ config.save('saveLogs', answer.saveLogs, { global: true });
61
+
62
+ if (answer.saveLogs) {
63
+ console.log(chalk.green('✓ Logs will be saved to .magentrix/logs/'));
64
+ } else {
65
+ console.log(chalk.gray('Logs disabled. You can enable them anytime with: magentrix config set logs true'));
66
+ }
67
+ console.log('');
68
+ }
69
+
70
+ /**
71
+ * Initialize log file if logging is enabled
72
+ */
73
+ async initLogFile() {
74
+ if (this.loggingEnabled === null) {
75
+ // First time - prompt user
76
+ await this.promptLoggingPreference();
77
+ }
78
+
79
+ if (this.loggingEnabled) {
80
+ // Ensure log directory exists
81
+ if (!fs.existsSync(this.logDir)) {
82
+ fs.mkdirSync(this.logDir, { recursive: true });
83
+ }
84
+
85
+ // Initialize log file
86
+ this.writeToFile(`=== ${this.operationName.toUpperCase()} LOG ===`);
87
+ this.writeToFile(`Started: ${new Date().toISOString()}`);
88
+ this.writeToFile(`Working Directory: ${process.cwd()}`);
89
+ this.writeToFile('');
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Write a line to the log file (internal method)
95
+ */
96
+ writeToFile(message) {
97
+ if (this.loggingEnabled) {
98
+ fs.appendFileSync(this.logFile, message + '\n');
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Log an info message
104
+ */
105
+ info(message, details = null) {
106
+ const timestamp = new Date().toISOString();
107
+ const entry = { timestamp, level: 'INFO', message, details };
108
+ this.infos.push(entry);
109
+
110
+ this.writeToFile(`[${timestamp}] INFO: ${message}`);
111
+ if (details) {
112
+ this.writeToFile(` Details: ${typeof details === 'object' ? JSON.stringify(details, null, 2) : details}`);
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Log a warning message
118
+ */
119
+ warning(message, details = null) {
120
+ const timestamp = new Date().toISOString();
121
+ const entry = { timestamp, level: 'WARNING', message, details };
122
+ this.warnings.push(entry);
123
+
124
+ this.writeToFile(`[${timestamp}] WARNING: ${message}`);
125
+ if (details) {
126
+ this.writeToFile(` Details: ${typeof details === 'object' ? JSON.stringify(details, null, 2) : details}`);
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Log an error message
132
+ */
133
+ error(message, error = null, details = null) {
134
+ const timestamp = new Date().toISOString();
135
+ const entry = {
136
+ timestamp,
137
+ level: 'ERROR',
138
+ message,
139
+ error: error ? error.toString() : null,
140
+ stack: error?.stack || null,
141
+ details
142
+ };
143
+ this.errors.push(entry);
144
+
145
+ this.writeToFile(`[${timestamp}] ERROR: ${message}`);
146
+ if (error) {
147
+ this.writeToFile(` Error: ${error.toString()}`);
148
+ if (error.stack) {
149
+ this.writeToFile(` Stack: ${error.stack}`);
150
+ }
151
+ }
152
+ if (details) {
153
+ this.writeToFile(` Details: ${typeof details === 'object' ? JSON.stringify(details, null, 2) : details}`);
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Get a summary of the log
159
+ */
160
+ getSummary() {
161
+ return {
162
+ errors: this.errors.length,
163
+ warnings: this.warnings.length,
164
+ infos: this.infos.length,
165
+ logFile: this.logFile
166
+ };
167
+ }
168
+
169
+ /**
170
+ * Get the most recent errors (for preview)
171
+ */
172
+ getRecentErrors(count = 3) {
173
+ return this.errors.slice(-count);
174
+ }
175
+
176
+ /**
177
+ * Get the most recent warnings (for preview)
178
+ */
179
+ getRecentWarnings(count = 3) {
180
+ return this.warnings.slice(-count);
181
+ }
182
+
183
+ /**
184
+ * Display a summary to the console
185
+ */
186
+ displaySummary() {
187
+ const summary = this.getSummary();
188
+
189
+ if (summary.errors > 0 || summary.warnings > 0) {
190
+ console.log('');
191
+ console.log(chalk.bold('Issues Summary:'));
192
+
193
+ if (summary.errors > 0) {
194
+ console.log(chalk.red(` ✗ ${summary.errors} error(s)`));
195
+
196
+ // Show preview of recent errors
197
+ const recentErrors = this.getRecentErrors(2);
198
+ recentErrors.forEach((err, idx) => {
199
+ console.log(chalk.red(` ${idx + 1}. ${err.message}`));
200
+ });
201
+
202
+ if (summary.errors > 2) {
203
+ console.log(chalk.gray(` ... and ${summary.errors - 2} more`));
204
+ }
205
+ }
206
+
207
+ if (summary.warnings > 0) {
208
+ console.log(chalk.yellow(` ⚠ ${summary.warnings} warning(s)`));
209
+
210
+ // Show preview of recent warnings
211
+ const recentWarnings = this.getRecentWarnings(2);
212
+ recentWarnings.forEach((warn, idx) => {
213
+ console.log(chalk.yellow(` ${idx + 1}. ${warn.message}`));
214
+ });
215
+
216
+ if (summary.warnings > 2) {
217
+ console.log(chalk.gray(` ... and ${summary.warnings - 2} more`));
218
+ }
219
+ }
220
+
221
+ // Only show log file path if logging is enabled
222
+ if (this.loggingEnabled) {
223
+ console.log('');
224
+ console.log(chalk.cyan(`📄 Full log: ${chalk.white(summary.logFile)}`));
225
+ console.log(chalk.gray(` View with: cat ${summary.logFile}`));
226
+ }
227
+ console.log('');
228
+ }
229
+ }
230
+
231
+ /**
232
+ * Finalize the log file
233
+ */
234
+ close() {
235
+ this.writeToFile('');
236
+ this.writeToFile(`Completed: ${new Date().toISOString()}`);
237
+ this.writeToFile(`Summary: ${this.errors.length} errors, ${this.warnings.length} warnings, ${this.infos.length} info messages`);
238
+ this.writeToFile('=== END LOG ===');
239
+ }
240
+
241
+ /**
242
+ * Clean up old log files (keep only last N logs)
243
+ */
244
+ static cleanupOldLogs(keepCount = 10) {
245
+ const logDir = '.magentrix/logs';
246
+
247
+ if (!fs.existsSync(logDir)) {
248
+ return;
249
+ }
250
+
251
+ try {
252
+ const files = fs.readdirSync(logDir)
253
+ .filter(f => f.endsWith('.log'))
254
+ .map(f => ({
255
+ name: f,
256
+ path: path.join(logDir, f),
257
+ time: fs.statSync(path.join(logDir, f)).mtime.getTime()
258
+ }))
259
+ .sort((a, b) => b.time - a.time); // Sort by newest first
260
+
261
+ // Delete files beyond keepCount
262
+ if (files.length > keepCount) {
263
+ const toDelete = files.slice(keepCount);
264
+ toDelete.forEach(file => {
265
+ try {
266
+ fs.unlinkSync(file.path);
267
+ } catch (err) {
268
+ // Ignore errors when deleting old logs
269
+ }
270
+ });
271
+ }
272
+ } catch (err) {
273
+ // Ignore errors in cleanup
274
+ }
275
+ }
276
+ }
277
+
278
+ /**
279
+ * Create a logger instance for an operation
280
+ */
281
+ export function createLogger(operationName) {
282
+ return new Logger(operationName);
283
+ }