@launchframe/cli 0.1.11 → 1.0.0-beta.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.
@@ -19,18 +19,38 @@ async function dockerUp(serviceName) {
19
19
  process.exit(1);
20
20
  }
21
21
 
22
+ // Check Docker Compose version for watch support
23
+ try {
24
+ const composeVersion = execSync('docker compose version', { encoding: 'utf8' });
25
+ const versionMatch = composeVersion.match(/v?(\d+)\.(\d+)\.(\d+)/);
26
+
27
+ if (versionMatch) {
28
+ const [, major, minor] = versionMatch.map(Number);
29
+
30
+ if (major < 2 || (major === 2 && minor < 22)) {
31
+ console.error(chalk.red('\n❌ Error: Docker Compose v2.22+ is required for watch support'));
32
+ console.log(chalk.yellow(`Current version: Docker Compose v${major}.${minor}`));
33
+ console.log(chalk.gray('\nPlease upgrade Docker Compose:'));
34
+ console.log(chalk.white(' https://docs.docker.com/compose/install/\n'));
35
+ process.exit(1);
36
+ }
37
+ }
38
+ } catch (error) {
39
+ console.warn(chalk.yellow('⚠️ Could not detect Docker Compose version'));
40
+ }
41
+
22
42
  if (serviceName) {
23
- console.log(chalk.blue.bold(`\n🚀 Starting ${serviceName} service\n`));
24
- console.log(chalk.gray(`Starting ${serviceName} in detached mode...\n`));
43
+ console.log(chalk.blue.bold(`\n🚀 Starting ${serviceName} service with watch\n`));
44
+ console.log(chalk.gray(`Starting ${serviceName} with file watching enabled...\n`));
25
45
  } else {
26
- console.log(chalk.blue.bold('\n🚀 Starting Docker Services\n'));
27
- console.log(chalk.gray('Starting all services in detached mode...\n'));
46
+ console.log(chalk.blue.bold('\n🚀 Starting Docker Services with Watch\n'));
47
+ console.log(chalk.gray('Starting all services with file watching enabled...\n'));
28
48
  }
29
49
 
30
50
  try {
31
51
  const upCommand = serviceName
32
- ? `docker-compose -f docker-compose.yml -f docker-compose.dev.yml up -d ${serviceName}`
33
- : 'docker-compose -f docker-compose.yml -f docker-compose.dev.yml up -d';
52
+ ? `docker-compose -f docker-compose.yml -f docker-compose.dev.yml watch ${serviceName}`
53
+ : 'docker-compose -f docker-compose.yml -f docker-compose.dev.yml watch';
34
54
 
35
55
  console.log(chalk.gray(`Running: ${upCommand}\n`));
36
56
 
@@ -40,12 +60,19 @@ async function dockerUp(serviceName) {
40
60
  });
41
61
 
