@launchframe/cli 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,231 @@
1
+ const chalk = require('chalk');
2
+ const path = require('path');
3
+ const fs = require('fs-extra');
4
+ const ora = require('ora');
5
+ const { requireProject, getProjectConfig, getPrimaryDomain, isWaitlistInstalled } = require('../utils/project-helpers');
6
+ const {
7
+ testSSHConnection,
8
+ checkSSHKeys,
9
+ executeSSH,
10
+ copyFileToVPS
11
+ } = require('../utils/ssh-helper');
12
+ const {
13
+ checkDockerRunning,
14
+ loginToGHCR,
15
+ buildWaitlistImage
16
+ } = require('../utils/docker-helper');
17
+
18
+ /**
19
+ * Deploy waitlist service to VPS
20
+ * - Builds waitlist Docker image
21
+ * - Copies docker-compose and .env.prod to VPS
22
+ * - Does NOT clone full repo (standalone deployment)
23
+ */
24
+ async function waitlistDeploy() {
25
+ requireProject();
26
+
27
+ console.log(chalk.blue.bold('\nšŸš€ Waitlist Service Deployment\n'));
28
+
29
+ const config = getProjectConfig();
30
+
31
+ // STEP 1: Validate waitlist is installed
32
+ if (!isWaitlistInstalled(config)) {
33
+ console.log(chalk.red('āŒ Error: Waitlist service not installed\n'));
34
+ console.log(chalk.gray('Run this command first:'));
35
+ console.log(chalk.white(' launchframe service:add waitlist\n'));
36
+ process.exit(1);
37
+ }
38
+
39
+ // STEP 2: Validate deployment is configured
40
+ if (!config.deployConfigured || !config.deployment) {
41
+ console.log(chalk.red('āŒ Error: Deployment not configured yet\n'));
42
+ console.log(chalk.gray('Run this command first:'));
43
+ console.log(chalk.white(' launchframe deploy:configure\n'));
44
+ process.exit(1);
45
+ }
46
+
47
+ const { vpsHost, vpsUser, ghcrToken, adminEmail } = config.deployment;
48
+ const { projectName, githubOrg, vpsAppFolder } = config;
49
+ const projectRoot = process.cwd();
50
+ const waitlistPath = path.join(projectRoot, 'waitlist');
51
+
52
+ // STEP 3: Validate waitlist .env.prod exists
53
+ console.log(chalk.yellow('šŸ“‹ Step 1: Validating waitlist environment...\n'));
54
+
55
+ const envProdPath = path.join(waitlistPath, '.env.prod');
56
+ if (!await fs.pathExists(envProdPath)) {
57
+ console.log(chalk.red('āŒ Error: waitlist/.env.prod not found\n'));
58
+ console.log(chalk.gray('The .env.prod file should be created during service installation.'));
59
+ console.log(chalk.gray('You can create it manually by copying waitlist/.env and updating values.\n'));
60
+ process.exit(1);
61
+ }
62
+
63
+ console.log(chalk.green('āœ“ Waitlist environment validated\n'));
64
+
65
+ // STEP 4: Check SSH keys and test connection
66
+ console.log(chalk.yellow('šŸ”‘ Step 2: Checking SSH configuration...\n'));
67
+
68
+ const { hasKeys } = await checkSSHKeys();
69
+ if (!hasKeys) {
70
+ console.log(chalk.yellow('āš ļø Warning: No SSH keys found in ~/.ssh/\n'));
71
+ }
72
+
73
+ const spinner = ora('Connecting to VPS...').start();
74
+ const connectionTest = await testSSHConnection(vpsUser, vpsHost);
75
+
76
+ if (!connectionTest.success) {
77
+ spinner.fail('Failed to connect to VPS');
78
+ console.log(chalk.red(`\nāŒ SSH connection failed: ${connectionTest.error}\n`));
79
+ process.exit(1);
80
+ }
81
+
82
+ spinner.succeed('Connected to VPS successfully');
83
+ console.log();
84
+
85
+ // STEP 5: Build and push Docker image
86
+ console.log(chalk.yellow('🐳 Step 3: Building waitlist Docker image...\n'));
87
+
88
+ const dockerRunning = await checkDockerRunning();
89
+ if (!dockerRunning) {
90
+ console.log(chalk.red('āŒ Docker is not running\n'));
91
+ console.log(chalk.gray('Please start Docker Desktop and try again.\n'));
92
+ process.exit(1);
93
+ }
94
+
95
+ if (!ghcrToken) {
96
+ console.log(chalk.red('āŒ GHCR token not configured\n'));
97
+ console.log(chalk.gray('Run: launchframe deploy:configure\n'));
98
+ process.exit(1);
99
+ }
100
+
101
+ try {
102
+ await loginToGHCR(githubOrg, ghcrToken);
103
+ await buildWaitlistImage(projectRoot, projectName, githubOrg);
104
+ console.log(chalk.green.bold('\nāœ… Waitlist image built and pushed!\n'));
105
+ } catch (error) {
106
+ console.log(chalk.red('\nāŒ Failed to build Docker image\n'));
107
+ console.log(chalk.gray('Error:'), error.message, '\n');
108
+ process.exit(1);
109
+ }
110
+
111
+ // STEP 6: Copy deployment files to VPS
112
+ console.log(chalk.yellow('šŸ“¦ Step 4: Copying deployment files to VPS...\n'));
113
+
114
+ const deploySpinner = ora('Creating app directory...').start();
115
+
116
+ try {
117
+ // Create waitlist directory on VPS
118
+ await executeSSH(vpsUser, vpsHost, `mkdir -p ${vpsAppFolder}/waitlist`);
119
+
120
+ // Copy docker-compose file
121
+ deploySpinner.text = 'Copying docker-compose file...';
122
+ const composeFile = path.join(waitlistPath, 'docker-compose.waitlist.yml');
123
+ await copyFileToVPS(composeFile, vpsUser, vpsHost, `${vpsAppFolder}/waitlist/docker-compose.waitlist.yml`);
124
+
125
+ // Copy .env.prod file (ensure ADMIN_EMAIL is included)
126
+ deploySpinner.text = 'Copying .env.prod file...';
127
+
128
+ // Read .env.prod and add ADMIN_EMAIL if missing
129
+ let envProdContent = await fs.readFile(envProdPath, 'utf8');
130
+ if (!envProdContent.includes('ADMIN_EMAIL=') && adminEmail) {
131
+ envProdContent += `ADMIN_EMAIL=${adminEmail}\n`;
132
+ await fs.writeFile(envProdPath, envProdContent, 'utf8');
133
+ }
134
+
135
+ await copyFileToVPS(envProdPath, vpsUser, vpsHost, `${vpsAppFolder}/waitlist/.env`);
136
+
137
+ deploySpinner.succeed('Deployment files copied to VPS');
138
+ } catch (error) {
139
+ deploySpinner.fail('Failed to copy files');
140
+ console.log(chalk.red(`\nāŒ Error: ${error.message}\n`));
141
+ process.exit(1);
142
+ }
143
+
144
+ // STEP 7: Pull Docker image on VPS
145
+ console.log(chalk.yellow('\n🐳 Step 5: Pulling waitlist Docker image on VPS...\n'));
146
+
147
+ const pullSpinner = ora('Pulling Docker image...').start();
148
+
149
+ try {
150
+ await executeSSH(
151
+ vpsUser,
152
+ vpsHost,
153
+ `cd ${vpsAppFolder}/waitlist && docker-compose -f docker-compose.waitlist.yml pull`,
154
+ { timeout: 300000 } // 5 minutes
155
+ );
156
+ pullSpinner.succeed('Docker image pulled successfully');
157
+ } catch (error) {
158
+ pullSpinner.fail('Failed to pull Docker image');
159
+ console.log(chalk.yellow(`\nāš ļø Warning: ${error.message}\n`));
160
+ console.log(chalk.gray('You can try pulling the image manually on the VPS.\n'));
161
+ }
162
+
163
+ // Success!
164
+ console.log(chalk.green.bold('\nāœ… Waitlist deployment initialized!\n'));
165
+
166
+ console.log(chalk.white('Summary:'));
167
+ console.log(chalk.gray(` - Deployment files copied to: ${vpsAppFolder}/waitlist`));
168
+ console.log(chalk.gray(' - Docker image pulled from GHCR'));
169
+ console.log(chalk.gray(' - Production .env configured\n'));
170
+
171
+ // STEP 8: Automatically start the waitlist
172
+ console.log(chalk.yellow('šŸš€ Step 6: Starting waitlist containers...\n'));
173
+
174
+ const startSpinner = ora('Starting waitlist on VPS...').start();
175
+
176
+ try {
177
+ await executeSSH(
178
+ vpsUser,
179
+ vpsHost,
180
+ `cd ${vpsAppFolder}/waitlist && docker-compose -f docker-compose.waitlist.yml up -d`,
181
+ { timeout: 180000 } // 3 minutes
182
+ );
183
+
184
+ startSpinner.succeed('Waitlist started successfully');
185
+ } catch (error) {
186
+ startSpinner.fail('Failed to start waitlist');
187
+ console.log(chalk.yellow(`\nāš ļø Warning: ${error.message}\n`));
188
+ console.log(chalk.gray('You can start it manually with: launchframe waitlist:up\n'));
189
+ process.exit(1);
190
+ }
191
+
192
+ // Verify services are running
193
+ console.log(chalk.yellow('\nšŸ” Step 7: Verifying deployment...\n'));
194
+
195
+ const verifySpinner = ora('Checking service status...').start();
196
+
197
+ try {
198
+ const { stdout: psOutput } = await executeSSH(
199
+ vpsUser,
200
+ vpsHost,
201
+ `cd ${vpsAppFolder}/waitlist && docker-compose -f docker-compose.waitlist.yml ps`,
202
+ { timeout: 30000 }
203
+ );
204
+
205
+ verifySpinner.succeed('Services verified');
206
+ console.log(chalk.gray('\n' + psOutput));
207
+ } catch (error) {
208
+ verifySpinner.warn('Could not verify services');
209
+ }
210
+
211
+ // Final success message
212
+ const primaryDomain = getPrimaryDomain(config);
213
+
214
+ console.log(chalk.green.bold('\nāœ… Waitlist is now live!\n'));
215
+
216
+ console.log(chalk.white('Your waitlist landing page is available at:\n'));
217
+ console.log(chalk.cyan(` šŸŒ Waitlist: https://${primaryDomain || 'your-domain.com'}`));
218
+ console.log(chalk.gray(` āœ“ Standalone Traefik instance (SSL + reverse proxy)`));
219
+ console.log(chalk.gray(` āœ“ Automatic Let's Encrypt SSL certificates`));
220
+ console.log(chalk.gray(` āœ“ Fully isolated from full-app deployment\n`));
221
+
222
+ console.log(chalk.white('ā° Note: SSL certificates may take a few minutes to provision.\n'));
223
+
224
+ console.log(chalk.white('Monitor waitlist:'));
225
+ console.log(chalk.gray(` launchframe waitlist:logs\n`));
226
+
227
+ console.log(chalk.white('Stop waitlist:'));
228
+ console.log(chalk.gray(` launchframe waitlist:down\n`));
229
+ }
230
+
231
+ module.exports = { waitlistDeploy };
@@ -0,0 +1,50 @@
1
+ const chalk = require('chalk');
2
+ const { exec } = require('child_process');
3
+ const { promisify } = require('util');
4
+ const path = require('path');
5
+ const ora = require('ora');
6
+ const { requireProject, getProjectConfig, isWaitlistInstalled } = require('../utils/project-helpers');
7
+
8
+ const execAsync = promisify(exec);
9
+
10
+ /**
11
+ * Stop waitlist service locally
12
+ */
13
+ async function waitlistDown() {
14
+ requireProject();
15
+
16
+ console.log(chalk.blue.bold('\nšŸ›‘ Stopping Waitlist Service (Local)\n'));
17
+
18
+ const config = getProjectConfig();
19
+
20
+ // Validate waitlist is installed
21
+ if (!isWaitlistInstalled(config)) {
22
+ console.log(chalk.red('āŒ Error: Waitlist service not installed\n'));
23
+ console.log(chalk.gray('Run: launchframe service:add waitlist\n'));
24
+ process.exit(1);
25
+ }
26
+
27
+ const projectRoot = process.cwd();
28
+ const waitlistPath = path.join(projectRoot, 'waitlist');
29
+
30
+ // Stop waitlist containers
31
+ const spinner = ora('Stopping waitlist containers...').start();
32
+
33
+ try {
34
+ await execAsync(
35
+ `cd ${waitlistPath} && docker-compose -f docker-compose.waitlist.yml -f docker-compose.waitlist.dev.yml down`,
36
+ { timeout: 60000 } // 1 minute
37
+ );
38
+
39
+ spinner.succeed('Waitlist stopped successfully');
40
+ } catch (error) {
41
+ spinner.fail('Failed to stop waitlist');
42
+ console.log(chalk.red(`\nāŒ Error: ${error.message}\n`));
43
+ process.exit(1);
44
+ }
45
+
46
+ console.log(chalk.green.bold('\nāœ… Waitlist service stopped!\n'));
47
+ console.log(chalk.gray('To start again: launchframe waitlist:up\n'));
48
+ }
49
+
50
+ module.exports = { waitlistDown };
@@ -0,0 +1,55 @@
1
+ const chalk = require('chalk');
2
+ const { spawn } = require('child_process');
3
+ const { requireProject, getProjectConfig, isWaitlistInstalled } = require('../utils/project-helpers');
4
+
5
+ /**
6
+ * View waitlist logs from VPS (streaming)
7
+ */
8
+ async function waitlistLogs() {
9
+ requireProject();
10
+
11
+ console.log(chalk.blue.bold('\nšŸ“‹ Waitlist Logs\n'));
12
+
13
+ const config = getProjectConfig();
14
+
15
+ // Validate waitlist is installed
16
+ if (!isWaitlistInstalled(config)) {
17
+ console.log(chalk.red('āŒ Error: Waitlist service not installed\n'));
18
+ console.log(chalk.gray('Run: launchframe service:add waitlist\n'));
19
+ process.exit(1);
20
+ }
21
+
22
+ // Validate deployment is configured
23
+ if (!config.deployConfigured || !config.deployment) {
24
+ console.log(chalk.red('āŒ Error: Deployment not configured yet\n'));
25
+ console.log(chalk.gray('Run: launchframe deploy:configure\n'));
26
+ process.exit(1);
27
+ }
28
+
29
+ const { vpsHost, vpsUser } = config.deployment;
30
+ const { vpsAppFolder } = config;
31
+
32
+ console.log(chalk.gray('Connecting to VPS and streaming logs...\n'));
33
+ console.log(chalk.gray('Press Ctrl+C to exit\n'));
34
+
35
+ // Use spawn instead of exec for streaming logs
36
+ const logsProcess = spawn('ssh', [
37
+ `${vpsUser}@${vpsHost}`,
38
+ `cd ${vpsAppFolder}/waitlist && docker-compose -f docker-compose.waitlist.yml logs -f --tail=100`
39
+ ], {
40
+ stdio: 'inherit' // Stream output directly to terminal
41
+ });
42
+
43
+ logsProcess.on('error', (error) => {
44
+ console.log(chalk.red(`\nāŒ Error: ${error.message}\n`));
45
+ process.exit(1);
46
+ });
47
+
48
+ logsProcess.on('exit', (code) => {
49
+ if (code !== 0 && code !== null) {
50
+ console.log(chalk.yellow(`\nāš ļø Process exited with code ${code}\n`));
51
+ }
52
+ });
53
+ }
54
+
55
+ module.exports = { waitlistLogs };
@@ -0,0 +1,95 @@
1
+ const chalk = require('chalk');
2
+ const { exec } = require('child_process');
3
+ const { promisify } = require('util');
4
+ const path = require('path');
5
+ const ora = require('ora');
6
+ const { requireProject, getProjectConfig, isWaitlistInstalled } = require('../utils/project-helpers');
7
+
8
+ const execAsync = promisify(exec);
9
+
10
+ /**
11
+ * Start waitlist service locally
12
+ */
13
+ async function waitlistUp() {
14
+ requireProject();
15
+
16
+ console.log(chalk.blue.bold('\nšŸš€ Starting Waitlist Service (Local)\n'));
17
+
18
+ const config = getProjectConfig();
19
+
20
+ // Validate waitlist is installed
21
+ if (!isWaitlistInstalled(config)) {
22
+ console.log(chalk.red('āŒ Error: Waitlist service not installed\n'));
23
+ console.log(chalk.gray('Run: launchframe service:add waitlist\n'));
24
+ process.exit(1);
25
+ }
26
+
27
+ const projectRoot = process.cwd();
28
+ const waitlistPath = path.join(projectRoot, 'waitlist');
29
+
30
+ // STEP 1: Check Docker version
31
+ console.log(chalk.yellow('🐳 Step 1: Checking Docker...\n'));
32
+
33
+ const dockerSpinner = ora('Checking Docker installation...').start();
34
+
35
+ try {
36
+ const { stdout: versionOutput } = await execAsync('docker version --format "{{.Client.Version}}"');
37
+ const version = versionOutput.trim();
38
+ dockerSpinner.succeed(`Docker ${version} (compatible)`);
39
+ } catch (error) {
40
+ dockerSpinner.fail('Docker not found');
41
+ console.log(chalk.red('\nāŒ Docker is not installed or not in PATH\n'));
42
+ process.exit(1);
43
+ }
44
+
45
+ // STEP 2: Start waitlist locally
46
+ console.log(chalk.yellow('\nšŸš€ Step 2: Starting waitlist containers...\n'));
47
+
48
+ const deploySpinner = ora('Starting waitlist containers...').start();
49
+
50
+ try {
51
+ await execAsync(
52
+ `cd ${waitlistPath} && docker-compose -f docker-compose.waitlist.yml -f docker-compose.waitlist.dev.yml up -d`,
53
+ { timeout: 180000 } // 3 minutes
54
+ );
55
+
56
+ deploySpinner.succeed('Waitlist started successfully');
57
+ } catch (error) {
58
+ deploySpinner.fail('Failed to start waitlist');
59
+ console.log(chalk.red(`\nāŒ Error: ${error.message}\n`));
60
+ process.exit(1);
61
+ }
62
+
63
+ // STEP 3: Verify services are running
64
+ console.log(chalk.yellow('\nšŸ” Step 3: Verifying containers...\n'));
65
+
66
+ const verifySpinner = ora('Checking service status...').start();
67
+
68
+ try {
69
+ const { stdout: psOutput } = await execAsync(
70
+ `cd ${waitlistPath} && docker-compose -f docker-compose.waitlist.yml -f docker-compose.waitlist.dev.yml ps`,
71
+ { timeout: 30000 }
72
+ );
73
+
74
+ verifySpinner.succeed('Services verified');
75
+ console.log(chalk.gray('\n' + psOutput));
76
+ } catch (error) {
77
+ verifySpinner.warn('Could not verify services');
78
+ }
79
+
80
+ // Success!
81
+ console.log(chalk.green.bold('\nāœ… Waitlist is now running locally!\n'));
82
+
83
+ console.log(chalk.white('Your waitlist landing page is available at:\n'));
84
+ 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`));
90
+
91
+ console.log(chalk.white('Stop waitlist:'));
92
+ console.log(chalk.gray(` launchframe waitlist:down\n`));
93
+ }
94
+
95
+ module.exports = { waitlistUp };
@@ -0,0 +1,190 @@
1
+ const path = require('path');
2
+ const fs = require('fs-extra');
3
+ const { execSync } = require('child_process');
4
+ const { replaceVariables } = require('./utils/variable-replacer');
5
+ const { copyDirectory } = require('./utils/file-ops');
6
+ const { generateEnvFile } = require('./utils/env-generator');
7
+ const { processServiceVariant } = require('./utils/variant-processor');
8
+ const { resolveVariantChoices } = require('./services/variant-config');
9
+
10
+ /**
11
+ * Initialize git repository in a service directory
12
+ * @param {string} servicePath - Path to service directory
13
+ * @param {string} serviceName - Name of the service (for logging)
14
+ */
15
+ function initGitRepo(servicePath, serviceName) {
16
+ try {
17
+ console.log(`šŸ”§ Initializing git repository for ${serviceName}...`);
18
+ execSync('git init', { cwd: servicePath, stdio: 'ignore' });
19
+ execSync('git add .', { cwd: servicePath, stdio: 'ignore' });
20
+ execSync('git commit -m "Initial commit"', { cwd: servicePath, stdio: 'ignore' });
21
+ console.log(`āœ… Git repository initialized for ${serviceName}`);
22
+ } catch (error) {
23
+ console.warn(`āš ļø Could not initialize git repository for ${serviceName}: ${error.message}`);
24
+ }
25
+ }
26
+
27
+ /**
28
+ * Main project generation function
29
+ * @param {Object} answers - User answers from prompts
30
+ * @param {Object} variantChoices - User's variant selections (tenancy, userModel)
31
+ */
32
+ async function generateProject(answers, variantChoices) {
33
+ const { projectName } = answers;
34
+
35
+ // Define source (template) and destination paths
36
+ const templateRoot = path.resolve(__dirname, '../../modules');
37
+ const projectRoot = path.resolve(__dirname, '../..'); // For root-level files like .github, CLAUDE.md
38
+ const destinationRoot = path.resolve(process.cwd(), projectName);
39
+
40
+ console.log(`šŸ“ Template source: ${templateRoot}`);
41
+ console.log(`šŸ“ Destination: ${destinationRoot}\n`);
42
+
43
+ // Ensure destination directory exists
44
+ await fs.ensureDir(destinationRoot);
45
+
46
+ // Template variable replacements
47
+ const variables = {
48
+ '{{PROJECT_NAME}}': answers.projectName,
49
+ '{{PROJECT_NAME_UPPER}}': answers.projectNameUpper,
50
+ '{{PROJECT_DISPLAY_NAME}}': answers.projectDisplayName,
51
+ // Leave these as template variables for deploy:configure to replace
52
+ '{{GITHUB_ORG}}': '{{GITHUB_ORG}}',
53
+ '{{PRIMARY_DOMAIN}}': '{{PRIMARY_DOMAIN}}',
54
+ '{{ADMIN_EMAIL}}': '{{ADMIN_EMAIL}}',
55
+ '{{VPS_HOST}}': '{{VPS_HOST}}'
56
+ };
57
+
58
+ // Resolve variant choices for all services
59
+ const allServiceVariants = resolveVariantChoices(variantChoices);
60
+
61
+ // Step 1: Process backend service with variants
62
+ console.log('šŸ”§ Processing backend service...');
63
+ await processServiceVariant(
64
+ 'backend',
65
+ allServiceVariants.backend,
66
+ path.join(destinationRoot, 'backend'),
67
+ variables,
68
+ templateRoot
69
+ );
70
+ initGitRepo(path.join(destinationRoot, 'backend'), 'backend');
71
+
72
+ // Step 2: Process admin-portal service with variants
73
+ // Note: admin-portal folder might not exist yet in templates, skip if missing
74
+ const adminPortalTemplatePath = path.join(templateRoot, 'admin-portal/templates/base');
75
+ if (await fs.pathExists(adminPortalTemplatePath)) {
76
+ console.log('šŸ”§ Processing admin-portal service...');
77
+ await processServiceVariant(
78
+ 'admin-portal',
79
+ allServiceVariants['admin-portal'],
80
+ path.join(destinationRoot, 'admin-portal'),
81
+ variables,
82
+ templateRoot
83
+ );
84
+ initGitRepo(path.join(destinationRoot, 'admin-portal'), 'admin-portal');
85
+ } else {
86
+ // Fallback: Copy admin-portal directly without variants (for now)
87
+ console.log('šŸ“‹ Copying admin-portal service (no variants yet)...');
88
+ const adminPortalSource = path.join(templateRoot, 'admin-portal');
89
+ if (await fs.pathExists(adminPortalSource)) {
90
+ await copyDirectory(adminPortalSource, path.join(destinationRoot, 'admin-portal'));
91
+ await replaceVariables(path.join(destinationRoot, 'admin-portal'), variables);
92
+ initGitRepo(path.join(destinationRoot, 'admin-portal'), 'admin-portal');
93
+ }
94
+ }
95
+
96
+ // Step 3: Process customers-portal service (ONLY if B2B2C selected)
97
+ if (variantChoices.userModel === 'b2b2c') {
98
+ // Note: customers-portal folder might not exist yet in templates, skip if missing
99
+ const customersPortalTemplatePath = path.join(templateRoot, 'customers-portal/templates/base');
100
+ if (await fs.pathExists(customersPortalTemplatePath)) {
101
+ console.log('šŸ”§ Processing customers-portal service...');
102
+ await processServiceVariant(
103
+ 'customers-portal',
104
+ allServiceVariants['customers-portal'],
105
+ path.join(destinationRoot, 'customers-portal'),
106
+ variables,
107
+ templateRoot
108
+ );
109
+ initGitRepo(path.join(destinationRoot, 'customers-portal'), 'customers-portal');
110
+ } else {
111
+ // Fallback: Copy customers-portal directly without variants (for now)
112
+ console.log('šŸ“‹ Copying customers-portal service (B2B2C mode)...');
113
+ const customersPortalSource = path.join(templateRoot, 'customers-portal');
114
+ if (await fs.pathExists(customersPortalSource)) {
115
+ await copyDirectory(customersPortalSource, path.join(destinationRoot, 'customers-portal'));
116
+ await replaceVariables(path.join(destinationRoot, 'customers-portal'), variables);
117
+ initGitRepo(path.join(destinationRoot, 'customers-portal'), 'customers-portal');
118
+ }
119
+ }
120
+ } else {
121
+ console.log('šŸ“‹ Skipping customers-portal (B2B mode - admin users only)');
122
+ }
123
+
124
+ // Step 4: Process infrastructure with variants (docker-compose files conditionally include customers-portal)
125
+ console.log('šŸ”§ Processing infrastructure...');
126
+ await processServiceVariant(
127
+ 'infrastructure',
128
+ allServiceVariants.infrastructure,
129
+ path.join(destinationRoot, 'infrastructure'),
130
+ variables,
131
+ templateRoot
132
+ );
133
+ initGitRepo(path.join(destinationRoot, 'infrastructure'), 'infrastructure');
134
+
135
+ console.log('šŸ“‹ Copying website...');
136
+ await copyDirectory(
137
+ path.join(templateRoot, 'website'),
138
+ path.join(destinationRoot, 'website')
139
+ );
140
+ await replaceVariables(path.join(destinationRoot, 'website'), variables);
141
+ initGitRepo(path.join(destinationRoot, 'website'), 'website');
142
+
143
+ // Step 5: Copy additional files (from project root, not modules/)
144
+ console.log('šŸ“‹ Copying additional files...');
145
+ const additionalFiles = [
146
+ '.github',
147
+ 'CLAUDE.md',
148
+ 'README.md',
149
+ '.gitignore',
150
+ 'LICENSE'
151
+ ];
152
+
153
+ for (const file of additionalFiles) {
154
+ const sourcePath = path.join(projectRoot, file);
155
+ const destPath = path.join(destinationRoot, file);
156
+
157
+ if (await fs.pathExists(sourcePath)) {
158
+ const stats = await fs.stat(sourcePath);
159
+ if (stats.isDirectory()) {
160
+ await copyDirectory(sourcePath, destPath);
161
+ } else {
162
+ await fs.copy(sourcePath, destPath);
163
+ }
164
+ await replaceVariables(destPath, variables);
165
+ }
166
+ }
167
+
168
+ // Step 6: Generate .env file with localhost defaults
169
+ console.log('\nšŸ” Generating .env file with secure secrets...');
170
+ const { envPath } = await generateEnvFile(destinationRoot, answers);
171
+ console.log(`āœ… Environment file created: ${envPath}`);
172
+
173
+ // Step 7: Create .launchframe marker file with variant metadata
174
+ console.log('šŸ“ Creating LaunchFrame marker file...');
175
+ const markerPath = path.join(destinationRoot, '.launchframe');
176
+ const markerContent = {
177
+ version: '0.1.0',
178
+ createdAt: new Date().toISOString(),
179
+ projectName: answers.projectName,
180
+ projectDisplayName: answers.projectDisplayName,
181
+ deployConfigured: false,
182
+ // Store variant choices for future reference
183
+ variants: variantChoices
184
+ };
185
+ await fs.writeJson(markerPath, markerContent, { spaces: 2 });
186
+
187
+ console.log('āœ… Base project generated with variants applied');
188
+ }
189
+
190
+ module.exports = { generateProject };