@launchframe/cli 1.0.0-beta.9 → 1.0.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.
Files changed (47) 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 +19 -14
  8. package/src/commands/database-console.js +124 -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 +235 -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 +161 -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/docker-logs.js +26 -7
  21. package/src/commands/docker-up.js +17 -3
  22. package/src/commands/help.js +33 -9
  23. package/src/commands/init.js +44 -52
  24. package/src/commands/migration-create.js +38 -0
  25. package/src/commands/migration-revert.js +32 -0
  26. package/src/commands/migration-run.js +32 -0
  27. package/src/commands/module.js +148 -0
  28. package/src/commands/service.js +22 -12
  29. package/src/commands/waitlist-deploy.js +1 -0
  30. package/src/commands/waitlist-logs.js +20 -3
  31. package/src/generator.js +41 -40
  32. package/src/index.js +115 -10
  33. package/src/services/module-config.js +25 -0
  34. package/src/services/module-registry.js +12 -0
  35. package/src/services/variant-config.js +24 -13
  36. package/src/utils/docker-helper.js +116 -2
  37. package/src/utils/env-generator.js +9 -6
  38. package/src/utils/env-validator.js +4 -2
  39. package/src/utils/github-access.js +15 -13
  40. package/src/utils/logger.js +93 -0
  41. package/src/utils/module-installer.js +58 -0
  42. package/src/utils/project-helpers.js +34 -1
  43. package/src/utils/service-cache.js +12 -18
  44. package/src/utils/ssh-helper.js +51 -1
  45. package/src/utils/telemetry.js +238 -0
  46. package/src/utils/variable-replacer.js +18 -23
  47. package/src/utils/variant-processor.js +35 -42