42
62
  if (serviceName) {
43
- console.log(chalk.green.bold(`\n✅ ${serviceName} service started successfully!\n`));
44
- console.log(chalk.white('Useful commands:'));
45
- console.log(chalk.gray(` launchframe docker:logs ${serviceName} # View logs`));
46
- console.log(chalk.gray(` docker-compose -f docker-compose.yml -f docker-compose.dev.yml down ${serviceName} # Stop service\n`));
63
+ console.log(chalk.green.bold(`\n✅ ${serviceName} service started with watch!\n`));
64
+ console.log(chalk.yellow('📺 Watching for file changes (press Ctrl+C to stop)...\n'));
65
+ console.log(chalk.white('Watch behavior:'));
66
+ console.log(chalk.gray(' Code changes Auto-sync to container'));
67
+ console.log(chalk.gray(' • package.json → Auto-rebuild & restart\n'));
68
+ console.log(chalk.white('To stop:'));
69
+ console.log(chalk.gray(' Press Ctrl+C in this terminal\n'));
47
70
  } else {
48
- console.log(chalk.green.bold('\n✅ All services started successfully!\n'));
71
+ console.log(chalk.green.bold('\n✅ All services started with watch!\n'));
72
+ console.log(chalk.yellow('📺 Watching for file changes (press Ctrl+C to stop)...\n'));
73
+ console.log(chalk.white('Watch behavior:'));
74
+ console.log(chalk.gray(' • Code changes → Auto-sync to containers'));
75
+ console.log(chalk.gray(' • package.json → Auto-rebuild & restart\n'));
49
76
  console.log(chalk.white('Services running at:'));
50
77
  console.log(chalk.gray(' Backend API: http://localhost:4000'));
51
78
  console.log(chalk.gray(' Admin Panel: http://localhost:3001'));
@@ -57,11 +84,10 @@ async function dockerUp(serviceName) {
57
84
  }
58
85
 
59
86
  console.log(chalk.gray(' Marketing Site: http://localhost:8080\n'));
60
- console.log(chalk.white('Useful commands:'));
61
- console.log(chalk.gray(' launchframe docker:logs # View logs from all services'));
62
- console.log(chalk.gray(' launchframe docker:logs backend # View logs from specific service'));
63
- console.log(chalk.gray(' launchframe docker:down # Stop services (keeps data)'));
64
- console.log(chalk.gray(' launchframe docker:destroy # Remove all resources\n'));
87
+ console.log(chalk.white('To stop all services:'));
88
+ console.log(chalk.gray(' Press Ctrl+C in this terminal'));
89
+ console.log(chalk.gray(' Or run: launchframe docker:down\n'));
90
+ console.log(chalk.cyan('💡 Tip (Linux/Mac): Add & at the end to run in background\n'));
65
91
  }
66
92
 
67
93
  } catch (error) {
@@ -39,6 +39,10 @@ function help() {
39
39
  console.log(chalk.gray(' service:remove <name> Remove installed service\n'));
40
40
  console.log(chalk.white('Available Services:'));
41
41
  console.log(chalk.gray(' waitlist Coming soon page with email collection\n'));
42
+ console.log(chalk.white('Cache Management:'));
43
+ console.log(chalk.gray(' cache:info Show cache location, size, and cached modules'));
44
+ console.log(chalk.gray(' cache:update Force update cache to latest version'));
45
+ console.log(chalk.gray(' cache:clear Delete cache (re-download on next use)\n'));
42
46
  console.log(chalk.white('Other commands:'));
43
47
  console.log(chalk.gray(' doctor Check project health and configuration'));
44
48
  console.log(chalk.gray(' help Show this help message\n'));
@@ -67,12 +71,18 @@ function help() {
67
71
  console.log(chalk.white('Available commands:'));
68
72
  console.log(chalk.gray(' init Initialize a new LaunchFrame project'));
69
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)'));
70
76
  console.log(chalk.gray(' help Show this help message\n'));
77
+ console.log(chalk.white('Cache Management:'));
78
+ console.log(chalk.gray(' cache:info Show cache location, size, and cached modules'));
79
+ console.log(chalk.gray(' cache:update Force update cache to latest version'));
80
+ console.log(chalk.gray(' cache:clear Delete cache (re-download on next use)\n'));
71
81
  console.log(chalk.white('Examples:'));
72
82
  console.log(chalk.gray(' # Interactive mode'));
73
83
  console.log(chalk.gray(' launchframe init\n'));
74
84
  console.log(chalk.gray(' # Non-interactive mode'));
75
- console.log(chalk.gray(' launchframe init --project-name my-saas\n'));
85
+ console.log(chalk.gray(' launchframe init --project-name my-saas --tenancy single --user-model b2b\n'));
76
86
  }
77
87
  }
78
88
 
@@ -3,6 +3,8 @@ const path = require('path');
3
3
  const chalk = require('chalk');
4
4
  const { runInitPrompts, runVariantPrompts } = require('../prompts');
5
5
  const { generateProject } = require('../generator');
6
+ const { checkGitHubAccess, showAccessDeniedMessage } = require('../utils/github-access');
7
+ const { ensureCacheReady } = require('../utils/module-cache');
6
8
 
