@magentrix-corp/magentrix-cli 1.1.5 → 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/README.md +156 -8
- package/actions/config.js +182 -0
- package/actions/create.js +30 -9
- package/actions/publish.js +509 -82
- package/actions/pull.js +494 -199
- package/actions/setup.js +63 -16
- package/actions/update.js +248 -0
- package/bin/magentrix.js +13 -1
- package/package.json +1 -1
- package/utils/assetPaths.js +24 -4
- package/utils/cacher.js +122 -53
- package/utils/cli/helpers/ensureApiKey.js +28 -22
- package/utils/cli/helpers/ensureInstanceUrl.js +36 -28
- package/utils/cli/writeRecords.js +47 -8
- package/utils/config.js +76 -0
- package/utils/diagnostics/testPublishLogic.js +96 -0
- package/utils/downloadAssets.js +230 -19
- package/utils/logger.js +283 -0
- package/utils/magentrix/api/assets.js +65 -10
- package/utils/magentrix/api/auth.js +45 -6
- package/utils/progress.js +383 -0
- package/utils/updateFileBase.js +6 -1
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
|
|
|
@@ -23,7 +24,7 @@ const config = new Config();
|
|
|
23
24
|
*/
|
|
24
25
|
export const setup = async (cliOptions = {}) => {
|
|
25
26
|
// Validation for CLI-provided values
|
|
26
|
-
const urlRegex = /^https:\/\/[a-zA-Z0-9-]
|
|
27
|
+
const urlRegex = /^https:\/\/[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
|
|
27
28
|
|
|
28
29
|
if (cliOptions.apiKey) {
|
|
29
30
|
if (cliOptions.apiKey.trim().length < 12) {
|
|
@@ -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.
|
|
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
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { execSync } from 'child_process';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
import { withSpinner } from '../utils/spinner.js';
|
|
7
|
+
|
|
8
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
9
|
+
const __dirname = path.dirname(__filename);
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Detects how the CLI was installed (global npm, local npm, or development).
|
|
13
|
+
* @returns {Object} { method: 'global'|'local'|'dev', packagePath: string }
|
|
14
|
+
*/
|
|
15
|
+
const detectInstallationMethod = () => {
|
|
16
|
+
try {
|
|
17
|
+
// Get the real path of the binary (resolves symlinks)
|
|
18
|
+
const binaryPath = path.resolve(__dirname, '../bin/magentrix.js');
|
|
19
|
+
const realBinaryPath = fs.realpathSync(binaryPath);
|
|
20
|
+
|
|
21
|
+
// Check if running from node_modules
|
|
22
|
+
const isInNodeModules = realBinaryPath.includes('node_modules');
|
|
23
|
+
|
|
24
|
+
if (!isInNodeModules) {
|
|
25
|
+
// Running from source (development)
|
|
26
|
+
return { method: 'dev', packagePath: path.resolve(__dirname, '..') };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Check if it's a global installation by looking at the path structure
|
|
30
|
+
// Global npm packages on Unix: /usr/local/lib/node_modules or ~/.npm-global/lib/node_modules
|
|
31
|
+
// Global npm packages on Windows: %APPDATA%\npm\node_modules
|
|
32
|
+
const isGlobal = realBinaryPath.includes('/lib/node_modules/') ||
|
|
33
|
+
realBinaryPath.includes('\\npm\\node_modules\\') ||
|
|
34
|
+
realBinaryPath.includes('/.npm-global/') ||
|
|
35
|
+
realBinaryPath.includes('\\.npm-global\\');
|
|
36
|
+
|
|
37
|
+
if (isGlobal) {
|
|
38
|
+
return { method: 'global', packagePath: null };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Local installation - find the nearest package.json in node_modules
|
|
42
|
+
let currentPath = realBinaryPath;
|
|
43
|
+
while (currentPath !== path.dirname(currentPath)) {
|
|
44
|
+
if (path.basename(currentPath) === 'node_modules') {
|
|
45
|
+
return { method: 'local', packagePath: path.dirname(currentPath) };
|
|
46
|
+
}
|
|
47
|
+
currentPath = path.dirname(currentPath);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return { method: 'local', packagePath: process.cwd() };
|
|
51
|
+
} catch (error) {
|
|
52
|
+
console.warn(chalk.yellow(`Warning: Could not detect installation method: ${error.message}`));
|
|
53
|
+
return { method: 'unknown', packagePath: null };
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Gets the current installed version from package.json.
|
|
59
|
+
* @returns {string} Version string
|
|
60
|
+
*/
|
|
61
|
+
const getCurrentVersion = () => {
|
|
62
|
+
try {
|
|
63
|
+
const packageJsonPath = path.resolve(__dirname, '../package.json');
|
|
64
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
65
|
+
return packageJson.version;
|
|
66
|
+
} catch (error) {
|
|
67
|
+
return 'unknown';
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Gets the latest available version from npm registry.
|
|
73
|
+
* @param {string} packageName - NPM package name
|
|
74
|
+
* @returns {Promise<string>} Latest version string
|
|
75
|
+
*/
|
|
76
|
+
const getLatestVersion = async (packageName) => {
|
|
77
|
+
try {
|
|
78
|
+
const result = execSync(`npm view ${packageName} version`, { encoding: 'utf-8' });
|
|
79
|
+
return result.trim();
|
|
80
|
+
} catch (error) {
|
|
81
|
+
throw new Error(`Failed to check latest version: ${error.message}`);
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Attempts to update the package based on installation method.
|
|
87
|
+
* @param {string} method - Installation method ('global', 'local', or 'dev')
|
|
88
|
+
* @param {string} packagePath - Path to package directory (for local installs)
|
|
89
|
+
* @returns {Promise<Object>} { success: boolean, message: string }
|
|
90
|
+
*/
|
|
91
|
+
const performUpdate = async (method, packagePath) => {
|
|
92
|
+
const packageName = '@magentrix-corp/magentrix-cli';
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
let command;
|
|
96
|
+
let cwd = process.cwd();
|
|
97
|
+
|
|
98
|
+
switch (method) {
|
|
99
|
+
case 'global':
|
|
100
|
+
command = `npm install -g ${packageName}@latest`;
|
|
101
|
+
break;
|
|
102
|
+
|
|
103
|
+
case 'local':
|
|
104
|
+
command = `npm install ${packageName}@latest`;
|
|
105
|
+
cwd = packagePath || process.cwd();
|
|
106
|
+
break;
|
|
107
|
+
|
|
108
|
+
case 'dev':
|
|
109
|
+
return {
|
|
110
|
+
success: false,
|
|
111
|
+
isDev: true,
|
|
112
|
+
message: 'Cannot auto-update: Running from source code (development mode)'
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
default:
|
|
116
|
+
return {
|
|
117
|
+
success: false,
|
|
118
|
+
message: 'Cannot auto-update: Unable to detect installation method'
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Execute the update command
|
|
123
|
+
execSync(command, {
|
|
124
|
+
cwd,
|
|
125
|
+
stdio: 'inherit', // Show npm output to user
|
|
126
|
+
encoding: 'utf-8'
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
return { success: true, message: 'Update completed successfully!' };
|
|
130
|
+
} catch (error) {
|
|
131
|
+
// Parse the error to provide helpful messages
|
|
132
|
+
const errorMessage = error.message || String(error);
|
|
133
|
+
|
|
134
|
+
// Check for common error patterns
|
|
135
|
+
if (errorMessage.includes('EACCES') || errorMessage.includes('permission denied')) {
|
|
136
|
+
return {
|
|
137
|
+
success: false,
|
|
138
|
+
needsSudo: true,
|
|
139
|
+
message: 'Update failed: Permission denied'
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (errorMessage.includes('ENOTFOUND') || errorMessage.includes('network')) {
|
|
144
|
+
return {
|
|
145
|
+
success: false,
|
|
146
|
+
message: 'Update failed: Network error (check your internet connection)'
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
success: false,
|
|
152
|
+
message: `Update failed: ${errorMessage}`
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Main update command handler.
|
|
159
|
+
*/
|
|
160
|
+
export const update = async () => {
|
|
161
|
+
console.log(chalk.bold.cyan('\n🔄 MagentrixCLI Updater\n'));
|
|
162
|
+
|
|
163
|
+
// Step 1: Detect installation method
|
|
164
|
+
const { method, packagePath } = detectInstallationMethod();
|
|
165
|
+
const currentVersion = getCurrentVersion();
|
|
166
|
+
|
|
167
|
+
console.log(chalk.gray('Current version:'), chalk.white(currentVersion));
|
|
168
|
+
console.log(chalk.gray('Installation method:'), chalk.white(method));
|
|
169
|
+
|
|
170
|
+
if (method === 'dev') {
|
|
171
|
+
console.log();
|
|
172
|
+
console.log(chalk.yellow('⚠️ Development Mode Detected'));
|
|
173
|
+
console.log(chalk.gray('You are running from source code.'));
|
|
174
|
+
console.log(chalk.gray('To update, use:'), chalk.cyan('git pull'));
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Step 2: Check for latest version
|
|
179
|
+
console.log();
|
|
180
|
+
const latestVersion = await withSpinner('Checking for updates...', async () => {
|
|
181
|
+
return await getLatestVersion('@magentrix-corp/magentrix-cli');
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
console.log(chalk.gray('Latest version:'), chalk.white(latestVersion));
|
|
185
|
+
|
|
186
|
+
// Step 3: Compare versions
|
|
187
|
+
if (currentVersion === latestVersion) {
|
|
188
|
+
console.log();
|
|
189
|
+
console.log(chalk.green('✓ You are already on the latest version!'));
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
console.log();
|
|
194
|
+
console.log(chalk.yellow(`📦 Update available: ${currentVersion} → ${latestVersion}`));
|
|
195
|
+
console.log();
|
|
196
|
+
|
|
197
|
+
// Step 4: Attempt update
|
|
198
|
+
console.log(chalk.blue('Starting update...\n'));
|
|
199
|
+
|
|
200
|
+
const result = await performUpdate(method, packagePath);
|
|
201
|
+
|
|
202
|
+
console.log();
|
|
203
|
+
|
|
204
|
+
if (result.success) {
|
|
205
|
+
console.log(chalk.green('✅ ' + result.message));
|
|
206
|
+
console.log(chalk.gray('\nRun'), chalk.cyan('magentrix --version'), chalk.gray('to verify the update.'));
|
|
207
|
+
} else {
|
|
208
|
+
console.log(chalk.bgRed.bold.white(' ✖ Update Failed '));
|
|
209
|
+
console.log(chalk.redBright('─'.repeat(60)));
|
|
210
|
+
console.log(chalk.red(result.message));
|
|
211
|
+
console.log();
|
|
212
|
+
|
|
213
|
+
// Show helpful troubleshooting tips
|
|
214
|
+
console.log(chalk.yellow('Common solutions:'));
|
|
215
|
+
console.log();
|
|
216
|
+
|
|
217
|
+
if (result.needsSudo || result.message.includes('Permission')) {
|
|
218
|
+
console.log(chalk.white('1. ') + chalk.gray('Try running with elevated permissions:'));
|
|
219
|
+
if (method === 'global') {
|
|
220
|
+
console.log(chalk.cyan(' sudo npm install -g @magentrix-corp/magentrix-cli@latest'));
|
|
221
|
+
} else {
|
|
222
|
+
console.log(chalk.cyan(' sudo npm install @magentrix-corp/magentrix-cli@latest'));
|
|
223
|
+
}
|
|
224
|
+
console.log();
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
console.log(chalk.white('2. ') + chalk.gray('Manually update using npm:'));
|
|
228
|
+
if (method === 'global') {
|
|
229
|
+
console.log(chalk.cyan(' npm install -g @magentrix-corp/magentrix-cli@latest'));
|
|
230
|
+
} else {
|
|
231
|
+
console.log(chalk.cyan(' npm install @magentrix-corp/magentrix-cli@latest'));
|
|
232
|
+
}
|
|
233
|
+
console.log();
|
|
234
|
+
|
|
235
|
+
console.log(chalk.white('3. ') + chalk.gray('Check npm configuration:'));
|
|
236
|
+
console.log(chalk.cyan(' npm config list'));
|
|
237
|
+
console.log();
|
|
238
|
+
|
|
239
|
+
console.log(chalk.white('4. ') + chalk.gray('Verify you have write access to npm directories'));
|
|
240
|
+
console.log();
|
|
241
|
+
|
|
242
|
+
console.log(chalk.white('5. ') + chalk.gray('If using a proxy, ensure npm is configured correctly:'));
|
|
243
|
+
console.log(chalk.cyan(' npm config set proxy http://your-proxy:port'));
|
|
244
|
+
console.log();
|
|
245
|
+
|
|
246
|
+
console.log(chalk.redBright('─'.repeat(60)));
|
|
247
|
+
}
|
|
248
|
+
};
|
package/bin/magentrix.js
CHANGED
|
@@ -13,6 +13,8 @@ import { status } from '../actions/status.js';
|
|
|
13
13
|
import { cacheDir, recacheFileIdIndex } from '../utils/cacher.js';
|
|
14
14
|
import { EXPORT_ROOT } from '../vars/global.js';
|
|
15
15
|
import { publish } from '../actions/publish.js';
|
|
16
|
+
import { update } from '../actions/update.js';
|
|
17
|
+
import { configWizard } from '../actions/config.js';
|
|
16
18
|
|
|
17
19
|
// ── Middleware ────────────────────────────────
|
|
18
20
|
async function preMiddleware() {
|
|
@@ -52,11 +54,13 @@ program
|
|
|
52
54
|
help += `${chalk.bold.yellow('COMMANDS')}\n`;
|
|
53
55
|
const commands = [
|
|
54
56
|
{ name: 'setup', desc: 'Configure your Magentrix API key', icon: '⚙️ ' },
|
|
57
|
+
{ name: 'config', desc: 'Manage CLI settings', icon: '🔧 ' },
|
|
55
58
|
{ name: 'pull', desc: 'Pull files from the remote server', icon: '📥 ' },
|
|
56
59
|
{ name: 'create', desc: 'Create files locally', icon: '✨ ' },
|
|
57
60
|
{ name: 'status', desc: 'Show file conflicts and sync status', icon: '📊 ' },
|
|
58
61
|
{ name: 'publish', desc: 'Publish pending changes to the remote server', icon: '📤 ' },
|
|
59
|
-
{ name: 'autopublish', desc: 'Watch & sync changes in real time', icon: '🔄 ' }
|
|
62
|
+
{ name: 'autopublish', desc: 'Watch & sync changes in real time', icon: '🔄 ' },
|
|
63
|
+
{ name: 'update', desc: 'Update MagentrixCLI to the latest version', icon: '⬆️ ' }
|
|
60
64
|
];
|
|
61
65
|
|
|
62
66
|
const maxNameLen = Math.max(...commands.map(c => c.name.length));
|
|
@@ -177,7 +181,15 @@ createCommand.configureHelp({
|
|
|
177
181
|
});
|
|
178
182
|
program.command('status').description('Show file conflicts').action(withDefault(status));
|
|
179
183
|
program.command('autopublish').description('Watch & sync changes in real time').action(withDefault(autoPublish));
|
|
184
|
+
// Publish does its own comprehensive file scanning, so skip the pre-cache middleware
|
|
180
185
|
program.command('publish').description('Publish pending changes to the remote server').action(withDefault(publish));
|
|
186
|
+
program.command('update').description('Update MagentrixCLI to the latest version').action(update);
|
|
187
|
+
|
|
188
|
+
// Config command - interactive wizard
|
|
189
|
+
program
|
|
190
|
+
.command('config')
|
|
191
|
+
.description('Configure CLI settings')
|
|
192
|
+
.action(configWizard);
|
|
181
193
|
|
|
182
194
|
// ── Unknown Command Handler ──────────────────
|
|
183
195
|
program.argument('[command]', 'command to run').action((cmd) => {
|
package/package.json
CHANGED
package/utils/assetPaths.js
CHANGED
|
@@ -27,9 +27,19 @@ export const toApiPath = (localPath) => {
|
|
|
27
27
|
return '/contents/assets';
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
// Normalize the path
|
|
30
|
+
// Normalize the path
|
|
31
31
|
const normalized = path.normalize(localPath);
|
|
32
|
-
|
|
32
|
+
|
|
33
|
+
// Remove EXPORT_ROOT from path (handle both absolute and relative paths)
|
|
34
|
+
let relative;
|
|
35
|
+
if (path.isAbsolute(normalized)) {
|
|
36
|
+
// For absolute paths, find and remove everything up to and including EXPORT_ROOT
|
|
37
|
+
const exportRootPattern = new RegExp(`.*[\\\\/]${EXPORT_ROOT}[\\\\/]`);
|
|
38
|
+
relative = normalized.replace(exportRootPattern, '');
|
|
39
|
+
} else {
|
|
40
|
+
// For relative paths, just remove EXPORT_ROOT prefix
|
|
41
|
+
relative = normalized.replace(new RegExp(`^${EXPORT_ROOT}[\\\\/]?`), '');
|
|
42
|
+
}
|
|
33
43
|
|
|
34
44
|
// Replace 'Assets' with 'contents/assets' (case insensitive search, but replace with lowercase)
|
|
35
45
|
const apiPath = relative.replace(/^Assets/i, 'contents/assets');
|
|
@@ -84,9 +94,19 @@ export const toApiFolderPath = (localFolderPath) => {
|
|
|
84
94
|
return '/contents/assets';
|
|
85
95
|
}
|
|
86
96
|
|
|
87
|
-
// Normalize the path
|
|
97
|
+
// Normalize the path
|
|
88
98
|
const normalized = path.normalize(localFolderPath);
|
|
89
|
-
|
|
99
|
+
|
|
100
|
+
// Remove EXPORT_ROOT from path (handle both absolute and relative paths)
|
|
101
|
+
let relative;
|
|
102
|
+
if (path.isAbsolute(normalized)) {
|
|
103
|
+
// For absolute paths, find and remove everything up to and including EXPORT_ROOT
|
|
104
|
+
const exportRootPattern = new RegExp(`.*[\\\\/]${EXPORT_ROOT}[\\\\/]`);
|
|
105
|
+
relative = normalized.replace(exportRootPattern, '');
|
|
106
|
+
} else {
|
|
107
|
+
// For relative paths, just remove EXPORT_ROOT prefix
|
|
108
|
+
relative = normalized.replace(new RegExp(`^${EXPORT_ROOT}[\\\\/]?`), '');
|
|
109
|
+
}
|
|
90
110
|
|
|
91
111
|
// Replace 'Assets' with 'contents/assets' (case insensitive search, but replace with lowercase)
|
|
92
112
|
let apiPath = relative.replace(/^Assets/i, 'contents/assets');
|
package/utils/cacher.js
CHANGED
|
@@ -10,6 +10,7 @@ const config = new Config();
|
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Recursively caches all files in a directory with tagging and content snapshotting
|
|
13
|
+
* OPTIMIZED: Parallel processing + mtime checks for maximum speed
|
|
13
14
|
* @param {string} dir - Directory to cache
|
|
14
15
|
* @returns {Promise<void>}
|
|
15
16
|
*/
|
|
@@ -17,69 +18,135 @@ export const cacheDir = async (dir) => {
|
|
|
17
18
|
if (!fs.existsSync(dir)) return;
|
|
18
19
|
|
|
19
20
|
const absDir = path.resolve(dir);
|
|
20
|
-
const files = await walkFiles(absDir);
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
for (const file of files) {
|
|
25
|
-
const stats = fs.statSync(file);
|
|
26
|
-
if (!stats.isFile()) continue;
|
|
22
|
+
// Walk files but exclude Assets folder (tracked separately in base.json)
|
|
23
|
+
const files = await walkFiles(absDir, { ignore: [path.join(absDir, 'Assets')] });
|
|
27
24
|
|
|
28
|
-
|
|
29
|
-
// Check file tag
|
|
30
|
-
const tag = await getFileTag(file);
|
|
25
|
+
const cache = config.read('cachedFiles', { global: false, filename: 'fileCache.json' }) || {};
|
|
31
26
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
27
|
+
// Process files in parallel batches
|
|
28
|
+
const BATCH_SIZE = 50;
|
|
29
|
+
const updates = [];
|
|
30
|
+
|
|
31
|
+
for (let i = 0; i < files.length; i += BATCH_SIZE) {
|
|
32
|
+
const batch = files.slice(i, i + BATCH_SIZE);
|
|
33
|
+
|
|
34
|
+
// Process batch in parallel
|
|
35
|
+
const batchResults = await Promise.all(
|
|
36
|
+
batch.map(async (file) => {
|
|
37
|
+
try {
|
|
38
|
+
const stats = fs.statSync(file);
|
|
39
|
+
if (!stats.isFile()) return null;
|
|
40
|
+
|
|
41
|
+
const checkFileTag = async (retry = true) => {
|
|
42
|
+
const tag = await getFileTag(file);
|
|
43
|
+
if (!tag) {
|
|
44
|
+
const dirLinkedTag = isPathLinkedToTagByLastKnownPath(file);
|
|
45
|
+
if (dirLinkedTag && retry) {
|
|
46
|
+
await setFileTag(file, dirLinkedTag);
|
|
47
|
+
return await checkFileTag(false);
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
return tag;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const tag = await checkFileTag();
|
|
55
|
+
if (!tag) return null;
|
|
56
|
+
|
|
57
|
+
const objectId = tag;
|
|
58
|
+
|
|
59
|
+
// OPTIMIZATION: Only read/hash/compress if file changed (mtime check)
|
|
60
|
+
const existingCache = cache[objectId];
|
|
61
|
+
if (existingCache &&
|
|
62
|
+
existingCache.mtimeMs === stats.mtimeMs &&
|
|
63
|
+
existingCache.size === stats.size &&
|
|
64
|
+
existingCache.lastKnownPath === file) {
|
|
65
|
+
// File hasn't changed, skip expensive operations
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// File is new or changed - do the expensive operations
|
|
70
|
+
const content = fs.readFileSync(file, 'utf8');
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
objectId,
|
|
74
|
+
data: {
|
|
75
|
+
tag,
|
|
76
|
+
lastKnownPath: file,
|
|
77
|
+
contentHash: sha256(content),
|
|
78
|
+
compressedContent: compressString(content),
|
|
79
|
+
size: stats.size,
|
|
80
|
+
mtimeMs: stats.mtimeMs,
|
|
81
|
+
dev: stats.dev,
|
|
82
|
+
ino: stats.ino
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
} catch (err) {
|
|
86
|
+
return null;
|
|
38
87
|
}
|
|
39
|
-
|
|
40
|
-
|
|
88
|
+
})
|
|
89
|
+
);
|
|
41
90
|
|
|
42
|
-
|
|
43
|
-
|
|
91
|
+
// Collect updates
|
|
92
|
+
updates.push(...batchResults.filter(r => r !== null));
|
|
93
|
+
}
|
|
44
94
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
continue;
|
|
95
|
+
// Apply all updates to cache
|
|
96
|
+
if (updates.length > 0) {
|
|
97
|
+
for (const { objectId, data } of updates) {
|
|
98
|
+
cache[objectId] = data;
|
|
50
99
|
}
|
|
51
|
-
|
|
52
|
-
const content = fs.readFileSync(file, 'utf8');
|
|
53
|
-
const objectId = tag; // Use the tag so we don't get duplicates
|
|
54
|
-
|
|
55
|
-
cache[objectId] = {
|
|
56
|
-
tag,
|
|
57
|
-
lastKnownPath: file,
|
|
58
|
-
contentHash: sha256(content),
|
|
59
|
-
compressedContent: compressString(content),
|
|
60
|
-
size: stats.size,
|
|
61
|
-
mtimeMs: stats.mtimeMs,
|
|
62
|
-
dev: stats.dev,
|
|
63
|
-
ino: stats.ino
|
|
64
|
-
};
|
|
100
|
+
config.save('cachedFiles', cache, { global: false, filename: 'fileCache.json' });
|
|
65
101
|
}
|
|
66
|
-
|
|
67
|
-
config.save('cachedFiles', cache, { global: false, filename: 'fileCache.json' });
|
|
68
102
|
};
|
|
69
103
|
|
|
104
|
+
/**
|
|
105
|
+
* Recaches file ID index by walking files and updating tags
|
|
106
|
+
* OPTIMIZED: Parallel processing with batching for speed
|
|
107
|
+
* @param {string} dir - Directory to recache
|
|
108
|
+
* @returns {Promise<void>}
|
|
109
|
+
*/
|
|
70
110
|
export const recacheFileIdIndex = async (dir) => {
|
|
71
|
-
|
|
111
|
+
// Exclude Assets folder - they don't use file tags, tracked in base.json instead
|
|
112
|
+
const absDir = path.resolve(dir);
|
|
113
|
+
const ignorePath = path.join(absDir, 'Assets');
|
|
114
|
+
const files = await walkFiles(absDir, { ignore: [ignorePath] });
|
|
72
115
|
if (!files || files?.length < 1) return;
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
116
|
+
|
|
117
|
+
// Process files in parallel batches of 50 for speed
|
|
118
|
+
const BATCH_SIZE = 50;
|
|
119
|
+
const updates = [];
|
|
120
|
+
|
|
121
|
+
for (let i = 0; i < files.length; i += BATCH_SIZE) {
|
|
122
|
+
const batch = files.slice(i, i + BATCH_SIZE);
|
|
123
|
+
|
|
124
|
+
// Process batch in parallel
|
|
125
|
+
const batchResults = await Promise.all(
|
|
126
|
+
batch.map(async (file) => {
|
|
127
|
+
try {
|
|
128
|
+
const tag = await getFileTag(file);
|
|
129
|
+
if (!tag) return null;
|
|
130
|
+
|
|
131
|
+
// Check if path changed
|
|
132
|
+
const lastKnownPath = findFileByTag(tag);
|
|
133
|
+
if (lastKnownPath !== file) {
|
|
134
|
+
return { file, tag };
|
|
135
|
+
}
|
|
136
|
+
return null;
|
|
137
|
+
} catch (err) {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
})
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
// Collect updates
|
|
144
|
+
updates.push(...batchResults.filter(r => r !== null));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Apply all updates at once
|
|
148
|
+
for (const { file, tag } of updates) {
|
|
149
|
+
await setFileTag(file, tag);
|
|
83
150
|
}
|
|
84
151
|
}
|
|
85
152
|
|
|
@@ -96,11 +163,13 @@ export async function walkFiles(dir, settings) {
|
|
|
96
163
|
let paths = [];
|
|
97
164
|
|
|
98
165
|
for (const entry of entries) {
|
|
99
|
-
|
|
166
|
+
const fullPath = path.join(dir, entry.name);
|
|
167
|
+
|
|
168
|
+
// Check if this path should be ignored
|
|
169
|
+
if (ignore.find(p => fullPath.startsWith(p) || fullPath === p)) {
|
|
100
170
|
continue;
|
|
101
171
|
}
|
|
102
172
|
|
|
103
|
-
const fullPath = path.join(dir, entry.name);
|
|
104
173
|
if (entry.isDirectory()) {
|
|
105
174
|
const subPaths = await walkFiles(fullPath, settings);
|
|
106
175
|
if (subPaths && subPaths.length > 0) {
|