@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/publish.js +75 -6
- package/actions/pull.js +423 -333
- package/actions/setup.js +62 -15
- package/package.json +1 -1
- package/utils/cli/helpers/ensureApiKey.js +28 -22
- package/utils/cli/helpers/ensureInstanceUrl.js +35 -27
- package/utils/cli/writeRecords.js +13 -2
- package/utils/config.js +76 -0
- package/utils/magentrix/api/auth.js +45 -6
- package/utils/updateFileBase.js +4 -0
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
|
-
|
|
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 (
|
|
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
|
-
//
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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,4 +1,4 @@
|
|
|
1
|
-
import
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
44
|
+
// Trim and automatically strip trailing slashes
|
|
45
|
+
const trimmed = inputUrl.trim().replace(/\/+$/, '');
|
|
47
46
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
//
|
|
57
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
|
package/utils/updateFileBase.js
CHANGED
|
@@ -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
|
+
}
|