@magentrix-corp/magentrix-cli 1.1.5 → 1.2.1

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.
@@ -1,4 +1,5 @@
1
1
  import { fetchMagentrix } from "../fetch.js";
2
+ import chalk from "chalk";
2
3
 
3
4
  /**
4
5
  * Authenticates with Magentrix and retrieves an access token using the API key as a refresh token.
@@ -45,12 +46,50 @@ export const tryAuthenticate = async (apiKey, instanceUrl) => {
45
46
  try {
46
47
  return await getAccessToken(apiKey, instanceUrl);
47
48
  } catch (error) {
48
- throw new Error(
49
- `❌ Failed to authenticate with Magentrix:\n` +
50
- `The API key and/or instance URL you provided are incorrect or do not match.\n` +
51
- `Please double-check both values and try again.\n\n` +
52
- `Details: ${error.message || error}`
53
- );
49
+ const errorMessage = error.message || String(error);
50
+
51
+ // Build formatted error message with colors and spacing
52
+ let formattedMessage = '\n' + chalk.red.bold('✖ Authentication Failed') + '\n';
53
+ formattedMessage += chalk.dim('─'.repeat(50)) + '\n\n';
54
+
55
+ if (errorMessage.includes('Network error')) {
56
+ formattedMessage += chalk.cyan.bold('🌐 Unable to reach the Magentrix instance') + '\n\n';
57
+ formattedMessage += chalk.yellow(' Possible causes:') + '\n';
58
+ formattedMessage += chalk.gray(' • Check your internet connection') + '\n';
59
+ formattedMessage += chalk.gray(' • Verify the instance URL is correct') + '\n';
60
+ formattedMessage += chalk.gray(' • Ensure the server is online and accessible') + '\n';
61
+ } else if (errorMessage.includes('HTTP 401') || errorMessage.includes('HTTP 403') || errorMessage.includes('Unauthorized')) {
62
+ formattedMessage += chalk.cyan.bold('🔑 Invalid API Key') + '\n\n';
63
+ formattedMessage += chalk.yellow(' What to do:') + '\n';
64
+ formattedMessage += chalk.gray(' • The API key you entered is incorrect') + '\n';
65
+ formattedMessage += chalk.gray(' • Verify your API key from the Magentrix admin panel') + '\n';
66
+ } else if (errorMessage.includes('HTTP 404')) {
67
+ formattedMessage += chalk.cyan.bold('🔍 Invalid Magentrix Instance URL') + '\n\n';
68
+ formattedMessage += chalk.yellow(' What to do:') + '\n';
69
+ formattedMessage += chalk.gray(' • The URL does not appear to be a valid Magentrix server') + '\n';
70
+ formattedMessage += chalk.gray(' • Verify the URL matches your Magentrix instance') + '\n';
71
+ } else if (errorMessage.includes('HTTP 5')) {
72
+ formattedMessage += chalk.cyan.bold('⚠️ Magentrix Server Error') + '\n\n';
73
+ formattedMessage += chalk.yellow(' What to do:') + '\n';
74
+ formattedMessage += chalk.gray(' • The server is experiencing issues') + '\n';
75
+ formattedMessage += chalk.gray(' • Please try again in a few moments') + '\n';
76
+ formattedMessage += chalk.gray(' • Contact support if the issue persists') + '\n';
77
+ } else if (errorMessage.includes('timeout') || errorMessage.includes('ETIMEDOUT')) {
78
+ formattedMessage += chalk.cyan.bold('⏱️ Connection Timeout') + '\n\n';
79
+ formattedMessage += chalk.yellow(' What to do:') + '\n';
80
+ formattedMessage += chalk.gray(' • The server took too long to respond') + '\n';
81
+ formattedMessage += chalk.gray(' • Check your internet connection') + '\n';
82
+ formattedMessage += chalk.gray(' • Try again in a moment') + '\n';
83
+ } else {
84
+ formattedMessage += chalk.cyan.bold('❓ Unable to Authenticate') + '\n\n';
85
+ formattedMessage += chalk.yellow(' What to do:') + '\n';
86
+ formattedMessage += chalk.gray(' • Verify both your API key and instance URL are correct') + '\n';
87
+ formattedMessage += chalk.gray(' • Ensure the API key matches the instance URL') + '\n';
88
+ }
89
+
90
+ formattedMessage += '\n' + chalk.dim('─'.repeat(50)) + '\n';
91
+
92
+ throw new Error(formattedMessage);
54
93
  }
55
94
  };
56
95
 
@@ -0,0 +1,383 @@
1
+ import chalk from 'chalk';
2
+
3
+ /**
4
+ * Comprehensive progress tracker for multi-step operations with sub-progress.
5
+ *
6
+ * Features:
7
+ * - Multiple step tracking
8
+ * - Progress bars for individual steps
9
+ * - Nested progress (e.g., batches within a step)
10
+ * - Time tracking
11
+ * - Dynamic message updates
12
+ *
13
+ * @example
14
+ * const progress = new ProgressTracker('Pull Operation');
15
+ * progress.addStep('auth', 'Authenticating...');
16
+ * progress.addStep('download', 'Downloading files...');
17
+ * progress.addStep('process', 'Processing files...');
18
+ *
19
+ * progress.start();
20
+ * progress.startStep('auth');
21
+ * // ... do auth work
22
+ * progress.completeStep('auth', 'Authenticated');
23
+ *
24
+ * progress.startStep('download');
25
+ * progress.updateProgress('download', 0, 100);
26
+ * // ... download files
27
+ * progress.updateProgress('download', 50, 100);
28
+ * progress.completeStep('download', 'Downloaded 100 files');
29
+ *
30
+ * progress.finish();
31
+ */
32
+ export class ProgressTracker {
33
+ constructor(title = 'Operation') {
34
+ this.title = title;
35
+ this.steps = new Map();
36
+ this.currentStep = null;
37
+ this.startTime = null;
38
+ this.spinnerChars = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
39
+ this.spinnerIndex = 0;
40
+ this.interval = null;
41
+ this.lastRenderLength = 0;
42
+ this.isFinished = false;
43
+ }
44
+
45
+ /**
46
+ * Add a step to track
47
+ * @param {string} id - Unique identifier for the step
48
+ * @param {string} label - Display label for the step
49
+ * @param {object} options - Optional configuration
50
+ * @param {boolean} options.hasProgress - Whether this step has progress (0-100%)
51
+ */
52
+ addStep(id, label, options = {}) {
53
+ this.steps.set(id, {
54
+ id,
55
+ label,
56
+ status: 'pending', // pending, active, completed, failed
57
+ hasProgress: options.hasProgress || false,
58
+ progress: { current: 0, total: 100 },
59
+ message: '',
60
+ startTime: null,
61
+ endTime: null,
62
+ error: null
63
+ });
64
+ }
65
+
66
+ /**
67
+ * Start the progress tracker
68
+ */
69
+ start() {
70
+ this.startTime = Date.now();
71
+ this.isFinished = false;
72
+ this.render();
73
+ this.startSpinner();
74
+ }
75
+
76
+ /**
77
+ * Start a specific step
78
+ * @param {string} id - Step identifier
79
+ * @param {string} message - Optional custom message
80
+ */
81
+ startStep(id, message = '') {
82
+ const step = this.steps.get(id);
83
+ if (!step) {
84
+ throw new Error(`Step '${id}' not found`);
85
+ }
86
+
87
+ // Complete previous step if any
88
+ if (this.currentStep && this.steps.get(this.currentStep).status === 'active') {
89
+ this.completeStep(this.currentStep);
90
+ }
91
+
92
+ step.status = 'active';
93
+ step.startTime = Date.now();
94
+ step.message = message || step.label;
95
+ this.currentStep = id;
96
+ this.render();
97
+ }
98
+
99
+ /**
100
+ * Update progress for a step
101
+ * @param {string} id - Step identifier
102
+ * @param {number} current - Current progress value
103
+ * @param {number} total - Total progress value
104
+ * @param {string} message - Optional message to display
105
+ */
106
+ updateProgress(id, current, total, message = '') {
107
+ const step = this.steps.get(id);
108
+ if (!step) return;
109
+
110
+ step.progress.current = current;
111
+ step.progress.total = total;
112
+ if (message) {
113
+ step.message = message;
114
+ }
115
+ this.render();
116
+ }
117
+
118
+ /**
119
+ * Complete a step
120
+ * @param {string} id - Step identifier
121
+ * @param {string} message - Optional completion message
122
+ */
123
+ completeStep(id, message = '') {
124
+ const step = this.steps.get(id);
125
+ if (!step) return;
126
+
127
+ step.status = 'completed';
128
+ step.endTime = Date.now();
129
+ if (message) {
130
+ step.message = message;
131
+ }
132
+ this.render();
133
+ }
134
+
135
+ /**
136
+ * Mark a step as failed
137
+ * @param {string} id - Step identifier
138
+ * @param {string} error - Error message
139
+ */
140
+ failStep(id, error) {
141
+ const step = this.steps.get(id);
142
+ if (!step) return;
143
+
144
+ step.status = 'failed';
145
+ step.endTime = Date.now();
146
+ step.error = error;
147
+ this.render();
148
+ }
149
+
150
+ /**
151
+ * Update the message for the current step
152
+ * @param {string} message - New message
153
+ */
154
+ updateMessage(message) {
155
+ if (!this.currentStep) return;
156
+ const step = this.steps.get(this.currentStep);
157
+ if (step) {
158
+ step.message = message;
159
+ this.render();
160
+ }
161
+ }
162
+
163
+ /**
164
+ * Render the progress display
165
+ */
166
+ render() {
167
+ if (this.isFinished) return;
168
+
169
+ // Clear previous render
170
+ if (this.lastRenderLength > 0) {
171
+ for (let i = 0; i < this.lastRenderLength; i++) {
172
+ process.stdout.write('\x1B[1A\x1B[2K'); // Move up and clear line
173
+ }
174
+ }
175
+
176
+ const lines = [];
177
+
178
+ // Title with elapsed time
179
+ const elapsed = this.formatDuration(Date.now() - this.startTime);
180
+ lines.push(chalk.bold.blue(`\n${this.title}`) + chalk.gray(` (${elapsed})`));
181
+ lines.push('');
182
+
183
+ // Render each step
184
+ for (const [id, step] of this.steps) {
185
+ const prefix = this.getStepPrefix(step);
186
+ const label = step.status === 'active'
187
+ ? chalk.cyan(step.label)
188
+ : step.status === 'completed'
189
+ ? chalk.gray(step.label)
190
+ : step.status === 'failed'
191
+ ? chalk.red(step.label)
192
+ : chalk.gray(step.label);
193
+
194
+ let line = `${prefix} ${label}`;
195
+
196
+ // Add progress bar if applicable
197
+ if (step.hasProgress && step.status === 'active' && step.progress.total > 0) {
198
+ const percentage = Math.floor((step.progress.current / step.progress.total) * 100);
199
+ const progressBar = this.renderProgressBar(percentage);
200
+ line += ` ${progressBar} ${percentage}%`;
201
+ }
202
+
203
+ // Add message or duration
204
+ if (step.message && step.status === 'active') {
205
+ line += chalk.gray(` - ${step.message}`);
206
+ } else if (step.status === 'completed' && step.endTime && step.startTime) {
207
+ const duration = this.formatDuration(step.endTime - step.startTime);
208
+ line += chalk.gray(` (${duration})`);
209
+ if (step.message) {
210
+ line += chalk.gray(` - ${step.message}`);
211
+ }
212
+ } else if (step.status === 'failed' && step.error) {
213
+ line += chalk.red(` - ${step.error}`);
214
+ }
215
+
216
+ lines.push(line);
217
+ }
218
+
219
+ lines.push(''); // Empty line at the end
220
+
221
+ this.lastRenderLength = lines.length;
222
+ process.stdout.write(lines.join('\n'));
223
+ }
224
+
225
+ /**
226
+ * Get the prefix icon for a step
227
+ */
228
+ getStepPrefix(step) {
229
+ switch (step.status) {
230
+ case 'pending':
231
+ return chalk.gray('◯');
232
+ case 'active':
233
+ return chalk.cyan(this.spinnerChars[this.spinnerIndex]);
234
+ case 'completed':
235
+ return chalk.green('✓');
236
+ case 'failed':
237
+ return chalk.red('✗');
238
+ default:
239
+ return chalk.gray('◯');
240
+ }
241
+ }
242
+
243
+ /**
244
+ * Render a progress bar
245
+ * @param {number} percentage - Progress percentage (0-100)
246
+ */
247
+ renderProgressBar(percentage) {
248
+ const width = 20;
249
+ // Clamp percentage between 0 and 100 to prevent negative values
250
+ const clampedPercentage = Math.max(0, Math.min(100, percentage));
251
+ const filled = Math.floor((clampedPercentage / 100) * width);
252
+ const empty = width - filled;
253
+
254
+ const bar = chalk.cyan('█'.repeat(filled)) + chalk.gray('░'.repeat(empty));
255
+ return `[${bar}]`;
256
+ }
257
+
258
+ /**
259
+ * Format duration in ms to human readable format
260
+ */
261
+ formatDuration(ms) {
262
+ if (ms < 1000) return `${ms}ms`;
263
+ if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
264
+ const minutes = Math.floor(ms / 60000);
265
+ const seconds = Math.floor((ms % 60000) / 1000);
266
+ return `${minutes}m ${seconds}s`;
267
+ }
268
+
269
+ /**
270
+ * Start the spinner animation
271
+ */
272
+ startSpinner() {
273
+ this.interval = setInterval(() => {
274
+ this.spinnerIndex = (this.spinnerIndex + 1) % this.spinnerChars.length;
275
+ this.render();
276
+ }, 80);
277
+ }
278
+
279
+ /**
280
+ * Stop the spinner animation
281
+ */
282
+ stopSpinner() {
283
+ if (this.interval) {
284
+ clearInterval(this.interval);
285
+ this.interval = null;
286
+ }
287
+ }
288
+
289
+ /**
290
+ * Finish the progress tracker
291
+ * @param {string} message - Optional final message
292
+ */
293
+ finish(message = '') {
294
+ this.stopSpinner();
295
+ this.isFinished = true;
296
+
297
+ // Complete any active steps
298
+ if (this.currentStep && this.steps.get(this.currentStep).status === 'active') {
299
+ this.completeStep(this.currentStep);
300
+ }
301
+
302
+ this.render();
303
+
304
+ // Print summary
305
+ const totalTime = this.formatDuration(Date.now() - this.startTime);
306
+ const completed = Array.from(this.steps.values()).filter(s => s.status === 'completed').length;
307
+ const failed = Array.from(this.steps.values()).filter(s => s.status === 'failed').length;
308
+
309
+ if (message) {
310
+ console.log(chalk.bold.green(`\n${message}`));
311
+ }
312
+
313
+ console.log(chalk.gray(`Total time: ${totalTime}`));
314
+
315
+ if (failed > 0) {
316
+ console.log(chalk.red(`✗ ${failed} step(s) failed`));
317
+ } else {
318
+ console.log(chalk.green(`✓ All ${completed} step(s) completed successfully`));
319
+ }
320
+ console.log('');
321
+ }
322
+
323
+ /**
324
+ * Abort the progress tracker with an error
325
+ * @param {string} error - Error message
326
+ */
327
+ abort(error) {
328
+ this.stopSpinner();
329
+ this.isFinished = true;
330
+
331
+ if (this.currentStep) {
332
+ this.failStep(this.currentStep, error);
333
+ }
334
+
335
+ this.render();
336
+ console.log(chalk.bold.red(`\n✗ ${error}\n`));
337
+ }
338
+ }
339
+
340
+ /**
341
+ * Helper function to create and use a simple progress tracker
342
+ * @param {string} title - Title of the operation
343
+ * @param {Array} steps - Array of step definitions
344
+ * @param {Function} fn - Async function that receives the progress tracker
345
+ * @returns {Promise} Result of the function
346
+ *
347
+ * @example
348
+ * await withProgress('Pull Operation', [
349
+ * { id: 'auth', label: 'Authenticating...' },
350
+ * { id: 'download', label: 'Downloading files...', hasProgress: true },
351
+ * { id: 'process', label: 'Processing files...' }
352
+ * ], async (progress) => {
353
+ * progress.startStep('auth');
354
+ * await authenticate();
355
+ * progress.completeStep('auth');
356
+ *
357
+ * progress.startStep('download');
358
+ * for (let i = 0; i <= 100; i += 10) {
359
+ * progress.updateProgress('download', i, 100);
360
+ * await downloadBatch(i);
361
+ * }
362
+ * progress.completeStep('download');
363
+ * });
364
+ */
365
+ export async function withProgress(title, steps, fn) {
366
+ const progress = new ProgressTracker(title);
367
+
368
+ // Add all steps
369
+ for (const step of steps) {
370
+ progress.addStep(step.id, step.label, step);
371
+ }
372
+
373
+ progress.start();
374
+
375
+ try {
376
+ const result = await fn(progress);
377
+ progress.finish();
378
+ return result;
379
+ } catch (error) {
380
+ progress.abort(error.message);
381
+ throw error;
382
+ }
383
+ }
@@ -53,7 +53,8 @@ export const updateBase = (filePath, record, actualPath = '', contentSnapshot =
53
53
  const fileSystemLocation = actualPath || path.resolve(filePath);
54
54
 
55
55
  if (!fs.existsSync(fileSystemLocation)) {
56
- console.error(`❌ File does not exist: ${filePath}`);
56
+ // Silently skip files that don't exist - they may have failed to download
57
+ // This is expected behavior for files that returned 404 during download
57
58
  return;
58
59
  }
59
60
 
@@ -106,3 +107,7 @@ export const updateBase = (filePath, record, actualPath = '', contentSnapshot =
106
107
  export const removeFromBase = (recordId) => {
107
108
  config.removeKey(recordId, { filename: "base.json" });
108
109
  }
110
+
111
+ export const removeFromBaseBulk = (recordIds) => {
112
+ config.removeKeys(recordIds, { filename: "base.json" });
113
+ }