@@ -0,0 +1,148 @@
1
+ const chalk = require('chalk');
2
+ const inquirer = require('inquirer');
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { execSync } = require('child_process');
6
+ const { MODULE_REGISTRY } = require('../services/module-registry');
7
+ const { MODULE_CONFIG } = require('../services/module-config');
8
+ const { installModule } = require('../utils/module-installer');
9
+ const { dockerBuild } = require('./docker-build');
10
+ const { dockerDown } = require('./docker-down');
11
+ const { dockerUp } = require('./docker-up');
12
+ const {
13
+ requireProject,
14
+ getProjectConfig,
15
+ getInstalledModules,
16
+ isModuleInstalled,
17
+ addInstalledModule
18
+ } = require('../utils/project-helpers');
19
+
20
+ // Core services that can't be added via service:add
21
+ const CORE_SERVICES = ['backend', 'admin-portal', 'infrastructure', 'website'];
22
+
23
+ async function moduleList() {
24
+ requireProject();
25
+
26
+ const installedModules = getInstalledModules();
27
+ const modules = Object.values(MODULE_REGISTRY);
28
+
29
+ console.log(chalk.blue('\nAvailable Modules:\n'));
30
+
31
+ modules.forEach(mod => {
32
+ const installed = installedModules.includes(mod.name);
33
+ const status = installed ? chalk.green(' [installed]') : '';
34
+ console.log(chalk.green(` ${mod.name}`) + status);
35
+ console.log(` ${mod.description}`);
36
+ console.log(` Affects services: ${mod.services.join(', ')}`);
37
+ console.log('');
38
+ });
39
+
40
+ console.log('To install a module:');
41
+ console.log(' launchframe module:add <module-name>');
42
+ }
43
+
44
+ async function moduleAdd(moduleName, flags = {}) {
45
+ requireProject();
46
+
47
+ // Validate module exists in registry
48
+ const mod = MODULE_REGISTRY[moduleName];
49
+ if (!mod) {
50
+ console.error(chalk.red(`Error: Module "${moduleName}" not found`));
51
+ console.log('\nAvailable modules:');
52
+ Object.keys(MODULE_REGISTRY).forEach(key => {
53
+ console.log(` - ${key}`);
54
+ });
55
+ process.exit(1);
56
+ }
57
+
58
+ // Check not already installed
59
+ if (isModuleInstalled(moduleName)) {
60
+ console.error(chalk.red(`Error: Module "${moduleName}" is already installed`));
61
+ process.exit(1);
62
+ }
63
+
64
+ // Validate required services
65
+ const config = getProjectConfig();
66
+ const installedServices = config.installedServices || [];
67
+ const errors = [];
68
+
69
+ for (const service of mod.services) {
70
+ // Check if service is in installedServices
71
+ if (!installedServices.includes(service)) {
72
+ if (CORE_SERVICES.includes(service)) {
73
+ errors.push(`Core service '${service}' is missing from your project`);
74
+ } else {
75
+ errors.push(`Service '${service}' is not installed. Install it first with: launchframe service:add ${service}`);
76
+ }
77
+ continue;
78
+ }
79
+
80
+ // Check if service directory exists on disk
81
+ const serviceDir = path.join(process.cwd(), service);
82
+ if (!fs.existsSync(serviceDir)) {
83
+ errors.push(`Service '${service}' directory not found. Your project structure may be corrupted.`);
84
+ }
85
+ }
86
+
87
+ if (errors.length > 0) {
88
+ console.error(chalk.red(`\nCannot install module "${moduleName}":\n`));
89
+ errors.forEach(err => {
90
+ console.error(chalk.red(` - ${err}`));
91
+ });
92
+ process.exit(1);
93
+ }
94
+
95
+ // Display module info and confirm
96
+ console.log(chalk.green(`\n${mod.displayName}`));
97
+ console.log(mod.description);
98
+ console.log(`Affects services: ${mod.services.join(', ')}`);
99
+
100
+ if (!(flags.yes || flags.y)) {
101
+ const { confirmed } = await inquirer.prompt([{
102
+ type: 'confirm',
103
+ name: 'confirmed',
104
+ message: `Add module "${mod.displayName}" to your project?`,
105
+ default: true
106
+ }]);
107
+
108
+ if (!confirmed) {
109
+ console.log('Installation cancelled');
110
+ process.exit(0);
111
+ }
112
+ }
113
+
114
+ const affectedServices = [...new Set(Object.keys(MODULE_CONFIG[moduleName] || {}))].filter(s => s !== 'infrastructure');
115
+ const infrastructurePath = path.join(process.cwd(), 'infrastructure');
116
+ const composeCmd = 'docker-compose -f docker-compose.yml -f docker-compose.dev.yml';
117
+
118
+ // Bring the stack down and remove affected containers before touching files
119
+ await dockerDown();
120
+ for (const service of affectedServices) {
121
+ try {
122
+ execSync(`${composeCmd} rm -f ${service}`, { cwd: infrastructurePath, stdio: 'inherit' });
123
+ } catch (_) {
124
+ // Container may already be gone — that's fine
125
+ }
126
+ }
127
+
128
+ // Register module in .launchframe
129
+ addInstalledModule(moduleName);
130
+
131
+ // Install module files, sections, and dependencies
132
+ const moduleServiceConfig = MODULE_CONFIG[moduleName];
133
+ if (moduleServiceConfig) {
134
+ await installModule(moduleName, moduleServiceConfig);
135
+ }
136
+
137
+ console.log(chalk.green(`\n✓ Module "${moduleName}" installed successfully!`));
138
+
139
+ // Rebuild affected containers
140
+ for (const service of affectedServices) {
141
+ await dockerBuild(service);
142
+ }
143
+
144
+ // Restart the full stack (detached when running non-interactively, watch otherwise)
145
+ await dockerUp(undefined, flags.yes || flags.y ? { detach: true } : {});
146
+ }
147
+
148
+ module.exports = { moduleAdd, moduleList };
@@ -10,7 +10,7 @@ const { updateEnvFile } = require('../utils/env-generator');
10
10
  const { checkGitHubAccess, showAccessDeniedMessage } = require('../utils/github-access');
11
11
  const { ensureCacheReady, getServicePath } = require('../utils/service-cache');
12
12
 
