@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.
- package/README.md +59 -0
- package/package.json +45 -0
- package/src/commands/deploy-configure.js +219 -0
- package/src/commands/deploy-init.js +277 -0
- package/src/commands/deploy-set-env.js +232 -0
- package/src/commands/deploy-up.js +144 -0
- package/src/commands/docker-build.js +44 -0
- package/src/commands/docker-destroy.js +93 -0
- package/src/commands/docker-down.js +44 -0
- package/src/commands/docker-logs.js +69 -0
- package/src/commands/docker-up.js +73 -0
- package/src/commands/doctor.js +20 -0
- package/src/commands/help.js +79 -0
- package/src/commands/init.js +126 -0
- package/src/commands/service.js +569 -0
- package/src/commands/waitlist-deploy.js +231 -0
- package/src/commands/waitlist-down.js +50 -0
- package/src/commands/waitlist-logs.js +55 -0
- package/src/commands/waitlist-up.js +95 -0
- package/src/generator.js +190 -0
- package/src/index.js +158 -0
- package/src/prompts.js +200 -0
- package/src/services/registry.js +48 -0
- package/src/services/variant-config.js +349 -0
- package/src/utils/docker-helper.js +237 -0
- package/src/utils/env-generator.js +88 -0
- package/src/utils/env-validator.js +75 -0
- package/src/utils/file-ops.js +87 -0
- package/src/utils/project-helpers.js +104 -0
- package/src/utils/section-replacer.js +71 -0
- package/src/utils/ssh-helper.js +220 -0
- package/src/utils/variable-replacer.js +95 -0
- package/src/utils/variant-processor.js +313 -0
|
@@ -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 };
|
package/src/generator.js
ADDED
|
@@ -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 };
|