@magentrix-corp/magentrix-cli 1.0.1 → 1.1.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.
package/README.md CHANGED
@@ -63,9 +63,59 @@ You should see the version number displayed.
63
63
 
64
64
  ---
65
65
 
66
+ ## Updating the Package
67
+
68
+ To update MagentrixCLI to the latest version:
69
+
70
+ ### Global Installation Update
71
+ If you installed globally (recommended):
72
+
73
+ ```bash
74
+ npm update -g @magentrix-corp/magentrix-cli
75
+ ```
76
+
77
+ Or to ensure you get the absolute latest version:
78
+
79
+ ```bash
80
+ npm install -g @magentrix-corp/magentrix-cli@latest
81
+ ```
82
+
83
+ ### Local Installation Update
84
+ If you installed locally in your project:
85
+
86
+ ```bash
87
+ npm update @magentrix-corp/magentrix-cli
88
+ ```
89
+
90
+ Or for the latest version:
91
+
92
+ ```bash
93
+ npm install @magentrix-corp/magentrix-cli@latest
94
+ ```
95
+
96
+ ### Check for Updates
97
+ To see if you're running the latest version:
98
+
99
+ ```bash
100
+ # Check your current version
101
+ magentrix --version
102
+
103
+ # Check the latest available version on npm
104
+ npm view @magentrix-corp/magentrix-cli version
105
+ ```
106
+
107
+ ### After Updating
108
+ Your configuration and local files are preserved during updates. You don't need to run `magentrix setup` again unless there are breaking changes (which will be noted in release notes).
109
+
110
+ **Note**: It's a good practice to check for updates periodically to get the latest features, bug fixes, and performance improvements.
111
+
112
+ ---
113
+
66
114
  ## First Time Setup
67
115
 
68
116
  ### Step 1: Configure Your Credentials
117
+
118
+ #### Interactive Setup
69
119
  Run this command to set up your connection to Magentrix:
70
120
 
71
121
  ```bash
@@ -76,6 +126,17 @@ You'll be prompted for:
76
126
  - **API Key**: Paste your Magentrix API key
77
127
  - **Instance URL**: Enter your Magentrix URL (like `https://yourcompany.magentrixcloud.com`)
78
128
 
129
+ #### Non-Interactive Setup
130
+ For automation or CI/CD, you can provide credentials via command-line flags:
131
+
132
+ ```bash
133
+ magentrix setup --api-key YOUR_API_KEY --instance-url https://yourcompany.magentrixcloud.com
134
+ ```
135
+
136
+ **Available flags:**
137
+ - `--api-key <apiKey>` - Your Magentrix API key
138
+ - `--instance-url <instanceUrl>` - Your Magentrix instance URL
139
+
79
140
  The tool will test your credentials and save them securely. It will also automatically configure VS Code file associations for syntax highlighting of Magentrix file types (.ac, .ctrl, .trigger, .aspx files).
80
141
 
81
142
  ### Editor Support
@@ -96,7 +157,7 @@ This creates a `src` folder with all your files organized into:
96
157
  - `Triggers/` - Your trigger code (`.trigger` files)
97
158
  - `Pages/` - Your ASPX pages (`.aspx` files)
98
159
  - `Templates/` - Your templates (`.aspx` files)
99
- - `Contents/Assets/` - Your static assets (images, CSS, JavaScript, etc.)
160
+ - `Assets/` - Your static assets (images, CSS, JavaScript, etc.)
100
161
 
101
162
  ---
102
163
 
@@ -178,6 +239,43 @@ This starts an interactive wizard:
178
239
 
179
240
  The tool creates the file both locally and on your Magentrix server with proper templates.
180
241
 
