@magentrix-corp/magentrix-cli 1.2.1 → 1.3.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,235 @@
1
+ import { fetchMagentrix } from "../fetch.js";
2
+ import { File } from "node:buffer";
3
+
4
+ /**
5
+ * List all deployed Iris applications.
6
+ * @param {string} instanceUrl - Magentrix instance base URL
7
+ * @param {string} token - OAuth2 bearer token
8
+ * @returns {Promise<{success: boolean, apps: Array<{folderName: string, uploadedOn: string, modifiedOn: string, size: number}>}>}
9
+ */
10
+ export const listApps = async (instanceUrl, token) => {
11
+ if (!instanceUrl || !token) {
12
+ throw new Error('Missing required Magentrix instanceUrl or token');
13
+ }
14
+
15
+ const response = await fetchMagentrix({
16
+ instanceUrl,
17
+ token,
18
+ path: '/iris/listapps',
19
+ method: 'GET',
20
+ returnErrorObject: true
21
+ });
22
+
23
+ return response;
24
+ };
25
+
26
+ /**
27
+ * Publish (upload) an Iris Vue.js application.
28
+ * @param {string} instanceUrl - Magentrix instance base URL
29
+ * @param {string} token - OAuth2 bearer token
30
+ * @param {Buffer} zipBuffer - The zip file as a Buffer
31
+ * @param {string} filename - The filename for the zip (e.g., "my-app.zip")
32
+ * @param {string} appName - The user-friendly display name (required for navigation)
33
+ * @returns {Promise<{success: boolean, message: string, folderName: string}>}
34
+ */
35
+ export const publishApp = async (instanceUrl, token, zipBuffer, filename, appName) => {
36
+ if (!instanceUrl || !token) {
37
+ throw new Error('Missing required Magentrix instanceUrl or token');
38
+ }
39
+
40
+ if (!zipBuffer || !Buffer.isBuffer(zipBuffer)) {
41
+ throw new Error('zipBuffer must be a valid Buffer');
42
+ }
43
+
44
+ if (!filename) {
45
+ throw new Error('filename is required');
46
+ }
47
+
48
+ if (!appName) {
49
+ throw new Error('appName is required for navigation menu updates');
50
+ }
51
+
52
+ // Create a File object from the buffer for FormData
53
+ const file = new File([zipBuffer], filename, { type: 'application/zip' });
54
+
55
+ const formData = new FormData();
56
+ formData.append('file', file);
57
+
58
+ // app-name is passed as URL parameter (required for navigation)
59
+ const response = await fetchMagentrix({
60
+ instanceUrl,
61
+ token,
62
+ path: `/iris/publishapp?app-name=${encodeURIComponent(appName)}`,
63
+ method: 'POST',
64
+ body: formData,
65
+ ignoreContentType: true,
66
+ returnErrorObject: true
67
+ });
68
+
69
+ return response;
70
+ };
71
+
72
+ /**
73
+ * Delete an Iris application.
74
+ * @param {string} instanceUrl - Magentrix instance base URL
75
+ * @param {string} token - OAuth2 bearer token
76
+ * @param {string} folderName - The folder name of the app to delete
77
+ * @returns {Promise<{success: boolean, message: string}>}
78
+ */
79
+ export const deleteApp = async (instanceUrl, token, folderName) => {
80
+ if (!instanceUrl || !token) {
81
+ throw new Error('Missing required Magentrix instanceUrl or token');
82
+ }
83
+
84
+ if (!folderName) {
85
+ throw new Error('folderName is required');
86
+ }
87
+
88
+ const response = await fetchMagentrix({
89
+ instanceUrl,
90
+ token,
91
+ path: `/iris/deleteapp?folderName=${encodeURIComponent(folderName)}`,
92
+ method: 'POST',
93
+ returnErrorObject: true
94
+ });
95
+
96
+ return response;
97
+ };
98
+
99
+ /**
100
+ * Download a single Iris application as a buffer.
101
+ * @param {string} instanceUrl - Magentrix instance base URL
102
+ * @param {string} token - OAuth2 bearer token
103
+ * @param {string} folderName - The folder name of the app to download
104
+ * @returns {Promise<{buffer: Buffer, filename: string}>}
105
+ */
106
+ export const downloadApp = async (instanceUrl, token, folderName) => {
107
+ if (!instanceUrl || !token) {
108
+ throw new Error('Missing required Magentrix instanceUrl or token');
109
+ }
110
+
111
+ if (!folderName) {
112
+ throw new Error('folderName is required');
113
+ }
114
+
115
+ const url = `${instanceUrl.replace(/\/$/, '')}/iris/downloadapp?folderName=${encodeURIComponent(folderName)}`;
116
+
117
+ const response = await fetch(url, {
118
+ method: 'GET',
119
+ headers: {
120
+ 'Authorization': `Bearer ${token}`
121
+ }
122
+ });
123
+
124
+ if (!response.ok) {
125
+ let errorMessage = `Download failed (${response.status})`;
126
+ try {
127
+ const errorData = await response.json();
128
+ if (errorData.message) {
129
+ errorMessage = errorData.message;
130
+ }
131
+ } catch {
132
+ errorMessage = `Download failed: ${response.statusText}`;
133
+ }
134
+ throw new Error(errorMessage);
135
+ }
136
+
137
+ // Get filename from Content-Disposition header
138
+ const contentDisposition = response.headers.get('content-disposition') || '';
139
+ const filenameMatch = /filename\*?=(?:UTF-8'')?["']?([^"';]+)["']?/i.exec(contentDisposition);
140
+ const filename = filenameMatch ? decodeURIComponent(filenameMatch[1]) : `${folderName}.zip`;
141
+
142
+ const arrayBuffer = await response.arrayBuffer();
143
+ const buffer = Buffer.from(arrayBuffer);
144
+
145
+ return { buffer, filename };
146
+ };
147
+
148
+ /**
149
+ * Download all Iris applications as a single zip buffer.
150
+ * @param {string} instanceUrl - Magentrix instance base URL
151
+ * @param {string} token - OAuth2 bearer token
152
+ * @returns {Promise<{buffer: Buffer, filename: string}>}
153
+ */
154
+ export const downloadAllApps = async (instanceUrl, token) => {
155
+ if (!instanceUrl || !token) {
156
+ throw new Error('Missing required Magentrix instanceUrl or token');
157
+ }
158
+
159
+ const url = `${instanceUrl.replace(/\/$/, '')}/iris/downloadallapps`;
160
+
161
+ const response = await fetch(url, {
162
+ method: 'GET',
163
+ headers: {
164
+ 'Authorization': `Bearer ${token}`
165
+ }
166
+ });
167
+
168
+ if (!response.ok) {
169
+ let errorMessage = `Download failed (${response.status})`;
170
+ try {
171
+ const errorData = await response.json();
172
+ if (errorData.message) {
173
+ errorMessage = errorData.message;
174
+ }
175
+ } catch {
176
+ errorMessage = `Download failed: ${response.statusText}`;
177
+ }
178
+ throw new Error(errorMessage);
179
+ }
180
+
181
+ // Get filename from Content-Disposition header
182
+ const contentDisposition = response.headers.get('content-disposition') || '';
183
+ const filenameMatch = /filename\*?=(?:UTF-8'')?["']?([^"';]+)["']?/i.exec(contentDisposition);
184
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
185
+ const filename = filenameMatch ? decodeURIComponent(filenameMatch[1]) : `iris-apps_${timestamp}.zip`;
186
+
187
+ const arrayBuffer = await response.arrayBuffer();
188
+ const buffer = Buffer.from(arrayBuffer);
189
+
190
+ return { buffer, filename };
191
+ };
192
+
193
+ /**
194
+ * Get platform assets for local development (CSS, fonts, etc.).
195
+ * @param {string} siteUrl - Magentrix instance base URL (from Vue config.ts)
196
+ * @param {string} token - OAuth2 bearer token
197
+ * @returns {Promise<{success: boolean, assets: string[]}>}
198
+ */
199
+ export const getIrisAssets = async (siteUrl, token) => {
200
+ if (!siteUrl) {
201
+ throw new Error('Missing required siteUrl');
202
+ }
203
+
204
+ const url = `${siteUrl.replace(/\/$/, '')}/iris/getirisassets`;
205
+
206
+ const headers = {
207
+ 'Accept': 'application/json'
208
+ };
209
+
210
+ // Token is optional for this endpoint (might be public)
211
+ if (token) {
212
+ headers['Authorization'] = `Bearer ${token}`;
213
+ }
214
+
215
+ const response = await fetch(url, {
216
+ method: 'GET',
217
+ headers
218
+ });
219
+
220
+ if (!response.ok) {
221
+ let errorMessage = `Failed to fetch Iris assets (${response.status})`;
222
+ try {
223
+ const errorData = await response.json();
224
+ if (errorData.message) {
225
+ errorMessage = errorData.message;
226
+ }
227
+ } catch {
228
+ errorMessage = `Failed to fetch Iris assets: ${response.statusText}`;
229
+ }
230
+ throw new Error(errorMessage);
231
+ }
232
+
233
+ const data = await response.json();
234
+ return data;
235
+ };
@@ -0,0 +1,70 @@
1
+ import chalk from 'chalk';
2
+ import os from 'os';
3
+
4
+ /**
5
+ * Display a user-friendly permission error message with platform-specific guidance.
6
+ *
7
+ * @param {object} options - Options for the error message
8
+ * @param {string} options.operation - What operation failed ('delete' or 'restore')
9
+ * @param {string} options.targetPath - The path that couldn't be accessed
10
+ * @param {string} [options.backupPath] - Optional backup path (for restore operations)
11
+ * @param {string} [options.slug] - Optional app slug (for restore operations)
12
+ */
13
+ export function showPermissionError({ operation, targetPath, backupPath, slug }) {
14
+ const platform = os.platform();
15
+ const username = os.userInfo().username;
16
+ const group = platform === 'darwin' ? 'staff' : username;
17
+
18
+ console.log();
19
+ console.log(chalk.bgYellow.bold.black(' ⚠ Permission Denied '));
20
+ console.log(chalk.yellow('─'.repeat(48)));
21
+ console.log(chalk.white(`Cannot ${operation} files due to permission issues.`));
22
+ console.log();
23
+ console.log(chalk.white('This may happen if:'));
24
+ console.log(chalk.gray(' • A process has the folder locked'));
25
+ console.log(chalk.gray(' • The folder is owned by another user (e.g., root)'));
26
+ if (platform === 'darwin') {
27
+ console.log(chalk.gray(' • macOS Gatekeeper is blocking the operation'));
28
+ }
29
+ console.log();
30
+ console.log(chalk.white('To fix this, try:'));
31
+ console.log(chalk.cyan(' 1. Close any editors that have the folder open'));
32
+ console.log(chalk.cyan(' 2. Run the command again'));
33
+ console.log();
34
+
35
+ // Platform-specific ownership fix
36
+ if (platform === 'win32') {
37
+ console.log(chalk.white('If the folder has wrong ownership, run as Administrator:'));
38
+ console.log(chalk.cyan(` icacls "${targetPath}" /grant ${username}:F /T`));
39
+ } else {
40
+ console.log(chalk.white('If the folder has wrong ownership, run:'));
41
+ console.log(chalk.cyan(` sudo chown -R ${username}:${group} "${targetPath}"`));
42
+ }
43
+ console.log();
44
+
45
+ // Operation-specific manual commands
46
+ if (operation === 'delete') {
47
+ console.log(chalk.white('Or delete manually:'));
48
+ if (platform === 'win32') {
49
+ console.log(chalk.cyan(` rmdir /s /q "${targetPath}"`));
50
+ } else {
51
+ console.log(chalk.cyan(` rm -rf "${targetPath}"`));
52
+ }
53
+ } else if (operation === 'restore' && backupPath && slug) {
54
+ console.log(chalk.white('Or manually restore:'));
55
+ const zipPath = `${backupPath}/${slug}.zip`;
56
+ const destDir = targetPath.replace(/[/\\][^/\\]+$/, ''); // Parent directory
57
+ if (platform === 'win32') {
58
+ console.log(chalk.cyan(` tar -xf "${zipPath}" -C "${destDir}"`));
59
+ } else {
60
+ console.log(chalk.cyan(` unzip "${zipPath}" -d "${destDir}"`));
61
+ }
62
+ }
63
+
64
+ console.log(chalk.yellow('─'.repeat(48)));
65
+
66
+ if (backupPath) {
67
+ console.log();
68
+ console.log(chalk.gray(`Backup preserved at: ${backupPath}`));
69
+ }
70
+ }
package/utils/progress.js CHANGED
@@ -40,6 +40,7 @@ export class ProgressTracker {
40
40
  this.interval = null;
41
41
  this.lastRenderLength = 0;
42
42
  this.isFinished = false;
43
+ this.issues = []; // Collected issues/warnings to display at end
43
44
  }
