@launchframe/cli 0.1.11 → 1.0.0-beta.10

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.
@@ -47,36 +47,66 @@ async function dockerDestroy(options = {}) {
47
47
 
48
48
  console.log(chalk.yellow('\nšŸ—‘ļø Destroying Docker resources...\n'));
49
49
 
50
- // Step 1: Stop and remove all containers
51
- console.log(chalk.gray('Stopping and removing containers...'));
50
+ // Step 1: Stop all running containers first
51
+ console.log(chalk.gray('Stopping running containers...'));
52
52
  try {
53
- execSync(`docker ps -a --filter "name=${projectName}" -q | xargs -r docker rm -f`, { stdio: 'inherit' });
53
+ const runningContainerIds = execSync(`docker ps --filter "name=${projectName}" -q`, { encoding: 'utf8' }).trim();
54
+ if (runningContainerIds) {
55
+ const ids = runningContainerIds.replace(/\n/g, ' ');
56
+ // Use pipe mode instead of inherit to avoid Windows stdio issues
57
+ const output = execSync(`docker stop ${ids}`, { encoding: 'utf8' });
58
+ if (output) console.log(output);
59
+ }
60
+ } catch (error) {
61
+ // Ignore errors if no running containers
62
+ }
63
+
64
+ // Step 2: Remove all containers (running and stopped)
65
+ console.log(chalk.gray('Removing containers...'));
66
+ try {
67
+ const containerIds = execSync(`docker ps -a --filter "name=${projectName}" -q`, { encoding: 'utf8' }).trim();
68
+ if (containerIds) {
69
+ const ids = containerIds.replace(/\n/g, ' ');
70
+ const output = execSync(`docker rm -f ${ids}`, { encoding: 'utf8' });
71
+ if (output) console.log(output);
72
+ }
54
73
  } catch (error) {
55
74
  // Ignore errors if no containers found
56
75
  }
57
76
 
58
- // Step 2: Remove all volumes
59
- console.log(chalk.gray('Removing volumes...'));
77
+ // Step 3: Remove network (must be after containers are removed)
78
+ console.log(chalk.gray('Removing network...'));
60
79
  try {
61
- execSync(`docker volume ls --filter "name=${projectName}" -q | xargs -r docker volume rm`, { stdio: 'inherit' });
80
+ const output = execSync(`docker network rm ${projectName}-network`, { encoding: 'utf8' });
81
+ if (output) console.log(output);
62
82
  } catch (error) {
63
- // Ignore errors if no volumes found
83
+ // Ignore errors if network doesn't exist or has active endpoints
64
84
  }
65
85
 
66
- // Step 3: Remove all images
67
- console.log(chalk.gray('Removing images...'));
86
+ // Step 4: Remove all volumes (must be after containers are removed)
87
+ console.log(chalk.gray('Removing volumes...'));
68
88
  try {
69
- execSync(`docker images --filter "reference=${projectName}*" -q | xargs -r docker rmi -f`, { stdio: 'inherit' });
89
+ const volumeIds = execSync(`docker volume ls --filter "name=${projectName}" -q`, { encoding: 'utf8' }).trim();
90
+ if (volumeIds) {
91
+ const ids = volumeIds.replace(/\n/g, ' ');
92
+ const output = execSync(`docker volume rm ${ids}`, { encoding: 'utf8' });
93
+ if (output) console.log(output);
94
+ }
70
95
  } catch (error) {
71
- // Ignore errors if no images found
96
+ // Ignore errors if no volumes found
72
97
  }
73
98
 
74
- // Step 4: Remove network
75
- console.log(chalk.gray('Removing network...'));
99
+ // Step 5: Remove all images (do this last)
100
+ console.log(chalk.gray('Removing images...'));
76
101
  try {
77
- execSync(`docker network rm ${projectName}-network`, { stdio: 'inherit' });
102
+ const imageIds = execSync(`docker images --filter "reference=${projectName}*" -q`, { encoding: 'utf8' }).trim();
103
+ if (imageIds) {
104
+ const ids = imageIds.replace(/\n/g, ' ');
105
+ const output = execSync(`docker rmi -f ${ids}`, { encoding: 'utf8' });
106
+ if (output) console.log(output);
107
+ }
78
108
  } catch (error) {
79
- // Ignore errors if network doesn't exist
109
+ // Ignore errors if no images found
80
110
  }
81
111
 
82
112
  console.log(chalk.green.bold('\nāœ… All Docker resources destroyed successfully!\n'));
@@ -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 services'));
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 services'));
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,82 +3,45 @@ const path = require('path');
3
3
  const chalk = require('chalk');
4
4
  const { runInitPrompts, runVariantPrompts } = require('../prompts');
5
5
  const { generateProject } = require('../generator');
6
-
7
- /**
8
- * Check if current directory is a LaunchFrame project
9
- */
10
- function isLaunchFrameProject() {
11
- const markerPath = path.join(process.cwd(), '.launchframe');
12
- return fs.existsSync(markerPath);
13
- }
6
+ const { checkGitHubAccess, showAccessDeniedMessage } = require('../utils/github-access');
7
+ const { ensureCacheReady } = require('../utils/service-cache');
8
+ const { isLaunchFrameProject } = require('../utils/project-helpers');
14
9
 
15
10
  /**
16
11
  * Check if running in development mode (local) vs production (npm install)
17
12
  */
18
13
  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;
14
+ // Only use dev mode if LAUNCHFRAME_DEV is explicitly set to 'true'
15
+ return process.env.LAUNCHFRAME_DEV === 'true';
44
16
  }
