@magentrix-corp/magentrix-cli 1.2.0 → 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.
package/actions/setup.js CHANGED
@@ -4,6 +4,7 @@ import Config from "../utils/config.js";
4
4
  import { getAccessToken, tryAuthenticate } from "../utils/magentrix/api/auth.js";
5
5
  import { ensureVSCodeFileAssociation } from "../utils/preferences.js";
6
6
  import { EXPORT_ROOT, HASHED_CWD } from "../vars/global.js";
7
+ import { select } from "@inquirer/prompts";
7
8
 
8
9
  const config = new Config();
9
10
 
@@ -35,24 +36,70 @@ export const setup = async (cliOptions = {}) => {
35
36
  }
36
37
 
37
38
  if (cliOptions.instanceUrl) {
38
- const trimmedUrl = cliOptions.instanceUrl.trim();
39
+ let trimmedUrl = cliOptions.instanceUrl.trim();
40
+ // Automatically strip trailing slashes
41
+ trimmedUrl = trimmedUrl.replace(/\/+$/, '');
42
+
39
43
  if (!urlRegex.test(trimmedUrl)) {
40
- throw new Error('--instance-url must be in the form: https://subdomain.domain.com (NO http, NO trailing /, NO extra path)');
44
+ throw new Error('--instance-url must be in the form: https://subdomain.domain.com (no http, no extra path)');
41
45
  }
46
+ // Update the option with the cleaned URL
47
+ cliOptions.instanceUrl = trimmedUrl;
42
48
  }
43
49
 
44
- // Get API key (from CLI or prompt)
45
- const apiKey = cliOptions.apiKey
46
- ? cliOptions.apiKey.trim()
47
- : await ensureApiKey(true);
48
-
49
- // Get instance URL (from CLI or prompt)
50
- const instanceUrl = cliOptions.instanceUrl
51
- ? cliOptions.instanceUrl.trim()
52
- : await ensureInstanceUrl(true);
53
-
54
- // Validate credentials by attempting to fetch an access token
55
- const tokenData = await tryAuthenticate(apiKey, instanceUrl);
50
+ // Retry loop for authentication
51
+ let authSuccess = false;
52
+ let apiKey, instanceUrl, tokenData;
53
+
54
+ while (!authSuccess) {
55
+ // Get API key (from CLI or prompt)
56
+ apiKey = cliOptions.apiKey
57
+ ? cliOptions.apiKey.trim()
58
+ : await ensureApiKey(true);
59
+
60
+ // Get instance URL (from CLI or prompt)
61
+ instanceUrl = cliOptions.instanceUrl
62
+ ? cliOptions.instanceUrl.trim()
63
+ : await ensureInstanceUrl(true);
64
+
65
+ // Validate credentials by attempting to fetch an access token
66
+ try {
67
+ tokenData = await tryAuthenticate(apiKey, instanceUrl);
68
+ authSuccess = true;
69
+ } catch (error) {
70
+ console.log(error.message);
71
+ console.log();
72
+
73
+ // Ask user if they want to retry
74
+ try {
75
+ const retry = await select({
76
+ message: 'What would you like to do?',
77
+ choices: [
78
+ { name: 'Try again', value: 'retry' },
79
+ { name: 'Exit setup', value: 'exit' }
80
+ ],
81
+ default: 'retry'
82
+ });
83
+
84
+ if (retry !== 'retry') {
85
+ console.log('❌ Setup cancelled.');
86
+ process.exit(0);
87
+ }
88
+
89
+ // Reset CLI options so user can re-enter them
90
+ cliOptions.apiKey = null;
91
+ cliOptions.instanceUrl = null;
92
+ console.log(); // Blank line for spacing
93
+ } catch (error) {
94
+ // Handle Ctrl+C or other cancellation
95
+ if (error.name === 'ExitPromptError') {
96
+ console.log('\n❌ Setup cancelled.');
97
+ process.exit(0);
98
+ }
99
+ throw error;
100
+ }
101
+ }
102
+ }
56
103
 
57
104
  console.log(); // Blank line for spacing
58
105
 
@@ -69,7 +116,7 @@ export const setup = async (cliOptions = {}) => {
69
116
  { global: true, pathHash: HASHED_CWD }
70
117
  );
71
118
 
72
- // Set up the editor
119
+ // Set up the editor
73
120
  await ensureVSCodeFileAssociation('./');
74
121
 
75
122
  console.log(); // Blank line for spacing
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@magentrix-corp/magentrix-cli",
3
- "version": "1.2.0",
3
+ "version": "1.2.1",
4
4
  "description": "CLI tool for synchronizing local files with Magentrix cloud platform",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -1,4 +1,4 @@
1
- import prompts from "prompts";
1
+ import { password } from "@inquirer/prompts";
2
2
  import Config from "../../config.js";
3
3
  import { HASHED_CWD } from "../../../vars/global.js";
4
4
 
