@launchframe/cli 1.0.0-beta.8 → 1.0.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.
Files changed (44) hide show
  1. package/.claude/settings.local.json +12 -0
  2. package/CLAUDE.md +27 -0
  3. package/CODE_OF_CONDUCT.md +128 -0
  4. package/LICENSE +21 -0
  5. package/README.md +7 -1
  6. package/package.json +9 -6
  7. package/src/commands/cache.js +14 -14
  8. package/src/commands/database-console.js +84 -0
  9. package/src/commands/deploy-build.js +76 -0
  10. package/src/commands/deploy-configure.js +10 -3
  11. package/src/commands/deploy-init.js +24 -57
  12. package/src/commands/deploy-set-env.js +17 -7
  13. package/src/commands/deploy-sync-features.js +233 -0
  14. package/src/commands/deploy-up.js +4 -3
  15. package/src/commands/dev-add-user.js +165 -0
  16. package/src/commands/dev-logo.js +160 -0
  17. package/src/commands/dev-npm-install.js +33 -0
  18. package/src/commands/dev-queue.js +85 -0
  19. package/src/commands/docker-build.js +9 -6
  20. package/src/commands/help.js +35 -11
  21. package/src/commands/init.js +48 -56
  22. package/src/commands/migration-create.js +40 -0
  23. package/src/commands/migration-revert.js +32 -0
  24. package/src/commands/migration-run.js +32 -0
  25. package/src/commands/module.js +146 -0
  26. package/src/commands/service.js +6 -6
  27. package/src/commands/waitlist-deploy.js +1 -0
  28. package/src/generator.js +43 -42
  29. package/src/index.js +109 -4
  30. package/src/services/module-config.js +25 -0
  31. package/src/services/module-registry.js +12 -0
  32. package/src/services/variant-config.js +24 -13
  33. package/src/utils/docker-helper.js +116 -2
  34. package/src/utils/env-generator.js +9 -6
  35. package/src/utils/env-validator.js +4 -2
  36. package/src/utils/github-access.js +19 -17
  37. package/src/utils/logger.js +93 -0
  38. package/src/utils/module-installer.js +58 -0
  39. package/src/utils/project-helpers.js +34 -1
  40. package/src/utils/{module-cache.js → service-cache.js} +67 -73
  41. package/src/utils/ssh-helper.js +51 -1
  42. package/src/utils/telemetry.js +238 -0
  43. package/src/utils/variable-replacer.js +18 -23
  44. package/src/utils/variant-processor.js +35 -42
@@ -3,8 +3,9 @@ const fs = require('fs-extra');
3
3
  const os = require('os');
4
4
  const { execSync } = require('child_process');
5
5
  const chalk = require('chalk');
6
+ const logger = require('./logger');
6
7
 
7
- const MODULES_REPO = 'git@github.com:launchframe-dev/modules.git';
8
+ const SERVICES_REPO = 'git@github.com:launchframe-dev/services.git';
8
9
  const BRANCH = 'main';
9
10
 
10
11
  /**
@@ -15,9 +16,9 @@ const BRANCH = 'main';
15
16
  function getCacheDir() {
16
17
  const homeDir = os.homedir();
17
18
  // Use same path structure on all platforms
18
- // Windows: C:\Users\username\.launchframe\cache\modules
19
- // Mac/Linux: /home/username/.launchframe/cache/modules
20
- return path.join(homeDir, '.launchframe', 'cache', 'modules');
19
+ // Windows: C:\Users\username\.launchframe\cache\services
20
+ // Mac/Linux: /home/username/.launchframe/cache/services
21
+ return path.join(homeDir, '.launchframe', 'cache', 'services');
21
22
  }
22
23
 
23
24
  /**
@@ -32,36 +33,32 @@ async function cacheExists() {
32
33
 
33
34
  /**
34
35
  * Initialize cache with sparse checkout
35
- * Clones only the repository structure, no modules yet
36
+ * Clones only the repository structure, no services yet
36
37
  * @returns {Promise<void>}
37
38
  */