44
45
 
45
46
  /**
@@ -147,6 +148,36 @@ export class ProgressTracker {
147
148
  this.render();
148
149
  }
149
150
 
151
+ /**
152
+ * Add an issue/warning to be displayed at the end
153
+ * @param {string} type - 'error' | 'warning' | 'info'
154
+ * @param {string} message - The issue message
155
+ * @param {string} [hint] - Optional hint for resolution
156
+ */
157
+ addIssue(type, message, hint = null) {
158
+ this.issues.push({ type, message, hint });
159
+ }
160
+
161
+ /**
162
+ * Check if there are any issues collected
163
+ * @returns {boolean}
164
+ */
165
+ hasIssues() {
166
+ return this.issues.length > 0;
167
+ }
168
+
169
+ /**
170
+ * Get issues by type
171
+ * @param {string} type - 'error' | 'warning' | 'info'
172
+ * @returns {Array}
173
+ */
174
+ getIssues(type = null) {
175
+ if (type) {
176
+ return this.issues.filter(i => i.type === type);
177
+ }
178
+ return this.issues;
179
+ }
180
+
150
181
  /**
151
182
  * Update the message for the current step
152
183
  * @param {string} message - New message
@@ -289,8 +320,12 @@ export class ProgressTracker {
289
320
  /**
290
321
  * Finish the progress tracker
291
322
  * @param {string} message - Optional final message
323
+ * @param {object} options - Optional configuration
324
+ * @param {boolean} options.showIssues - Whether to show collected issues (default: false)
292
325
  */
