@launchframe/cli 1.0.0-beta.21 → 1.0.0-beta.22

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.
@@ -0,0 +1,10 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "WebSearch",
5
+ "WebFetch(domain:developer.mixpanel.com)",
6
+ "Bash(node -c:*)",
7
+ "Bash(node:*)"
8
+ ]
9
+ }
10
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@launchframe/cli",
3
- "version": "1.0.0-beta.21",
3
+ "version": "1.0.0-beta.22",
4
4
  "description": "Production-ready B2B SaaS boilerplate with subscriptions, credits, and multi-tenancy",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -49,6 +49,9 @@ function help() {
49
49
  console.log(chalk.gray(' cache:clear Delete cache (re-download on next use)\n'));
50
50
  console.log(chalk.white('Other commands:'));
51
51
  console.log(chalk.gray(' doctor Check project health and configuration'));
52
+ console.log(chalk.gray(' telemetry Show telemetry status'));
53
+ console.log(chalk.gray(' --enable Enable anonymous usage data collection'));
54
+ console.log(chalk.gray(' --disable Disable anonymous usage data collection'));
52
55
  console.log(chalk.gray(' help Show this help message\n'));
53
56
  console.log(chalk.white('Examples:'));
54
57
  console.log(chalk.gray(' # Deploy full app to production'));
@@ -78,6 +81,10 @@ function help() {
78
81
  console.log(chalk.gray(' --tenancy <single|multi> Tenancy model (skips prompt)'));
79
82
  console.log(chalk.gray(' --user-model <b2b|b2b2c> User model (skips prompt)'));
80
83
  console.log(chalk.gray(' help Show this help message\n'));
84
+ console.log(chalk.white('Telemetry:'));
85
+ console.log(chalk.gray(' telemetry Show telemetry status'));
86
+ console.log(chalk.gray(' --enable Enable anonymous usage data collection'));
87
+ console.log(chalk.gray(' --disable Disable anonymous usage data collection\n'));
81
88
  console.log(chalk.white('Cache Management:'));
82
89
  console.log(chalk.gray(' cache:info Show cache location, size, and cached services'));
83
90
  console.log(chalk.gray(' cache:update Force update cache to latest version'));
@@ -7,6 +7,7 @@ const { checkGitHubAccess, showAccessDeniedMessage } = require('../utils/github-
7
7
  const { ensureCacheReady } = require('../utils/service-cache');
8
8
  const { isLaunchFrameProject } = require('../utils/project-helpers');
9
9
  const logger = require('../utils/logger');
10
+ const { trackEvent } = require('../utils/telemetry');
10
11
 
11
12
  /**
12
13
  * Check if running in development mode (local) vs production (npm install)
@@ -135,6 +136,13 @@ async function init(options = {}) {
135
136
  console.log(chalk.white('\nGenerating project...\n'));
136
137
  await generateProject(answers, variantChoices, templateRoot);
137
138
 
139
+ trackEvent('command_executed', {
140
+ command: 'init',
141
+ success: true,
142
+ tenancy: variantChoices.tenancy,
143
+ user_model: variantChoices.userModel
144
+ });
145
+
138
146
  console.log(chalk.green.bold('\nProject created successfully!\n'));
139
147
  console.log(chalk.white('Next steps:'));
140
148
  console.log(chalk.gray(` cd ${answers.projectName}`));
package/src/index.js CHANGED
@@ -3,6 +3,7 @@
3
3
  const chalk = require('chalk');
4
4
  const { isLaunchFrameProject } = require('./utils/project-helpers');
5
5
  const logger = require('./utils/logger');
6
+ const { initTelemetry, trackEvent, sanitize, setTelemetryEnabled, showTelemetryStatus } = require('./utils/telemetry');
6
7
 
7
8
  // Import commands
8
9
  const { init } = require('./commands/init');
@@ -64,6 +65,8 @@ function parseFlags(args) {
64
65
  * Main CLI router
65
66
  */
66
67
  async function main() {
68
+ initTelemetry();
69
+
67
70
  const inProject = isLaunchFrameProject();
68
71
  const flags = parseFlags(args);
69
72
 
@@ -167,6 +170,15 @@ async function main() {
167
170
  case 'cache:update':
168
171
  await cacheUpdate();
169
172
  break;
173
+ case 'telemetry':
174
+ if (flags.disable) {
175
+ setTelemetryEnabled(false);
176
+ } else if (flags.enable) {
177
+ setTelemetryEnabled(true);
178
+ } else {
179
+ showTelemetryStatus();
180
+ }
181
+ break;
170
182
  case 'help':
171
183
  case '--help':
172
184
  case '-h':
@@ -179,4 +191,18 @@ async function main() {
179
191
  }
180
192
  }
181
193
 
182
- main();
194
+ main()
195
+ .then(() => {
196
+ if (command && command !== 'help' && command !== '--help' && command !== '-h' && command !== '--version') {
197
+ trackEvent('command_executed', { command, success: true });
198
+ }
199
+ })
200
+ .catch((error) => {
201
+ trackEvent('command_executed', {
202
+ command,
203
+ success: false,
204
+ error_message: sanitize(error.message)
205
+ });
206
+ console.error(chalk.red(error.message));
207
+ process.exit(1);
208
+ });
@@ -0,0 +1,229 @@
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 telemetry is enabled
68
+ * @returns {boolean} True if telemetry is enabled
69
+ */
70
+ function isEnabled() {
71
+ if (isDisabledByEnv()) return false;
72
+ if (!config || !config.telemetry) return false;
73
+ return config.telemetry.enabled !== false;
74
+ }
75
+
76
+ /**
77
+ * Initialize telemetry — call once at CLI startup.
78
+ * Reads/creates config, shows first-run notice if needed.
79
+ * Synchronous and fast.
80
+ */
81
+ function initTelemetry() {
82
+ try {
83
+ config = readConfig();
84
+
85
+ if (!config.telemetry) {
86
+ config.telemetry = {
87
+ enabled: true,
88
+ noticeShown: false,
89
+ anonymousId: crypto.randomUUID()
90
+ };
91
+ writeConfig(config);
92
+ }
93
+
94
+ if (!config.telemetry.anonymousId) {
95
+ config.telemetry.anonymousId = crypto.randomUUID();
96
+ writeConfig(config);
97
+ }
98
+
99
+ if (!config.telemetry.noticeShown && !isDisabledByEnv()) {
100
+ console.log(
101
+ chalk.gray(
102
+ '\nLaunchFrame collects anonymous usage data to improve the CLI.\n' +
103
+ 'No personal information is collected. Run `launchframe telemetry --disable` to opt out.\n' +
104
+ 'Learn more: https://launchframe.dev/privacy\n'
105
+ )
106
+ );
107
+ config.telemetry.noticeShown = true;
108
+ writeConfig(config);
109
+ }
110
+ } catch {
111
+ // Telemetry init must never break the CLI
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Fire-and-forget event tracking.
117
+ * @param {string} name - Event name
118
+ * @param {Object} properties - Event properties
119
+ */
120
+ function trackEvent(name, properties = {}) {
121
+ try {
122
+ if (!isEnabled()) return;
123
+
124
+ const cliVersion = (() => {
125
+ try {
126
+ return require('../../package.json').version;
127
+ } catch {
128
+ return 'unknown';
129
+ }
130
+ })();
131
+
132
+ const payload = JSON.stringify([
133
+ {
134
+ event: name,
135
+ properties: {
136
+ token: MIXPANEL_TOKEN,
137
+ distinct_id: config.telemetry.anonymousId,
138
+ cli_version: cliVersion,
139
+ node_version: process.version,
140
+ os: process.platform,
141
+ os_arch: process.arch,
142
+ ...properties
143
+ }
144
+ }
145
+ ]);
146
+
147
+ const req = https.request(
148
+ {
149
+ hostname: 'api-eu.mixpanel.com',
150
+ path: '/track',
151
+ method: 'POST',
152
+ headers: {
153
+ 'Content-Type': 'application/json',
154
+ Accept: 'text/plain',
155
+ 'Content-Length': Buffer.byteLength(payload)
156
+ }
157
+ },
158
+ (res) => { res.resume(); }
159
+ );
160
+
161
+ req.on('error', () => {});
162
+ req.write(payload);
163
+ req.end();
164
+ req.unref();
165
+ } catch {
166
+ // Telemetry must never break the CLI
167
+ }
168
+ }
169
+
170
+ /**
171
+ * Enable or disable telemetry
172
+ * @param {boolean} enabled - Whether to enable telemetry
173
+ */
174
+ function setTelemetryEnabled(enabled) {
175
+ config = readConfig();
176
+
177
+ if (!config.telemetry) {
178
+ config.telemetry = {
179
+ enabled,
180
+ noticeShown: true,
181
+ anonymousId: crypto.randomUUID()
182
+ };
183
+ } else {
184
+ config.telemetry.enabled = enabled;
185
+ config.telemetry.noticeShown = true;
186
+ }
187
+
188
+ writeConfig(config);
189
+
190
+ if (enabled) {
191
+ console.log(chalk.green('\nTelemetry enabled. Thank you for helping improve LaunchFrame!\n'));
192
+ } else {
193
+ console.log(chalk.yellow('\nTelemetry disabled. No data will be collected.\n'));
194
+ }
195
+ }
196
+
197
+ /**
198
+ * Show current telemetry status
199
+ */
200
+ function showTelemetryStatus() {
201
+ config = readConfig();
202
+ const envDisabled = isDisabledByEnv();
203
+
204
+ console.log(chalk.blue.bold('\nTelemetry Status\n'));
205
+
206
+ if (envDisabled) {
207
+ console.log(chalk.yellow(' Disabled via environment variable'));
208
+ if (process.env.DO_NOT_TRACK === '1') {
209
+ console.log(chalk.gray(' DO_NOT_TRACK=1'));
210
+ }
211
+ if (process.env.LAUNCHFRAME_TELEMETRY_DISABLED === '1') {
212
+ console.log(chalk.gray(' LAUNCHFRAME_TELEMETRY_DISABLED=1'));
213
+ }
214
+ } else if (config.telemetry && config.telemetry.enabled !== false) {
215
+ console.log(chalk.green(' Enabled'));
216
+ } else {
217
+ console.log(chalk.yellow(' Disabled'));
218
+ }
219
+
220
+ if (config.telemetry && config.telemetry.anonymousId) {
221
+ console.log(chalk.gray(` Anonymous ID: ${config.telemetry.anonymousId}`));
222
+ }
223
+
224
+ console.log(chalk.gray('\n To disable: launchframe telemetry --disable'));
225
+ console.log(chalk.gray(' To enable: launchframe telemetry --enable'));
226
+ console.log(chalk.gray(' Env vars: DO_NOT_TRACK=1 or LAUNCHFRAME_TELEMETRY_DISABLED=1\n'));
227
+ }
228
+
229
+ module.exports = { initTelemetry, trackEvent, sanitize, setTelemetryEnabled, showTelemetryStatus };