242
+ ### Non-Interactive File Creation
243
+ For automation or scripting, you can provide all parameters via command-line flags:
244
+
245
+ #### Create a Controller
246
+ ```bash
247
+ magentrix create --type class --class-type controller --name UserController --description "Handles user operations"
248
+ ```
249
+
250
+ #### Create a Utility Class
251
+ ```bash
252
+ magentrix create --type class --class-type utility --name EmailHelper --description "Email utility functions"
253
+ ```
254
+
255
+ #### Create a Trigger
256
+ ```bash
257
+ magentrix create --type class --class-type trigger --entity-id ENTITY_ID --name AccountTrigger --description "Account trigger logic"
258
+ ```
259
+
260
+ #### Create a Page
261
+ ```bash
262
+ magentrix create --type page --name Dashboard --description "Main dashboard page"
263
+ ```
264
+
265
+ #### Create a Template
266
+ ```bash
267
+ magentrix create --type template --name EmailTemplate --description "Email notification template"
268
+ ```
269
+
270
+ **Available flags:**
271
+ - `--type <type>` - Entity type: `class`, `page`, or `template`
272
+ - `--class-type <classType>` - For classes: `controller`, `utility`, or `trigger`
273
+ - `--name <name>` - Name of the file to create
274
+ - `--description <description>` - Optional description
275
+ - `--entity-id <entityId>` - Required for triggers: the entity ID to attach the trigger to
276
+
277
+ **Tip**: You can mix interactive and non-interactive modes. For example, provide just the `--name` flag and the tool will prompt you for the rest.
278
+
181
279
  ---
182
280
 
183
281
  ## Working with Files
@@ -117,11 +117,11 @@ const readFileSafe = (filePath) => {
117
117
  };
118
118
 
119
119
  /**
120
- * Checks if file is within Contents/Assets (static asset)
120
+ * Checks if file is within Assets (static asset)
121
121
  */
122
122
  const isStaticAsset = (filePath) => {
123
123
  const normalizedPath = path.normalize(filePath);
124
- return normalizedPath.includes(path.join('Contents', 'Assets'));
124
+ return normalizedPath.includes(path.join('Assets'));
125
125
  };
126
126
 
127
127
  /**
package/actions/create.js CHANGED
@@ -10,6 +10,7 @@ import fs from 'fs';
10
10
  import chalk from 'chalk';
11
11
  import { setFileTag } from "../utils/filetag.js";
12
12
  import { updateBase } from "../utils/updateFileBase.js";
13
+ import { sha256 } from "../utils/hash.js";
13
14
 
14
15
  // Credentials and entity cache (module-scope for re-use during prompt session)
15
16
  let credentials = {};
@@ -57,42 +58,68 @@ const searchEntities = async (term = '') => {
57
58
 
58
59
  /**
59
60
  * CLI prompt flow for creating an ActiveClass record.
60
- *
61
+ *
61
62
  * - Prompts for type (Controller, Utility, or Trigger)
62
63
  * - For Trigger, also prompts for target entity selection (searchable)
63
64
  * - Prompts for class name and optional description
64
- *
65
+ *
65
66
  * @async
67
+ * @param {Object} options - CLI options to bypass prompts
66
68
  * @returns {Promise<Object>} The creation payload for ActiveClass.
67
69
  */
68
- const createActiveClass = async () => {
69
- const classType = await select({
70
- message: "Select ActiveClass type:",
71
- choices: [
72
- { name: "Controller", value: "Controller" },
73
- { name: "Class", value: "Utility" },
74
- { name: "Trigger", value: "Trigger" }
75
- ]
76
- });
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
+ }
77
94
 
78
95
  if (classType === "Trigger") {
79
- // Ensure entity list is loaded for search prompt
80
- await loadEntities();
96
+ let entityId = options.entityId;
81
97
 
82
- const entityId = await search({
83
- message: "Search and select entity for this Trigger:",
84
- source: searchEntities,
85
- pageSize: 8
86
- });
98
+ if (!entityId) {
99
+ // Ensure entity list is loaded for search prompt
100
+ await loadEntities();
87
101
 
88
- const className = await input({
89
- message: "Trigger class name:",
90
- validate: (input) => input.length > 0 || "Please enter a class name"
91
- });
102
+ entityId = await search({
103
+ message: "Search and select entity for this Trigger:",
104
+ source: searchEntities,
105
+ pageSize: 8
106
+ });
107
+ }
92
108
 
93
- const description = await input({
94
- message: "Description (optional):"
95
- });
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
+ }
96
123
 