7
9
  /**
8
10
  * Check if current directory is a LaunchFrame project
@@ -16,69 +18,37 @@ function isLaunchFrameProject() {
16
18
  * Check if running in development mode (local) vs production (npm install)
17
19
  */
18
20
  function isDevMode() {
19
- // If LAUNCHFRAME_DEV env var is explicitly set, use it
20
- if (process.env.LAUNCHFRAME_DEV === 'true') {
21
- return true;
22
- }
23
- if (process.env.LAUNCHFRAME_DEV === 'false') {
24
- return false;
25
- }
26
-
27
- // Check if running from node_modules (production) or local directory (dev)
28
- const scriptPath = __dirname;
29
-
30
- // Check multiple indicators that we're in production:
31
- // 1. Running from node_modules
32
- // 2. Running from npm cache (_npx or .npm)
33
- // 3. Package name in path
34
- const isInNodeModules = scriptPath.includes('node_modules');
35
- const isInNpmCache = scriptPath.includes('_npx') || scriptPath.includes('.npm');
36
- const isInPackage = scriptPath.includes('@launchframe');
37
-
38
- // If any production indicator is found, we're NOT in dev mode
39
- if (isInNodeModules || isInNpmCache || isInPackage) {
40
- return false;
41
- }
42
-
43
- return true;
21
+ // Only use dev mode if LAUNCHFRAME_DEV is explicitly set to 'true'
22
+ return process.env.LAUNCHFRAME_DEV === 'true';
44
23
  }
45
24
 
46
25
  /**
47
26
  * Initialize a new LaunchFrame project
48
27
  * @param {Object} options - Command options
49
28
  * @param {string} options.projectName - Project name (skips prompt if provided)
29
+ * @param {string} options.tenancy - Tenancy model: 'single' or 'multi' (skips prompt if provided)
30
+ * @param {string} options.userModel - User model: 'b2b' or 'b2b2c' (skips prompt if provided)
50
31
  */
