@magentrix-corp/magentrix-cli 1.0.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.
Files changed (43) hide show
  1. package/LICENSE +25 -0
  2. package/README.md +471 -0
  3. package/actions/autopublish.js +283 -0
  4. package/actions/autopublish.old.js +293 -0
  5. package/actions/autopublish.v2.js +447 -0
  6. package/actions/create.js +329 -0
  7. package/actions/help.js +165 -0
  8. package/actions/main.js +81 -0
  9. package/actions/publish.js +567 -0
  10. package/actions/pull.js +139 -0
  11. package/actions/setup.js +61 -0
  12. package/actions/status.js +17 -0
  13. package/bin/magentrix.js +159 -0
  14. package/package.json +61 -0
  15. package/utils/cacher.js +112 -0
  16. package/utils/cli/checkInstanceUrl.js +29 -0
  17. package/utils/cli/helpers/compare.js +281 -0
  18. package/utils/cli/helpers/ensureApiKey.js +57 -0
  19. package/utils/cli/helpers/ensureCredentials.js +60 -0
  20. package/utils/cli/helpers/ensureInstanceUrl.js +63 -0
  21. package/utils/cli/writeRecords.js +223 -0
  22. package/utils/compare.js +135 -0
  23. package/utils/compress.js +18 -0
  24. package/utils/config.js +451 -0
  25. package/utils/diff.js +49 -0
  26. package/utils/downloadAssets.js +75 -0
  27. package/utils/filetag.js +115 -0
  28. package/utils/hash.js +14 -0
  29. package/utils/magentrix/api/assets.js +145 -0
  30. package/utils/magentrix/api/auth.js +56 -0
  31. package/utils/magentrix/api/createEntity.js +61 -0
  32. package/utils/magentrix/api/deleteEntity.js +55 -0
  33. package/utils/magentrix/api/meqlQuery.js +31 -0
  34. package/utils/magentrix/api/retrieveEntity.js +32 -0
  35. package/utils/magentrix/api/updateEntity.js +66 -0
  36. package/utils/magentrix/fetch.js +154 -0
  37. package/utils/merge.js +22 -0
  38. package/utils/preferences.js +40 -0
  39. package/utils/spinner.js +43 -0
  40. package/utils/template.js +52 -0
  41. package/utils/updateFileBase.js +103 -0
  42. package/vars/config.js +1 -0
  43. package/vars/global.js +33 -0
