@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,85 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const chalk = require('chalk');
4
+ const { spawnSync, spawn } = require('child_process');
5
+ const { requireProject } = require('../utils/project-helpers');
6
+
7
+ async function devQueue() {
8
+ requireProject();
9
+
10
+ const infrastructurePath = path.join(process.cwd(), 'infrastructure');
11
+
12
+ if (!fs.existsSync(infrastructurePath)) {
13
+ console.error(chalk.red('\n❌ Error: infrastructure/ directory not found'));
14
+ console.log(chalk.gray('Make sure you are in the root of your LaunchFrame project.\n'));
15
+ process.exit(1);
16
+ }
17
+
18
+ // Check backend container is running
19
+ const psResult = spawnSync(
20
+ 'docker',
21
+ [
22
+ 'compose', '-f', 'docker-compose.yml', '-f', 'docker-compose.dev.yml',
23
+ 'ps', '--status', 'running', '-q', 'backend'
24
+ ],
25
+ { cwd: infrastructurePath, encoding: 'utf8' }
26
+ );
27
+
28
+ if (!psResult.stdout || psResult.stdout.trim() === '') {
29
+ console.error(chalk.red('\n❌ Backend container is not running.'));
30
+ console.log(chalk.gray('Start local services first:'));
31
+ console.log(chalk.white(' launchframe docker:up\n'));
32
+ process.exit(1);
33
+ }
34
+
35
+ // Read BULL_ADMIN_TOKEN from backend container
36
+ const tokenResult = spawnSync(
37
+ 'docker',
38
+ [
39
+ 'compose', '-f', 'docker-compose.yml', '-f', 'docker-compose.dev.yml',
40
+ 'exec', '-T', 'backend', 'sh', '-c', 'echo $BULL_ADMIN_TOKEN'
41
+ ],
42
+ { cwd: infrastructurePath, encoding: 'utf8' }
43
+ );
44
+
45
+ const token = (tokenResult.stdout || '').trim();
46
+
47
+ if (!token) {
48
+ console.error(chalk.red('\n❌ Could not read BULL_ADMIN_TOKEN from backend container.'));
49
+ console.log(chalk.gray('Make sure the backend is fully started and BULL_ADMIN_TOKEN is set in your .env\n'));
50
+ process.exit(1);
51
+ }
52
+
53
+ // Read BACKEND_PORT from backend container (fallback to 4000)
54
+ const portResult = spawnSync(
55
+ 'docker',
56
+ [
57
+ 'compose', '-f', 'docker-compose.yml', '-f', 'docker-compose.dev.yml',
58
+ 'exec', '-T', 'backend', 'sh', '-c', 'echo $BACKEND_PORT'
59
+ ],
60
+ { cwd: infrastructurePath, encoding: 'utf8' }
61
+ );
62
+
63
+ const port = (portResult.stdout || '').trim() || '4000';
64
+
65
+ const url = `http://localhost:${port}/admin/queues/${token}`;
66
+
67
+ console.log(chalk.green('\n Opening Bull queue dashboard...'));
68
+ console.log(chalk.gray(` ${url}\n`));
69
+
70
+ // Open URL with platform-appropriate command
71
+ const platform = process.platform;
72
+ let openCmd;
73
+ if (platform === 'darwin') {
74
+ openCmd = 'open';
75
+ } else if (platform === 'win32') {
76
+ openCmd = 'start';
77
+ } else {
78
+ openCmd = 'xdg-open';
79
+ }
80
+
81
+ const child = spawn(openCmd, [url], { detached: true, stdio: 'ignore' });
82
+ child.unref();
83
+ }
84
+
85
+ module.exports = { devQueue };
@@ -6,8 +6,9 @@ const { requireProject } = require('../utils/project-helpers');
6
6
 
7
7
  /**
8
8
  * Build Docker images for the project
9
+ * @param {string} [serviceName] - Optional service to build (builds all if omitted)
9
10
  */