97
124
  return {
98
125
  type: "ActiveClass",
@@ -106,14 +133,20 @@ const createActiveClass = async () => {
106
133
  const hint = classType === "Controller" ? "(without controller keyword)" : ""
107
134
 
108
135
  // Flow for Controller or Utility
109
- const className = await input({
110
- message: `${classType} class name ${hint}:`,
111
- validate: (input) => input.length > 0 || "Please enter a class name"
112
- });
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
+ }
113
143
 
114
- const description = await input({
115
- message: "Description (optional):"
116
- });
144
+ let description = options.description;
145
+ if (description === undefined) {
146
+ description = await input({
147
+ message: "Description (optional):"
148
+ });
149
+ }
117
150
 
118
151
  return {
119
152
  type: "ActiveClass",
@@ -125,29 +158,50 @@ const createActiveClass = async () => {
125
158
 
126
159
  /**
127
160
  * CLI prompt flow for creating an ActivePage record.
128
- *
161
+ *
129
162
  * - Prompts for page name and optional description.
130
- *
163
+ *
131
164
  * @async
165
+ * @param {Object} options - CLI options to bypass prompts
166
+ * @param {string} options.pageType - Pre-determined page type (Page or Template)
132
167
  * @returns {Promise<Object>} The creation payload for ActivePage.
133
168
  */
134
- const createActivePage = async () => {
135
- const pageType = await select({
136
- message: "Select ActivePage type:",
137
- choices: [
138
- { name: "Page", value: "Page" },
139
- { name: "Template", value: "Template" }
140
- ]
141
- });
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
+ }
142
180
 
143
- const pageName = await input({
144
- message: `${pageType} name:`,
145
- validate: (input) => input.length > 0 || `Please enter a ${pageType.toLowerCase()} name`
146
- });
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
+ }
147
190
 
148
- const description = await input({
149
- message: "Description (optional):"
150
- });
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
+ }
151
205
 
152
206
  return {
153
207
  type: pageType,
@@ -193,24 +247,63 @@ const saveToFile = async (entityType, formattedData, recordId) => {
193
247
 
194
248
  // Add a file tag so we can keep track of file changes
195
249
  await setFileTag(filePath, recordId);
196
- updateBase(filePath, { Id: recordId, Type: mapKey }); // Update base
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
+ );
197
259
 
198
260
  return filePath;
199
261
  };
200
262
 
201
263
  /**
202
264
  * Main CLI handler for `magentrix create`.
203
- *
265
+ *
204
266
  * - Ensures valid Magentrix credentials.
205
267
  * - Prompts user to choose entity type (ActiveClass or ActivePage).
206
268
  * - Runs the appropriate prompt flow to gather data.
207
269
  * - Logs (or sends) the resulting payload.
208
- *
270
+ *
209
271
  * @async
210
272
  * @function create
273
+ * @param {Object} cliOptions - Options passed from CLI flags
211
274
  * @returns {Promise<void>}
212
275
  */
213
- export const create = async () => {
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
+
214
307
  // Clear the terminal
215
308
  process.stdout.write('\x1Bc');
216
309
 
@@ -219,21 +312,44 @@ export const create = async () => {
219
312
  return await ensureValidCredentials();
220
313
  });
221
314
 
222
- // 2. Prompt user to select the type of entity to create
223
- const entityType = await select({
224
- message: "What would you like to create?",
225
- choices: [
226
- { name: "ActiveClass (Controller, Class, Trigger)", value: "ActiveClass" },
227
- { name: "ActivePage (ASPX Page)", value: "ActivePage" },
228
- ]
229
- });
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
+ }
230
346
 
231
347
  // 3. Build payload via relevant prompt flow
232
348
  let result;
233
349
  if (entityType === 'ActiveClass') {
234
- result = await createActiveClass();
350
+ result = await createActiveClass(cliOptions);
235
351
  } else if (entityType === 'ActivePage') {
236
- result = await createActivePage();
352
+ result = await createActivePage({ ...cliOptions, pageType });
237
353
  } else {
238
354
  // Unknown
239
355
  throw new Error("Unknown type selected.");
@@ -302,7 +418,7 @@ export const create = async () => {
302
418
  const errors = creationResponse.errors || [];
303
419
 
304
420
  if (errors.length > 0) {
305
- errors.forEach((err, i) => {
421
+ errors.forEach((err) => {
306
422
  const code = err.code ? chalk.gray(`[${err.code}] `) : '';
307
423
  const status = err.status ? chalk.yellow(`[${err.status}] `) : '';
308
424
  const msg = chalk.whiteBright(err.message);