@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/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-]+\.magentrix(cloud)?\.com$/;
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
- 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.magentrixcloud.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
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@magentrix-corp/magentrix-cli",
3
- "version": "1.1.5",
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",
@@ -27,9 +27,19 @@ export const toApiPath = (localPath) => {
27
27
  return '/contents/assets';
28
28
  }
29
29
 
30
- // Normalize the path and remove the EXPORT_ROOT
30
+ // Normalize the path
31
31
  const normalized = path.normalize(localPath);
32
- const relative = normalized.replace(new RegExp(`^${EXPORT_ROOT}[\\\\/]?`), '');
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 and remove the EXPORT_ROOT
97
+ // Normalize the path
88
98
  const normalized = path.normalize(localFolderPath);
89
- const relative = normalized.replace(new RegExp(`^${EXPORT_ROOT}[\\\\/]?`), '');
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
- const cache = config.read('cachedFiles', { global: false, filename: 'fileCache.json' }) || {};
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
- const checkFileTag = async (retry = true) => {
29
- // Check file tag
30
- const tag = await getFileTag(file);
25
+ const cache = config.read('cachedFiles', { global: false, filename: 'fileCache.json' }) || {};
31
26
 
32
- if (!tag) {
33
- // Try to repair if there is a tag linked to that path
34
- const dirLinkedTag = isPathLinkedToTagByLastKnownPath(file);
35
- if (dirLinkedTag && retry) {
36
- await setFileTag(file, dirLinkedTag);
37
- return await checkFileTag(false);
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
- return null;
40
- }
88
+ })
89
+ );
41
90
 
42
- return tag;
43
- }
91
+ // Collect updates
92
+ updates.push(...batchResults.filter(r => r !== null));
93
+ }
44
94
 
45
- const tag = await checkFileTag();
46
- if (!tag) {
47
- // There may not be a tag if the user manually created a file
48
- // console.warn(`Warning: failed to tag file: ${file}`);
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
- const files = await walkFiles(dir);
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
- for (const file of files) {
75
- const tag = await getFileTag(file);
76
- if (!tag) continue;
77
-
78
- // Update the index id cache
79
- const lastKnownPath = findFileByTag(tag);
80
- if (lastKnownPath !== file) {
81
- await setFileTag(file, tag);
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
- if (ignore.find(p => entry.path.startsWith(p) || entry.path === p)) {
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) {