@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 +99 -1
- package/actions/autopublish.v2.js +2 -2
- package/actions/create.js +180 -64
- package/actions/publish.js +348 -253
- package/actions/pull.js +53 -8
- package/actions/setup.js +34 -7
- package/bin/magentrix.js +61 -2
- package/package.json +3 -2
- package/utils/assetPaths.js +138 -0
- package/utils/cacher.js +5 -2
- package/utils/cli/helpers/compare.js +4 -2
- package/utils/downloadAssets.js +14 -8
- package/utils/magentrix/api/assets.js +21 -1
- package/utils/magentrix/api/retrieveEntity.js +55 -1
- package/utils/updateFileBase.js +8 -3
- package/vars/global.js +1 -0
package/actions/pull.js
CHANGED
|
@@ -14,6 +14,7 @@ import { getFileTag } from "../utils/filetag.js";
|
|
|
14
14
|
import { downloadAssetsZip, listAssets } from "../utils/magentrix/api/assets.js";
|
|
15
15
|
import { downloadAssets, walkAssets } from "../utils/downloadAssets.js";
|
|
16
16
|
import { v4 as uuidv4 } from 'uuid';
|
|
17
|
+
import readlineSync from 'readline-sync';
|
|
17
18
|
|
|
18
19
|
const config = new Config();
|
|
19
20
|
|
|
@@ -42,7 +43,44 @@ export const pull = async () => {
|
|
|
42
43
|
// Clear the terminal
|
|
43
44
|
process.stdout.write('\x1Bc');
|
|
44
45
|
|
|
45
|
-
// Step 2:
|
|
46
|
+
// Step 2: Check if instance URL has changed (credential switch detected)
|
|
47
|
+
const lastInstanceUrl = config.read('lastInstanceUrl', { global: false, filename: 'config.json' });
|
|
48
|
+
const instanceChanged = lastInstanceUrl && lastInstanceUrl !== instanceUrl;
|
|
49
|
+
|
|
50
|
+
if (instanceChanged) {
|
|
51
|
+
console.log(chalk.yellow.bold(`\n⚠️ INSTANCE CHANGE DETECTED`));
|
|
52
|
+
console.log(chalk.yellow(`Previous instance: ${chalk.cyan(lastInstanceUrl)}`));
|
|
53
|
+
console.log(chalk.yellow(`New instance: ${chalk.cyan(instanceUrl)}`));
|
|
54
|
+
console.log();
|
|
55
|
+
console.log(chalk.red.bold(`⚠️ WARNING: This will DELETE your existing ${chalk.white(EXPORT_ROOT + '/')} directory!`));
|
|
56
|
+
console.log(chalk.gray(`This is necessary to prevent mixing files from different instances.`));
|
|
57
|
+
console.log();
|
|
58
|
+
|
|
59
|
+
const confirm = readlineSync.question(
|
|
60
|
+
chalk.yellow(`Type ${chalk.white.bold('yes')} to continue and delete ${EXPORT_ROOT}/, or ${chalk.white.bold('no')} to cancel: `)
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
if (confirm.trim().toLowerCase() !== 'yes') {
|
|
64
|
+
console.log(chalk.red('\n❌ Pull cancelled. No files were deleted.'));
|
|
65
|
+
console.log(chalk.gray(`Tip: To pull from ${chalk.cyan(lastInstanceUrl)}, switch back to those credentials.`));
|
|
66
|
+
process.exit(0);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
console.log(chalk.yellow(`\n🗑️ Removing existing ${EXPORT_ROOT}/ directory...`));
|
|
70
|
+
if (fs.existsSync(EXPORT_ROOT)) {
|
|
71
|
+
fs.rmSync(EXPORT_ROOT, { recursive: true, force: true });
|
|
72
|
+
}
|
|
73
|
+
// Clear the base.json cache as well since it's from a different instance
|
|
74
|
+
fs.writeFileSync('.magentrix/base.json', JSON.stringify({}));
|
|
75
|
+
config.save('cachedFiles', {}, { filename: 'fileCache.json' });
|
|
76
|
+
config.save('trackedFileTags', {}, { filename: 'fileIdIndex.json' });
|
|
77
|
+
console.log(chalk.green(`✓ Removed ${EXPORT_ROOT}/ directory\n`));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Save the current instance URL for future comparisons
|
|
81
|
+
config.save('lastInstanceUrl', instanceUrl, { global: false, filename: 'config.json' });
|
|
82
|
+
|
|
83
|
+
// Step 3: Prepare queries for both ActiveClass and ActivePage
|
|
46
84
|
const queries = [
|
|
47
85
|
{
|
|
48
86
|
name: "ActiveClass",
|
|
@@ -63,7 +101,7 @@ export const pull = async () => {
|
|
|
63
101
|
);
|
|
64
102
|
|
|
65
103
|
const assetTree = await downloadAssets(instanceUrl, token.value);
|
|
66
|
-
|
|
104
|
+
|
|
67
105
|
return [
|
|
68
106
|
...meqlResults,
|
|
69
107
|
assetTree
|
|
@@ -74,13 +112,23 @@ export const pull = async () => {
|
|
|
74
112
|
const processAssets = (records) => {
|
|
75
113
|
for (const record of records) {
|
|
76
114
|
if (record?.Type === 'Folder') {
|
|
77
|
-
|
|
78
|
-
|
|
115
|
+
// Cache the folder itself
|
|
116
|
+
updateBase(
|
|
117
|
+
path.join(EXPORT_ROOT, record?.Path),
|
|
118
|
+
{
|
|
119
|
+
...record,
|
|
120
|
+
Id: path.join(EXPORT_ROOT, record?.Path)
|
|
121
|
+
}
|
|
122
|
+
);
|
|
123
|
+
// Process children if any
|
|
124
|
+
if (record?.Children?.length > 0) {
|
|
125
|
+
processAssets(record.Children);
|
|
126
|
+
}
|
|
79
127
|
continue;
|
|
80
128
|
}
|
|
81
129
|
|
|
82
130
|
updateBase(
|
|
83
|
-
path.join(EXPORT_ROOT, record?.Path),
|
|
131
|
+
path.join(EXPORT_ROOT, record?.Path),
|
|
84
132
|
{
|
|
85
133
|
...record,
|
|
86
134
|
Id: path.join(EXPORT_ROOT, record?.Path)
|
|
@@ -91,9 +139,6 @@ export const pull = async () => {
|
|
|
91
139
|
|
|
92
140
|
processAssets(assets.tree);
|
|
93
141
|
|
|
94
|
-
// Remove (clean) the export root directory
|
|
95
|
-
// fs.rmSync(EXPORT_ROOT, { recursive: true, force: true });
|
|
96
|
-
|
|
97
142
|
// Check for conflicts and have user select conflict resolution
|
|
98
143
|
const activeClassRecords = (activeClassResult.Records || []).map(record => {
|
|
99
144
|
record.Content = record.Body;
|
package/actions/setup.js
CHANGED
|
@@ -9,20 +9,47 @@ const config = new Config();
|
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Runs the global setup for the Magentrix CLI.
|
|
12
|
-
*
|
|
12
|
+
*
|
|
13
13
|
* Prompts the user for their API key and instance URL if needed,
|
|
14
14
|
* validates credentials by attempting authentication,
|
|
15
15
|
* and saves them to the global config if successful.
|
|
16
|
-
*
|
|
16
|
+
*
|
|
17
17
|
* @async
|
|
18
|
-
* @param {
|
|
18
|
+
* @param {Object} cliOptions - CLI options passed from command flags
|
|
19
|
+
* @param {string} [cliOptions.apiKey] - API key from CLI
|
|
20
|
+
* @param {string} [cliOptions.instanceUrl] - Instance URL from CLI
|
|
19
21
|
* @returns {Promise<{apiKey: string, instanceUrl: string}>} The saved API key and instance URL.
|
|
20
22
|
* @throws {Error} If authentication fails with provided credentials.
|
|
21
23
|
*/
|
|
22
|
-
export const setup = async () => {
|
|
23
|
-
//
|
|
24
|
-
const
|
|
25
|
-
|
|
24
|
+
export const setup = async (cliOptions = {}) => {
|
|
25
|
+
// Validation for CLI-provided values
|
|
26
|
+
const urlRegex = /^https:\/\/[a-zA-Z0-9-]+\.magentrix(cloud)?\.com$/;
|
|
27
|
+
|
|
28
|
+
if (cliOptions.apiKey) {
|
|
29
|
+
if (cliOptions.apiKey.trim().length < 12) {
|
|
30
|
+
throw new Error('--api-key must be at least 12 characters long');
|
|
31
|
+
}
|
|
32
|
+
if (/\s/.test(cliOptions.apiKey)) {
|
|
33
|
+
throw new Error('--api-key cannot contain spaces');
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (cliOptions.instanceUrl) {
|
|
38
|
+
const trimmedUrl = cliOptions.instanceUrl.trim();
|
|
39
|
+
if (!urlRegex.test(trimmedUrl)) {
|
|
40
|
+
throw new Error('--instance-url must be in the form: https://subdomain.magentrixcloud.com (NO http, NO trailing /, NO extra path)');
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
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);
|
|
26
53
|
|
|
27
54
|
// Validate credentials by attempting to fetch an access token
|
|
28
55
|
const tokenData = await tryAuthenticate(apiKey, instanceUrl);
|
package/bin/magentrix.js
CHANGED
|
@@ -113,9 +113,68 @@ program.configureOutput({
|
|
|
113
113
|
});
|
|
114
114
|
|
|
115
115
|
// ── Commands ─────────────────────────────────
|
|
116
|
-
program
|
|
116
|
+
program
|
|
117
|
+
.command('setup')
|
|
118
|
+
.description('Configure your Magentrix API key')
|
|
119
|
+
.option('--api-key <apiKey>', 'Magentrix API key')
|
|
120
|
+
.option('--instance-url <instanceUrl>', 'Magentrix instance URL (e.g., https://example.magentrixcloud.com)')
|
|
121
|
+
.action(withDefault(setup));
|
|
117
122
|
program.command('pull').description('Pull files from the remote server').action(withDefault(pull));
|
|
118
|
-
|
|
123
|
+
const createCommand = program
|
|
124
|
+
.command('create')
|
|
125
|
+
.description('Create files locally')
|
|
126
|
+
.option('--type <type>', 'Entity type: class, page, or template')
|
|
127
|
+
.option('--class-type <classType>', 'Class type: controller, utility, or trigger (for --type class)')
|
|
128
|
+
.option('--name <name>', 'Name of the file to create')
|
|
129
|
+
.option('--description <description>', 'Optional description')
|
|
130
|
+
.option('--entity-id <entityId>', 'Entity ID (required for triggers)')
|
|
131
|
+
.action(withDefault(create));
|
|
132
|
+
|
|
133
|
+
// Override help for create command to show options
|
|
134
|
+
createCommand.configureHelp({
|
|
135
|
+
formatHelp: () => {
|
|
136
|
+
const divider = chalk.gray('━'.repeat(60));
|
|
137
|
+
const titleBar = chalk.bold.bgBlue.white(' Magentrix CLI - Create Command ');
|
|
138
|
+
|
|
139
|
+
let help = `\n${divider}\n${titleBar}\n${divider}\n\n`;
|
|
140
|
+
help += `${chalk.dim('Create files locally with optional parameters to bypass interactive prompts')}\n\n`;
|
|
141
|
+
|
|
142
|
+
help += `${chalk.bold.yellow('USAGE')}\n`;
|
|
143
|
+
help += ` ${chalk.cyan('magentrix create')} ${chalk.dim('[options]')}\n\n`;
|
|
144
|
+
|
|
145
|
+
help += `${chalk.bold.yellow('OPTIONS')}\n`;
|
|
146
|
+
const options = [
|
|
147
|
+
{ name: '--type <type>', desc: 'Entity type: class, page, or template' },
|
|
148
|
+
{ name: '--class-type <classType>', desc: 'Class type: controller, utility, or trigger (for --type class)' },
|
|
149
|
+
{ name: '--name <name>', desc: 'Name of the file to create' },
|
|
150
|
+
{ name: '--description <description>', desc: 'Optional description' },
|
|
151
|
+
{ name: '--entity-id <entityId>', desc: 'Entity ID (required for triggers)' },
|
|
152
|
+
{ name: '-h, --help', desc: 'Display this help message' }
|
|
153
|
+
];
|
|
154
|
+
|
|
155
|
+
const maxNameLen = Math.max(...options.map(o => o.name.length));
|
|
156
|
+
options.forEach(opt => {
|
|
157
|
+
const padding = ' '.repeat(maxNameLen - opt.name.length);
|
|
158
|
+
help += ` ${chalk.cyan(opt.name)}${padding} ${chalk.dim(opt.desc)}\n`;
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
help += `\n${chalk.bold.yellow('EXAMPLES')}\n`;
|
|
162
|
+
help += ` ${chalk.dim('# Create a controller non-interactively')}\n`;
|
|
163
|
+
help += ` ${chalk.cyan('magentrix create --type class --class-type controller --name UserController')}\n\n`;
|
|
164
|
+
help += ` ${chalk.dim('# Create a trigger')}\n`;
|
|
165
|
+
help += ` ${chalk.cyan('magentrix create --type class --class-type trigger --entity-id abc123 --name AccountTrigger')}\n\n`;
|
|
166
|
+
help += ` ${chalk.dim('# Create a page')}\n`;
|
|
167
|
+
help += ` ${chalk.cyan('magentrix create --type page --name HomePage --description "Main landing page"')}\n\n`;
|
|
168
|
+
help += ` ${chalk.dim('# Create a template')}\n`;
|
|
169
|
+
help += ` ${chalk.cyan('magentrix create --type template --name EmailTemplate')}\n\n`;
|
|
170
|
+
help += ` ${chalk.dim('# Mix interactive and non-interactive (will prompt for missing info)')}\n`;
|
|
171
|
+
help += ` ${chalk.cyan('magentrix create --name MyClass')}\n`;
|
|
172
|
+
|
|
173
|
+
help += `\n${divider}\n`;
|
|
174
|
+
|
|
175
|
+
return help;
|
|
176
|
+
}
|
|
177
|
+
});
|
|
119
178
|
program.command('status').description('Show file conflicts').action(withDefault(status));
|
|
120
179
|
program.command('autopublish').description('Watch & sync changes in real time').action(withDefault(autoPublish));
|
|
121
180
|
program.command('publish').description('Publish pending changes to the remote server').action(withDefault(publish));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@magentrix-corp/magentrix-cli",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "CLI tool for synchronizing local files with Magentrix cloud platform",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"magentrix": "./bin/magentrix.js"
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
|
-
"test": "
|
|
11
|
+
"test": "node tests/all.js"
|
|
12
12
|
},
|
|
13
13
|
"keywords": [
|
|
14
14
|
"magentrix",
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
"chokidar": "^4.0.3",
|
|
41
41
|
"commander": "^14.0.0",
|
|
42
42
|
"diff": "^8.0.2",
|
|
43
|
+
"dotenv": "^17.2.3",
|
|
43
44
|
"extract-zip": "^2.0.1",
|
|
44
45
|
"fuzzy": "^0.1.3",
|
|
45
46
|
"inquirer": "^12.7.0",
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { EXPORT_ROOT } from '../vars/global.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Asset path utilities for handling the difference between local and API paths.
|
|
6
|
+
*
|
|
7
|
+
* Local structure: src/Assets/...
|
|
8
|
+
* API structure: /contents/assets/...
|
|
9
|
+
*
|
|
10
|
+
* These helpers abstract away the "Contents" prefix requirement for the Magentrix API.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Convert a local asset path to an API path by adding the 'contents' prefix
|
|
15
|
+
* and normalizing to lowercase with forward slashes.
|
|
16
|
+
*
|
|
17
|
+
* @param {string} localPath - Local file path (e.g., "src/Assets/images/logo.png")
|
|
18
|
+
* @returns {string} API path (e.g., "/contents/assets/images")
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* toApiPath("src/Assets/images/logo.png") // "/contents/assets/images"
|
|
22
|
+
* toApiPath("src/Assets") // "/contents/assets"
|
|
23
|
+
*/
|
|
24
|
+
export const toApiPath = (localPath) => {
|
|
25
|
+
// Handle undefined or empty paths
|
|
26
|
+
if (!localPath) {
|
|
27
|
+
return '/contents/assets';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Normalize the path and remove the EXPORT_ROOT
|
|
31
|
+
const normalized = path.normalize(localPath);
|
|
32
|
+
const relative = normalized.replace(new RegExp(`^${EXPORT_ROOT}[\\\\/]?`), '');
|
|
33
|
+
|
|
34
|
+
// Replace 'Assets' with 'contents/assets'
|
|
35
|
+
const apiPath = relative.replace(/^Assets/i, 'contents/assets');
|
|
36
|
+
|
|
37
|
+
// Normalize to forward slashes and lowercase, remove filename
|
|
38
|
+
let dirPath = path.dirname(apiPath).replace(/\\/g, '/').toLowerCase();
|
|
39
|
+
|
|
40
|
+
// Handle edge case where dirname returns '.' for root
|
|
41
|
+
if (dirPath === '.') {
|
|
42
|
+
dirPath = 'contents/assets';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Ensure it starts with /
|
|
46
|
+
return dirPath.startsWith('/') ? dirPath : `/${dirPath}`;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Convert an API path to a local path by removing the 'contents' prefix.
|
|
51
|
+
* Returns a relative path (without EXPORT_ROOT) that can be joined with EXPORT_ROOT.
|
|
52
|
+
*
|
|
53
|
+
* @param {string} apiPath - API path (e.g., "/contents/assets/images/logo.png")
|
|
54
|
+
* @returns {string} Relative local path (e.g., "Assets/images/logo.png")
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* toLocalPath("/contents/assets/images") // "Assets/images"
|
|
58
|
+
* toLocalPath("/contents/assets") // "Assets"
|
|
59
|
+
*/
|
|
60
|
+
export const toLocalPath = (apiPath) => {
|
|
61
|
+
// Remove leading slash and 'contents/' prefix
|
|
62
|
+
const cleaned = apiPath.replace(/^\/+/, '').replace(/^contents\//i, '');
|
|
63
|
+
|
|
64
|
+
// Capitalize 'assets' to 'Assets'
|
|
65
|
+
const withCapitalAssets = cleaned.replace(/^assets/i, 'Assets');
|
|
66
|
+
|
|
67
|
+
return withCapitalAssets;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Convert a local folder path to an API path (keeps the folder, doesn't extract parent).
|
|
72
|
+
* Similar to toApiPath but for folders.
|
|
73
|
+
*
|
|
74
|
+
* @param {string} localFolderPath - Local folder path (e.g., "src/Assets/images")
|
|
75
|
+
* @returns {string} API path (e.g., "/contents/assets/images")
|
|
76
|
+
*
|
|
77
|
+
* @example
|
|
78
|
+
* toApiFolderPath("src/Assets/images") // "/contents/assets/images"
|
|
79
|
+
* toApiFolderPath("src/Assets") // "/contents/assets"
|
|
80
|
+
*/
|
|
81
|
+
export const toApiFolderPath = (localFolderPath) => {
|
|
82
|
+
// Handle undefined or empty paths
|
|
83
|
+
if (!localFolderPath) {
|
|
84
|
+
return '/contents/assets';
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Normalize the path and remove the EXPORT_ROOT
|
|
88
|
+
const normalized = path.normalize(localFolderPath);
|
|
89
|
+
const relative = normalized.replace(new RegExp(`^${EXPORT_ROOT}[\\\\/]?`), '');
|
|
90
|
+
|
|
91
|
+
// Replace 'Assets' with 'contents/assets'
|
|
92
|
+
let apiPath = relative.replace(/^Assets/i, 'contents/assets');
|
|
93
|
+
|
|
94
|
+
// Normalize to forward slashes and lowercase (but don't get dirname!)
|
|
95
|
+
apiPath = apiPath.replace(/\\/g, '/').toLowerCase();
|
|
96
|
+
|
|
97
|
+
// Handle edge case where path is just 'Assets'
|
|
98
|
+
if (apiPath === 'contents/assets' || apiPath === '') {
|
|
99
|
+
return '/contents/assets';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Ensure it starts with /
|
|
103
|
+
return apiPath.startsWith('/') ? apiPath : `/${apiPath}`;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Check if a local path is within the Assets directory.
|
|
108
|
+
*
|
|
109
|
+
* @param {string} localPath - Local file path
|
|
110
|
+
* @returns {boolean} True if path is within Assets directory
|
|
111
|
+
*
|
|
112
|
+
* @example
|
|
113
|
+
* isAssetPath("src/Assets/images/logo.png") // true
|
|
114
|
+
* isAssetPath("src/Classes/MyClass.ac") // false
|
|
115
|
+
*/
|
|
116
|
+
export const isAssetPath = (localPath) => {
|
|
117
|
+
const normalized = path.normalize(localPath);
|
|
118
|
+
const assetsDir = path.join(EXPORT_ROOT, 'Assets');
|
|
119
|
+
return normalized.startsWith(assetsDir);
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Extract the folder path from a full file path (removes filename).
|
|
124
|
+
* Returns path relative to Assets root.
|
|
125
|
+
*
|
|
126
|
+
* @param {string} filePath - Full file path
|
|
127
|
+
* @returns {string} Folder path relative to EXPORT_ROOT
|
|
128
|
+
*
|
|
129
|
+
* @example
|
|
130
|
+
* getAssetFolder("src/Assets/images/logo.png") // "Assets/images"
|
|
131
|
+
* getAssetFolder("src/Assets/logo.png") // "Assets"
|
|
132
|
+
*/
|
|
133
|
+
export const getAssetFolder = (filePath) => {
|
|
134
|
+
const normalized = path.normalize(filePath);
|
|
135
|
+
const relative = normalized.replace(new RegExp(`^${EXPORT_ROOT}[\\\\/]?`), '');
|
|
136
|
+
const dirPath = path.dirname(relative);
|
|
137
|
+
return dirPath === '.' ? 'Assets' : dirPath;
|
|
138
|
+
};
|
package/utils/cacher.js
CHANGED
|
@@ -91,7 +91,7 @@ export const recacheFileIdIndex = async (dir) => {
|
|
|
91
91
|
export async function walkFiles(dir, settings) {
|
|
92
92
|
const ignore = settings?.ignore || [];
|
|
93
93
|
|
|
94
|
-
if (!fs.existsSync(dir)) return;
|
|
94
|
+
if (!fs.existsSync(dir)) return [];
|
|
95
95
|
let entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
96
96
|
let paths = [];
|
|
97
97
|
|
|
@@ -102,7 +102,10 @@ export async function walkFiles(dir, settings) {
|
|
|
102
102
|
|
|
103
103
|
const fullPath = path.join(dir, entry.name);
|
|
104
104
|
if (entry.isDirectory()) {
|
|
105
|
-
|
|
105
|
+
const subPaths = await walkFiles(fullPath, settings);
|
|
106
|
+
if (subPaths && subPaths.length > 0) {
|
|
107
|
+
paths.push(...subPaths);
|
|
108
|
+
}
|
|
106
109
|
} else if (entry.isFile()) {
|
|
107
110
|
paths.push(fullPath);
|
|
108
111
|
}
|
|
@@ -243,8 +243,10 @@ export async function showCurrentConflicts(rootDir, instanceUrl, token, forceCon
|
|
|
243
243
|
export async function promptConflictResolution(fileIssues) {
|
|
244
244
|
if (!fileIssues.length) return 'skip';
|
|
245
245
|
|
|
246
|
-
// Clear for better UX
|
|
247
|
-
|
|
246
|
+
// Clear for better UX (skip in test mode to avoid clearing test output)
|
|
247
|
+
if (!process.env.MAGENTRIX_TEST_MODE) {
|
|
248
|
+
console.clear();
|
|
249
|
+
}
|
|
248
250
|
console.log(
|
|
249
251
|
chalk.bold.yellow(
|
|
250
252
|
`\n${fileIssues.length} file${fileIssues.length > 1 ? 's' : ''} require conflict resolution:\n`
|
package/utils/downloadAssets.js
CHANGED
|
@@ -5,6 +5,7 @@ import extract from 'extract-zip';
|
|
|
5
5
|
import { v4 as uuidv4 } from 'uuid';
|
|
6
6
|
import fspath from 'path';
|
|
7
7
|
import { setFileTag } from "./filetag.js";
|
|
8
|
+
import { toLocalPath } from "./assetPaths.js";
|
|
8
9
|
|
|
9
10
|
export const walkAssets = async (instanceUrl, token, assetPath) => {
|
|
10
11
|
const assetResults = await listAssets(instanceUrl, token, assetPath);
|
|
@@ -14,15 +15,19 @@ export const walkAssets = async (instanceUrl, token, assetPath) => {
|
|
|
14
15
|
if (asset.Type === 'Folder') {
|
|
15
16
|
walkedAssets.push({
|
|
16
17
|
...asset,
|
|
17
|
-
|
|
18
|
+
ApiPath: asset.Path, // Keep original API path
|
|
19
|
+
Path: toLocalPath(asset.Path), // Convert for local file system
|
|
18
20
|
Children: await walkAssets(instanceUrl, token, asset.Path),
|
|
19
|
-
ParentFolder: assetResults.CurrentPath
|
|
21
|
+
ParentFolder: toLocalPath(assetResults.CurrentPath),
|
|
22
|
+
ParentApiPath: assetResults.CurrentPath, // Keep API path for downloads
|
|
20
23
|
})
|
|
21
24
|
} else {
|
|
22
25
|
walkedAssets.push({
|
|
23
26
|
...asset,
|
|
24
|
-
|
|
25
|
-
|
|
27
|
+
ApiPath: asset.Path, // Keep original API path
|
|
28
|
+
Path: toLocalPath(asset.Path), // Convert for local file system
|
|
29
|
+
ParentFolder: toLocalPath(assetResults.CurrentPath),
|
|
30
|
+
ParentApiPath: assetResults.CurrentPath, // Keep API path for downloads
|
|
26
31
|
});
|
|
27
32
|
}
|
|
28
33
|
}
|
|
@@ -34,7 +39,8 @@ export const downloadAssets = async (instanceUrl, token, path) => {
|
|
|
34
39
|
const allAssets = await walkAssets(instanceUrl, token, path);
|
|
35
40
|
|
|
36
41
|
const iterateDownload = async (assets) => {
|
|
37
|
-
const
|
|
42
|
+
const parentApiPath = assets?.[0]?.ParentApiPath; // Use API path for API calls
|
|
43
|
+
const parentLocalFolder = assets?.[0]?.ParentFolder; // Use local path for file system
|
|
38
44
|
const folders = assets.filter(asset => asset.Type === 'Folder');
|
|
39
45
|
const files = assets.filter(asset => asset.Type === 'File');
|
|
40
46
|
|
|
@@ -47,13 +53,13 @@ export const downloadAssets = async (instanceUrl, token, path) => {
|
|
|
47
53
|
const savedAs = await downloadAssetsZip({
|
|
48
54
|
baseUrl: instanceUrl,
|
|
49
55
|
token: token, // "Bearer" prefix added in code
|
|
50
|
-
path:
|
|
56
|
+
path: parentApiPath, // Use API path for API call
|
|
51
57
|
names: files.map(file => file.Name),
|
|
52
|
-
outFile: fspath.join(EXPORT_ROOT,
|
|
58
|
+
outFile: fspath.join(EXPORT_ROOT, parentLocalFolder, 'assets.zip'), // Use local path for file system
|
|
53
59
|
});
|
|
54
60
|
|
|
55
61
|
await extract(savedAs, {
|
|
56
|
-
dir: fspath.resolve(fspath.join(EXPORT_ROOT,
|
|
62
|
+
dir: fspath.resolve(fspath.join(EXPORT_ROOT, parentLocalFolder)) // Use local path for extraction
|
|
57
63
|
});
|
|
58
64
|
|
|
59
65
|
// for (const file of files) {
|
|
@@ -80,7 +80,7 @@ export const deleteAsset = async (instanceUrl, token, path = '/contents/assets',
|
|
|
80
80
|
if (!path) throw new Error("Path is required when deleting assets.");
|
|
81
81
|
if (!Array.isArray(names) || names?.length === 0) throw new Error("At least one file name is required when deleting static assets.");
|
|
82
82
|
|
|
83
|
-
let reqPath = `/api/3.0/staticassets?path=${path}&names=${names.join(",")}`;
|
|
83
|
+
let reqPath = `/api/3.0/staticassets?path=${encodeURIComponent(path)}&names=${names.join(",")}`;
|
|
84
84
|
|
|
85
85
|
const response = await fetchMagentrix({
|
|
86
86
|
instanceUrl,
|
|
@@ -93,6 +93,26 @@ export const deleteAsset = async (instanceUrl, token, path = '/contents/assets',
|
|
|
93
93
|
return response;
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
export const createFolder = async (instanceUrl, token, path = '/contents/assets', name) => {
|
|
97
|
+
if (!instanceUrl || !token) {
|
|
98
|
+
throw new Error('Missing required Magentrix instanceUrl or token');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (!name) throw new Error("Folder name is required when creating a folder.");
|
|
102
|
+
|
|
103
|
+
let reqPath = `/api/3.0/staticassets/folder?path=${encodeURIComponent(path)}&name=${encodeURIComponent(name)}`;
|
|
104
|
+
|
|
105
|
+
const response = await fetchMagentrix({
|
|
106
|
+
instanceUrl,
|
|
107
|
+
token,
|
|
108
|
+
path: reqPath,
|
|
109
|
+
method: "POST",
|
|
110
|
+
returnErrorObject: true
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
return response;
|
|
114
|
+
}
|
|
115
|
+
|
|
96
116
|
/**
|
|
97
117
|
* Download multiple static assets as a ZIP.
|
|
98
118
|
* @param {object} opts
|
|
@@ -3,7 +3,7 @@ import { fetchMagentrix } from "../fetch.js";
|
|
|
3
3
|
/**
|
|
4
4
|
* Lists all entities available from the Magentrix API.
|
|
5
5
|
*
|
|
6
|
-
* Makes an authenticated GET request to `/api/3.0/entity` to retrieve metadata
|
|
6
|
+
* Makes an authenticated GET request to `/api/3.0/entity` to retrieve metadata
|
|
7
7
|
* about all available entities (objects) in the Magentrix instance.
|
|
8
8
|
*
|
|
9
9
|
* Handles and throws both network errors and API-level errors with detailed messages.
|
|
@@ -30,3 +30,57 @@ export const listEntities = async (instanceUrl, token) => {
|
|
|
30
30
|
|
|
31
31
|
return data;
|
|
32
32
|
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Retrieves a specific ActiveClass or ActivePage entity by ID from Magentrix via the REST API.
|
|
36
|
+
*
|
|
37
|
+
* @async
|
|
38
|
+
* @function retrieveEntity
|
|
39
|
+
* @param {string} instanceUrl - The base URL of the Magentrix instance (e.g. "https://your.magentrix.com").
|
|
40
|
+
* @param {string} token - The OAuth2 bearer token for authentication.
|
|
41
|
+
* @param {string} entityName - The Magentrix entity type. Allowed: "ActiveClass" or "ActivePage" (case-insensitive).
|
|
42
|
+
* @param {string} recordId - The unique Magentrix record ID to retrieve.
|
|
43
|
+
* @returns {Promise<Object>} The API response object containing the record data.
|
|
44
|
+
* @throws {Error} If required parameters are missing, entityName is invalid, or recordId is not provided.
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* const record = await retrieveEntity(
|
|
48
|
+
* "https://your.magentrix.com",
|
|
49
|
+
* "yourToken",
|
|
50
|
+
* "ActiveClass",
|
|
51
|
+
* "06bdc45e-8222-44f5-9ed2-40f5a7bc6cb3"
|
|
52
|
+
* );
|
|
53
|
+
*/
|
|
54
|
+
export const retrieveEntity = async (instanceUrl, token, entityName, recordId) => {
|
|
55
|
+
// --- Validate required parameters ---
|
|
56
|
+
if (!instanceUrl || typeof instanceUrl !== 'string') {
|
|
57
|
+
throw new Error('Missing or invalid Magentrix instanceUrl');
|
|
58
|
+
}
|
|
59
|
+
if (!token || typeof token !== 'string') {
|
|
60
|
+
throw new Error('Missing or invalid Magentrix token');
|
|
61
|
+
}
|
|
62
|
+
if (!entityName || typeof entityName !== 'string') {
|
|
63
|
+
throw new Error("Missing or invalid 'entityName' (must be 'ActiveClass' or 'ActivePage')");
|
|
64
|
+
}
|
|
65
|
+
if (!recordId || typeof recordId !== 'string') {
|
|
66
|
+
throw new Error("Missing or invalid 'recordId' (must be a Magentrix record GUID string)");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// --- Validate entity type ---
|
|
70
|
+
const allowedEntities = ['activeclass', 'activepage'];
|
|
71
|
+
const entity = entityName.trim().toLowerCase();
|
|
72
|
+
if (!allowedEntities.includes(entity)) {
|
|
73
|
+
throw new Error("Invalid 'entityName'. Allowed: 'ActiveClass' or 'ActivePage'");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// --- Make GET request to Magentrix API ---
|
|
77
|
+
const response = await fetchMagentrix({
|
|
78
|
+
instanceUrl,
|
|
79
|
+
token,
|
|
80
|
+
path: `/api/3.0/entity/${entity}/${recordId}`,
|
|
81
|
+
method: "GET",
|
|
82
|
+
returnErrorObject: true
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
return response;
|
|
86
|
+
};
|
package/utils/updateFileBase.js
CHANGED
|
@@ -59,10 +59,15 @@ export const updateBase = (filePath, record, actualPath = '', contentSnapshot =
|
|
|
59
59
|
|
|
60
60
|
// Get file stats for mtime
|
|
61
61
|
const fileStats = fs.statSync(fileSystemLocation);
|
|
62
|
+
const isDirectory = fileStats.isDirectory();
|
|
62
63
|
|
|
63
64
|
// Use snapshot if provided (to avoid race conditions), otherwise read from disk
|
|
64
65
|
let fileContent, contentHash;
|
|
65
|
-
if (
|
|
66
|
+
if (isDirectory) {
|
|
67
|
+
// Folders don't have content
|
|
68
|
+
fileContent = '';
|
|
69
|
+
contentHash = '';
|
|
70
|
+
} else if (contentSnapshot && contentSnapshot.content) {
|
|
66
71
|
// Use the snapshot of what was actually published
|
|
67
72
|
fileContent = contentSnapshot.content;
|
|
68
73
|
contentHash = contentSnapshot.hash;
|
|
@@ -79,7 +84,7 @@ export const updateBase = (filePath, record, actualPath = '', contentSnapshot =
|
|
|
79
84
|
const saveData = {
|
|
80
85
|
lastModified: fileStats.mtimeMs,
|
|
81
86
|
contentHash,
|
|
82
|
-
compressedContent: compressString(fileContent),
|
|
87
|
+
compressedContent: isDirectory ? '' : compressString(fileContent),
|
|
83
88
|
recordId: record.Id,
|
|
84
89
|
type: record.Type,
|
|
85
90
|
filePath,
|
|
@@ -87,7 +92,7 @@ export const updateBase = (filePath, record, actualPath = '', contentSnapshot =
|
|
|
87
92
|
lastKnownPath: path.resolve(filePath)
|
|
88
93
|
}
|
|
89
94
|
|
|
90
|
-
if (saveData.type === 'File') delete saveData.compressedContent;
|
|
95
|
+
if (saveData.type === 'File' || saveData.type === 'Folder') delete saveData.compressedContent;
|
|
91
96
|
|
|
92
97
|
config.save(
|
|
93
98
|
record.Id,
|
package/vars/global.js
CHANGED
|
@@ -3,6 +3,7 @@ import { sha256 } from '../utils/hash.js'; // Or wherever your hash function liv
|
|
|
3
3
|
export const CWD = process.cwd();
|
|
4
4
|
export const HASHED_CWD = sha256(CWD);
|
|
5
5
|
export const EXPORT_ROOT = "src";
|
|
6
|
+
export const ASSETS_DIR = "Assets"; // Local directory name for static assets (API uses /contents/assets)
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Maps Magentrix Type fields to local folder names and extensions.
|