51
32
  async function init(options = {}) {
52
33
  console.log(chalk.blue.bold('\n🚀 Welcome to LaunchFrame!\n'));
53
34
 
54
- // If running via npm (production), show waitlist message
55
- if (!isDevMode()) {
56
- console.log(chalk.white('LaunchFrame is a production-ready B2B SaaS boilerplate that gives you:'));
57
- console.log(chalk.gray(' Single VPS deployment ($7-20/mo with Docker + Traefik)'));
58
- console.log(chalk.gray(' • Variant selection on init (single/multi-tenant, B2B/B2B2C)'));
59
- console.log(chalk.gray(' Service registry (add docs, waitlist, admin tools - zero config)'));
60
- console.log(chalk.gray(' • Full-stack TypeScript (NestJS + React + Next.js)'));
61
- console.log(chalk.gray(' • Subscriptions + credits (hybrid monetization)'));
62
- console.log(chalk.gray(' • Feature guard system (tier-based access control)'));
63
- console.log(chalk.gray(' • API key management + auto-generated docs'));
64
- console.log(chalk.gray(' • Resilient webhook architecture\n'));
65
-
66
- console.log(chalk.yellow.bold('📋 LaunchFrame is currently in private beta.\n'));
67
-
68
- console.log(chalk.white('To get early access and be notified when the full version is available:'));
69
- console.log(chalk.cyan.bold('\n 👉 Join the waitlist at https://launchframe.dev\n'));
70
-
71
- console.log(chalk.gray('Founding members get:'));
72
- console.log(chalk.gray(' • Exclusive early access'));
73
- console.log(chalk.gray(' • Lifetime updates'));
74
- console.log(chalk.gray(' • Priority support'));
75
- console.log(chalk.gray(' • Special launch pricing\n'));
76
-
77
- console.log(chalk.white('Questions? Email support@launchframe.dev\n'));
78
- return;
35
+ // Check if in development mode
36
+ const devMode = isDevMode();
37
+
38
+ if (!devMode) {
39
+ // Production mode: Check GitHub access
40
+ console.log(chalk.blue('🔍 Checking repository access...\n'));
41
+
42
+ const accessCheck = await checkGitHubAccess();
43
+
44
+ if (!accessCheck.hasAccess) {
45
+ // No access - show purchase/setup message
46
+ showAccessDeniedMessage();
47
+ process.exit(1); // Exit with error code
48
+ }
49
+
50
+ console.log(chalk.green(' Repository access confirmed\n'));
79
51
  }
80
-
81
- // Dev mode - continue with normal project generation
82
52
  // Check if already in a LaunchFrame project
83
53
  if (isLaunchFrameProject()) {
84
54
  console.error(chalk.red('\n❌ Error: Already in a LaunchFrame project'));
@@ -102,26 +72,90 @@ async function init(options = {}) {
102
72
  .map(word => word.charAt(0).toUpperCase() + word.slice(1))
103
73
  .join(' ');
104
74
 
75
+ const projectDescription = `${projectDisplayName} - Modern SaaS Platform`;
76
+
105
77
  answers = {
106
78
  projectName: options.projectName,
107
79
  projectDisplayName: projectDisplayName,
80
+ projectDescription: projectDescription,
108
81
  projectNameUpper: options.projectName.toUpperCase().replace(/-/g, '_'),
109
82
  projectNameCamel: options.projectName.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase())
110
83
  };
111
84
 
112
85
  console.log(chalk.gray(`Using project name: ${options.projectName}`));
113
- console.log(chalk.gray(`Using display name: ${projectDisplayName}\n`));
86
+ console.log(chalk.gray(`Using display name: ${projectDisplayName}`));
87
+ console.log(chalk.gray(`Using description: ${projectDescription}\n`));
114
88
  } else {
115
89
  // Get user inputs via interactive prompts
116
90
  answers = await runInitPrompts();
117
91
  }
118
92
 
119
93
  // Get variant selections (multi-tenancy, B2B vs B2B2C)
120
- const variantChoices = await runVariantPrompts();
94
+ let variantChoices;
95
+
96
+ // If both flags provided, skip variant prompts
97
+ if (options.tenancy && options.userModel) {
98
+ // Validate tenancy value
99
+ if (!['single', 'multi'].includes(options.tenancy)) {
100
+ throw new Error('Invalid --tenancy value. Must be "single" or "multi"');
101
+ }
102
+
103
+ // Validate userModel value
104
+ if (!['b2b', 'b2b2c'].includes(options.userModel)) {
105
+ throw new Error('Invalid --user-model value. Must be "b2b" or "b2b2c"');
106
+ }
107
+
108
+ // Convert short flag values to full variant names
109
+ const tenancyMap = {
110
+ 'single': 'single-tenant',
111
+ 'multi': 'multi-tenant'
112
+ };
113
+
114
+ variantChoices = {
115
+ tenancy: tenancyMap[options.tenancy],
116
+ userModel: options.userModel
117
+ };
118
+
119
+ console.log(chalk.gray(`Using tenancy: ${options.tenancy}`));
120
+ console.log(chalk.gray(`Using user model: ${options.userModel}\n`));
121
+ } else {
122
+ // Run interactive variant prompts
123
+ variantChoices = await runVariantPrompts();
124
+ }
125
+
126
+ // Determine which modules are needed based on variant choices
127
+ const requiredModules = [
128
+ 'backend',
129
+ 'admin-portal',
130
+ 'infrastructure',
131
+ 'website'
132
+ ];
133
+
134
+ // Add customers-portal only if B2B2C mode
135
+ if (variantChoices.userModel === 'b2b2c') {
136
+ requiredModules.push('customers-portal');
137
+ }
138
+
139
+ // Determine template source (dev mode = local, production = cache)
140
+ let templateRoot;
141
+
142
+ if (devMode) {
143
+ // Dev mode: Use local modules directory
144
+ templateRoot = path.resolve(__dirname, '../../../modules');
145
+ console.log(chalk.gray(`[DEV MODE] Using local modules: ${templateRoot}\n`));
146
+ } else {
147
+ // Production mode: Use cache
148
+ try {
149
+ templateRoot = await ensureCacheReady(requiredModules);
150
+ } catch (error) {
151
+ console.error(chalk.red(`\n❌ Error: ${error.message}\n`));
152
+ process.exit(1);
153
+ }
154
+ }
121
155
 
122
156
  // Generate project with variant selections
123
157
  console.log(chalk.yellow('\n⚙️ Generating project...\n'));
124
- await generateProject(answers, variantChoices);
158
+ await generateProject(answers, variantChoices, templateRoot);
125
159
 
126
160
  console.log(chalk.green.bold('\n✅ Project generated successfully!\n'));
127
161
  console.log(chalk.white('Next steps:'));
@@ -7,6 +7,8 @@ const { SERVICE_REGISTRY } = require('../services/registry');
7
7
  const { isLaunchFrameProject, getProjectConfig, updateProjectConfig, getPrimaryDomain } = require('../utils/project-helpers');
8
8
  const { replaceVariables } = require('../utils/variable-replacer');
9
9
  const { updateEnvFile } = require('../utils/env-generator');
10
+ const { checkGitHubAccess, showAccessDeniedMessage } = require('../utils/github-access');
11
+ const { ensureCacheReady, getModulePath } = require('../utils/module-cache');
10
12
 
11
13
  async function serviceAdd(serviceName) {
12
14
  // STEP 1: Validation
@@ -59,7 +61,7 @@ async function serviceAdd(serviceName) {
59
61
  process.exit(0);
60
62
  }
61
63
 
62
- // STEP 3: Clone repository or copy from local (dev mode)
64
+ // STEP 3: Get service files (from cache in production, local in dev)
63
65
  const installPath = path.resolve(process.cwd(), serviceName);
64
66
 
65
67
  if (fs.existsSync(installPath)) {
@@ -67,50 +69,59 @@ async function serviceAdd(serviceName) {
67
69
  process.exit(1);
68
70
  }
69
71
 
70
- // Check if in development mode (for LaunchFrame development itself)
71
- const isDevMode = process.env.LAUNCHFRAME_DEV_MODE === 'true';
72
+ // Check if in development mode
73
+ const isDevMode = process.env.LAUNCHFRAME_DEV === 'true';
74
+
75
+ let sourceDir;
72
76
 
73
77
  if (isDevMode) {
74
78
  // Local development: copy from launchframe-dev/modules directory
75
79
  console.log(chalk.blue('\n[DEV MODE] Copying service from local directory...'));
76
- const sourceDir = path.resolve('/home/matfish/Work/launchframe-dev/modules', serviceName);
80
+ sourceDir = path.resolve(__dirname, '../../../modules', serviceName);
77
81
 
78
82
  if (!fs.existsSync(sourceDir)) {
79
83
  console.error(chalk.red(`Error: Local service directory not found: ${sourceDir}`));
80
- console.log('Make sure the service exists in the launchframe-dev/modules directory');
84
+ console.log('Make sure the service exists in the modules directory');
81
85
  process.exit(1);
82
86
  }
83
-
84
- try {
85
- // Copy directory recursively, excluding .git, node_modules, .next, etc.
86
- await fs.copy(sourceDir, installPath, {
87
- filter: (src) => {
88
- const basename = path.basename(src);
89
- return !['node_modules', '.git', '.next', 'dist', 'build', '.env'].includes(basename);
90
- }
91
- });
92
- console.log(chalk.green('✓ Service copied successfully'));
93
- } catch (error) {
94
- console.error(chalk.red('Failed to copy service directory'));
95
- console.error(error.message);
87
+ } else {
88
+ // Production mode: Check access and use cache
89
+ console.log(chalk.blue('\n🔍 Checking repository access...'));
90
+
91
+ const accessCheck = await checkGitHubAccess();
92
+
93
+ if (!accessCheck.hasAccess) {
94
+ showAccessDeniedMessage();
96
95
  process.exit(1);
97
96
  }
98
- } else {
99
- // Production mode: clone from git repository
100
- console.log(chalk.blue('\nCloning service repository...'));
101
-
102
- // Replace {{GITHUB_ORG}} in repo URL with actual org from project config
103
- const repoUrl = service.repoUrl.replace('{{GITHUB_ORG}}', projectConfig.githubOrg || 'launchframe');
104
-
97
+
98
+ console.log(chalk.green('✓ Repository access confirmed'));
99
+
105
100
  try {
106
- execSync(`git clone ${repoUrl} ${installPath}`, { stdio: 'inherit' });
101
+ // Ensure cache has this service module
102
+ await ensureCacheReady([serviceName]);
103
+ sourceDir = getModulePath(serviceName);
107
104
  } catch (error) {
108
- console.error(chalk.red('Failed to clone repository'));
109
- console.log('Make sure you have access to the repository');
105
+ console.error(chalk.red(`\n❌ Error: ${error.message}\n`));
110
106
  process.exit(1);
111
107
  }
112
108
  }
113
109
 
110
+ // Copy service from source to installation path
111
+ try {
112
+ await fs.copy(sourceDir, installPath, {
113
+ filter: (src) => {
114
+ const basename = path.basename(src);
115
+ return !['node_modules', '.git', '.next', 'dist', 'build', '.env'].includes(basename);
116
+ }
117
+ });
118
+ console.log(chalk.green('✓ Service files copied successfully'));
119
+ } catch (error) {
120
+ console.error(chalk.red('Failed to copy service directory'));
121
+ console.error(error.message);
122
+ process.exit(1);
123
+ }
124
+
114
125
  // STEP 4: Service-specific prompts (e.g., Airtable credentials)
115
126
  console.log(chalk.blue('\nConfiguring service...'));
116
127
  const envValues = await runServicePrompts(service);
@@ -124,7 +135,7 @@ async function serviceAdd(serviceName) {
124
135
  '{{PROJECT_NAME_UPPER}}': projectName.toUpperCase().replace(/-/g, '_'),
125
136
  '{{PROJECT_DISPLAY_NAME}}': projectConfig.projectDisplayName || projectName,
126
137
  '{{PRIMARY_DOMAIN}}': getPrimaryDomain(projectConfig) || 'example.com',
127
- '{{GITHUB_ORG}}': projectConfig.githubOrg || 'launchframe'
138
+ '{{GITHUB_ORG}}': projectConfig.deployment?.githubOrg || projectConfig.githubOrg || 'launchframe'
128
139
  };
129
140
 
130
141
  // Add service-specific env var values as {{VAR_NAME}} (with double curly braces)
@@ -335,7 +346,7 @@ function getDockerComposeDefinitions(serviceName, service, projectName, projectC
335
346
  const definitions = {};
336
347
 
337
348
  // Base configuration (docker-compose.yml)
338
- if (serviceName === 'customers-frontend') {
349
+ if (serviceName === 'customers-portal') {
339
350
  definitions.base = {
340
351
  service: `
341
352
  # ---------------------------------------------------------------------------
@@ -380,12 +391,25 @@ function getDockerComposeDefinitions(serviceName, service, projectName, projectC
380
391
  target: development
381
392
  image: ${projectName}-${serviceName}:dev
382
393
  restart: "no"
383
- volumes:
384
- - ../${serviceName}/.vitepress:/app/.vitepress
385
- - ../${serviceName}/index.md:/app/index.md
386
- - ../${serviceName}/guide:/app/guide
387
- - ../${serviceName}/public:/app/public
388
- - ${serviceName}_node_modules:/app/node_modules
394
+ # File watching for automatic sync and rebuild
395
+ # - Code changes are synced automatically
396
+ # - package.json changes trigger rebuild + restart
397
+ develop:
398
+ watch:
399
+ - action: sync
400
+ path: ../${serviceName}
401
+ target: /app
402
+ ignore:
403
+ - node_modules/
404
+ - dist/
405
+ - .vitepress/dist/
406
+ - coverage/
407
+ - '*.log'
408
+ - .git/
409
+ - action: rebuild
410
+ path: ../${serviceName}/package.json
411
+ - action: rebuild
412
+ path: ../${serviceName}/package-lock.json
389
413
  environment:
390
414
  - NODE_ENV=development
391
415
  ports:
@@ -395,7 +419,7 @@ function getDockerComposeDefinitions(serviceName, service, projectName, projectC
395
419
  name: ${projectName}-${serviceName}-node-modules
396
420
  `
397
421
  };
398
- } else if (serviceName === 'customers-frontend') {
422
+ } else if (serviceName === 'customers-portal') {
399
423
  definitions.dev = {
400
424
  service: `
401
425
  ${serviceName}:
@@ -431,8 +455,8 @@ function getDockerComposeDefinitions(serviceName, service, projectName, projectC
431
455
  }
432
456
 
433
457
  // Production configuration (docker-compose.prod.yml)
434
- const githubOrg = projectConfig.githubOrg || 'launchframe';
435
- const primaryDomain = projectConfig.primaryDomain || 'example.com';
458
+ const githubOrg = projectConfig.deployment?.githubOrg || 'launchframe';
459
+ const primaryDomain = projectConfig.deployment?.primaryDomain || 'example.com';
436
460
 
437
461
  if (serviceName === 'docs') {
438
462
  definitions.prod = {
@@ -449,7 +473,7 @@ function getDockerComposeDefinitions(serviceName, service, projectName, projectC
449
473
  `,
450
474
  volumes: null // No volumes needed in prod config
451
475
  };
452
- } else if (serviceName === 'customers-frontend') {
476
+ } else if (serviceName === 'customers-portal') {
453
477
  definitions.prod = {
454
478
  service: `
455
479
  ${serviceName}:
@@ -44,8 +44,8 @@ async function waitlistDeploy() {
44
44
  process.exit(1);
45
45
  }
46
46
 
47
- const { vpsHost, vpsUser, ghcrToken, adminEmail } = config.deployment;
48
- const { projectName, githubOrg, vpsAppFolder } = config;
47
+ const { vpsHost, vpsUser, vpsAppFolder, ghcrToken, adminEmail, githubOrg } = config.deployment;
48
+ const { projectName } = config;
49
49
  const projectRoot = process.cwd();
50
50
  const waitlistPath = path.join(projectRoot, 'waitlist');
51
51
 
@@ -26,8 +26,7 @@ async function waitlistLogs() {
26
26
  process.exit(1);
27
27
  }
28
28
 
29
- const { vpsHost, vpsUser } = config.deployment;
30
- const { vpsAppFolder } = config;
29
+ const { vpsHost, vpsUser, vpsAppFolder } = config.deployment;
31
30
 
32
31
  console.log(chalk.gray('Connecting to VPS and streaming logs...\n'));
33
32
  console.log(chalk.gray('Press Ctrl+C to exit\n'));
@@ -9,11 +9,13 @@ const execAsync = promisify(exec);
9
9
 
10
10
  /**
11
11
  * Start waitlist service locally
12
+ * @param {Object} flags - Command line flags
13
+ * @param {boolean} flags.build - Force rebuild of containers
12
14
  */
13
- async function waitlistUp() {
15
+ async function waitlistUp(flags = {}) {
14
16
  requireProject();
15
17
 
16
- console.log(chalk.blue.bold('\n🚀 Starting Waitlist Service (Local)\n'));
18
+ console.log(chalk.blue.bold('\n🚀 Starting Waitlist Service with Watch (Local)\n'));
17
19
 
18
20
  const config = getProjectConfig();
19
21
 
@@ -42,18 +44,46 @@ async function waitlistUp() {
42
44
  process.exit(1);
43
45
  }
44
46
 
45
- // STEP 2: Start waitlist locally
46
- console.log(chalk.yellow('\n🚀 Step 2: Starting waitlist containers...\n'));
47
+ // STEP 1b: Check Docker Compose version for watch support
48
+ console.log(chalk.yellow('\n🔍 Checking Docker Compose version...\n'));
47
49
 
48
- const deploySpinner = ora('Starting waitlist containers...').start();
50
+ const composeSpinner = ora('Verifying Docker Compose v2.22+...').start();
51
+
52
+ try {
53
+ const { stdout: composeVersion } = await execAsync('docker compose version');
54
+ const versionMatch = composeVersion.match(/v?(\d+)\.(\d+)\.(\d+)/);
55
+
56
+ if (versionMatch) {
57
+ const [, major, minor] = versionMatch.map(Number);
58
+
59
+ if (major < 2 || (major === 2 && minor < 22)) {
60
+ composeSpinner.fail(`Docker Compose v${major}.${minor} is too old`);
61
+ console.log(chalk.red('\n❌ Docker Compose v2.22+ is required for watch support\n'));
62
+ console.log(chalk.gray('Please upgrade Docker Compose:'));
63
+ console.log(chalk.white(' https://docs.docker.com/compose/install/\n'));
64
+ process.exit(1);
65
+ }
66
+ composeSpinner.succeed(`Docker Compose v${major}.${minor} (compatible)`);
67
+ } else {
68
+ composeSpinner.warn('Could not parse version, proceeding anyway...');
69
+ }
70
+ } catch (error) {
71
+ composeSpinner.warn('Could not detect Docker Compose version');
72
+ }
73
+
74
+ // STEP 2: Start waitlist with watch
75
+ console.log(chalk.yellow('\n🚀 Step 2: Starting waitlist with watch...\n'));
76
+
77
+ const buildFlag = flags.build ? '--build' : '';
78
+ const deploySpinner = ora('Starting waitlist with watch...').start();
49
79
 
50
80
  try {
51
81
  await execAsync(
52
- `cd ${waitlistPath} && docker-compose -f docker-compose.waitlist.yml -f docker-compose.waitlist.dev.yml up -d`,
82
+ `cd ${waitlistPath} && docker-compose -f docker-compose.waitlist.yml -f docker-compose.waitlist.dev.yml watch ${buildFlag}`.trim(),
53
83
  { timeout: 180000 } // 3 minutes
54
84
  );
55
85
 
56
- deploySpinner.succeed('Waitlist started successfully');
86
+ deploySpinner.succeed('Waitlist started with watch');
57
87
  } catch (error) {
58
88
  deploySpinner.fail('Failed to start waitlist');
59
89
  console.log(chalk.red(`\n❌ Error: ${error.message}\n`));
@@ -78,18 +108,23 @@ async function waitlistUp() {
78
108
  }
79
109
 
80
110
  // Success!
81
- console.log(chalk.green.bold('\n✅ Waitlist is now running locally!\n'));
111
+ console.log(chalk.green.bold('\n✅ Waitlist started with watch mode!\n'));
112
+
113
+ console.log(chalk.yellow('📺 Watching for file changes (press Ctrl+C to stop)...\n'));
114
+
115
+ console.log(chalk.white('Watch behavior:'));
116
+ console.log(chalk.gray(' • Code changes → Auto-sync to container'));
117
+ console.log(chalk.gray(' • package.json → Auto-rebuild & restart\n'));
82
118
 
83
119
  console.log(chalk.white('Your waitlist landing page is available at:\n'));
84
120
  console.log(chalk.cyan(` 🌍 Waitlist: http://localhost:3002`));
85
- console.log(chalk.gray(` ✓ Running in development mode with hot reload`));
86
- console.log(chalk.gray(` ✓ Source code mounted from ./waitlist/src\n`));
87
-
88
- console.log(chalk.white('Monitor waitlist:'));
89
- console.log(chalk.gray(` launchframe waitlist:logs\n`));
121
+ console.log(chalk.gray(` ✓ Running in development mode with file watching\n`));
90
122
 
91
- console.log(chalk.white('Stop waitlist:'));
92
- console.log(chalk.gray(` launchframe waitlist:down\n`));
123
+ console.log(chalk.white('To stop:'));
124
+ console.log(chalk.gray(' Press Ctrl+C in this terminal'));
125
+ console.log(chalk.gray(' Or run: launchframe waitlist:down\n'));
126
+
127
+ console.log(chalk.cyan('💡 Tip (Linux/Mac): Add & at the end to run in background\n'));
93
128
  }
94
129
 
95
130
  module.exports = { waitlistUp };