@magentrix-corp/magentrix-cli 1.3.16 → 1.3.17

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.
Files changed (68) hide show
  1. package/LICENSE +25 -25
  2. package/README.md +1166 -1166
  3. package/actions/autopublish.old.js +293 -293
  4. package/actions/config.js +182 -182
  5. package/actions/create.js +466 -466
  6. package/actions/help.js +164 -164
  7. package/actions/iris/buildStage.js +874 -874
  8. package/actions/iris/delete.js +256 -256
  9. package/actions/iris/dev.js +391 -391
  10. package/actions/iris/index.js +6 -6
  11. package/actions/iris/link.js +375 -375
  12. package/actions/iris/recover.js +268 -268
  13. package/actions/main.js +80 -80
  14. package/actions/publish.js +1420 -1420
  15. package/actions/pull.js +684 -684
  16. package/actions/setup.js +148 -148
  17. package/actions/status.js +17 -17
  18. package/actions/update.js +248 -248
  19. package/bin/magentrix.js +393 -393
  20. package/package.json +55 -55
  21. package/utils/assetPaths.js +158 -158
  22. package/utils/autopublishLock.js +77 -77
  23. package/utils/cacher.js +206 -206
  24. package/utils/cli/checkInstanceUrl.js +76 -74
  25. package/utils/cli/helpers/compare.js +282 -282
  26. package/utils/cli/helpers/ensureApiKey.js +63 -63
  27. package/utils/cli/helpers/ensureCredentials.js +68 -68
  28. package/utils/cli/helpers/ensureInstanceUrl.js +75 -75
  29. package/utils/cli/writeRecords.js +262 -262
  30. package/utils/compare.js +135 -135
  31. package/utils/compress.js +17 -17
  32. package/utils/config.js +527 -527
  33. package/utils/debug.js +144 -144
  34. package/utils/diagnostics/testPublishLogic.js +96 -96
  35. package/utils/diff.js +49 -49
  36. package/utils/downloadAssets.js +291 -291
  37. package/utils/filetag.js +115 -115
  38. package/utils/hash.js +14 -14
  39. package/utils/iris/backup.js +411 -411
  40. package/utils/iris/builder.js +541 -541
  41. package/utils/iris/config-reader.js +664 -664
  42. package/utils/iris/deleteHelper.js +150 -150
  43. package/utils/iris/errors.js +537 -537
  44. package/utils/iris/linker.js +601 -601
  45. package/utils/iris/lock.js +360 -360
  46. package/utils/iris/validation.js +360 -360
  47. package/utils/iris/validator.js +281 -281
  48. package/utils/iris/zipper.js +248 -248
  49. package/utils/logger.js +291 -291
  50. package/utils/magentrix/api/assets.js +220 -220
  51. package/utils/magentrix/api/auth.js +107 -107
  52. package/utils/magentrix/api/createEntity.js +61 -61
  53. package/utils/magentrix/api/deleteEntity.js +55 -55
  54. package/utils/magentrix/api/iris.js +251 -251
  55. package/utils/magentrix/api/meqlQuery.js +36 -36
  56. package/utils/magentrix/api/retrieveEntity.js +86 -86
  57. package/utils/magentrix/api/updateEntity.js +66 -66
  58. package/utils/magentrix/fetch.js +168 -168
  59. package/utils/merge.js +22 -22
  60. package/utils/permissionError.js +70 -70
  61. package/utils/preferences.js +40 -40
  62. package/utils/progress.js +469 -469
  63. package/utils/spinner.js +43 -43
  64. package/utils/template.js +52 -52
  65. package/utils/updateFileBase.js +121 -121
  66. package/utils/workspaces.js +108 -108
  67. package/vars/config.js +11 -11
  68. package/vars/global.js +50 -50