13
- async function serviceAdd(serviceName) {
13
+ async function serviceAdd(serviceName, flags = {}) {
14
14
  // STEP 1: Validation
15
15
  console.log(chalk.blue(`Installing ${serviceName} service...`));
16
16
 
@@ -49,16 +49,18 @@ async function serviceAdd(serviceName) {
49
49
  console.log(`Tech stack: ${service.techStack}`);
50
50
  console.log(`Dependencies: ${service.dependencies.join(', ')}`);
51
51
 
52
- const { confirmed } = await inquirer.prompt([{
53
- type: 'confirm',
54
- name: 'confirmed',
55
- message: 'Continue with installation?',
56
- default: true
57
- }]);
52
+ if (!(flags.yes || flags.y)) {
53
+ const { confirmed } = await inquirer.prompt([{
54
+ type: 'confirm',
55
+ name: 'confirmed',
56
+ message: 'Continue with installation?',
57
+ default: true
58
+ }]);
58
59
 
59
- if (!confirmed) {
60
- console.log('Installation cancelled');
61
- process.exit(0);
60
+ if (!confirmed) {
61
+ console.log('Installation cancelled');
62
+ process.exit(0);
63
+ }
62
64
  }
63
65
 
64
66
  // STEP 3: Get service files (from cache in production, local in dev)
@@ -124,7 +126,7 @@ async function serviceAdd(serviceName) {
124
126
 
125
127
  // STEP 4: Service-specific prompts (e.g., Airtable credentials)
126
128
  console.log(chalk.blue('\nConfiguring service...'));
127
- const envValues = await runServicePrompts(service);
129
+ const envValues = await runServicePrompts(service, flags);
128
130
 
129
131
  // STEP 5: Replace template variables
130
132
  console.log(chalk.blue('\nCustomizing service for your project...'));
@@ -265,9 +267,17 @@ async function serviceAdd(serviceName) {
265
267
  console.log(`\n📖 See README.md in ${serviceName}/ for more details.`);
266
268
  }
267
269
 
268
- async function runServicePrompts(service) {
270
+ async function runServicePrompts(service, flags = {}) {
269
271
  const envValues = {};
270
272
 
273
+ if (flags.yes || flags.y) {
274
+ // Non-interactive mode — use empty values; configure manually after install
275
+ for (const key of Object.keys(service.envVars)) {
276
+ envValues[key] = '';
277
+ }
278
+ return envValues;
279
+ }
280
+
271
281
  // Prompt for each required env var
272
282
  for (const [key, description] of Object.entries(service.envVars)) {
273
283
  const { value } = await inquirer.prompt([{
@@ -205,6 +205,7 @@ async function waitlistDeploy() {
205
205
  verifySpinner.succeed('Services verified');
206
206
  console.log(chalk.gray('\n' + psOutput));
207
207
  } catch (error) {
208
+ console.error(chalk.yellow(`\n⚠️ Error: ${error.message}\n`));
208
209
  verifySpinner.warn('Could not verify services');
209
210
  }
210
211
 
@@ -1,11 +1,14 @@
1
1
  const chalk = require('chalk');
2
- const { spawn } = require('child_process');
2
+ const { spawn, spawnSync } = require('child_process');
3
3
  const { requireProject, getProjectConfig, isWaitlistInstalled } = require('../utils/project-helpers');
4
4
 
5
5
  /**
6
- * View waitlist logs from VPS (streaming)
6
+ * View waitlist logs from VPS
7
+ * @param {Object} flags - Optional flags
8
+ * @param {boolean} flags['no-follow'] - Snapshot mode: print tail and exit (non-interactive)
9
+ * @param {number} flags.tail - Number of lines to show (default 100, used with --no-follow)
7
10
  */
8
- async function waitlistLogs() {
11
+ async function waitlistLogs(flags = {}) {
9
12
  requireProject();
10
13
 
11
14
  console.log(chalk.blue.bold('\n📋 Waitlist Logs\n'));
@@ -28,6 +31,20 @@ async function waitlistLogs() {
28
31
 
29
32
  const { vpsHost, vpsUser, vpsAppFolder } = config.deployment;
30
33
 
34
+ if (flags['no-follow']) {
35
+ // Snapshot mode — print tail and exit (non-interactive, suitable for MCP)
36
+ const tail = flags.tail || 100;
37
+ const sshCmd = `cd ${vpsAppFolder}/waitlist && docker-compose -f docker-compose.waitlist.yml logs --no-follow --tail=${tail}`;
38
+
39
+ const result = spawnSync('ssh', [`${vpsUser}@${vpsHost}`, sshCmd], { stdio: 'inherit' });
40
+
41
+ if (result.status !== 0 && result.status !== null) {
42
+ console.log(chalk.yellow(`\n⚠️ Process exited with code ${result.status}\n`));
43
+ }
44
+ return;
45
+ }
46
+
47
+ // Streaming mode (interactive)
31
48
  console.log(chalk.gray('Connecting to VPS and streaming logs...\n'));
32
49
  console.log(chalk.gray('Press Ctrl+C to exit\n'));
33
50
 
package/src/generator.js CHANGED
@@ -1,11 +1,13 @@
1
1
  const path = require('path');
2
2
  const fs = require('fs-extra');
3
3
  const { execSync } = require('child_process');
4
+ const chalk = require('chalk');
4
5
  const { replaceVariables } = require('./utils/variable-replacer');
5
6
  const { copyDirectory } = require('./utils/file-ops');
6
7
  const { generateEnvFile } = require('./utils/env-generator');
7
8
  const { processServiceVariant } = require('./utils/variant-processor');
8
9
  const { resolveVariantChoices } = require('./services/variant-config');
10
+ const logger = require('./utils/logger');
9
11
 
10
12
  /**
11
13
  * Initialize git repository in a service directory
@@ -14,13 +16,13 @@ const { resolveVariantChoices } = require('./services/variant-config');
14
16
  */
15
17
  function initGitRepo(servicePath, serviceName) {
16
18
  try {
17
- console.log(`🔧 Initializing git repository for ${serviceName}...`);
19
+ logger.detail(`Initializing git repository for ${serviceName}`);
18
20
  execSync('git init', { cwd: servicePath, stdio: 'ignore' });
19
21
  execSync('git add .', { cwd: servicePath, stdio: 'ignore' });
20
22
  execSync('git commit -m "Initial commit"', { cwd: servicePath, stdio: 'ignore' });
21
- console.log(`✅ Git repository initialized for ${serviceName}`);
23
+ logger.detail(`Git initialized: ${serviceName}`);
22
24
  } catch (error) {
23
- console.warn(`⚠️ Could not initialize git repository for ${serviceName}: ${error.message}`);
25
+ logger.warn(`Could not initialize git for ${serviceName}: ${error.message}`);
24
26
  }
25
27
  }
26
28
 
@@ -34,12 +36,11 @@ async function generateProject(answers, variantChoices, templateRoot) {
34
36
  const { projectName } = answers;
35
37
 
36
38
  // Define source (template) and destination paths
37
- // templateRoot is now passed as parameter (cache or local dev path)
38
- const projectRoot = path.resolve(__dirname, '../..'); // For root-level files like .github, README.md
39
+ const projectRoot = path.resolve(__dirname, '../..'); // For root-level files
39
40
  const destinationRoot = path.resolve(process.cwd(), projectName);
40
41
 
41
- console.log(`📁 Template source: ${templateRoot}`);
42
- console.log(`📁 Destination: ${destinationRoot}\n`);
42
+ logger.detail(`Template source: ${templateRoot}`);
43
+ logger.detail(`Destination: ${destinationRoot}`);
43
44
 
44
45
  // Ensure destination directory exists
45
46
  await fs.ensureDir(destinationRoot);
@@ -60,8 +61,8 @@ async function generateProject(answers, variantChoices, templateRoot) {
60
61
  // Resolve variant choices for all services
61
62
  const allServiceVariants = resolveVariantChoices(variantChoices);
62
63
 
63
- // Step 1: Process backend service with variants
64
- console.log('🔧 Processing backend service...');
64
+ // Process backend
65
+ console.log(chalk.gray(' Processing backend...'));
65
66
  await processServiceVariant(
66
67
  'backend',
67
68
  allServiceVariants.backend,
@@ -71,11 +72,10 @@ async function generateProject(answers, variantChoices, templateRoot) {
71
72
  );
72
73
  initGitRepo(path.join(destinationRoot, 'backend'), 'backend');
73
74
 
74
- // Step 2: Process admin-portal service with variants
75
- // Note: admin-portal folder might not exist yet in templates, skip if missing
75
+ // Process admin-portal
76
76
  const adminPortalTemplatePath = path.join(templateRoot, 'admin-portal/base');
77
77
  if (await fs.pathExists(adminPortalTemplatePath)) {
78
- console.log('🔧 Processing admin-portal service...');
78
+ console.log(chalk.gray(' Processing admin-portal...'));
79
79
  await processServiceVariant(
80
80
  'admin-portal',
81
81
  allServiceVariants['admin-portal'],
@@ -85,8 +85,8 @@ async function generateProject(answers, variantChoices, templateRoot) {
85
85
  );
86
86
  initGitRepo(path.join(destinationRoot, 'admin-portal'), 'admin-portal');
87
87
  } else {
88
- // Fallback: Copy admin-portal directly without variants (for now)
89
- console.log('📋 Copying admin-portal service (no variants yet)...');
88
+ // Fallback: Copy admin-portal directly without variants
89
+ console.log(chalk.gray(' Copying admin-portal...'));
90
90
  const adminPortalSource = path.join(templateRoot, 'admin-portal');
91
91
  if (await fs.pathExists(adminPortalSource)) {
92
92
  await copyDirectory(adminPortalSource, path.join(destinationRoot, 'admin-portal'));
@@ -95,12 +95,11 @@ async function generateProject(answers, variantChoices, templateRoot) {
95
95
  }
96
96
  }
97
97
 
98
- // Step 3: Process customers-portal service (ONLY if B2B2C selected)
98
+ // Process customers-portal (only if B2B2C)
99
99
  if (variantChoices.userModel === 'b2b2c') {
100
- // Note: customers-portal folder might not exist yet in templates, skip if missing
101
100
  const customersPortalTemplatePath = path.join(templateRoot, 'customers-portal/base');
102
101
  if (await fs.pathExists(customersPortalTemplatePath)) {
103
- console.log('🔧 Processing customers-portal service...');
102
+ console.log(chalk.gray(' Processing customers-portal...'));
104
103
  await processServiceVariant(
105
104
  'customers-portal',
106
105
  allServiceVariants['customers-portal'],
@@ -110,8 +109,7 @@ async function generateProject(answers, variantChoices, templateRoot) {
110
109
  );
111
110
  initGitRepo(path.join(destinationRoot, 'customers-portal'), 'customers-portal');
112
111
  } else {
113
- // Fallback: Copy customers-portal directly without variants (for now)
114
- console.log('📋 Copying customers-portal service (B2B2C mode)...');
112
+ console.log(chalk.gray(' Copying customers-portal...'));
115
113
  const customersPortalSource = path.join(templateRoot, 'customers-portal');
116
114
  if (await fs.pathExists(customersPortalSource)) {
117
115
  await copyDirectory(customersPortalSource, path.join(destinationRoot, 'customers-portal'));
@@ -120,11 +118,11 @@ async function generateProject(answers, variantChoices, templateRoot) {
120
118
  }
121
119
  }
122
120
  } else {
123
- console.log('📋 Skipping customers-portal (B2B mode - admin users only)');
121
+ logger.detail('Skipping customers-portal (B2B mode)');
124
122
  }
125
123
 
126
- // Step 4: Process infrastructure with variants (docker-compose files conditionally include customers-portal)
127
- console.log('🔧 Processing infrastructure...');
124
+ // Process infrastructure
125
+ console.log(chalk.gray(' Processing infrastructure...'));
128
126
  await processServiceVariant(
129
127
  'infrastructure',
130
128
  allServiceVariants.infrastructure,
@@ -134,7 +132,8 @@ async function generateProject(answers, variantChoices, templateRoot) {
134
132
  );
135
133
  initGitRepo(path.join(destinationRoot, 'infrastructure'), 'infrastructure');
136
134
 
137
- console.log('📋 Copying website...');
135
+ // Process website
136
+ console.log(chalk.gray(' Processing website...'));
138
137
  await copyDirectory(
139
138
  path.join(templateRoot, 'website'),
140
139
  path.join(destinationRoot, 'website')
@@ -142,14 +141,9 @@ async function generateProject(answers, variantChoices, templateRoot) {
142
141
  await replaceVariables(path.join(destinationRoot, 'website'), variables);
143
142
  initGitRepo(path.join(destinationRoot, 'website'), 'website');
144
143
 
145
- // Step 5: Copy additional files (from project root, not services/)
146
- console.log('📋 Copying additional files...');
147
- const additionalFiles = [
148
- '.github',
149
- 'README.md',
150
- '.gitignore',
151
- 'LICENSE'
152
- ];
144
+ // Copy additional files
145
+ logger.detail('Copying additional files...');
146
+ const additionalFiles = ['.github', 'README.md', '.gitignore', 'LICENSE'];
153
147
 
154
148
  for (const file of additionalFiles) {
155
149
  const sourcePath = path.join(projectRoot, file);
@@ -166,16 +160,26 @@ async function generateProject(answers, variantChoices, templateRoot) {
166
160
  }
167
161
  }
168
162
 
169
- // Step 6: Generate .env file with localhost defaults
170
- console.log('\n🔐 Generating .env file with secure secrets...');
163
+ // Copy MCP configuration files from template root
164
+ logger.detail('Copying MCP configuration files...');
165
+ const mcpFiles = ['CLAUDE.md', '.mcp.json'];
166
+ for (const file of mcpFiles) {
167
+ const sourcePath = path.join(templateRoot, file);
168
+ const destPath = path.join(destinationRoot, file);
169
+ if (await fs.pathExists(sourcePath)) {
170
+ await fs.copy(sourcePath, destPath);
171
+ }
172
+ }
173
+
174
+ // Generate .env file
175
+ console.log(chalk.gray(' Generating environment file...'));
171
176
  const { envPath } = await generateEnvFile(destinationRoot, answers);
172
- console.log(`✅ Environment file created: ${envPath}`);
177
+ logger.detail(`Environment file: ${envPath}`);
173
178
 
174
- // Step 7: Create .launchframe marker file with variant metadata
175
- console.log('📝 Creating LaunchFrame marker file...');
179
+ // Create .launchframe marker file
180
+ logger.detail('Creating project marker file...');
176
181
  const markerPath = path.join(destinationRoot, '.launchframe');
177
182
 
178
- // Determine which services were installed
179
183
  const installedServices = ['backend', 'admin-portal', 'infrastructure', 'website'];
180
184
  if (variantChoices.userModel === 'b2b2c') {
181
185
  installedServices.push('customers-portal');
@@ -188,12 +192,9 @@ async function generateProject(answers, variantChoices, templateRoot) {
188
192
  projectDisplayName: answers.projectDisplayName,
189
193
  deployConfigured: false,
190
194
  installedServices: installedServices,
191
- // Store variant choices for future reference
192
195
  variants: variantChoices
193
196
  };
194
197
  await fs.writeJson(markerPath, markerContent, { spaces: 2 });
195
-
196
- console.log('✅ Base project generated with variants applied');
197
198
  }
198
199
 
199
200
  module.exports = { generateProject };
package/src/index.js CHANGED
@@ -2,6 +2,17 @@
2
2
 
3
3
  const chalk = require('chalk');
4
4
  const { isLaunchFrameProject } = require('./utils/project-helpers');
5
+ const logger = require('./utils/logger');
6
+ const { initTelemetry, trackEvent, sanitize, setTelemetryEnabled, showTelemetryStatus } = require('./utils/telemetry');
7
+
8
+ // Detect locally linked version: npm link installs to global node_modules
9
+ // as a symlink. When running from a real install, __dirname is inside the
10
+ // global node_modules folder. When linked, it resolves to the source directory.
11
+ const isDevMode = !__dirname.includes('node_modules');
12
+ if (isDevMode) {
13
+ const packageJson = require('../package.json');
14
+ console.log(chalk.yellow(`⚠ Running locally linked CLI v${packageJson.version} (${__dirname})`));
15
+ }
5
16
 
6
17
  // Import commands
7
18
  const { init } = require('./commands/init');
@@ -9,6 +20,7 @@ const { deployConfigure } = require('./commands/deploy-configure');
9
20
  const { deploySetEnv } = require('./commands/deploy-set-env');
10
21
  const { deployInit } = require('./commands/deploy-init');
11
22
  const { deployUp } = require('./commands/deploy-up');
23
+ const { deployBuild } = require('./commands/deploy-build');
12
24
  const { waitlistDeploy } = require('./commands/waitlist-deploy');
13
25
  const { waitlistUp } = require('./commands/waitlist-up');
14
26
  const { waitlistDown } = require('./commands/waitlist-down');
@@ -17,6 +29,10 @@ const { dockerBuild } = require('./commands/docker-build');
17
29
  const { dockerUp } = require('./commands/docker-up');
18
30
  const { dockerDown } = require('./commands/docker-down');
19
31
  const { dockerLogs } = require('./commands/docker-logs');
32
+ const { migrateRun } = require('./commands/migration-run');
33
+ const { migrateCreate } = require('./commands/migration-create');
34
+ const { migrateRevert } = require('./commands/migration-revert');
35
+ const { databaseConsole } = require('./commands/database-console');
20
36
  const { dockerDestroy } = require('./commands/docker-destroy');
21
37
  const { doctor } = require('./commands/doctor');
22
38
  const { help } = require('./commands/help');
@@ -25,7 +41,13 @@ const {
25
41
  serviceList,
26
42
  serviceRemove
27
43
  } = require('./commands/service');
44
+ const { moduleAdd, moduleList } = require('./commands/module');
28
45
  const { cacheClear, cacheInfo, cacheUpdate } = require('./commands/cache');
46
+ const { devAddUser } = require('./commands/dev-add-user');
47
+ const { devQueue } = require('./commands/dev-queue');
48
+ const { devLogo } = require('./commands/dev-logo');
49
+ const { devNpmInstall } = require('./commands/dev-npm-install');
50
+ const { deploySyncFeatures } = require('./commands/deploy-sync-features');
29
51
 
30
52
  // Get command and arguments
31
53
  const command = process.argv[2];
@@ -62,9 +84,23 @@ function parseFlags(args) {
62
84
  * Main CLI router
63
85
  */
64
86
  async function main() {
87
+ initTelemetry();
88
+
65
89
  const inProject = isLaunchFrameProject();
66
90
  const flags = parseFlags(args);
67
91
 
92
+ // Handle version flag (only as standalone command)
93
+ if (command === '--version') {
94
+ const packageJson = require('../package.json');
95
+ console.log(packageJson.version);
96
+ process.exit(0);
97
+ }
98
+
99
+ // Set verbose mode globally
100
+ if (flags.verbose || flags.v) {
101
+ logger.setVerbose(true);
102
+ }
103
+
68
104
  // No command provided
69
105
  if (!command) {
70
106
  help();
@@ -74,7 +110,7 @@ async function main() {
74
110
  // Route commands
75
111
  switch (command) {
76
112
  case 'init':
77
- await init({
113
+ await init({
78
114
  projectName: flags['project-name'],
79
115
  tenancy: flags['tenancy'],
80
116
  userModel: flags['user-model']
@@ -92,6 +128,12 @@ async function main() {
92
128
  case 'deploy:up':
93
129
  await deployUp();
94
130
  break;
131
+ case 'deploy:build':
132
+ await deployBuild(args[1]); // Optional service name
133
+ break;
134
+ case 'deploy:sync-features':
135
+ await deploySyncFeatures(flags);
136
+ break;
95
137
  case 'waitlist:deploy':
96
138
  await waitlistDeploy();
97
139
  break;
@@ -102,33 +144,45 @@ async function main() {
102
144
  await waitlistDown();
103
145
  break;
104
146
  case 'waitlist:logs':
105
- await waitlistLogs();
147
+ await waitlistLogs(flags);
106
148
  break;
107
149
  case 'docker:build':
108
- await dockerBuild();
150
+ await dockerBuild(args[1]); // Optional service name
109
151
  break;
110
152
  case 'docker:up':
111
- await dockerUp(args[1]); // Pass optional service name
153
+ await dockerUp(args[1] || flags.service, flags);
112
154
  break;
113
155
  case 'docker:down':
114
156
  await dockerDown();
115
157
  break;
116
158
  case 'docker:logs':
117
- await dockerLogs();
159
+ await dockerLogs(args[1] || flags.service, flags);
118
160
  break;
119
161
  case 'docker:destroy':
120
162
  await dockerDestroy({ force: flags.force || flags.f });
121
163
  break;
164
+ case 'migration:run':
165
+ await migrateRun();
166
+ break;
167
+ case 'migration:create':
168
+ await migrateCreate(args[1] || flags.name);
169
+ break;
170
+ case 'migration:revert':
171
+ await migrateRevert();
172
+ break;
173
+ case 'database:console':
174
+ await databaseConsole({ remote: flags.remote, query: flags.query });
175
+ break;
122
176
  case 'doctor':
123
177
  await doctor();
124
178
  break;
125
179
  case 'service:add':
126
- if (!args[1]) {
180
+ if (!args[1] && !flags.service) {
127
181
  console.error(chalk.red('Error: Service name required'));
128
182
  console.log('Usage: launchframe service:add <service-name>');
129
183
  process.exit(1);
130
184
  }
131
- await serviceAdd(args[1]);
185
+ await serviceAdd(args[1] || flags.service, flags);
132
186
  break;
133
187
  case 'service:list':
134
188
  await serviceList();
@@ -141,8 +195,19 @@ async function main() {
141
195
  }
142
196
  await serviceRemove(args[1]);
143
197
  break;
198
+ case 'module:add':
199
+ if (!args[1] && !flags.name) {
200
+ console.error(chalk.red('Error: Module name required'));
201
+ console.log('Usage: launchframe module:add <module-name>');
202
+ process.exit(1);
203
+ }
204
+ await moduleAdd(args[1] || flags.name, flags);
205
+ break;
206
+ case 'module:list':
207
+ await moduleList();
208
+ break;
144
209
  case 'cache:clear':
145
- await cacheClear();
210
+ await cacheClear(flags);
146
211
  break;
147
212
  case 'cache:info':
148
213
  await cacheInfo();
@@ -150,16 +215,56 @@ async function main() {
150
215
  case 'cache:update':
151
216
  await cacheUpdate();
152
217
  break;
218
+ case 'dev:add-user':
219
+ await devAddUser();
220
+ break;
221
+ case 'dev:queue':
222
+ await devQueue();
223
+ break;
224
+ case 'dev:logo':
225
+ await devLogo(args[1] || flags.svg);
226
+ break;
227
+ case 'dev:npm-install':
228
+ if (!args[1]) {
229
+ console.error(chalk.red('Error: Service name required'));
230
+ console.log('Usage: launchframe dev:npm-install <service> [packages...]');
231
+ process.exit(1);
232
+ }
233
+ await devNpmInstall(args[1], args.slice(2));
234
+ break;
235
+ case 'telemetry':
236
+ if (flags.disable) {
237
+ setTelemetryEnabled(false);
238
+ } else if (flags.enable) {
239
+ setTelemetryEnabled(true);
240
+ } else {
241
+ showTelemetryStatus();
242
+ }
243
+ break;
153
244
  case 'help':
154
245
  case '--help':
155
246
  case '-h':
156
247
  help();
157
248
  break;
158
249
  default:
159
- console.error(chalk.red(`\n❌ Unknown command: ${command}\n`));
250
+ console.error(chalk.red(`\nUnknown command: ${command}\n`));
160
251
  help();
161
252
  process.exit(1);
162
253
  }
163
254
  }
164
255
 
165
- main();
256
+ main()
257
+ .then(() => {
258
+ if (command && command !== 'help' && command !== '--help' && command !== '-h' && command !== '--version') {
259
+ trackEvent('command_executed', { command, success: true });
260
+ }
261
+ })
262
+ .catch((error) => {
263
+ trackEvent('command_executed', {
264
+ command,
265
+ success: false,
266
+ error_message: sanitize(error.message)
267
+ });
268
+ console.error(chalk.red(error.message));
269
+ process.exit(1);
270
+ });