@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.
- package/.claude/settings.local.json +12 -0
- package/CLAUDE.md +27 -0
- package/CODE_OF_CONDUCT.md +128 -0
- package/LICENSE +21 -0
- package/README.md +7 -1
- package/package.json +9 -6
- package/src/commands/cache.js +14 -14
- package/src/commands/database-console.js +84 -0
- package/src/commands/deploy-build.js +76 -0
- package/src/commands/deploy-configure.js +10 -3
- package/src/commands/deploy-init.js +24 -57
- package/src/commands/deploy-set-env.js +17 -7
- package/src/commands/deploy-sync-features.js +233 -0
- package/src/commands/deploy-up.js +4 -3
- package/src/commands/dev-add-user.js +165 -0
- package/src/commands/dev-logo.js +160 -0
- package/src/commands/dev-npm-install.js +33 -0
- package/src/commands/dev-queue.js +85 -0
- package/src/commands/docker-build.js +9 -6
- package/src/commands/help.js +35 -11
- package/src/commands/init.js +48 -56
- package/src/commands/migration-create.js +40 -0
- package/src/commands/migration-revert.js +32 -0
- package/src/commands/migration-run.js +32 -0
- package/src/commands/module.js +146 -0
- package/src/commands/service.js +6 -6
- package/src/commands/waitlist-deploy.js +1 -0
- package/src/generator.js +43 -42
- package/src/index.js +109 -4
- package/src/services/module-config.js +25 -0
- package/src/services/module-registry.js +12 -0
- package/src/services/variant-config.js +24 -13
- package/src/utils/docker-helper.js +116 -2
- package/src/utils/env-generator.js +9 -6
- package/src/utils/env-validator.js +4 -2
- package/src/utils/github-access.js +19 -17
- package/src/utils/logger.js +93 -0
- package/src/utils/module-installer.js +58 -0
- package/src/utils/project-helpers.js +34 -1
- package/src/utils/{module-cache.js → service-cache.js} +67 -73
- package/src/utils/ssh-helper.js +51 -1
- package/src/utils/telemetry.js +238 -0
- package/src/utils/variable-replacer.js +18 -23
- 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
|
|
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\
|
|
19
|
-
// Mac/Linux: /home/username/.launchframe/cache/
|
|
20
|
-
return path.join(homeDir, '.launchframe', 'cache', '
|
|
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
|
|
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
|
-
|
|
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} ${
|
|
50
|
-
{
|
|
51
|
-
stdio: 'pipe',
|
|
52
|
-
timeout: 60000
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
81
|
+
timeout: 30000
|
|
85
82
|
});
|
|
86
|
-
|
|
87
|
-
|
|
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
|
|
95
|
-
* @param {string[]}
|
|
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
|
|
95
|
+
async function expandServices(serviceNames) {
|
|
99
96
|
const cacheDir = getCacheDir();
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
97
|
+
|
|
98
|
+
logger.detail(`Loading services: ${serviceNames.join(', ')}...`);
|
|
99
|
+
|
|
103
100
|
try {
|
|
104
|
-
|
|
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
|
-
|
|
108
|
+
currentServices = output.trim().split('\n').filter(Boolean);
|
|
113
109
|
} catch (error) {
|
|
114
|
-
// No
|
|
110
|
+
// No services yet, that's fine
|
|
115
111
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
|
118
|
+
timeout: 60000
|
|
125
119
|
});
|
|
126
|
-
|
|
127
|
-
|
|
120
|
+
|
|
121
|
+
logger.detail('Services loaded');
|
|
128
122
|
} catch (error) {
|
|
129
|
-
throw new Error(`Failed to expand
|
|
123
|
+
throw new Error(`Failed to expand services: ${error.message}`);
|
|
130
124
|
}
|
|
131
125
|
}
|
|
132
126
|
|
|
133
127
|
/**
|
|
134
|
-
* Get path to a specific
|
|
135
|
-
* @param {string}
|
|
136
|
-
* @returns {string} Absolute path to
|
|
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
|
|
132
|
+
function getServicePath(serviceName) {
|
|
139
133
|
const cacheDir = getCacheDir();
|
|
140
|
-
return path.join(cacheDir,
|
|
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
|
|
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('
|
|
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,
|
|
169
|
-
* @returns {Promise<{exists: boolean, path: string, size?: number,
|
|
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
|
|
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.
|
|
206
|
+
info.services = output.trim().split('\n').filter(Boolean);
|
|
213
207
|
} catch (error) {
|
|
214
|
-
info.
|
|
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[]}
|
|
229
|
+
* @param {string[]} requiredServices - Services needed for the operation
|
|
236
230
|
* @returns {Promise<string>} Path to cache root
|
|
237
231
|
*/
|
|
238
|
-
async function ensureCacheReady(
|
|
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
|
|
249
|
-
await
|
|
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
|
-
|
|
269
|
-
|
|
262
|
+
expandServices,
|
|
263
|
+
getServicePath,
|
|
270
264
|
getCachePath,
|
|
271
265
|
clearCache,
|
|
272
266
|
getCacheInfo,
|
package/src/utils/ssh-helper.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
|
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
|
}
|