38
39
  async function initializeCache() {
39
40
  const cacheDir = getCacheDir();
40
-
41
- console.log(chalk.blue('🔄 Initializing module cache...'));
42
-
41
+
42
+ logger.detail('Initializing services cache...');
43
+
43
44
  try {
44
- // Ensure parent directory exists
45
45
  await fs.ensureDir(path.dirname(cacheDir));
46
-
47
- // Sparse clone (only root files, no modules)
46
+
48
47
  execSync(
49
- `git clone --sparse --depth 1 --branch ${BRANCH} ${MODULES_REPO} "${cacheDir}"`,
50
- {
51
- stdio: 'pipe', // Hide output
52
- timeout: 60000 // 1 minute timeout
48
+ `git clone --sparse --depth 1 --branch ${BRANCH} ${SERVICES_REPO} "${cacheDir}"`,
49
+ {
50
+ stdio: 'pipe',
51
+ timeout: 60000
53
52
  }
54
53
  );
55
-
56
- // Configure sparse checkout (starts with empty set)
54
+
57
55
  execSync('git sparse-checkout init --cone', {
58
56
  cwd: cacheDir,
59
57
  stdio: 'pipe'
60
58
  });
61
-
62
- console.log(chalk.green('Cache initialized'));
59
+
60
+ logger.detail('Cache initialized');
63
61
  } catch (error) {
64
- // Clean up partial clone on failure
65
62
  await fs.remove(cacheDir);
66
63
  throw new Error(`Failed to initialize cache: ${error.message}`);
67
64
  }
@@ -74,70 +71,67 @@ async function initializeCache() {
74
71
  */
75
72
  async function updateCache() {
76
73
  const cacheDir = getCacheDir();
77
-
78
- console.log(chalk.blue('🔄 Updating module cache...'));
79
-
74
+
75
+ logger.detail('Updating service cache...');
76
+
80
77
  try {
81
78
  execSync('git pull origin main', {
82
79
  cwd: cacheDir,
83
80
  stdio: 'pipe',
84
- timeout: 30000 // 30 seconds
81
+ timeout: 30000
85
82
  });
86
-
87
- console.log(chalk.green('Cache updated'));
83
+
84
+ logger.detail('Cache updated');
88
85
  } catch (error) {
89
86
  throw new Error(`Failed to update cache: ${error.message}`);
90
87
  }
91
88
  }
92
89
 
93
90
  /**
94
- * Expand sparse checkout to include specific modules
95
- * @param {string[]} moduleNames - Array of module names to expand
91
+ * Expand sparse checkout to include specific services
92
+ * @param {string[]} serviceNames - Array of services names to expand
96
93
  * @returns {Promise<void>}
97
94
  */
98
- async function expandModules(moduleNames) {
95
+ async function expandServices(serviceNames) {
99
96
  const cacheDir = getCacheDir();
100
-
101
- console.log(chalk.blue(`📦 Loading modules: ${moduleNames.join(', ')}...`));
102
-
97
+
98
+ logger.detail(`Loading services: ${serviceNames.join(', ')}...`);
99
+
103
100
  try {
104
- // Get current sparse checkout list
105
- let currentModules = [];
101
+ let currentServices = [];
106
102
  try {
107
103
  const output = execSync('git sparse-checkout list', {
108
104
  cwd: cacheDir,
109
105
  stdio: 'pipe',
110
106
  encoding: 'utf8'
111
107
  });
112
- currentModules = output.trim().split('\n').filter(Boolean);
108
+ currentServices = output.trim().split('\n').filter(Boolean);
113
109
  } catch (error) {
114
- // No modules yet, that's fine
110
+ // No services yet, that's fine
115
111
  }
116
-
117
- // Add new modules to the list
118
- const allModules = [...new Set([...currentModules, ...moduleNames])];
119
-
120
- // Set sparse checkout to include all modules
121
- execSync(`git sparse-checkout set ${allModules.join(' ')}`, {
112
+
113
+ const allServices = [...new Set([...currentServices, ...serviceNames])];
114
+
115
+ execSync(`git sparse-checkout set ${allServices.join(' ')}`, {
122
116
  cwd: cacheDir,
123
117
  stdio: 'pipe',
124
- timeout: 60000 // 1 minute (may need to download files)
118
+ timeout: 60000
125
119
  });
126
-
127
- console.log(chalk.green(' Modules loaded'));
120
+
121
+ logger.detail('Services loaded');
128
122
  } catch (error) {
129
- throw new Error(`Failed to expand modules: ${error.message}`);
123
+ throw new Error(`Failed to expand services: ${error.message}`);
130
124
  }
131
125
  }
132
126
 
133
127
  /**
134
- * Get path to a specific module in the cache
135
- * @param {string} moduleName - Module name (e.g., 'backend', 'admin-portal')
136
- * @returns {string} Absolute path to module
128
+ * Get path to a specific service in the cache
129
+ * @param {string} serviceName - Service name (e.g., 'backend', 'admin-portal')
130
+ * @returns {string} Absolute path to service
137
131
  */
138
- function getModulePath(moduleName) {
132
+ function getServicePath(serviceName) {
139
133
  const cacheDir = getCacheDir();
140
- return path.join(cacheDir, moduleName);
134
+ return path.join(cacheDir, serviceName);
141
135
  }
142
136
 
143
137
  /**
@@ -149,24 +143,24 @@ function getCachePath() {
149
143
  }
150
144
 
151
145
  /**
152
- * Clear the entire module cache
146
+ * Clear the entire service cache
153
147
  * Useful for troubleshooting or forcing fresh download
154
148
  * @returns {Promise<void>}
155
149
  */
156
150
  async function clearCache() {
157
151
  const cacheDir = getCacheDir();
158
-
152
+
159
153
  if (await fs.pathExists(cacheDir)) {
160
154
  await fs.remove(cacheDir);
161
- console.log(chalk.green('Cache cleared'));
155
+ console.log(chalk.green('Cache cleared'));
162
156
  } else {
163
157
  console.log(chalk.gray('Cache is already empty'));
164
158
  }
165
159
  }
166
160
 
167
161
  /**
168
- * Get cache information (size, last update, modules)
169
- * @returns {Promise<{exists: boolean, path: string, size?: number, modules?: string[], lastUpdate?: Date}>}
162
+ * Get cache information (size, last update, services)
163
+ * @returns {Promise<{exists: boolean, path: string, size?: number, services?: string[], lastUpdate?: Date}>}
170
164
  */
171
165
  async function getCacheInfo() {
172
166
  const cacheDir = getCacheDir();
@@ -174,13 +168,13 @@ async function getCacheInfo() {
174
168
  exists: false,
175
169
  path: cacheDir
176
170
  };
177
-
171
+
178
172
  if (!(await cacheExists())) {
179
173
  return info;
180
174
  }
181
-
175
+
182
176
  info.exists = true;
183
-
177
+
184
178
  try {
185
179
  // Get cache size (du command works on Unix/Mac, different on Windows)
186
180
  if (process.platform === 'win32') {
@@ -201,19 +195,19 @@ async function getCacheInfo() {
201
195
  } catch (error) {
202
196
  // Size calculation failed, not critical
203
197
  }
204
-
198
+
205
199
  try {
206
- // Get list of expanded modules
200
+ // Get list of expanded services
207
201
  const output = execSync('git sparse-checkout list', {
208
202
  cwd: cacheDir,
209
203
  encoding: 'utf8',
210
204
  stdio: 'pipe'
211
205
  });
212
- info.modules = output.trim().split('\n').filter(Boolean);
206
+ info.services = output.trim().split('\n').filter(Boolean);
213
207
  } catch (error) {
214
- info.modules = [];
208
+ info.services = [];
215
209
  }
216
-
210
+
217
211
  try {
218
212
  // Get last update time from git log
219
213
  const output = execSync('git log -1 --format=%cd --date=iso', {
@@ -225,17 +219,17 @@ async function getCacheInfo() {
225
219
  } catch (error) {
226
220
  // Last update time failed, not critical
227
221
  }
228
-
222
+
229
223
  return info;
230
224
  }
231
225
 
232
226
  /**
233
227
  * Ensure cache is ready (initialize if needed, update if exists)
234
228
  * This is the main entry point for cache management
235
- * @param {string[]} requiredModules - Modules needed for the operation
229
+ * @param {string[]} requiredServices - Services needed for the operation
236
230
  * @returns {Promise<string>} Path to cache root
237
231
  */
238
- async function ensureCacheReady(requiredModules) {
232
+ async function ensureCacheReady(requiredServices) {
239
233
  try {
240
234
  if (!(await cacheExists())) {
241
235
  // Cache doesn't exist, initialize it
@@ -244,10 +238,10 @@ async function ensureCacheReady(requiredModules) {
244
238
  // Cache exists, update it
245
239
  await updateCache();
246
240
  }
247
-
248
- // Expand sparse checkout to include required modules
249
- await expandModules(requiredModules);
250
-
241
+
242
+ // Expand sparse checkout to include required services
243
+ await expandServices(requiredServices);
244
+
251
245
  return getCachePath();
252
246
  } catch (error) {
253
247
  // If we fail and it's a network error, provide helpful message
@@ -265,8 +259,8 @@ module.exports = {
265
259
  cacheExists,
266
260
  initializeCache,
267
261
  updateCache,
268
- expandModules,
269
- getModulePath,
262
+ expandServices,
263
+ getServicePath,
270
264
  getCachePath,
271
265
  clearCache,
272
266
  getCacheInfo,
@@ -209,6 +209,54 @@ function showDeployKeyInstructions(vpsUser, vpsHost, githubOrg, projectName) {
209
209
  console.log(chalk.gray(' launchframe deploy:init\n'));
210
210
  }
211
211
 
212
+ /**
213
+ * Pull Docker images on VPS
214
+ * @param {string} vpsUser - SSH username
215
+ * @param {string} vpsHost - VPS hostname or IP
216
+ * @param {string} vpsAppFolder - App folder path on VPS
217
+ * @returns {Promise<void>}
218
+ */
219
+ async function pullImagesOnVPS(vpsUser, vpsHost, vpsAppFolder) {
220
+ const ora = require('ora');
221
+
222
+ const spinner = ora('Pulling images on VPS...').start();
223
+
224
+ try {
225
+ await execAsync(
226
+ `ssh ${vpsUser}@${vpsHost} "cd ${vpsAppFolder}/infrastructure && docker-compose -f docker-compose.yml -f docker-compose.prod.yml pull"`,
227
+ { timeout: 600000 } // 10 minutes
228
+ );
229
+ spinner.succeed('Images pulled on VPS');
230
+ } catch (error) {
231
+ spinner.fail('Failed to pull images on VPS');
232
+ throw new Error(`Failed to pull images: ${error.message}`);
233
+ }
234
+ }
235
+
236
+ /**
237
+ * Restart services on VPS
238
+ * @param {string} vpsUser - SSH username
239
+ * @param {string} vpsHost - VPS hostname or IP
240
+ * @param {string} vpsAppFolder - App folder path on VPS
241
+ * @returns {Promise<void>}
242
+ */
243
+ async function restartServicesOnVPS(vpsUser, vpsHost, vpsAppFolder) {
244
+ const ora = require('ora');
245
+
246
+ const spinner = ora('Restarting services...').start();
247
+
248
+ try {
249
+ await execAsync(
250
+ `ssh ${vpsUser}@${vpsHost} "cd ${vpsAppFolder}/infrastructure && docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -d"`,
251
+ { timeout: 300000 } // 5 minutes
252
+ );
253
+ spinner.succeed('Services restarted');
254
+ } catch (error) {
255
+ spinner.fail('Failed to restart services');
256
+ throw new Error(`Failed to restart services: ${error.message}`);
257
+ }
258
+ }
259
+
212
260
  module.exports = {
213
261
  testSSHConnection,
214
262
  checkSSHKeys,
@@ -216,5 +264,7 @@ module.exports = {
216
264
  copyFileToVPS,
217
265
  copyDirectoryToVPS,
218
266
  checkRepoPrivacy,
219
- showDeployKeyInstructions
267
+ showDeployKeyInstructions,
268
+ pullImagesOnVPS,
269
+ restartServicesOnVPS
220
270
  };
@@ -0,0 +1,238 @@
1
+ const https = require('https');
2
+ const crypto = require('crypto');
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const os = require('os');
6
+ const chalk = require('chalk');
7
+
8
+ const MIXPANEL_TOKEN = '3e6214f33ba535dec14021547039427c';
9
+ const CONFIG_DIR = path.join(os.homedir(), '.launchframe');
10
+ const CONFIG_PATH = path.join(CONFIG_DIR, 'config.json');
11
+
12
+ let config = null;
13
+
14
+ /**
15
+ * Sanitize error messages by stripping file paths and capping length
16
+ * @param {string} message - Raw error message
17
+ * @returns {string} Sanitized message
18
+ */
19
+ function sanitize(message) {
20
+ if (!message) return 'unknown';
21
+ return message
22
+ .replace(/\/[\w\-\/.]+/g, '<path>')
23
+ .replace(/[A-Z]:\\[\w\-\\.\\]+/g, '<path>')
24
+ .substring(0, 200);
25
+ }
26
+
27
+ /**
28
+ * Read config from disk, or return defaults
29
+ * @returns {Object} Config object
30
+ */
31
+ function readConfig() {
32
+ try {
33
+ if (fs.existsSync(CONFIG_PATH)) {
34
+ const raw = fs.readFileSync(CONFIG_PATH, 'utf8');
35
+ return JSON.parse(raw);
36
+ }
37
+ } catch {
38
+ // Corrupted config — start fresh
39
+ }
40
+ return {};
41
+ }
42
+
43
+ /**
44
+ * Write config to disk
45
+ * @param {Object} cfg - Config object to write
46
+ */
47
+ function writeConfig(cfg) {
48
+ try {
49
+ if (!fs.existsSync(CONFIG_DIR)) {
50
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
51
+ }
52
+ fs.writeFileSync(CONFIG_PATH, JSON.stringify(cfg, null, 2) + '\n');
53
+ } catch {
54
+ // Silently ignore write failures
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Check if telemetry is disabled via environment variables
60
+ * @returns {boolean} True if disabled via env
61
+ */
62
+ function isDisabledByEnv() {
63
+ return process.env.DO_NOT_TRACK === '1' || process.env.LAUNCHFRAME_TELEMETRY_DISABLED === '1';
64
+ }
65
+
66
+ /**
67
+ * Check if running from a locally linked (dev) version
68
+ * @returns {boolean} True if running via npm link
69
+ */
70
+ function isDevMode() {
71
+ return !__dirname.includes('node_modules');
72
+ }
73
+
74
+ /**
75
+ * Check if telemetry is enabled
76
+ * @returns {boolean} True if telemetry is enabled
77
+ */
78
+ function isEnabled() {
79
+ if (isDevMode()) return false;
80
+ if (isDisabledByEnv()) return false;
81
+ if (!config || !config.telemetry) return false;
82
+ return config.telemetry.enabled !== false;
83
+ }
84
+
85
+ /**
86
+ * Initialize telemetry — call once at CLI startup.
87
+ * Reads/creates config, shows first-run notice if needed.
88
+ * Synchronous and fast.
89
+ */
90
+ function initTelemetry() {
91
+ try {
92
+ config = readConfig();
93
+
94
+ if (!config.telemetry) {
95
+ config.telemetry = {
96
+ enabled: true,
97
+ noticeShown: false,
98
+ anonymousId: crypto.randomUUID()
99
+ };
100
+ writeConfig(config);
101
+ }
102
+
103
+ if (!config.telemetry.anonymousId) {
104
+ config.telemetry.anonymousId = crypto.randomUUID();
105
+ writeConfig(config);
106
+ }
107
+
108
+ if (!config.telemetry.noticeShown && !isDisabledByEnv() && !isDevMode()) {
109
+ console.log(
110
+ chalk.gray(
111
+ '\nLaunchFrame collects anonymous usage data to improve the CLI.\n' +
112
+ 'No personal information is collected. Run `launchframe telemetry --disable` to opt out.\n' +
113
+ 'Learn more: https://launchframe.dev/privacy\n'
114
+ )
115
+ );
116
+ config.telemetry.noticeShown = true;
117
+ writeConfig(config);
118
+ }
119
+ } catch {
120
+ // Telemetry init must never break the CLI
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Fire-and-forget event tracking.
126
+ * @param {string} name - Event name
127
+ * @param {Object} properties - Event properties
128
+ */
129
+ function trackEvent(name, properties = {}) {
130
+ try {
131
+ if (!isEnabled()) return;
132
+
133
+ const cliVersion = (() => {
134
+ try {
135
+ return require('../../package.json').version;
136
+ } catch {
137
+ return 'unknown';
138
+ }
139
+ })();
140
+
141
+ const payload = JSON.stringify([
142
+ {
143
+ event: name,
144
+ properties: {
145
+ token: MIXPANEL_TOKEN,
146
+ distinct_id: config.telemetry.anonymousId,
147
+ cli_version: cliVersion,
148
+ node_version: process.version,
149
+ os: process.platform,
150
+ os_arch: process.arch,
151
+ ...properties
152
+ }
153
+ }
154
+ ]);
155
+
156
+ const req = https.request(
157
+ {
158
+ hostname: 'api-eu.mixpanel.com',
159
+ path: '/track',
160
+ method: 'POST',
161
+ headers: {
162
+ 'Content-Type': 'application/json',
163
+ Accept: 'text/plain',
164
+ 'Content-Length': Buffer.byteLength(payload)
165
+ }
166
+ },
167
+ (res) => { res.resume(); }
168
+ );
169
+
170
+ req.on('error', () => {});
171
+ req.write(payload);
172
+ req.end();
173
+ req.unref();
174
+ } catch {
175
+ // Telemetry must never break the CLI
176
+ }
177
+ }
178
+
179
+ /**
180
+ * Enable or disable telemetry
181
+ * @param {boolean} enabled - Whether to enable telemetry
182
+ */
183
+ function setTelemetryEnabled(enabled) {
184
+ config = readConfig();
185
+
186
+ if (!config.telemetry) {
187
+ config.telemetry = {
188
+ enabled,
189
+ noticeShown: true,
190
+ anonymousId: crypto.randomUUID()
191
+ };
192
+ } else {
193
+ config.telemetry.enabled = enabled;
194
+ config.telemetry.noticeShown = true;
195
+ }
196
+
197
+ writeConfig(config);
198
+
199
+ if (enabled) {
200
+ console.log(chalk.green('\nTelemetry enabled. Thank you for helping improve LaunchFrame!\n'));
201
+ } else {
202
+ console.log(chalk.yellow('\nTelemetry disabled. No data will be collected.\n'));
203
+ }
204
+ }
205
+
206
+ /**
207
+ * Show current telemetry status
208
+ */
209
+ function showTelemetryStatus() {
210
+ config = readConfig();
211
+ const envDisabled = isDisabledByEnv();
212
+
213
+ console.log(chalk.blue.bold('\nTelemetry Status\n'));
214
+
215
+ if (envDisabled) {
216
+ console.log(chalk.yellow(' Disabled via environment variable'));
217
+ if (process.env.DO_NOT_TRACK === '1') {
218
+ console.log(chalk.gray(' DO_NOT_TRACK=1'));
219
+ }
220
+ if (process.env.LAUNCHFRAME_TELEMETRY_DISABLED === '1') {
221
+ console.log(chalk.gray(' LAUNCHFRAME_TELEMETRY_DISABLED=1'));
222
+ }
223
+ } else if (config.telemetry && config.telemetry.enabled !== false) {
224
+ console.log(chalk.green(' Enabled'));
225
+ } else {
226
+ console.log(chalk.yellow(' Disabled'));
227
+ }
228
+
229
+ if (config.telemetry && config.telemetry.anonymousId) {
230
+ console.log(chalk.gray(` Anonymous ID: ${config.telemetry.anonymousId}`));
231
+ }
232
+
233
+ console.log(chalk.gray('\n To disable: launchframe telemetry --disable'));
234
+ console.log(chalk.gray(' To enable: launchframe telemetry --enable'));
235
+ console.log(chalk.gray(' Env vars: DO_NOT_TRACK=1 or LAUNCHFRAME_TELEMETRY_DISABLED=1\n'));
236
+ }
237
+
238
+ module.exports = { initTelemetry, trackEvent, sanitize, setTelemetryEnabled, showTelemetryStatus };
@@ -1,6 +1,9 @@
1
1
  const fs = require('fs-extra');
2
+ const { glob } = require('node:fs');
2
3
  const path = require('path');
3
- const { glob } = require('glob');
4
+
5
+ const EXCLUDED_DIRS = new Set(['node_modules', '.git', '.next', 'dist', 'build']);
6
+ const BINARY_EXTENSIONS = new Set(['.png', '.jpg', '.jpeg', '.gif', '.ico', '.pdf', '.woff', '.woff2', '.ttf', '.eot']);
4
7
 
5
8
  /**
6
9
  * Replace template variables in all files within a directory
@@ -9,31 +12,23 @@ const { glob } = require('glob');
9
12
  */
10
13
  async function replaceVariables(directory, variables) {
11
14
  // Find all files (excluding node_modules, .git, binary files)
12
- const files = await glob('**/*', {
13
- cwd: directory,
14
- nodir: true,
15
- dot: true, // Include hidden files/directories like .vitepress
16
- ignore: [
17
- '**/node_modules/**',
18
- '**/.git/**',
19
- '**/.next/**',
20
- '**/dist/**',
21
- '**/build/**',
22
- '**/*.png',
23
- '**/*.jpg',
24
- '**/*.jpeg',
25
- '**/*.gif',
26
- '**/*.ico',
27
- '**/*.pdf',
28
- '**/*.woff',
29
- '**/*.woff2',
30
- '**/*.ttf',
31
- '**/*.eot'
32
- ]
15
+ const files = await new Promise((resolve, reject) => {
16
+ glob('**/*', {
17
+ cwd: directory,
18
+ exclude: (name) => EXCLUDED_DIRS.has(name),
19
+ }, (err, matches) => err ? reject(err) : resolve(matches));
20
+ });
21
+
22
+ const filtered = files.filter(f => {
23
+ const ext = path.extname(f).toLowerCase();
24
+ return !BINARY_EXTENSIONS.has(ext);
33
25
  });
34
26
 
35
- for (const file of files) {
27
+ for (const file of filtered) {
36
28
  const filePath = path.join(directory, file);
29
+ // Skip directories (fs.glob includes them unlike third-party glob packages)
30
+ const stat = await fs.stat(filePath);
31
+ if (stat.isDirectory()) continue;
37
32
  await replaceVariablesInFile(filePath, variables);
38
33
  }
39
34
  }