45
17
 
46
18
  /**
47
19
  * Initialize a new LaunchFrame project
48
20
  * @param {Object} options - Command options
49
21
  * @param {string} options.projectName - Project name (skips prompt if provided)
22
+ * @param {string} options.tenancy - Tenancy model: 'single' or 'multi' (skips prompt if provided)
23
+ * @param {string} options.userModel - User model: 'b2b' or 'b2b2c' (skips prompt if provided)
50
24
  */
51
25
  async function init(options = {}) {
52
26
  console.log(chalk.blue.bold('\nšŸš€ Welcome to LaunchFrame!\n'));
53
27
 
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;
28
+ // Check if in development mode
29
+ const devMode = isDevMode();
30
+
31
+ if (!devMode) {
32
+ // Production mode: Check GitHub access
33
+ console.log(chalk.blue('šŸ” Checking repository access...\n'));
34
+
35
+ const accessCheck = await checkGitHubAccess();
36
+
37
+ if (!accessCheck.hasAccess) {
38
+ // No access - show purchase/setup message
39
+ showAccessDeniedMessage();
40
+ process.exit(1); // Exit with error code
41
+ }
42
+
43
+ console.log(chalk.green('āœ“ Repository access confirmed\n'));
79
44
  }
80
-
81
- // Dev mode - continue with normal project generation
82
45
  // Check if already in a LaunchFrame project
83
46
  if (isLaunchFrameProject()) {
84
47
  console.error(chalk.red('\nāŒ Error: Already in a LaunchFrame project'));
@@ -102,26 +65,90 @@ async function init(options = {}) {
102
65
  .map(word => word.charAt(0).toUpperCase() + word.slice(1))
103
66
  .join(' ');
104
67
 
68
+ const projectDescription = `${projectDisplayName} - Modern SaaS Platform`;
69
+
105
70
  answers = {
106
71
  projectName: options.projectName,
107
72
  projectDisplayName: projectDisplayName,
73
+ projectDescription: projectDescription,
108
74
  projectNameUpper: options.projectName.toUpperCase().replace(/-/g, '_'),
109
75
  projectNameCamel: options.projectName.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase())
110
76
  };
111
77
 
112
78
  console.log(chalk.gray(`Using project name: ${options.projectName}`));
113
- console.log(chalk.gray(`Using display name: ${projectDisplayName}\n`));
79
+ console.log(chalk.gray(`Using display name: ${projectDisplayName}`));
80
+ console.log(chalk.gray(`Using description: ${projectDescription}\n`));
114
81
  } else {
115
82
  // Get user inputs via interactive prompts
116
83
  answers = await runInitPrompts();
117
84
  }
118
85
 
119
86
  // Get variant selections (multi-tenancy, B2B vs B2B2C)
120
- const variantChoices = await runVariantPrompts();
87
+ let variantChoices;
88
+
89
+ // If both flags provided, skip variant prompts
90
+ if (options.tenancy && options.userModel) {
91
+ // Validate tenancy value
92
+ if (!['single', 'multi'].includes(options.tenancy)) {
93
+ throw new Error('Invalid --tenancy value. Must be "single" or "multi"');
94
+ }
95
+
96
+ // Validate userModel value
97
+ if (!['b2b', 'b2b2c'].includes(options.userModel)) {
98
+ throw new Error('Invalid --user-model value. Must be "b2b" or "b2b2c"');
99
+ }
100
+
101
+ // Convert short flag values to full variant names
102
+ const tenancyMap = {
103
+ 'single': 'single-tenant',
104
+ 'multi': 'multi-tenant'
105
+ };
106
+
107
+ variantChoices = {
108
+ tenancy: tenancyMap[options.tenancy],
109
+ userModel: options.userModel
110
+ };
111
+
112
+ console.log(chalk.gray(`Using tenancy: ${options.tenancy}`));
113
+ console.log(chalk.gray(`Using user model: ${options.userModel}\n`));
114
+ } else {
115
+ // Run interactive variant prompts
116
+ variantChoices = await runVariantPrompts();
117
+ }
118
+
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
128
+ if (variantChoices.userModel === 'b2b2c') {
129
+ requiredServices.push('customers-portal');
130
+ }
131
+
132
+ // Determine template source (dev mode = local, production = cache)
133
+ let templateRoot;
134
+
135
+ if (devMode) {
136
+ // Dev mode: Use local services directory
137
+ templateRoot = path.resolve(__dirname, '../../../services');
138
+ console.log(chalk.gray(`[DEV MODE] Using local services: ${templateRoot}\n`));
139
+ } else {
140
+ // Production mode: Use cache
141
+ try {
142
+ templateRoot = await ensureCacheReady(requiredServices);
143
+ } catch (error) {
144
+ console.error(chalk.red(`\nāŒ Error: ${error.message}\n`));
145
+ process.exit(1);
146
+ }
147
+ }
121
148
 
122
149
  // Generate project with variant selections
123
150
  console.log(chalk.yellow('\nāš™ļø Generating project...\n'));
124
- await generateProject(answers, variantChoices);
151
+ await generateProject(answers, variantChoices, templateRoot);
125
152
 
126
153
  console.log(chalk.green.bold('\nāœ… Project generated successfully!\n'));
127
154
  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, getServicePath } = require('../utils/service-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
- // Local development: copy from launchframe-dev/modules directory
78
+ // Local development: copy from launchframe-dev/services 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, '../../../services', 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 services 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
102
+ await ensureCacheReady([serviceName]);
103
+ sourceDir = getServicePath(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 = {
@@ -440,6 +464,12 @@ function getDockerComposeDefinitions(serviceName, service, projectName, projectC
440
464
  ${serviceName}:
441
465
  image: ghcr.io/${githubOrg}/${projectName}-${serviceName}:latest
442
466
  restart: unless-stopped
467
+ healthcheck:
468
+ test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:3000/"]
469
+ interval: 30s
470
+ timeout: 10s
471
+ start_period: 40s
472
+ retries: 3
443
473
  labels:
444
474
  - "traefik.enable=true"
445
475
  - "traefik.http.routers.${serviceName}.rule=Host(\`${serviceName}.\${PRIMARY_DOMAIN}\`)"
@@ -449,7 +479,7 @@ function getDockerComposeDefinitions(serviceName, service, projectName, projectC
449
479
  `,
450
480
  volumes: null // No volumes needed in prod config
451
481
  };
452
- } else if (serviceName === 'customers-frontend') {
482
+ } else if (serviceName === 'customers-portal') {
453
483
  definitions.prod = {
454
484
  service: `
455
485
  ${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'));