@@ -0,0 +1,329 @@
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
+
14
+ // Credentials and entity cache (module-scope for re-use during prompt session)
15
+ let credentials = {};
16
+ let triggerEntities = [];
17
+
18
+ /**
19
+ * Loads all entities from the Magentrix API and prepares them
20
+ * for searchable selection in the CLI.
21
+ * Populates the `triggerEntities` array.
22
+ *
23
+ * @async
24
+ * @returns {Promise<void>}
25
+ */
26
+ const loadEntities = async () => {
27
+ const entities = await listEntities(credentials.instanceUrl, credentials.token.value);
28
+ // Map to inquirer-friendly objects: { name, value }
29
+ triggerEntities = entities.map(entity => ({
30
+ label: entity.Label,
31
+ value: entity.Id,
32
+ name: entity.Name
33
+ }));
34
+ };
35
+
36
+ /**
37
+ * Search function for entity selection using @inquirer/prompts.
38
+ * Filters `triggerEntities` array based on the user's input term.
39
+ *
40
+ * @async
41
+ * @param {string} term - User's search input.
42
+ * @returns {Promise<Array>} Filtered entity choices for prompt.
43
+ */
44
+ const searchEntities = async (term = '') => {
45
+ // Case-insensitive substring match against entity names
46
+ const filteredEntities = triggerEntities.filter(entity =>
47
+ entity.name.toLowerCase().includes(term.toLowerCase())
48
+ );
49
+
50
+ // Format for inquirer searchable prompt
51
+ return filteredEntities.map(entity => ({
52
+ name: `${entity.label} (${entity.value})`,
53
+ value: entity.value,
54
+ // Optionally: description: `Entity ID ${entity.value}`
55
+ }));
56
+ };
57
+
58
+ /**
59
+ * CLI prompt flow for creating an ActiveClass record.
60
+ *
61
+ * - Prompts for type (Controller, Utility, or Trigger)
62
+ * - For Trigger, also prompts for target entity selection (searchable)
63
+ * - Prompts for class name and optional description
64
+ *
65
+ * @async
66
+ * @returns {Promise<Object>} The creation payload for ActiveClass.
67
+ */
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
+ });
77
+
78
+ if (classType === "Trigger") {
79
+ // Ensure entity list is loaded for search prompt
80
+ await loadEntities();
81
+
82
+ const entityId = await search({
83
+ message: "Search and select entity for this Trigger:",
84
+ source: searchEntities,
85
+ pageSize: 8
86
+ });
87
+
88
+ const className = await input({
89
+ message: "Trigger class name:",
90
+ validate: (input) => input.length > 0 || "Please enter a class name"
91
+ });
92
+
93
+ const description = await input({
94
+ message: "Description (optional):"
95
+ });
96
+
97
+ return {
98
+ type: "ActiveClass",
99
+ classType,
100
+ triggerEntityId: entityId,
101
+ className,
102
+ description,
103
+ };
104
+ }
105
+
106
+ const hint = classType === "Controller" ? "(without controller keyword)" : ""
107
+
108
+ // 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
+ });
113
+
114
+ const description = await input({
115
+ message: "Description (optional):"
116
+ });
117
+
118
+ return {
119
+ type: "ActiveClass",
120
+ classType,
121
+ className,
122
+ description,
123
+ };
124
+ };
125
+
126
+ /**
127
+ * CLI prompt flow for creating an ActivePage record.
128
+ *
129
+ * - Prompts for page name and optional description.
130
+ *
131
+ * @async
132
+ * @returns {Promise<Object>} The creation payload for ActivePage.
133
+ */
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
+ });
142
+
143
+ const pageName = await input({
144
+ message: `${pageType} name:`,
145
+ validate: (input) => input.length > 0 || `Please enter a ${pageType.toLowerCase()} name`
146
+ });
147
+
148
+ const description = await input({
149
+ message: "Description (optional):"
150
+ });
151
+
152
+ return {
153
+ type: pageType,
154
+ pageName,
155
+ description,
156
+ };
157
+ };
158
+
159
+ /**
160
+ * Saves the generated file content to the appropriate local folder.
161
+ * @param {string} entityType
162
+ * @param {object} formattedData
163
+ * @param {string} recordId
164
+ * @returns {Promise<string>} Full path of created file.
165
+ */
166
+ const saveToFile = async (entityType, formattedData, recordId) => {
167
+ let fileDir, fileExt, fileName, fileContent;
168
+
169
+ let mapKey; // fallback to Page
170
+ if (entityType === "ActivePage") {
171
+ mapKey = formattedData.Type || "Active Page";
172
+ fileDir = TYPE_DIR_MAP[mapKey].directory;
173
+ fileExt = TYPE_DIR_MAP[mapKey].extension;
174
+ fileName = `${formattedData.Name}.${fileExt}`;
175
+ fileContent = formattedData.Content || "";
176
+ } else {
177
+ mapKey = formattedData.Type || "Class"; // fallback to Class
178
+ fileDir = TYPE_DIR_MAP[mapKey].directory;
179
+ fileExt = TYPE_DIR_MAP[mapKey].extension;
180
+ fileName = `${formattedData.Name}.${fileExt}`;
181
+ fileContent = formattedData.Body || "";
182
+ }
183
+
184
+ // Ensure directory exists
185
+ const outputDir = path.join(process.cwd(), EXPORT_ROOT, fileDir);
186
+ fs.mkdirSync(outputDir, { recursive: true });
187
+
188
+ // Full file path
189
+ const filePath = path.join(outputDir, fileName);
190
+
191
+ // Write file
192
+ fs.writeFileSync(filePath, fileContent);
193
+
194
+ // Add a file tag so we can keep track of file changes
195
+ await setFileTag(filePath, recordId);
196
+ updateBase(filePath, { Id: recordId, Type: mapKey }); // Update base
197
+
198
+ return filePath;
199
+ };
200
+
201
+ /**
202
+ * Main CLI handler for `magentrix create`.
203
+ *
204
+ * - Ensures valid Magentrix credentials.
205
+ * - Prompts user to choose entity type (ActiveClass or ActivePage).
206
+ * - Runs the appropriate prompt flow to gather data.
207
+ * - Logs (or sends) the resulting payload.
208
+ *
209
+ * @async
210
+ * @function create
211
+ * @returns {Promise<void>}
212
+ */
213
+ export const create = async () => {
214
+ // Clear the terminal
215
+ process.stdout.write('\x1Bc');
216
+
217
+ // 1. Prompt for and validate Magentrix credentials
218
+ credentials = await withSpinner('Authenticating...', async () => {
219
+ return await ensureValidCredentials();
220
+ });
221
+
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
+ });
230
+
231
+ // 3. Build payload via relevant prompt flow
232
+ let result;
233
+ if (entityType === 'ActiveClass') {
234
+ result = await createActiveClass();
235
+ } else if (entityType === 'ActivePage') {
236
+ result = await createActivePage();
237
+ } else {
238
+ // Unknown
239
+ throw new Error("Unknown type selected.");
240
+ }
241
+
242
+ // 4. Display or send payload to Magentrix API
243
+ let formattedData;
244
+
245
+ if (entityType === "ActivePage") {
246
+ formattedData = {
247
+ Label: result.pageName,
248
+ Name: result.pageName,
249
+ Description: result.description || "",
250
+ Type: `Active ${result.type}`,
251
+ Content: getPageTemplate(result.pageName, result.pageName)
252
+ };
253
+ } else if (entityType === "ActiveClass") {
254
+ let name = result.className;
255
+
256
+ if (result.classType === "Controller") {
257
+ if (!name.endsWith("Controller")) name += "Controller";
258
+ formattedData = {
259
+ Name: name,
260
+ Body: getControllerTemplate(name),
261
+ Description: result.description || "",
262
+ Type: "Controller"
263
+ };
264
+ } else if (result.classType === "Utility" || result.classType === "Class") {
265
+ formattedData = {
266
+ Name: name,
267
+ Body: getClassTemplate(name),
268
+ Description: result.description || "",
269
+ Type: "Class"
270
+ };
271
+ } else if (result.classType === "Trigger") {
272
+ const selectedTriggerEntity = triggerEntities.find(entity => entity.value === result.triggerEntityId);
273
+
274
+ formattedData = {
275
+ Name: name,
276
+ Body: getTriggerTemplate(name, selectedTriggerEntity.name),
277
+ Description: result.description || "",
278
+ Type: "Trigger",
279
+ EntityId: result.triggerEntityId
280
+ };
281
+ }
282
+ }
283
+
284
+ console.log();
285
+
286
+ // Uncomment to perform creation via API:
287
+ const creationResponse = await withSpinner('Creating file...', async () => {
288
+ return await createEntity(
289
+ credentials.instanceUrl,
290
+ credentials.token.value,
291
+ entityType,
292
+ formattedData
293
+ ).catch(err => {
294
+ return { ...err?.response, hasErrors: true };
295
+ });
296
+ });
297
+
298
+ if (creationResponse?.hasErrors) {
299
+ console.log();
300
+ console.log(chalk.bgRed.bold.white(' ✖ Creation Failed '));
301
+ console.log(chalk.redBright('─'.repeat(48)));
302
+ const errors = creationResponse.errors || [];
303
+
304
+ if (errors.length > 0) {
305
+ errors.forEach((err, i) => {
306
+ const code = err.code ? chalk.gray(`[${err.code}] `) : '';
307
+ const status = err.status ? chalk.yellow(`[${err.status}] `) : '';
308
+ const msg = chalk.whiteBright(err.message);
309
+ console.log(`${chalk.redBright(' •')} ${status}${code}${msg}`);
310
+ });
311
+ } else {
312
+ console.log(chalk.red('An unknown error occurred during deletion.'));
313
+ }
314
+
315
+ return;
316
+ }
317
+
318
+ console.log(`✅ File successfully created on Magentrix server.`);
319
+
320
+ try {
321
+ const filePath = await saveToFile(entityType, formattedData, creationResponse.id);
322
+ console.log(`📄 Local copy saved at: ${filePath}`);
323
+ console.log('✨ You can now edit this file locally. Don\'t forget to push changes when ready!');
324
+ } catch (err) {
325
+ console.error(`🚨 Error: Unable to save file locally (${err.message}).`);
326
+ console.error('You may need to check directory permissions or disk space.');
327
+ }
328
+
329
+ };
@@ -0,0 +1,165 @@
1
+ import chalk from "chalk";
2
+ import { ensureValidCredentials } from "../utils/cli/helpers/ensureCredentials.js";
3
+
4
+ /* ─────────────────────────────
5
+ 🎨 COLOR & STYLE SHORTCUTS
6
+ ───────────────────────────── */
7
+ const style = {
8
+ header: chalk.bold.magenta,
9
+ section: chalk.underline.bold,
10
+ label: chalk.gray,
11
+ command: chalk.cyan,
12
+ highlight: chalk.yellow,
13
+ bullet: chalk.green("●"),
14
+ dim: chalk.dim,
15
+ };
16
+
17
+ /* ─────────────────────────────
18
+ 📋 DATA SOURCES
19
+ ───────────────────────────── */
20
+
21
+ /** All CLI commands with their one-line descriptions. */
22
+ const COMMANDS = [
23
+ { command: "magentrix", description: "Show current environment and status" },
24
+ { command: "magentrix setup", description: "Configure / update URL & API key" },
25
+ { command: "magentrix pull", description: "Fetch all code into this folder" },
26
+ { command: "magentrix lint", description: "Check for stale files or conflicts" },
27
+ { command: "magentrix autopublish", description: "Automatically push local file changes safely to Magentrix" },
28
+ { command: "magentrix status", description: "Show local vs remote differences" },
29
+ { command: "magentrix create", description: "Interactive wizard for new files" },
30
+ { command: "magentrix --help", description: "Show this comprehensive help guide" },
31
+ ];
32
+
33
+ /** Key folders that appear after a successful `magentrix pull`. */
34
+ const FOLDERS = [
35
+ { path: "src/pages", note: "Active Pages (.aspx)" },
36
+ { path: "src/controllers", note: "Controller_*.cls files" },
37
+ { path: "src/classes", note: "Utility / helper .cls files" },
38
+ { path: "src/triggers", note: "Trigger_*.cls files" },
39
+ { path: ".magentrix/index.json", note: "Mapping & metadata" },
40
+ ];
41
+
42
+ /* ─────────────────────────────
43
+ 🖨️ PRINT HELPERS
44
+ ───────────────────────────── */
45
+
46
+ /**
47
+ * Renders the comprehensive help header with current environment info.
48
+ *
49
+ * @param {string} maskedKey – API key with only last 4 chars visible.
50
+ * @param {string} hostname – Magentrix instance hostname.
51
+ */
52
+ const printHelpHeader = (maskedKey, hostname) => {
53
+ console.log(style.header("\n📚 Magentrix CLI - Help & Usage Guide"));
54
+ console.log(style.dim("─────────────────────────────────────────────"));
55
+ console.log(
56
+ `${style.label("API Key:")} ${style.highlight(maskedKey)} ` +
57
+ `${style.label("Environment:")} ${style.highlight(hostname)}\n`,
58
+ );
59
+ };
60
+
61
+ /** Prints a comprehensive description of what this tool does. */
62
+ const printDescription = () => {
63
+ console.log(
64
+ style.section("What is Magentrix CLI?"),
65
+ "\n" +
66
+ chalk.cyanBright(
67
+ "Sync Active Pages & Classes between Magentrix and your editor—",
68
+ ) +
69
+ "edit locally, lint, then publish back safely.\n" +
70
+ style.dim("No built-in Git hooks—manage Git however you like.\n"),
71
+ );
72
+ };
73
+
74
+ /** Prints the 5-step recommended workflow. */
75
+ const printWorkflow = () => {
76
+ console.log(style.section("Typical Workflow:"));
77
+ const STEPS = [
78
+ { label: "Setup", cmd: "magentrix setup", detail: "Enter URL & API key (one-time)" },
79
+ { label: "Pull", cmd: "magentrix pull", detail: "Fetch code locally" },
80
+ { label: "Edit", cmd: "—", detail: "Use any code editor" },
81
+ { label: "Lint", cmd: "magentrix lint", detail: "Check for conflicts" },
82
+ { label: "Publish", cmd: "magentrix publish", detail: "Push changes safely" },
83
+ ];
84
+ STEPS.forEach(({ label, cmd, detail }) => {
85
+ console.log(
86
+ ` ${style.bullet} ${label.padEnd(7)} ${style.command(cmd.padEnd(20))} ${style.dim(detail)}`,
87
+ );
88
+ });
89
+ console.log(); // blank line
90
+ };
91
+
92
+ /** Prints the local folder overview in an uncluttered list. */
93
+ const printFolders = () => {
94
+ console.log(style.section("Local Folder Structure:"));
95
+ FOLDERS.forEach(({ path, note }) => {
96
+ console.log(` ${style.command(path.padEnd(22))} ${style.dim(note)}`);
97
+ });
98
+ console.log();
99
+ };
100
+
101
+ /** Prints the full command table. */
102
+ const printCommands = () => {
103
+ console.log(style.section("Available Commands:"));
104
+ COMMANDS.forEach(({ command, description }) => {
105
+ console.log(` ${style.command(command.padEnd(22))} ${description}`);
106
+ });
107
+ console.log();
108
+ };
109
+
110
+ /** Prints quick best-practice reminders. */
111
+ const printTips = () => {
112
+ console.log(style.section("Best Practices:"));
113
+ [
114
+ "Always pull before editing.",
115
+ "Lint before publishing.",
116
+ "Optional: enable auto-publish on save during setup.",
117
+ "Magentrix is the source of truth; Git lives beside it.",
118
+ ].forEach(tip => console.log(` ${style.bullet} ${tip}`));
119
+ console.log();
120
+ };
121
+
122
+ /** Prints additional resources and support info. */
123
+ const printFooter = () => {
124
+ console.log(
125
+ style.dim("Need more details? See the project README or documentation.\n"),
126
+ );
127
+ };
128
+
129
+ /* ─────────────────────────────
130
+ 🚀 HELP ENTRY POINT
131
+ ───────────────────────────── */
132
+
133
+ /**
134
+ * Magentrix CLI comprehensive help display.
135
+ *
136
+ * 1. Ensure credentials are configured (to show current environment).
137
+ * 2. Display detailed usage guide, commands, workflow, and best practices.
138
+ *
139
+ * @async
140
+ * @returns {Promise<void>}
141
+ */
142
+ export const showHelp = async () => {
143
+ const { apiKey, instanceUrl } = await ensureValidCredentials();
144
+
145
+ /* Mask the key except last 4 chars */
146
+ const maskedKey = `****${apiKey.slice(-4)}`;
147
+
148
+ /* Extract hostname from URL—for display only */
149
+ const hostname = (() => {
150
+ try {
151
+ return new URL(instanceUrl).hostname;
152
+ } catch {
153
+ return instanceUrl.replace(/^https?:\/\//, "").split("/")[0];
154
+ }
155
+ })();
156
+
157
+ /* Render comprehensive help sections */
158
+ printHelpHeader(maskedKey, hostname);
159
+ printDescription();
160
+ printWorkflow();
161
+ printFolders();
162
+ printCommands();
163
+ printTips();
164
+ printFooter();
165
+ };
@@ -0,0 +1,81 @@
1
+ import chalk from "chalk";
2
+ import { ensureValidCredentials } from "../utils/cli/helpers/ensureCredentials.js";
3
+ import { withSpinner } from "../utils/spinner.js";
4
+
5
+ /* ─────────────────────────────
6
+ 🎨 COLOR & STYLE SHORTCUTS
7
+ ───────────────────────────── */
8
+ const style = {
9
+ header: chalk.bold.magenta,
10
+ label: chalk.gray,
11
+ highlight: chalk.yellow,
12
+ dim: chalk.dim,
13
+ command: chalk.cyan,
14
+ };
15
+
16
+ /* ─────────────────────────────
17
+ 🖨️ PRINT HELPERS
18
+ ───────────────────────────── */
19
+
20
+ /**
21
+ * Renders a simple welcome header with current environment info.
22
+ *
23
+ * @param {string} maskedKey – API key with only last 4 chars visible.
24
+ * @param {string} hostname – Magentrix instance hostname.
25
+ */
26
+ const printWelcomeHeader = (maskedKey, hostname) => {
27
+ console.log(style.header("\n🎉 Magentrix CLI"));
28
+ console.log(style.dim("─────────────────────────────────────────────"));
29
+ console.log(
30
+ `${style.label("API Key:")} ${style.highlight(maskedKey)} ` +
31
+ `${style.label("Environment:")} ${style.highlight(hostname)}\n`,
32
+ );
33
+ };
34
+
35
+ /**
36
+ * Prints a brief status message and help prompt.
37
+ */
38
+ const printStatusMessage = () => {
39
+ console.log("✅ Connected and ready to sync with Magentrix.");
40
+ console.log(
41
+ style.dim("Run ") +
42
+ style.command("magentrix --help") +
43
+ style.dim(" to see all available commands and workflow guidance.\n"),
44
+ );
45
+ };
46
+
47
+ /* ─────────────────────────────
48
+ 🚀 MAIN ENTRY POINT
49
+ ───────────────────────────── */
50
+
51
+ /**
52
+ * Magentrix CLI main entry point.
53
+ *
54
+ * 1. Ensure valid credentials are configured.
55
+ * 2. Display current environment and connection status.
56
+ * 3. Prompt user to run help for detailed usage.
57
+ *
58
+ * @async
59
+ * @returns {Promise<void>}
60
+ */
61
+ export const main = async () => {
62
+ const { apiKey, instanceUrl } = await withSpinner('Authenticating...', async () => {
63
+ return await ensureValidCredentials();
64
+ });
65
+
66
+ /* Mask the key except last 4 chars */
67
+ const maskedKey = `****${apiKey.slice(-4)}`;
68
+
69
+ /* Extract hostname from URL—for display only */
70
+ const hostname = (() => {
71
+ try {
72
+ return new URL(instanceUrl).hostname;
73
+ } catch {
74
+ return instanceUrl.replace(/^https?:\/\//, "").split("/")[0];
75
+ }
76
+ })();
77
+
78
+ /* Show current environment and status */
79
+ printWelcomeHeader(maskedKey, hostname);
80
+ printStatusMessage();
81
+ };