10
- async function dockerBuild() {
11
+ async function dockerBuild(serviceName) {
11
12
  requireProject();
12
13
 
13
14
  const infrastructurePath = path.join(process.cwd(), 'infrastructure');
@@ -18,11 +19,11 @@ async function dockerBuild() {
18
19
  process.exit(1);
19
20
  }
20
21
 
21
- console.log(chalk.blue.bold('\n🔨 Building Docker Images\n'));
22
- console.log(chalk.gray('This will build all service images for local development...\n'));
22
+ const target = serviceName ? `${serviceName} service` : 'all services';
23
+ console.log(chalk.blue.bold(`\n🔨 Building Docker Images (${target})\n`));
23
24
 
24
25
  try {
25
- const buildCommand = 'docker-compose -f docker-compose.yml -f docker-compose.dev.yml build';
26
+ const buildCommand = `docker-compose -f docker-compose.yml -f docker-compose.dev.yml build${serviceName ? ` ${serviceName}` : ''}`;
26
27
 
27
28
  console.log(chalk.gray(`Running: ${buildCommand}\n`));
28
29
 
@@ -32,8 +33,10 @@ async function dockerBuild() {
32
33
  });
33
34
 
34
35
  console.log(chalk.green.bold('\n✅ Docker images built successfully!\n'));
35
- console.log(chalk.white('Next steps:'));
36
- console.log(chalk.gray(' launchframe docker:up # Start all services\n'));
36
+ if (!serviceName) {
37
+ console.log(chalk.white('Next steps:'));
38
+ console.log(chalk.gray(' launchframe docker:up # Start all services\n'));
39
+ }
37
40
 
38
41
  } catch (error) {
39
42
  console.error(chalk.red('\n❌ Error during build:'), error.message);
@@ -1,13 +1,17 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
3
  const chalk = require('chalk');
4
- const { spawn } = require('child_process');
4
+ const { spawn, execSync } = require('child_process');
5
5
  const { requireProject } = require('../utils/project-helpers');
6
6
 
7
7
  /**
8
8
  * View logs from Docker services
9
+ * @param {string} service - Optional service name to filter logs
10
+ * @param {Object} flags - Optional flags
11
+ * @param {boolean} flags['no-follow'] - Snapshot mode: print lines and exit (non-interactive)
12
+ * @param {number} flags.tail - Number of lines to show (default 100, only used with --no-follow)
9
13
  */
10
- async function dockerLogs() {
14
+ async function dockerLogs(service, flags = {}) {
11
15
  requireProject();
12
16
 
13
17
  const infrastructurePath = path.join(process.cwd(), 'infrastructure');
@@ -18,9 +22,24 @@ async function dockerLogs() {
18
22
  process.exit(1);
19
23
  }
20
24
 
21
- // Get optional service name from args (e.g., launchframe docker:logs backend)
22
- const service = process.argv[3];
25
+ const noFollow = flags['no-follow'];
23
26
 
27
+ if (noFollow) {
28
+ // Snapshot mode — print tail and exit (non-interactive, suitable for MCP)
29
+ const tail = flags.tail || 100;
30
+ const args = ['-f', 'docker-compose.yml', '-f', 'docker-compose.dev.yml', 'logs', '--no-follow', '--tail', String(tail)];
31
+ if (service) args.push(service);
32
+
33
+ try {
34
+ execSync(`docker-compose ${args.join(' ')}`, { cwd: infrastructurePath, stdio: 'inherit' });
35
+ } catch (error) {
36
+ console.error(chalk.red('\n❌ Error viewing logs:'), error.message);
37
+ process.exit(1);
38
+ }
39
+ return;
40
+ }
41
+
42
+ // Streaming mode (interactive)
24
43
  console.log(chalk.blue.bold('\n📋 Docker Service Logs\n'));
25
44
 
26
45
  if (service) {
@@ -34,14 +53,14 @@ async function dockerLogs() {
34
53
 
35
54
  try {
36
55
  const logsCommand = 'docker-compose';
37
- const args = ['-f', 'docker-compose.yml', '-f', 'docker-compose.dev.yml', 'logs', '-f'];
56
+ const spawnArgs = ['-f', 'docker-compose.yml', '-f', 'docker-compose.dev.yml', 'logs', '-f'];
38
57
 
39
58
  if (service) {
40
- args.push(service);
59
+ spawnArgs.push(service);
41
60
  }
42
61
 
43
62
  // Use spawn to stream output in real-time
44
- const child = spawn(logsCommand, args, {
63
+ const child = spawn(logsCommand, spawnArgs, {
45
64
  cwd: infrastructurePath,
46
65
  stdio: 'inherit',
47
66
  shell: true
@@ -7,8 +7,10 @@ const { requireProject, getProjectConfig } = require('../utils/project-helpers')
7
7
  /**
8
8
  * Start Docker services (all or specific service)
9
9
  * @param {string} serviceName - Optional service name to start (e.g., 'docs', 'backend')
10
+ * @param {Object} flags - Optional flags
11
+ * @param {boolean} flags.detach - Run detached (docker-compose up -d) instead of watch mode
10
12
  */
11
- async function dockerUp(serviceName) {
13
+ async function dockerUp(serviceName, flags = {}) {
12
14
  requireProject();
13
15
 
14
16
  const infrastructurePath = path.join(process.cwd(), 'infrastructure');
@@ -19,14 +21,26 @@ async function dockerUp(serviceName) {
19
21
  process.exit(1);
20
22
  }
21
23
 
24
+ if (flags.detach) {
25
+ // Detached mode — start services in background (no watch, no blocking)
26
+ const upCommand = serviceName
27
+ ? `docker-compose -f docker-compose.yml -f docker-compose.dev.yml up -d ${serviceName}`
28
+ : 'docker-compose -f docker-compose.yml -f docker-compose.dev.yml up -d';
29
+
30
+ console.log(chalk.gray(`Running: ${upCommand}\n`));
31
+ execSync(upCommand, { cwd: infrastructurePath, stdio: 'inherit' });
32
+ console.log(chalk.green.bold('\n✅ Services started in detached mode.\n'));
33
+ return;
34
+ }
35
+
22
36
  // Check Docker Compose version for watch support
23
37
  try {
24
38
  const composeVersion = execSync('docker compose version', { encoding: 'utf8' });
25
39
  const versionMatch = composeVersion.match(/v?(\d+)\.(\d+)\.(\d+)/);
26
-
40
+
27
41
  if (versionMatch) {
28
42
  const [, major, minor] = versionMatch.map(Number);
29
-
43
+
30
44
  if (major < 2 || (major === 2 && minor < 22)) {
31
45
  console.error(chalk.red('\n❌ Error: Docker Compose v2.22+ is required for watch support'));
32
46
  console.log(chalk.yellow(`Current version: Docker Compose v${major}.${minor}`));
@@ -7,16 +7,21 @@ const { isLaunchFrameProject, isWaitlistInstalled } = require('../utils/project-
7
7
  function help() {
8
8
  const inProject = isLaunchFrameProject();
9
9
 
10
- console.log(chalk.blue.bold('\n🚀 LaunchFrame CLI\n'));
10
+ console.log(chalk.blue.bold('\nLaunchFrame CLI\n'));
11
11
  console.log(chalk.white('Usage:'));
12
- console.log(chalk.gray(' launchframe [command]\n'));
12
+ console.log(chalk.gray(' launchframe [command] [options]\n'));
13
+ console.log(chalk.white('Global options:'));
14
+ console.log(chalk.gray(' --verbose, -v Show detailed output'));
15
+ console.log(chalk.gray(' --version Show version number\n'));
13
16
 
14
17
  if (inProject) {
15
18
  console.log(chalk.white('Deployment commands:'));
16
19
  console.log(chalk.gray(' deploy:configure Configure production deployment settings'));
17
20
  console.log(chalk.gray(' deploy:set-env Configure production environment variables'));
18
21
  console.log(chalk.gray(' deploy:init Initialize VPS and build Docker images'));
19
- console.log(chalk.gray(' deploy:up Start services on VPS\n'));
22
+ console.log(chalk.gray(' deploy:up Start services on VPS'));
23
+ console.log(chalk.gray(' deploy:build [service] Build, push, and deploy (all or specific service)'));
24
+ console.log(chalk.gray(' deploy:sync-features Sync local features and plan assignments to production\n'));
20
25
 
21
26
  // Conditionally show waitlist commands
22
27
  if (isWaitlistInstalled()) {
@@ -33,18 +38,37 @@ function help() {
33
38
  console.log(chalk.gray(' docker:logs [service] View logs from all services or specific service'));
34
39
  console.log(chalk.gray(' docker:destroy Remove all resources (containers, volumes, images)'));
35
40
  console.log(chalk.gray(' --force, -f Skip confirmation prompt\n'));
41
+ console.log(chalk.white('Database Migrations:'));
42
+ console.log(chalk.gray(' migration:run Run pending database migrations'));
43
+ console.log(chalk.gray(' migration:create Create new database migration'));
44
+ console.log(chalk.gray(' migration:revert Revert last database migration\n'));
45
+ console.log(chalk.white('Database:'));
46
+ console.log(chalk.gray(' database:console Open a PostgreSQL console (local)'));
47
+ console.log(chalk.gray(' --remote Connect to the production database\n'));
48
+
36
49
  console.log(chalk.white('Service Management:'));
37
50
  console.log(chalk.gray(' service:add <name> Add an optional service to your project'));
38
51
  console.log(chalk.gray(' service:list List available services'));
39
52
  console.log(chalk.gray(' service:remove <name> Remove installed service\n'));
53
+ console.log(chalk.white('Module Management:'));
54
+ console.log(chalk.gray(' module:add <name> Add a module to your project'));
55
+ console.log(chalk.gray(' module:list List available modules\n'));
40
56
  console.log(chalk.white('Available Services:'));
41
57
  console.log(chalk.gray(' waitlist Coming soon page with email collection\n'));
42
58
  console.log(chalk.white('Cache Management:'));
43
59
  console.log(chalk.gray(' cache:info Show cache location, size, and cached services'));
44
60
  console.log(chalk.gray(' cache:update Force update cache to latest version'));
45
61
  console.log(chalk.gray(' cache:clear Delete cache (re-download on next use)\n'));
62
+ console.log(chalk.white('Dev Helpers:'));
63
+ console.log(chalk.gray(' dev:add-user Generate and insert a random test user into the local database'));
64
+ console.log(chalk.gray(' dev:queue Open the Bull queue dashboard in the browser'));
65
+ console.log(chalk.gray(' dev:logo Generate and inject favicon/logo assets from logo.svg'));
66
+ console.log(chalk.gray(' dev:npm-install <service> [packages...] Install npm packages using node:20-alpine\n'));
46
67
  console.log(chalk.white('Other commands:'));
47
68
  console.log(chalk.gray(' doctor Check project health and configuration'));
69
+ console.log(chalk.gray(' telemetry Show telemetry status'));
70
+ console.log(chalk.gray(' --enable Enable anonymous usage data collection'));
71
+ console.log(chalk.gray(' --disable Disable anonymous usage data collection'));
48
72
  console.log(chalk.gray(' help Show this help message\n'));
49
73
  console.log(chalk.white('Examples:'));
50
74
  console.log(chalk.gray(' # Deploy full app to production'));
@@ -70,19 +94,19 @@ function help() {
70
94
  } else {
71
95
  console.log(chalk.white('Available commands:'));
72
96
  console.log(chalk.gray(' init Initialize a new LaunchFrame project'));
73
- console.log(chalk.gray(' --project-name <name> Project name (skips prompt)'));
74
- console.log(chalk.gray(' --tenancy <single|multi> Tenancy model (skips prompt)'));
75
- console.log(chalk.gray(' --user-model <b2b|b2b2c> User model (skips prompt)'));
76
97
  console.log(chalk.gray(' help Show this help message\n'));
98
+ console.log(chalk.white('Telemetry:'));
99
+ console.log(chalk.gray(' telemetry Show telemetry status'));
100
+ console.log(chalk.gray(' --enable Enable anonymous usage data collection'));
101
+ console.log(chalk.gray(' --disable Disable anonymous usage data collection\n'));
77
102
  console.log(chalk.white('Cache Management:'));
78
103
  console.log(chalk.gray(' cache:info Show cache location, size, and cached services'));
79
104
  console.log(chalk.gray(' cache:update Force update cache to latest version'));
80
105
  console.log(chalk.gray(' cache:clear Delete cache (re-download on next use)\n'));
81
106
  console.log(chalk.white('Examples:'));
82
- console.log(chalk.gray(' # Interactive mode'));
83
107
  console.log(chalk.gray(' launchframe init\n'));
84
- console.log(chalk.gray(' # Non-interactive mode'));
85
- console.log(chalk.gray(' launchframe init --project-name my-saas --tenancy single --user-model b2b\n'));
108
+ console.log(chalk.gray(' # With verbose output'));
109
+ console.log(chalk.gray(' launchframe init --verbose\n'));
86
110
  }
87
111
  }
88
112
 
@@ -6,6 +6,8 @@ const { generateProject } = require('../generator');
6
6
  const { checkGitHubAccess, showAccessDeniedMessage } = require('../utils/github-access');
7
7
  const { ensureCacheReady } = require('../utils/service-cache');
8
8
  const { isLaunchFrameProject } = require('../utils/project-helpers');
9
+ const logger = require('../utils/logger');
10
+ const { trackEvent } = require('../utils/telemetry');
9
11
 
10
12
  /**
11
13
  * Check if running in development mode (local) vs production (npm install)
@@ -23,28 +25,30 @@ function isDevMode() {
23
25
  * @param {string} options.userModel - User model: 'b2b' or 'b2b2c' (skips prompt if provided)
24
26
  */
25
27
  async function init(options = {}) {
26
- console.log(chalk.blue.bold('\n🚀 Welcome to LaunchFrame!\n'));
28
+ console.log(chalk.blue.bold('\nLaunchFrame\n'));
27
29
 
28
30
  // Check if in development mode
29
31
  const devMode = isDevMode();
30
-
32
+
31
33
  if (!devMode) {
32
34
  // Production mode: Check GitHub access
33
- console.log(chalk.blue('🔍 Checking repository access...\n'));
34
-
35
+ console.log(chalk.gray('Checking repository access...'));
36
+
35
37
  const accessCheck = await checkGitHubAccess();
36
-
38
+
37
39
  if (!accessCheck.hasAccess) {
38
- // No access - show purchase/setup message
39
40
  showAccessDeniedMessage();
40
- process.exit(1); // Exit with error code
41
+ trackEvent('command_executed', { command: 'init', success: false, error_message: 'access_denied' });
42
+ await new Promise((resolve) => setTimeout(resolve, 100));
43
+ process.exit(1);
41
44
  }
42
-
43
- console.log(chalk.green('Repository access confirmed\n'));
45
+
46
+ console.log(chalk.green('Repository access confirmed\n'));
44
47
  }
48
+
45
49
  // Check if already in a LaunchFrame project
46
50
  if (isLaunchFrameProject()) {
47
- console.error(chalk.red('\n❌ Error: Already in a LaunchFrame project'));
51
+ console.error(chalk.red('Error: Already in a LaunchFrame project'));
48
52
  console.log(chalk.gray('Use other commands to manage your project, or run init from outside the project.\n'));
49
53
  process.exit(1);
50
54
  }
@@ -54,12 +58,10 @@ async function init(options = {}) {
54
58
 
55
59
  // If project name provided via flag, skip prompts
56
60
  if (options.projectName) {
57
- // Validate project name format
58
61
  if (!/^[a-z0-9-]+$/.test(options.projectName)) {
59
62
  throw new Error('Project name must contain only lowercase letters, numbers, and hyphens');
60
63
  }
61
64
 
62
- // Auto-generate display name from slug
63
65
  const projectDisplayName = options.projectName
64
66
  .split('-')
65
67
  .map(word => word.charAt(0).toUpperCase() + word.slice(1))
@@ -75,91 +77,81 @@ async function init(options = {}) {
75
77
  projectNameCamel: options.projectName.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase())
76
78
  };
77
79
 
78
- console.log(chalk.gray(`Using project name: ${options.projectName}`));
79
- console.log(chalk.gray(`Using display name: ${projectDisplayName}`));
80
- console.log(chalk.gray(`Using description: ${projectDescription}\n`));
80
+ logger.detail(`Project name: ${options.projectName}`);
81
+ logger.detail(`Display name: ${projectDisplayName}`);
82
+ logger.detail(`Description: ${projectDescription}`);
81
83
  } else {
82
- // Get user inputs via interactive prompts
83
84
  answers = await runInitPrompts();
84
85
  }
85
86
 
86
87
  // Get variant selections (multi-tenancy, B2B vs B2B2C)
87
88
  let variantChoices;
88
-
89
- // If both flags provided, skip variant prompts
89
+
90
90
  if (options.tenancy && options.userModel) {
91
- // Validate tenancy value
92
91
  if (!['single', 'multi'].includes(options.tenancy)) {
93
92
  throw new Error('Invalid --tenancy value. Must be "single" or "multi"');
94
93
  }
95
-
96
- // Validate userModel value
94
+
97
95
  if (!['b2b', 'b2b2c'].includes(options.userModel)) {
98
96
  throw new Error('Invalid --user-model value. Must be "b2b" or "b2b2c"');
99
97
  }
100
-
101
- // Convert short flag values to full variant names
98
+
102
99
  const tenancyMap = {
103
100
  'single': 'single-tenant',
104
101
  'multi': 'multi-tenant'
105
102
  };
106
-
103
+
107
104
  variantChoices = {
108
105
  tenancy: tenancyMap[options.tenancy],
109
106
  userModel: options.userModel
110
107
  };
111
-
112
- console.log(chalk.gray(`Using tenancy: ${options.tenancy}`));
113
- console.log(chalk.gray(`Using user model: ${options.userModel}\n`));
108
+
109
+ logger.detail(`Tenancy: ${options.tenancy}`);
110
+ logger.detail(`User model: ${options.userModel}`);
114
111
  } else {
115
- // Run interactive variant prompts
116
112
  variantChoices = await runVariantPrompts();
117
113
  }
118
114
 
119
- // Determine which services are needed based on variant choices
120
- const requiredServices = [
121
- 'backend',
122
- 'admin-portal',
123
- 'infrastructure',
124
- 'website'
125
- ];
126
-
127
- // Add customers-portal only if B2B2C mode
115
+ // Determine which services are needed
116
+ const requiredServices = ['backend', 'admin-portal', 'infrastructure', 'website'];
117
+
128
118
  if (variantChoices.userModel === 'b2b2c') {
129
119
  requiredServices.push('customers-portal');
130
120
  }
131
121
 
132
- // Determine template source (dev mode = local, production = cache)
122
+ // Determine template source
133
123
  let templateRoot;
134
-
124
+
135
125
  if (devMode) {
136
- // Dev mode: Use local services directory
137
126
  templateRoot = path.resolve(__dirname, '../../../services');
138
- console.log(chalk.gray(`[DEV MODE] Using local services: ${templateRoot}\n`));
127
+ logger.detail(`[DEV MODE] Using local services: ${templateRoot}`);
139
128
  } else {
140
- // Production mode: Use cache
141
129
  try {
142
130
  templateRoot = await ensureCacheReady(requiredServices);
143
131
  } catch (error) {
144
- console.error(chalk.red(`\n❌ Error: ${error.message}\n`));
132
+ console.error(chalk.red(`Error: ${error.message}\n`));
145
133
  process.exit(1);
146
134
  }
147
135
  }
148
136
 
149
- // Generate project with variant selections
150
- console.log(chalk.yellow('\n⚙️ Generating project...\n'));
137
+ // Generate project
138
+ console.log(chalk.white('\nGenerating project...\n'));
151
139
  await generateProject(answers, variantChoices, templateRoot);
152
140
 
153
- console.log(chalk.green.bold('\n✅ Project generated successfully!\n'));
141
+ trackEvent('command_executed', {
142
+ command: 'init',
143
+ success: true,
144
+ tenancy: variantChoices.tenancy,
145
+ user_model: variantChoices.userModel
146
+ });
147
+
148
+ console.log(chalk.green.bold('\nProject created successfully!\n'));
154
149
  console.log(chalk.white('Next steps:'));
155
- console.log(chalk.white(` cd ${answers.projectName}`));
156
- console.log(chalk.white(' launchframe docker:up # Start all services\n'));
157
- console.log(chalk.gray('Optional:'));
158
- console.log(chalk.gray(' # Review and customize infrastructure/.env if needed'));
159
- console.log(chalk.gray(' launchframe docker:build # Rebuild images after changes\n'));
150
+ console.log(chalk.gray(` cd ${answers.projectName}`));
151
+ console.log(chalk.gray(' launchframe docker:up\n'));
160
152
 
161
153
  } catch (error) {
162
- console.error(chalk.red('\n❌ Error:'), error.message);
154
+ console.error(chalk.red('Error:'), error.message);
163
155
  process.exit(1);
164
156
  }
165
157
  }
@@ -0,0 +1,38 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const chalk = require('chalk');
4
+ const { execSync } = require('child_process');
5
+ const { requireProject } = require('../utils/project-helpers');
6
+
7
+ async function migrateCreate(migrationName) {
8
+ requireProject();
9
+
10
+ const infrastructurePath = path.join(process.cwd(), 'infrastructure');
11
+
12
+ if (!fs.existsSync(infrastructurePath)) {
13
+ console.error(chalk.red('\n❌ Error: infrastructure/ directory not found'));
14
+ console.log(chalk.gray('Make sure you are in the root of your LaunchFrame project.\n'));
15
+ process.exit(1);
16
+ }
17
+
18
+ if (!migrationName) {
19
+ console.error(chalk.red('\n❌ Error: migration name is required'));
20
+ console.log(chalk.gray('Usage: launchframe migrate:create <name>\n'));
21
+ process.exit(1);
22
+ }
23
+
24
+ console.log(chalk.blue.bold(`\n🗄️ Creating migration: ${migrationName}\n`));
25
+
26
+ try {
27
+ execSync(`docker compose -f docker-compose.yml -f docker-compose.dev.yml exec backend npm run migration:create -- ${migrationName}`, {
28
+ cwd: infrastructurePath,
29
+ stdio: 'inherit'
30
+ });
31
+ console.log(chalk.green.bold('\n✅ Migration created successfully.\n'));
32
+ } catch (error) {
33
+ console.error(chalk.red('\n❌ Error creating migration:'), error.message);
34
+ process.exit(1);
35
+ }
36
+ }
37
+
38
+ module.exports = { migrateCreate };
@@ -0,0 +1,32 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const chalk = require('chalk');
4
+ const { execSync } = require('child_process');
5
+ const { requireProject } = require('../utils/project-helpers');
6
+
7
+ async function migrateRevert() {
8
+ requireProject();
9
+
10
+ const infrastructurePath = path.join(process.cwd(), 'infrastructure');
11
+
12
+ if (!fs.existsSync(infrastructurePath)) {
13
+ console.error(chalk.red('\n❌ Error: infrastructure/ directory not found'));
14
+ console.log(chalk.gray('Make sure you are in the root of your LaunchFrame project.\n'));
15
+ process.exit(1);
16
+ }
17
+
18
+ console.log(chalk.blue.bold('\n🗄️ Reverting last database migration\n'));
19
+
20
+ try {
21
+ execSync('docker compose -f docker-compose.yml -f docker-compose.dev.yml exec backend npm run migration:revert', {
22
+ cwd: infrastructurePath,
23
+ stdio: 'inherit'
24
+ });
25
+ console.log(chalk.green.bold('\n✅ Migration reverted successfully.\n'));
26
+ } catch (error) {
27
+ console.error(chalk.red('\n❌ Error reverting migration:'), error.message);
28
+ process.exit(1);
29
+ }
30
+ }
31
+
32
+ module.exports = { migrateRevert };
@@ -0,0 +1,32 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const chalk = require('chalk');
4
+ const { execSync } = require('child_process');
5
+ const { requireProject } = require('../utils/project-helpers');
6
+
7
+ async function migrateRun() {
8
+ requireProject();
9
+
10
+ const infrastructurePath = path.join(process.cwd(), 'infrastructure');
11
+
12
+ if (!fs.existsSync(infrastructurePath)) {
13
+ console.error(chalk.red('\n❌ Error: infrastructure/ directory not found'));
14
+ console.log(chalk.gray('Make sure you are in the root of your LaunchFrame project.\n'));
15
+ process.exit(1);
16
+ }
17
+
18
+ console.log(chalk.blue.bold('\n🗄️ Running database migrations\n'));
19
+
20
+ try {
21
+ execSync('docker compose -f docker-compose.yml -f docker-compose.dev.yml exec backend npm run migration:run', {
22
+ cwd: infrastructurePath,
23
+ stdio: 'inherit'
24
+ });
25
+ console.log(chalk.green.bold('\n✅ Migrations completed successfully.\n'));
26
+ } catch (error) {
27
+ console.error(chalk.red('\n❌ Error running migrations:'), error.message);
28
+ process.exit(1);
29
+ }
30
+ }
31
+
32
+ module.exports = { migrateRun };