@@ -27,27 +27,33 @@ export const ensureApiKey = async (forcePrompt = false) => {
27
27
  console.log('\n🚀 Magentrix CLI Setup:');
28
28
 
29
29
  while (true) {
30
- const response = await prompts({
31
- type: 'password',
32
- name: 'apiKey',
33
- message: 'Enter your Magentrix API key (no spaces):',
34
- validate: value =>
35
- !value || value.trim().length < 12
36
- ? "API key must be at least 12 characters."
37
- : /\s/.test(value)
38
- ? "API key cannot contain any spaces."
39
- : true,
40
- });
41
-
42
- if (!response.apiKey) {
43
- console.error('❌ API key is required. Exiting.');
44
- process.exit(1);
45
- }
46
-
47
- const trimmed = response.apiKey.trim();
48
-
49
- if (isValid(trimmed)) {
50
- return trimmed;
30
+ try {
31
+ const apiKeyInput = await password({
32
+ message: 'Enter your Magentrix API key (no spaces):',
33
+ mask: '*',
34
+ validate: (value) => {
35
+ if (!value || value.trim().length < 12) {
36
+ return "API key must be at least 12 characters.";
37
+ }
38
+ if (/\s/.test(value)) {
39
+ return "API key cannot contain any spaces.";
40
+ }
41
+ return true;
42
+ }
43
+ });
44
+
45
+ const trimmed = apiKeyInput.trim();
46
+
47
+ if (isValid(trimmed)) {
48
+ return trimmed;
49
+ }
50
+ } catch (error) {
51
+ // Handle Ctrl+C or other cancellation
52
+ if (error.name === 'ExitPromptError') {
53
+ console.error('\n❌ API key is required. Exiting.');
54
+ process.exit(1);
55
+ }
56
+ throw error;
51
57
  }
52
58
  // Invalid: will re-prompt.
53
59
  }
@@ -1,4 +1,4 @@
1
- import prompts from "prompts";
1
+ import { input } from "@inquirer/prompts";
2
2
  import Config from "../../config.js";
3
3
  import { checkInstanceUrl } from "../checkInstanceUrl.js";
4
4
  import { withSpinner } from "../../spinner.js";
@@ -27,37 +27,45 @@ export const ensureInstanceUrl = async (forcePrompt = false) => {
27
27
  console.log('\n🚀 Magentrix CLI Setup: Instance URL');
28
28
 
29
29
  while (true) {
30
- // Prompt: Only validate format, NOT reachability
31
- const { instanceUrl: inputUrl } = await prompts({
32
- type: 'text',
33
- name: 'instanceUrl',
34
- message: 'Enter your Magentrix instance URL (e.g., https://example.magentrix.com):',
35
- validate: value =>
36
- urlRegex.test(value.trim())
37
- ? true
38
- : 'Instance URL must be in the form: https://subdomain.domain.com (NO http, NO trailing /, NO extra path)',
39
- });
40
-
41
- if (!inputUrl) {
42
- console.error('❌ Instance URL is required. Exiting.');
43
- process.exit(1);
44
- }
30
+ try {
31
+ // Prompt: Only validate format, NOT reachability
32
+ const inputUrl = await input({
33
+ message: 'Enter your Magentrix instance URL (e.g., https://example.magentrix.com):',
34
+ validate: (value) => {
35
+ // Automatically strip trailing slashes for validation
36
+ const cleaned = value.trim().replace(/\/+$/, '');
37
+ if (!urlRegex.test(cleaned)) {
38
+ return 'Instance URL must be in the form: https://subdomain.domain.com (no http, no extra path)';
39
+ }
40
+ return true;
41
+ }
42
+ });
45
43
 
46
- const trimmed = inputUrl.trim();
44
+ // Trim and automatically strip trailing slashes
45
+ const trimmed = inputUrl.trim().replace(/\/+$/, '');
47
46
 
48
- // Now check reachability WITH a spinner
49
- try {
50
- await withSpinner('Checking instance URL...', () =>
51
- checkInstanceUrl(trimmed)
52
- );
53
-
54
- return trimmed;
47
+ // Now check reachability WITH a spinner
48
+ try {
49
+ await withSpinner('Checking instance URL...', () =>
50
+ checkInstanceUrl(trimmed)
51
+ );
52
+
53
+ return trimmed;
54
+ } catch (error) {
55
+ // Print error AFTER spinner, then re-prompt
56
+ console.error('❌ Instance URL not reachable. Try again.');
57
+ }
55
58
  } catch (error) {
56
- // Print error AFTER spinner, then re-prompt
57
- console.error('❌ Instance URL not reachable. Try again.');
59
+ // Handle Ctrl+C or other cancellation
60
+ if (error.name === 'ExitPromptError') {
61
+ console.error('\n❌ Instance URL is required. Exiting.');
62
+ process.exit(1);
63
+ }
64
+ throw error;
58
65
  }
59
66
  }
60
67
  }
61
68
 
62
- return instanceUrl.trim();
69
+ // Trim and automatically strip trailing slashes from existing URL
70
+ return instanceUrl.trim().replace(/\/+$/, '');
63
71
  };
@@ -39,7 +39,18 @@ export const mapRecordToFile = (record) => {
39
39
  ? path.join(mapping.directory, foundPathByRecordId.split(mapping.directory)[1] || "")
40
40
  : "";
41
41
 
42
- const filePath = foundPathByRecordId ? relativeFoundPath : path.join(mapping.directory, filename);
42
+ // Fix for Bug 1: If the file exists but the name doesn't match the record name,
43
+ // we should treat it as a rename and use the new name.
44
+ let useExistingPath = false;
45
+ if (foundPathByRecordId) {
46
+ const existingName = path.basename(foundPathByRecordId);
47
+ // Check if existing name matches the expected filename (ignoring extension case if needed, but strict for now)
48
+ if (existingName === filename) {
49
+ useExistingPath = true;
50
+ }
51
+ }
52
+
53
+ const filePath = useExistingPath ? relativeFoundPath : path.join(mapping.directory, filename);
43
54
 
44
55
  return {
45
56
  ...record,
@@ -198,7 +209,7 @@ export const writeRecords = async (records, resolutionMethod, progress = null, l
198
209
  try {
199
210
  // Lookup a matching file dir in our cache, in case the file was renamed
200
211
  const cachedFilePath = findFileByTag(record.Id) || path.resolve(filePath);
201
-
212
+
202
213
  // Ensure the directory exists (paranoia for deeply nested files)
203
214
  fs.mkdirSync(path.dirname(cachedFilePath), { recursive: true });
204
215
  fs.writeFileSync(cachedFilePath, finalContent, "utf8");
package/utils/config.js CHANGED
@@ -200,6 +200,82 @@ class Config {
200
200
  }
201
201
  }
202
202
 
203
+ /**
204
+ * Remove multiple keys from config (global or project) in a single save operation.
205
+ *
206
+ * @param {string[]} keys - Array of keys to remove.
207
+ * @param {Object} opts
208
+ * @param {boolean} opts.global - If true, use global config. Otherwise, use project config.
209
+ * @param {string} [opts.pathHash] - Optional. Hash for namespaced global config.
210
+ * @param {string} [opts.filename] - Optional. Custom filename for project config.
211
+ */
212
+ removeKeys(keys, opts = {}) {
213
+ const isGlobal = opts.global === true;
214
+ const filename = opts.filename;
215
+ const pathHash = opts.pathHash;
216
+
217
+ if (!Array.isArray(keys) || keys.length === 0) return;
218
+
219
+ if (isGlobal) {
220
+ if (this._globalConfig === null) this._globalConfig = this._loadConfig('global');
221
+
222
+ let changed = false;
223
+ if (pathHash) {
224
+ if (!this._globalConfig[pathHash]) return;
225
+ for (const key of keys) {
226
+ if (key in this._globalConfig[pathHash]) {
227
+ delete this._globalConfig[pathHash][key];
228
+ changed = true;
229
+ }
230
+ }
231
+ } else {
232
+ for (const key of keys) {
233
+ if (key in this._globalConfig) {
234
+ delete this._globalConfig[key];
235
+ changed = true;
236
+ }
237
+ }
238
+ }
239
+
240
+ if (changed) {
241
+ this._saveConfig(this._globalConfig, 'global');
242
+ }
243
+ } else {
244
+ if (filename) {
245
+ const projectFolderPath = path.join(this.projectDir, this.projectFolder);
246
+ if (!fs.existsSync(projectFolderPath)) return;
247
+ const customPath = path.join(projectFolderPath, filename);
248
+ const customConfig = this._loadConfig('project', customPath) || {};
249
+
250
+ let changed = false;
251
+ for (const key of keys) {
252
+ if (key in customConfig) {
253
+ delete customConfig[key];
254
+ changed = true;
255
+ }
256
+ }
257
+
258
+ if (changed) {
259
+ this._saveConfig(customConfig, 'project', customPath);
260
+ }
261
+ } else {
262
+ if (this._projectConfig === null) this._projectConfig = this._loadConfig('project');
263
+
264
+ let changed = false;
265
+ for (const key of keys) {
266
+ if (key in this._projectConfig) {
267
+ delete this._projectConfig[key];
268
+ changed = true;
269
+ }
270
+ }
271
+
272
+ if (changed) {
273
+ this._saveConfig(this._projectConfig, 'project');
274
+ }
275
+ }
276
+ }
277
+ }
278
+
203
279
 
204
280
  /**
205
281
  * Save a key-value pair to config (global or project).
@@ -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
 
@@ -107,3 +107,7 @@ export const updateBase = (filePath, record, actualPath = '', contentSnapshot =
107
107
  export const removeFromBase = (recordId) => {
108
108
  config.removeKey(recordId, { filename: "base.json" });
109
109
  }
110
+
111
+ export const removeFromBaseBulk = (recordIds) => {
112
+ config.removeKeys(recordIds, { filename: "base.json" });
113
+ }