package/actions/create.js CHANGED
@@ -1,466 +1,466 @@
1
- import { ensureValidCredentials } from "../utils/cli/helpers/ensureCredentials.js";
2
- import { createEntity } from "../utils/magentrix/api/createEntity.js";
3
- import { search, select, input } from "@inquirer/prompts";
4
- import { listEntities } from "../utils/magentrix/api/retrieveEntity.js";
5
- import { getClassTemplate, getControllerTemplate, getPageTemplate, getTriggerTemplate } from "../utils/template.js";
6
- import { withSpinner } from "../utils/spinner.js";
7
- import { EXPORT_ROOT, TYPE_DIR_MAP } from "../vars/global.js";
8
- import path from "path";
9
- import fs from 'fs';
10
- import chalk from 'chalk';
11
- import { setFileTag } from "../utils/filetag.js";
12
- import { updateBase } from "../utils/updateFileBase.js";
13
- import { sha256 } from "../utils/hash.js";
14
-
15
- // Credentials and entity cache (module-scope for re-use during prompt session)
16
- let credentials = {};
17
- let triggerEntities = [];
18
-
19
- /**
20
- * Loads all entities from the Magentrix API and prepares them
21
- * for searchable selection in the CLI.
22
- * Populates the `triggerEntities` array.
23
- *
24
- * @async
25
- * @returns {Promise<void>}
26
- */
27
- const loadEntities = async () => {
28
- const entities = await listEntities(credentials.instanceUrl, credentials.token.value);
29
- // Map to inquirer-friendly objects: { name, value }
30
- triggerEntities = entities.map(entity => ({
31
- label: entity.Label,
32
- value: entity.Id,
33
- name: entity.Name
34
- }));
35
- };
36
-
37
- /**
38
- * Search function for entity selection using @inquirer/prompts.
39
- * Filters `triggerEntities` array based on the user's input term.
40
- *
41
- * @async
42
- * @param {string} term - User's search input.
43
- * @returns {Promise<Array>} Filtered entity choices for prompt.
44
- */
45
- const searchEntities = async (term = '') => {
46
- // Case-insensitive substring match against entity names
47
- const filteredEntities = triggerEntities.filter(entity =>
48
- entity.name.toLowerCase().includes(term.toLowerCase())
49
- );
50
-
51
- // Format for inquirer searchable prompt
52
- return filteredEntities.map(entity => ({
53
- name: `${entity.label} (${entity.value})`,
54
- value: entity.value,
55
- // Optionally: description: `Entity ID ${entity.value}`
56
- }));
57
- };
58
-
59
- /**
60
- * CLI prompt flow for creating an ActiveClass record.
61
- *
62
- * - Prompts for type (Controller, Utility, or Trigger)
63
- * - For Trigger, also prompts for target entity selection (searchable)
64
- * - Prompts for class name and optional description
65
- *
66
- * @async
67
- * @param {Object} options - CLI options to bypass prompts
68
- * @returns {Promise<Object>} The creation payload for ActiveClass.
69
- */
70
- const createActiveClass = async (options = {}) => {
71
- let classType = options.classType;
72
-
73
- // Normalize class type from CLI format to internal format
74
- if (classType) {
75
- const typeMap = {
76
- 'controller': 'Controller',
77
- 'utility': 'Utility',
78
- 'class': 'Utility',
79
- 'trigger': 'Trigger'
80
- };
81
- classType = typeMap[classType.toLowerCase()] || classType;
82
- }
83
-
84
- if (!classType) {
85
- classType = await select({
86
- message: "Select ActiveClass type:",
87
- choices: [
88
- { name: "Controller", value: "Controller" },
89
- { name: "Class", value: "Utility" },
90
- { name: "Trigger", value: "Trigger" }
91
- ]
92
- });
93
- }
94
-
95
- if (classType === "Trigger") {
96
- let entityId = options.entityId;
97
-
98
- if (!entityId) {
99
- // Ensure entity list is loaded for search prompt
100
- await loadEntities();
101
-
102
- entityId = await search({
103
- message: "Search and select entity for this Trigger:",
104
- source: searchEntities,
105
- pageSize: 8
106
- });
107
- }
108
-
109
- let className = options.name;
110
- if (!className) {
111
- className = await input({
112
- message: "Trigger class name:",
113
- validate: (input) => input.length > 0 || "Please enter a class name"
114
- });
115
- }
116
-
117
- let description = options.description;
118
- if (description === undefined) {
119
- description = await input({
120
- message: "Description (optional):"
121
- });
122
- }
123
-
124
- return {
125
- type: "ActiveClass",
126
- classType,
127
- triggerEntityId: entityId,
128
- className,
129
- description,
130
- };
131
- }
132
-
133
- const hint = classType === "Controller" ? "(without controller keyword)" : ""
134
-
135
- // Flow for Controller or Utility
136
- let className = options.name;
137
- if (!className) {
138
- className = await input({
139
- message: `${classType} class name ${hint}:`,
140
- validate: (input) => input.length > 0 || "Please enter a class name"
141
- });
142
- }
143
-
144
- let description = options.description;
145
- if (description === undefined) {
146
- description = await input({
147
- message: "Description (optional):"
148
- });
149
- }
150
-
151
- return {
152
- type: "ActiveClass",
153
- classType,
154
- className,
155
- description,
156
- };
157
- };
158
-
159
- /**
160
- * CLI prompt flow for creating an ActivePage record.
161
- *
162
- * - Prompts for page name and optional description.
163
- *
164
- * @async
165
- * @param {Object} options - CLI options to bypass prompts
166
- * @param {string} options.pageType - Pre-determined page type (Page or Template)
167
- * @returns {Promise<Object>} The creation payload for ActivePage.
168
- */
169
- const createActivePage = async (options = {}) => {
170
- let pageType = options.pageType;
171
-
172
- // Normalize page type from CLI format to internal format
173
- if (pageType) {
174
- const typeMap = {
175
- 'page': 'Page',
176
- 'template': 'Template'
177
- };
178
- pageType = typeMap[pageType.toLowerCase()] || pageType;
179
- }
180
-
181
- if (!pageType) {
182
- pageType = await select({
183
- message: "Select ActivePage type:",
184
- choices: [
185
- { name: "Page", value: "Page" },
186
- { name: "Template", value: "Template" }
187
- ]
188
- });
189
- }
190
-
191
- let pageName = options.name;
192
- if (!pageName) {
193
- pageName = await input({
194
- message: `${pageType} name:`,
195
- validate: (input) => input.length > 0 || `Please enter a ${pageType.toLowerCase()} name`
196
- });
197
- }
198
-
199
- let description = options.description;
200
- if (description === undefined) {
201
- description = await input({
202
- message: "Description (optional):"
203
- });
204
- }
205
-
206
- return {
207
- type: pageType,
208
- pageName,
209
- description,
210
- };
211
- };
212
-
213
- /**
214
- * Saves the generated file content to the appropriate local folder.
215
- * @param {string} entityType
216
- * @param {object} formattedData
217
- * @param {string} recordId
218
- * @returns {Promise<string>} Full path of created file.
219
- */
220
- const saveToFile = async (entityType, formattedData, recordId) => {
221
- let fileDir, fileExt, fileName, fileContent;
222
-
223
- let mapKey; // fallback to Page
224
- if (entityType === "ActivePage") {
225
- mapKey = formattedData.Type || "Active Page";
226
- fileDir = TYPE_DIR_MAP[mapKey].directory;
227
- fileExt = TYPE_DIR_MAP[mapKey].extension;
228
- fileName = `${formattedData.Name}.${fileExt}`;
229
- fileContent = formattedData.Content || "";
230
- } else {
231
- mapKey = formattedData.Type || "Class"; // fallback to Class
232
- fileDir = TYPE_DIR_MAP[mapKey].directory;
233
- fileExt = TYPE_DIR_MAP[mapKey].extension;
234
- fileName = `${formattedData.Name}.${fileExt}`;
235
- fileContent = formattedData.Body || "";
236
- }
237
-
238
- // Ensure directory exists
239
- const outputDir = path.join(process.cwd(), EXPORT_ROOT, fileDir);
240
- fs.mkdirSync(outputDir, { recursive: true });
241
-
242
- // Full file path
243
- const filePath = path.join(outputDir, fileName);
244
-
245
- // Write file
246
- fs.writeFileSync(filePath, fileContent);
247
-
248
- // Add a file tag so we can keep track of file changes
249
- await setFileTag(filePath, recordId);
250
-
251
- // Update base with content snapshot to ensure cache is in sync
252
- const contentHash = sha256(fileContent);
253
- updateBase(
254
- filePath,
255
- { Id: recordId, Type: mapKey },
256
- '',
257
- { content: fileContent, hash: contentHash }
258
- );
259
-
260
- return filePath;
261
- };
262
-
263
- /**
264
- * Main CLI handler for `magentrix create`.
265
- *
266
- * - Ensures valid Magentrix credentials.
267
- * - Prompts user to choose entity type (ActiveClass or ActivePage).
268
- * - Runs the appropriate prompt flow to gather data.
269
- * - Logs (or sends) the resulting payload.
270
- *
271
- * @async
272
- * @function create
273
- * @param {Object} cliOptions - Options passed from CLI flags
274
- * @returns {Promise<void>}
275
- */
276
- export const create = async (cliOptions = {}) => {
277
- // Validate CLI options
278
- if (cliOptions.type) {
279
- const validTypes = ['class', 'page', 'template'];
280
- if (!validTypes.includes(cliOptions.type.toLowerCase())) {
281
- throw new Error(`Invalid --type: "${cliOptions.type}". Valid options are: ${validTypes.join(', ')}`);
282
- }
283
- }
284
-
285
- if (cliOptions.classType) {
286
- const validClassTypes = ['controller', 'utility', 'class', 'trigger'];
287
- if (!validClassTypes.includes(cliOptions.classType.toLowerCase())) {
288
- throw new Error(`Invalid --class-type: "${cliOptions.classType}". Valid options are: ${validClassTypes.join(', ')}`);
289
- }
290
-
291
- // Ensure --class-type is only used with --type class
292
- if (cliOptions.type && cliOptions.type.toLowerCase() !== 'class') {
293
- throw new Error('--class-type can only be used with --type class');
294
- }
295
- }
296
-
297
- if (cliOptions.entityId) {
298
- // Ensure --entity-id is only used with triggers
299
- if (cliOptions.classType && cliOptions.classType.toLowerCase() !== 'trigger') {
300
- throw new Error('--entity-id can only be used with --class-type trigger');
301
- }
302
- if (!cliOptions.classType) {
303
- console.log(chalk.yellow('⚠️ Warning: --entity-id provided without --class-type trigger. It will be ignored unless you select Trigger interactively.'));
304
- }
305
- }
306
-
307
- // Clear the terminal
308
- process.stdout.write('\x1Bc');
309
-
310
- // 1. Prompt for and validate Magentrix credentials
311
- credentials = await withSpinner('Authenticating...', async () => {
312
- return await ensureValidCredentials();
313
- });
314
-
315
- // 2. Determine entity type (from CLI or prompt)
316
- let entityType;
317
- let pageType;
318
-
319
- if (cliOptions.type) {
320
- const typeMap = {
321
- 'class': 'ActiveClass',
322
- 'page': 'ActivePage',
323
- 'template': 'ActivePage'
324
- };
325
- entityType = typeMap[cliOptions.type.toLowerCase()];
326
-
327
- // If template was specified, set the pageType
328
- if (cliOptions.type.toLowerCase() === 'template') {
329
- pageType = 'Template';
330
- } else if (cliOptions.type.toLowerCase() === 'page') {
331
- pageType = 'Page';
332
- }
333
-
334
- if (!entityType) {
335
- throw new Error(`Invalid type: ${cliOptions.type}. Valid types are: class, page, template`);
336
- }
337
- } else {
338
- entityType = await select({
339
- message: "What would you like to create?",
340
- choices: [
341
- { name: "ActiveClass (Controller, Class, Trigger)", value: "ActiveClass" },
342
- { name: "ActivePage (ASPX Page)", value: "ActivePage" },
343
- ]
344
- });
345
- }
346
-
347
- // 3. Build payload via relevant prompt flow
348
- let result;
349
- if (entityType === 'ActiveClass') {
350
- result = await createActiveClass(cliOptions);
351
- } else if (entityType === 'ActivePage') {
352
- result = await createActivePage({ ...cliOptions, pageType });
353
- } else {
354
- // Unknown
355
- throw new Error("Unknown type selected.");
356
- }
357
-
358
- // 4. Display or send payload to Magentrix API
359
- let formattedData;
360
-
361
- if (entityType === "ActivePage") {
362
- formattedData = {
363
- Label: result.pageName,
364
- Name: result.pageName,
365
- Description: result.description || "",
366
- Type: `Active ${result.type}`,
367
- Content: getPageTemplate(result.pageName, result.pageName)
368
- };
369
- } else if (entityType === "ActiveClass") {
370
- let name = result.className;
371
-
372
- if (result.classType === "Controller") {
373
- if (!name.endsWith("Controller")) name += "Controller";
374
- formattedData = {
375
- Name: name,
376
- Body: getControllerTemplate(name),
377
- Description: result.description || "",
378
- Type: "Controller"
379
- };
380
- } else if (result.classType === "Utility" || result.classType === "Class") {
381
- formattedData = {
382
- Name: name,
383
- Body: getClassTemplate(name),
384
- Description: result.description || "",
385
- Type: "Class"
386
- };
387
- } else if (result.classType === "Trigger") {
388
- const selectedTriggerEntity = triggerEntities.find(entity => entity.value === result.triggerEntityId);
389
-
390
- formattedData = {
391
- Name: name,
392
- Body: getTriggerTemplate(name, selectedTriggerEntity.name),
393
- Description: result.description || "",
394
- Type: "Trigger",
395
- EntityId: result.triggerEntityId
396
- };
397
- }
398
- }
399
-
400
- console.log();
401
-
402
- // Uncomment to perform creation via API:
403
- const creationResponse = await withSpinner('Creating file...', async () => {
404
- return await createEntity(
405
- credentials.instanceUrl,
406
- credentials.token.value,
407
- entityType,
408
- formattedData
409
- ).catch(err => {
410
- // The error object structure from fetchMagentrix:
411
- // - err.type: 'network' | 'http' | 'api'
412
- // - err.message: formatted error message
413
- // - err.response: the API response data (if available)
414
- // - err.status: HTTP status code (for http errors)
415
- return {
416
- hasErrors: true,
417
- errorType: err.type,
418
- errorMessage: err.message,
419
- status: err.status,
420
- statusText: err.statusText,
421
- errors: err.response?.errors || err.response?.Errors || [],
422
- rawResponse: err.response
423
- };
424
- });
425
- });
426
-
427
- if (creationResponse?.hasErrors) {
428
- console.log();
429
- console.log(chalk.bgRed.bold.white(' ✖ Creation Failed '));
430
- console.log(chalk.redBright('─'.repeat(48)));
431
-
432
- // Display HTTP status if available
433
- if (creationResponse.status) {
434
- console.log(chalk.yellow(` Status: ${creationResponse.status} ${creationResponse.statusText || ''}`));
435
- }
436
-
437
- const errors = creationResponse.errors || [];
438
-
439
- if (errors.length > 0) {
440
- errors.forEach((err) => {
441
- const code = err.code ? chalk.gray(`[${err.code}] `) : '';
442
- const msg = chalk.whiteBright(err.message || err);
443
- console.log(`${chalk.redBright(' •')} ${code}${msg}`);
444
- });
445
- } else if (creationResponse.errorMessage) {
446
- // Show the formatted error message from fetchMagentrix
447
- console.log(chalk.red(creationResponse.errorMessage));
448
- } else {
449
- console.log(chalk.red('An unknown error occurred during creation.'));
450
- }
451
-
452
- return;
453
- }
454
-
455
- console.log(`✅ File successfully created on Magentrix server.`);
456
-
457
- try {
458
- const filePath = await saveToFile(entityType, formattedData, creationResponse.id);
459
- console.log(`📄 Local copy saved at: ${filePath}`);
460
- console.log('✨ You can now edit this file locally. Don\'t forget to push changes when ready!');
461
- } catch (err) {
462
- console.error(`🚨 Error: Unable to save file locally (${err.message}).`);
463
- console.error('You may need to check directory permissions or disk space.');
464
- }
465
-
466
- };
1
+ import { ensureValidCredentials } from "../utils/cli/helpers/ensureCredentials.js";
2
+ import { createEntity } from "../utils/magentrix/api/createEntity.js";
3
+ import { search, select, input } from "@inquirer/prompts";
4
+ import { listEntities } from "../utils/magentrix/api/retrieveEntity.js";
5
+ import { getClassTemplate, getControllerTemplate, getPageTemplate, getTriggerTemplate } from "../utils/template.js";
6
+ import { withSpinner } from "../utils/spinner.js";
7
+ import { EXPORT_ROOT, TYPE_DIR_MAP } from "../vars/global.js";
8
+ import path from "path";
9
+ import fs from 'fs';
10
+ import chalk from 'chalk';
11
+ import { setFileTag } from "../utils/filetag.js";
12
+ import { updateBase } from "../utils/updateFileBase.js";
13
+ import { sha256 } from "../utils/hash.js";
14
+
15
+ // Credentials and entity cache (module-scope for re-use during prompt session)
16
+ let credentials = {};
17
+ let triggerEntities = [];
18
+
19
+ /**
20
+ * Loads all entities from the Magentrix API and prepares them
21
+ * for searchable selection in the CLI.
22
+ * Populates the `triggerEntities` array.
23
+ *
24
+ * @async
25
+ * @returns {Promise<void>}
26
+ */
27
+ const loadEntities = async () => {
28
+ const entities = await listEntities(credentials.instanceUrl, credentials.token.value);
29
+ // Map to inquirer-friendly objects: { name, value }
30
+ triggerEntities = entities.map(entity => ({
31
+ label: entity.Label,
32
+ value: entity.Id,
33
+ name: entity.Name
34
+ }));
35
+ };
36
+
37
+ /**
38
+ * Search function for entity selection using @inquirer/prompts.
39
+ * Filters `triggerEntities` array based on the user's input term.
40
+ *
41
+ * @async
42
+ * @param {string} term - User's search input.
43
+ * @returns {Promise<Array>} Filtered entity choices for prompt.
44
+ */
45
+ const searchEntities = async (term = '') => {
46
+ // Case-insensitive substring match against entity names
47
+ const filteredEntities = triggerEntities.filter(entity =>
48
+ entity.name.toLowerCase().includes(term.toLowerCase())
49
+ );
50
+
51
+ // Format for inquirer searchable prompt
52
+ return filteredEntities.map(entity => ({
53
+ name: `${entity.label} (${entity.value})`,
54
+ value: entity.value,
55
+ // Optionally: description: `Entity ID ${entity.value}`
56
+ }));
57
+ };
58
+
59
+ /**
60
+ * CLI prompt flow for creating an ActiveClass record.
61
+ *
62
+ * - Prompts for type (Controller, Utility, or Trigger)
63
+ * - For Trigger, also prompts for target entity selection (searchable)
64
+ * - Prompts for class name and optional description
65
+ *
66
+ * @async
67
+ * @param {Object} options - CLI options to bypass prompts
68
+ * @returns {Promise<Object>} The creation payload for ActiveClass.
69
+ */
70
+ const createActiveClass = async (options = {}) => {
71
+ let classType = options.classType;
72
+
73
+ // Normalize class type from CLI format to internal format
74
+ if (classType) {
75
+ const typeMap = {
76
+ 'controller': 'Controller',
77
+ 'utility': 'Utility',
78
+ 'class': 'Utility',
79
+ 'trigger': 'Trigger'
80
+ };
81
+ classType = typeMap[classType.toLowerCase()] || classType;
82
+ }
83
+
84
+ if (!classType) {
85
+ classType = await select({
86
+ message: "Select ActiveClass type:",
87
+ choices: [
88
+ { name: "Controller", value: "Controller" },
89
+ { name: "Class", value: "Utility" },
90
+ { name: "Trigger", value: "Trigger" }
91
+ ]
92
+ });
93
+ }
94
+
95
+ if (classType === "Trigger") {
96
+ let entityId = options.entityId;
97
+
98
+ if (!entityId) {
99
+ // Ensure entity list is loaded for search prompt
100
+ await loadEntities();
101
+
102
+ entityId = await search({
103
+ message: "Search and select entity for this Trigger:",
104
+ source: searchEntities,
105
+ pageSize: 8
106
+ });
107
+ }
108
+
109
+ let className = options.name;
110
+ if (!className) {
111
+ className = await input({
112
+ message: "Trigger class name:",
113
+ validate: (input) => input.length > 0 || "Please enter a class name"
114
+ });
115
+ }
116
+
117
+ let description = options.description;
118
+ if (description === undefined) {
119
+ description = await input({
120
+ message: "Description (optional):"
121
+ });
122
+ }
123
+
124
+ return {
125
+ type: "ActiveClass",
126
+ classType,
127
+ triggerEntityId: entityId,
128
+ className,
129
+ description,
130
+ };
131
+ }
132
+
133
+ const hint = classType === "Controller" ? "(without controller keyword)" : ""
134
+
135
+ // Flow for Controller or Utility
136
+ let className = options.name;
137
+ if (!className) {
138
+ className = await input({
139
+ message: `${classType} class name ${hint}:`,
140
+ validate: (input) => input.length > 0 || "Please enter a class name"
141
+ });
142
+ }
143
+
144
+ let description = options.description;
145
+ if (description === undefined) {
146
+ description = await input({
147
+ message: "Description (optional):"
148
+ });
149
+ }
150
+
151
+ return {
152
+ type: "ActiveClass",
153
+ classType,
154
+ className,
155
+ description,
156
+ };
157
+ };
158
+
159
+ /**
160
+ * CLI prompt flow for creating an ActivePage record.
161
+ *
162
+ * - Prompts for page name and optional description.
163
+ *
164
+ * @async
165
+ * @param {Object} options - CLI options to bypass prompts
166
+ * @param {string} options.pageType - Pre-determined page type (Page or Template)
167
+ * @returns {Promise<Object>} The creation payload for ActivePage.
168
+ */
169
+ const createActivePage = async (options = {}) => {
170
+ let pageType = options.pageType;
171
+
172
+ // Normalize page type from CLI format to internal format
173
+ if (pageType) {
174
+ const typeMap = {
175
+ 'page': 'Page',
176
+ 'template': 'Template'
177
+ };
178
+ pageType = typeMap[pageType.toLowerCase()] || pageType;
179
+ }
180
+
181
+ if (!pageType) {
182
+ pageType = await select({
183
+ message: "Select ActivePage type:",
184
+ choices: [
185
+ { name: "Page", value: "Page" },
186
+ { name: "Template", value: "Template" }
187
+ ]
188
+ });
189
+ }
190
+
191
+ let pageName = options.name;
192
+ if (!pageName) {
193
+ pageName = await input({
194
+ message: `${pageType} name:`,
195
+ validate: (input) => input.length > 0 || `Please enter a ${pageType.toLowerCase()} name`
196
+ });
197
+ }
198
+
199
+ let description = options.description;
200
+ if (description === undefined) {
201
+ description = await input({
202
+ message: "Description (optional):"
203
+ });
204
+ }
205
+
206
+ return {
207
+ type: pageType,
208
+ pageName,
209
+ description,
210
+ };
211
+ };
212
+
213
+ /**
214
+ * Saves the generated file content to the appropriate local folder.
215
+ * @param {string} entityType
216
+ * @param {object} formattedData
217
+ * @param {string} recordId
218
+ * @returns {Promise<string>} Full path of created file.
219
+ */
220
+ const saveToFile = async (entityType, formattedData, recordId) => {
221
+ let fileDir, fileExt, fileName, fileContent;
222
+
223
+ let mapKey; // fallback to Page
224
+ if (entityType === "ActivePage") {
225
+ mapKey = formattedData.Type || "Active Page";
226
+ fileDir = TYPE_DIR_MAP[mapKey].directory;
227
+ fileExt = TYPE_DIR_MAP[mapKey].extension;
228
+ fileName = `${formattedData.Name}.${fileExt}`;
229
+ fileContent = formattedData.Content || "";
230
+ } else {
231
+ mapKey = formattedData.Type || "Class"; // fallback to Class
232
+ fileDir = TYPE_DIR_MAP[mapKey].directory;
233
+ fileExt = TYPE_DIR_MAP[mapKey].extension;
234
+ fileName = `${formattedData.Name}.${fileExt}`;
235
+ fileContent = formattedData.Body || "";
236
+ }
237
+
238
+ // Ensure directory exists
239
+ const outputDir = path.join(process.cwd(), EXPORT_ROOT, fileDir);
240
+ fs.mkdirSync(outputDir, { recursive: true });
241
+
242
+ // Full file path
243
+ const filePath = path.join(outputDir, fileName);
244
+
245
+ // Write file
246
+ fs.writeFileSync(filePath, fileContent);
247
+
248
+ // Add a file tag so we can keep track of file changes
249
+ await setFileTag(filePath, recordId);
250
+
251
+ // Update base with content snapshot to ensure cache is in sync
252
+ const contentHash = sha256(fileContent);
253
+ updateBase(
254
+ filePath,
255
+ { Id: recordId, Type: mapKey },
256
+ '',
257
+ { content: fileContent, hash: contentHash }
258
+ );
259
+
260
+ return filePath;
261
+ };
262
+
263
+ /**
264
+ * Main CLI handler for `magentrix create`.
265
+ *
266
+ * - Ensures valid Magentrix credentials.
267
+ * - Prompts user to choose entity type (ActiveClass or ActivePage).
268
+ * - Runs the appropriate prompt flow to gather data.
269
+ * - Logs (or sends) the resulting payload.
270
+ *
271
+ * @async
272
+ * @function create
273
+ * @param {Object} cliOptions - Options passed from CLI flags
274
+ * @returns {Promise<void>}
275
+ */
276
+ export const create = async (cliOptions = {}) => {
277
+ // Validate CLI options
278
+ if (cliOptions.type) {
279
+ const validTypes = ['class', 'page', 'template'];
280
+ if (!validTypes.includes(cliOptions.type.toLowerCase())) {
281
+ throw new Error(`Invalid --type: "${cliOptions.type}". Valid options are: ${validTypes.join(', ')}`);
282
+ }
283
+ }
284
+
285
+ if (cliOptions.classType) {
286
+ const validClassTypes = ['controller', 'utility', 'class', 'trigger'];
287
+ if (!validClassTypes.includes(cliOptions.classType.toLowerCase())) {
288
+ throw new Error(`Invalid --class-type: "${cliOptions.classType}". Valid options are: ${validClassTypes.join(', ')}`);
289
+ }
290
+
291
+ // Ensure --class-type is only used with --type class
292
+ if (cliOptions.type && cliOptions.type.toLowerCase() !== 'class') {
293
+ throw new Error('--class-type can only be used with --type class');
294
+ }
295
+ }
296
+
297
+ if (cliOptions.entityId) {
298
+ // Ensure --entity-id is only used with triggers
299
+ if (cliOptions.classType && cliOptions.classType.toLowerCase() !== 'trigger') {
300
+ throw new Error('--entity-id can only be used with --class-type trigger');
301
+ }
302
+ if (!cliOptions.classType) {
303
+ console.log(chalk.yellow('⚠️ Warning: --entity-id provided without --class-type trigger. It will be ignored unless you select Trigger interactively.'));
304
+ }
305
+ }
306
+
307
+ // Clear the terminal
308
+ process.stdout.write('\x1Bc');
309
+
310
+ // 1. Prompt for and validate Magentrix credentials
311
+ credentials = await withSpinner('Authenticating...', async () => {
312
+ return await ensureValidCredentials();
313
+ });
314
+
315
+ // 2. Determine entity type (from CLI or prompt)
316
+ let entityType;
317
+ let pageType;
318
+
319
+ if (cliOptions.type) {
320
+ const typeMap = {
321
+ 'class': 'ActiveClass',
322
+ 'page': 'ActivePage',
323
+ 'template': 'ActivePage'
324
+ };
325
+ entityType = typeMap[cliOptions.type.toLowerCase()];
326
+
327
+ // If template was specified, set the pageType
328
+ if (cliOptions.type.toLowerCase() === 'template') {
329
+ pageType = 'Template';
330
+ } else if (cliOptions.type.toLowerCase() === 'page') {
331
+ pageType = 'Page';
332
+ }
333
+
334
+ if (!entityType) {
335
+ throw new Error(`Invalid type: ${cliOptions.type}. Valid types are: class, page, template`);
336
+ }
337
+ } else {
338
+ entityType = await select({
339
+ message: "What would you like to create?",
340
+ choices: [
341
+ { name: "ActiveClass (Controller, Class, Trigger)", value: "ActiveClass" },
342
+ { name: "ActivePage (ASPX Page)", value: "ActivePage" },
343
+ ]
344
+ });
345
+ }
346
+
347
+ // 3. Build payload via relevant prompt flow
348
+ let result;
349
+ if (entityType === 'ActiveClass') {
350
+ result = await createActiveClass(cliOptions);
351
+ } else if (entityType === 'ActivePage') {
352
+ result = await createActivePage({ ...cliOptions, pageType });
353
+ } else {
354
+ // Unknown
355
+ throw new Error("Unknown type selected.");
356
+ }
357
+
358
+ // 4. Display or send payload to Magentrix API
359
+ let formattedData;
360
+
361
+ if (entityType === "ActivePage") {
362
+ formattedData = {
363
+ Label: result.pageName,
364
+ Name: result.pageName,
365
+ Description: result.description || "",
366
+ Type: `Active ${result.type}`,
367
+ Content: getPageTemplate(result.pageName, result.pageName)
368
+ };
369
+ } else if (entityType === "ActiveClass") {
370
+ let name = result.className;
371
+
372
+ if (result.classType === "Controller") {
373
+ if (!name.endsWith("Controller")) name += "Controller";
374
+ formattedData = {
375
+ Name: name,
376
+ Body: getControllerTemplate(name),
377
+ Description: result.description || "",
378
+ Type: "Controller"
379
+ };
380
+ } else if (result.classType === "Utility" || result.classType === "Class") {
381
+ formattedData = {
382
+ Name: name,
383
+ Body: getClassTemplate(name),
384
+ Description: result.description || "",
385
+ Type: "Class"
386
+ };
387
+ } else if (result.classType === "Trigger") {
388
+ const selectedTriggerEntity = triggerEntities.find(entity => entity.value === result.triggerEntityId);
389
+
390
+ formattedData = {
391
+ Name: name,
392
+ Body: getTriggerTemplate(name, selectedTriggerEntity.name),
393
+ Description: result.description || "",
394
+ Type: "Trigger",
395
+ EntityId: result.triggerEntityId
396
+ };
397
+ }
398
+ }
399
+
400
+ console.log();
401
+
402
+ // Uncomment to perform creation via API:
403
+ const creationResponse = await withSpinner('Creating file...', async () => {
404
+ return await createEntity(
405
+ credentials.instanceUrl,
406
+ credentials.token.value,
407
+ entityType,
408
+ formattedData
409
+ ).catch(err => {
410
+ // The error object structure from fetchMagentrix:
411
+ // - err.type: 'network' | 'http' | 'api'
412
+ // - err.message: formatted error message
413
+ // - err.response: the API response data (if available)
414
+ // - err.status: HTTP status code (for http errors)
415
+ return {
416
+ hasErrors: true,
417
+ errorType: err.type,
418
+ errorMessage: err.message,
419
+ status: err.status,
420
+ statusText: err.statusText,
421
+ errors: err.response?.errors || err.response?.Errors || [],
422
+ rawResponse: err.response
423
+ };
424
+ });
425
+ });
426
+
427
+ if (creationResponse?.hasErrors) {
428
+ console.log();
429
+ console.log(chalk.bgRed.bold.white(' ✖ Creation Failed '));
430
+ console.log(chalk.redBright('─'.repeat(48)));
431
+
432
+ // Display HTTP status if available
433
+ if (creationResponse.status) {
434
+ console.log(chalk.yellow(` Status: ${creationResponse.status} ${creationResponse.statusText || ''}`));
435
+ }
436
+
437
+ const errors = creationResponse.errors || [];
438
+
439
+ if (errors.length > 0) {
440
+ errors.forEach((err) => {
441
+ const code = err.code ? chalk.gray(`[${err.code}] `) : '';
442
+ const msg = chalk.whiteBright(err.message || err);
443
+ console.log(`${chalk.redBright(' •')} ${code}${msg}`);
444
+ });
445
+ } else if (creationResponse.errorMessage) {
446
+ // Show the formatted error message from fetchMagentrix
447
+ console.log(chalk.red(creationResponse.errorMessage));
448
+ } else {
449
+ console.log(chalk.red('An unknown error occurred during creation.'));
450
+ }
451
+
452
+ return;
453
+ }
454
+
455
+ console.log(`✅ File successfully created on Magentrix server.`);
456
+
457
+ try {
458
+ const filePath = await saveToFile(entityType, formattedData, creationResponse.id);
459
+ console.log(`📄 Local copy saved at: ${filePath}`);
460
+ console.log('✨ You can now edit this file locally. Don\'t forget to push changes when ready!');
461
+ } catch (err) {
462
+ console.error(`🚨 Error: Unable to save file locally (${err.message}).`);
463
+ console.error('You may need to check directory permissions or disk space.');
464
+ }
465
+
466
+ };