293
- finish(message = '') {
326
+ finish(message = '', options = {}) {
327
+ const { showIssues = false } = options;
328
+
294
329
  this.stopSpinner();
295
330
  this.isFinished = true;
296
331
 
@@ -318,6 +353,57 @@ export class ProgressTracker {
318
353
  console.log(chalk.green(`✓ All ${completed} step(s) completed successfully`));
319
354
  }
320
355
  console.log('');
356
+
357
+ // Only display collected issues if explicitly requested
358
+ if (showIssues && this.issues.length > 0) {
359
+ this.displayIssues();
360
+ }
361
+ }
362
+
363
+ /**
364
+ * Display collected issues
365
+ * Can be called manually after finish() if needed
366
+ */
367
+ displayIssues() {
368
+ if (this.issues.length === 0) return;
369
+
370
+ const errors = this.issues.filter(i => i.type === 'error');
371
+ const warnings = this.issues.filter(i => i.type === 'warning');
372
+ const infos = this.issues.filter(i => i.type === 'info');
373
+
374
+ console.log(chalk.bold('Issues Summary:'));
375
+
376
+ if (errors.length > 0) {
377
+ console.log(chalk.red(` ✗ ${errors.length} error(s)`));
378
+ errors.forEach((issue, i) => {
379
+ console.log(chalk.red(` ${i + 1}. ${issue.message}`));
380
+ if (issue.hint) {
381
+ console.log(chalk.yellow(` ${issue.hint}`));
382
+ }
383
+ });
384
+ }
385
+
386
+ if (warnings.length > 0) {
387
+ console.log(chalk.yellow(` ⚠ ${warnings.length} warning(s)`));
388
+ warnings.forEach((issue, i) => {
389
+ console.log(chalk.yellow(` ${i + 1}. ${issue.message}`));
390
+ if (issue.hint) {
391
+ console.log(chalk.gray(` ${issue.hint}`));
392
+ }
393
+ });
394
+ }
395
+
396
+ if (infos.length > 0) {
397
+ console.log(chalk.cyan(` ℹ ${infos.length} info(s)`));
398
+ infos.forEach((issue, i) => {
399
+ console.log(chalk.cyan(` ${i + 1}. ${issue.message}`));
400
+ if (issue.hint) {
401
+ console.log(chalk.gray(` ${issue.hint}`));
402
+ }
403
+ });
404
+ }
405
+
406
+ console.log('');
321
407
  }
322
408
 
323
409
  /**
@@ -65,9 +65,9 @@ export const updateBase = (filePath, record, actualPath = '', contentSnapshot =
65
65
  // Use snapshot if provided (to avoid race conditions), otherwise read from disk
66
66
  let fileContent, contentHash;
67
67
  if (isDirectory) {
68
- // Folders don't have content
68
+ // Folders don't have content - use provided contentHash if any (e.g., for Iris apps)
69
69
  fileContent = '';
70
- contentHash = '';
70
+ contentHash = record.contentHash || '';
71
71
  } else if (contentSnapshot && contentSnapshot.content) {
72
72
  // Use the snapshot of what was actually published
73
73
  fileContent = contentSnapshot.content;
@@ -91,6 +91,14 @@ export const updateBase = (filePath, record, actualPath = '', contentSnapshot =
91
91
  filePath,
92
92
  lastKnownActualPath: fileSystemLocation,
93
93
  lastKnownPath: path.resolve(filePath)
94
+ };
95
+
96
+ // Preserve custom fields from record (e.g., for Iris apps: folderName, appName, modifiedOn)
97
+ const customFields = ['folderName', 'appName', 'modifiedOn', 'uploadedOn', 'size'];
98
+ for (const field of customFields) {
99
+ if (record[field] !== undefined) {
100
+ saveData[field] = record[field];
101
+ }
94
102
  }
95
103
 
96
104
  if (saveData.type === 'File' || saveData.type === 'Folder') delete saveData.compressedContent;
package/vars/global.js CHANGED
@@ -4,6 +4,7 @@ export const CWD = process.cwd();
4
4
  export const HASHED_CWD = sha256(CWD);
5
5
  export const EXPORT_ROOT = "src";
6
6
  export const ASSETS_DIR = "Assets"; // Local directory name for static assets (API uses /contents/assets)
7
+ export const IRIS_APPS_DIR = "iris-apps"; // Local directory for Iris Vue.js apps
7
8
 
8
9
  /**
9
10
  * Maps Magentrix Type fields to local folder names and extensions.