@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 ADDED
@@ -0,0 +1,59 @@
1
+ # LaunchFrame CLI
2
+
3
+ > Ship your B2B SaaS to production in hours, not weeks.
4
+
5
+ LaunchFrame is a production-ready SaaS boilerplate that deploys to a single affordable VPS. Get subscriptions, credits, multi-tenancy, feature gating, and API management out of the box.
6
+
7
+ ## What You Get
8
+
9
+ - **Single VPS deployment**: Everything runs on one $7-20/mo server (Docker + Traefik)
10
+ - **Variant selection on init**: Choose single/multi-tenant, B2B/B2B2C - optimized for your use case
11
+ - **Service registry**: Add new services (docs, waitlist, admin tools) with zero config
12
+ - **Full-stack TypeScript**: NestJS backend, React frontends, Next.js marketing site
13
+ - **Monetization built-in**: Subscriptions (Polar.sh MOR) + usage-based credits
14
+ - **Flexible tenancy models**: Single-tenant (simpler) or multi-tenant (workspaces + custom domains)
15
+ - **B2B + B2B2C support**: Admin-only or admin+end-user models
16
+ - **Feature guard system**: Tier-based access control across frontend and backend
17
+ - **Production-grade auth**: JWT + OAuth (Google), role-based access control
18
+ - **API-first**: Auto-generated OpenAPI docs, API key management
19
+ - **Resilient architecture**: Background jobs, webhook processing, health checks
20
+
21
+ ## Installation
22
+
23
+ ```bash
24
+ npx @launchframe/cli init
25
+ ```
26
+
27
+ ## Early Access
28
+
29
+ LaunchFrame is currently in **private beta**.
30
+
31
+ Join the waitlist at **[launchframe.dev](https://launchframe.dev)** to get early access, exclusive updates, and founding member perks.
32
+
33
+ ## Why LaunchFrame?
34
+
35
+ Most SaaS boilerplates give you authentication and a database. LaunchFrame gives you a **complete business**:
36
+
37
+ - Subscriptions AND credits (hybrid monetization)
38
+ - Feature flags tied to billing tiers
39
+ - Multi-tenant architecture with project isolation
40
+ - Webhook processing that actually scales
41
+ - API key system with usage tracking
42
+ - Zero-downtime deployment patterns
43
+ - Comprehensive documentation
44
+
45
+ All tested in production. All ready to customize.
46
+
47
+ ## Status
48
+
49
+ This CLI is **preview-only** - it demonstrates the architecture but doesn't yet generate fully functional projects.
50
+
51
+ For production-ready access and full documentation, **[join the waitlist](https://launchframe.dev)**.
52
+
53
+ ## License
54
+
55
+ MIT
56
+
57
+ ---
58
+
59
+ Built with ⚡ by developers who were tired of rebuilding the same SaaS infrastructure over and over.
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@launchframe/cli",
3
+ "version": "0.1.6",
4
+ "description": "Production-ready B2B SaaS boilerplate with subscriptions, credits, and multi-tenancy",
5
+ "main": "src/index.js",
6
+ "bin": {
7
+ "launchframe": "./src/index.js"
8
+ },
9
+ "scripts": {
10
+ "start": "node src/index.js"
11
+ },
12
+ "keywords": [
13
+ "saas",
14
+ "boilerplate",
15
+ "template",
16
+ "nestjs",
17
+ "react",
18
+ "nextjs",
19
+ "typescript",
20
+ "subscriptions",
21
+ "multi-tenant",
22
+ "b2b",
23
+ "starter-kit",
24
+ "cli"
25
+ ],
26
+ "author": "LaunchFrame",
27
+ "license": "MIT",
28
+ "homepage": "https://launchframe.dev",
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "https://github.com/launchframe/cli"
32
+ },
33
+ "bugs": {
34
+ "url": "https://github.com/launchframe/cli/issues"
35
+ },
36
+ "engines": {
37
+ "node": ">=16.0.0"
38
+ },
39
+ "dependencies": {
40
+ "inquirer": "^8.2.5",
41
+ "chalk": "^4.1.2",
42
+ "fs-extra": "^11.1.1",
43
+ "glob": "^10.3.10"
44
+ }
45
+ }
@@ -0,0 +1,219 @@
1
+ const chalk = require('chalk');
2
+ const inquirer = require('inquirer');
3
+ const path = require('path');
4
+ const ora = require('ora');
5
+ const { runDeployPrompts } = require('../prompts');
6
+ const { requireProject, getProjectConfig, updateProjectConfig } = require('../utils/project-helpers');
7
+ const { replaceVariablesInFile } = require('../utils/variable-replacer');
8
+ const { testSSHConnection, executeSSH } = require('../utils/ssh-helper');
9
+
10
+ /**
11
+ * Configure deployment settings
12
+ */
13
+ async function deployConfigure() {
14
+ requireProject();
15
+
16
+ console.log(chalk.blue.bold('\n🚀 LaunchFrame Deployment Configuration\n'));
17
+
18
+ // Check if already configured
19
+ const config = getProjectConfig();
20
+ if (config.deployConfigured) {
21
+ console.log(chalk.yellow('⚠️ Deployment already configured. This will update existing settings.\n'));
22
+ console.log(chalk.gray('Current configuration:'));
23
+ console.log(chalk.gray(` Domain: ${config.deployment?.primaryDomain || 'Not set'}`));
24
+ console.log(chalk.gray(` Email: ${config.deployment?.adminEmail || 'Not set'}`));
25
+ console.log(chalk.gray(` GitHub Org: ${config.deployment?.githubOrg || config.githubOrg || 'Not set'}`));
26
+ console.log(chalk.gray(` VPS Host: ${config.deployment?.vpsHost || 'Not set'}`));
27
+ console.log(chalk.gray(` VPS User: ${config.deployment?.vpsUser || 'Not set'}`));
28
+ console.log(chalk.gray(` VPS App Folder: ${config.deployment?.vpsAppFolder || config.vpsAppFolder || 'Not set'}\n`));
29
+
30
+ const { proceed } = await inquirer.prompt([
31
+ {
32
+ type: 'confirm',
33
+ name: 'proceed',
34
+ message: 'Continue with reconfiguration?',
35
+ default: false
36
+ }
37
+ ]);
38
+
39
+ if (!proceed) {
40
+ console.log(chalk.yellow('\n❌ Configuration cancelled\n'));
41
+ return;
42
+ }
43
+ }
44
+
45
+ console.log(chalk.yellow('Configure your production deployment settings.\n'));
46
+ console.log(chalk.gray('Note: VPS credentials (SSH keys, passwords) are managed via GitHub Secrets.'));
47
+ console.log(chalk.gray('This command configures domain and connection details.\n'));
48
+
49
+ const deployAnswers = await runDeployPrompts(config.projectName);
50
+
51
+ // Prepare variable mappings for file replacement
52
+ const variables = {
53
+ '{{PRIMARY_DOMAIN}}': deployAnswers.primaryDomain,
54
+ '{{ADMIN_EMAIL}}': deployAnswers.adminEmail,
55
+ '{{GITHUB_ORG}}': deployAnswers.githubOrg,
56
+ '{{VPS_APP_FOLDER}}': deployAnswers.vpsAppFolder
57
+ };
58
+
59
+ console.log(chalk.yellow('\n⚙️ Updating configuration files...\n'));
60
+
61
+ // Files that need template variable replacement
62
+ const filesToUpdate = [
63
+ 'infrastructure/.env',
64
+ 'infrastructure/.env.example'
65
+ ];
66
+
67
+ const projectRoot = process.cwd();
68
+ let filesUpdated = 0;
69
+
70
+ for (const relativePath of filesToUpdate) {
71
+ const filePath = path.join(projectRoot, relativePath);
72
+
73
+ try {
74
+ const fs = require('fs-extra');
75
+ if (!await fs.pathExists(filePath)) {
76
+ console.log(chalk.gray(` ⊘ Skipping ${relativePath} (not found)`));
77
+ continue;
78
+ }
79
+
80
+ const modified = await replaceVariablesInFile(filePath, variables);
81
+ if (modified) {
82
+ console.log(chalk.green(` ✓ Updated ${relativePath}`));
83
+ filesUpdated++;
84
+ } else {
85
+ console.log(chalk.gray(` − No changes needed in ${relativePath}`));
86
+ }
87
+ } catch (error) {
88
+ console.log(chalk.red(` ✗ Failed to update ${relativePath}: ${error.message}`));
89
+ }
90
+ }
91
+
92
+ // Update all installed component .env.prod files with new domain
93
+ const installedComponents = config.installedComponents || [];
94
+ if (installedComponents.length > 0) {
95
+ console.log(chalk.yellow('\n⚙️ Updating component environment files...\n'));
96
+
97
+ for (const componentName of installedComponents) {
98
+ const componentEnvProdPath = path.join(projectRoot, componentName, '.env.prod');
99
+
100
+ try {
101
+ const fs = require('fs-extra');
102
+ if (!await fs.pathExists(componentEnvProdPath)) {
103
+ console.log(chalk.gray(` ⊘ Skipping ${componentName}/.env.prod (not found)`));
104
+ continue;
105
+ }
106
+
107
+ // Read current .env.prod content
108
+ let envContent = await fs.readFile(componentEnvProdPath, 'utf8');
109
+
110
+ // Replace PRIMARY_DOMAIN with new value
111
+ const oldDomainMatch = envContent.match(/PRIMARY_DOMAIN=.*/);
112
+ if (oldDomainMatch) {
113
+ const newEnvContent = envContent.replace(
114
+ /PRIMARY_DOMAIN=.*/,
115
+ `PRIMARY_DOMAIN=${deployAnswers.primaryDomain}`
116
+ );
117
+
118
+ if (newEnvContent !== envContent) {
119
+ await fs.writeFile(componentEnvProdPath, newEnvContent, 'utf8');
120
+ console.log(chalk.green(` ✓ Updated ${componentName}/.env.prod`));
121
+ filesUpdated++;
122
+ } else {
123
+ console.log(chalk.gray(` − No changes needed in ${componentName}/.env.prod`));
124
+ }
125
+ }
126
+ } catch (error) {
127
+ console.log(chalk.red(` ✗ Failed to update ${componentName}/.env.prod: ${error.message}`));
128
+ }
129
+ }
130
+ }
131
+
132
+ // Update .launchframe config
133
+ console.log(chalk.yellow('\n📝 Saving configuration...\n'));
134
+
135
+ const updatedConfig = {
136
+ ...config,
137
+ primaryDomain: deployAnswers.primaryDomain,
138
+ githubOrg: deployAnswers.githubOrg,
139
+ vpsAppFolder: deployAnswers.vpsAppFolder,
140
+ deployConfigured: true,
141
+ deployment: {
142
+ adminEmail: deployAnswers.adminEmail,
143
+ vpsHost: deployAnswers.vpsHost,
144
+ vpsUser: deployAnswers.vpsUser,
145
+ ghcrToken: deployAnswers.ghcrToken,
146
+ configuredAt: new Date().toISOString()
147
+ }
148
+ };
149
+
150
+ updateProjectConfig(updatedConfig);
151
+
152
+ // Authenticate to GHCR on VPS
153
+ console.log(chalk.yellow('\n🔐 Authenticating to GitHub Container Registry on VPS...\n'));
154
+
155
+ const authSpinner = ora('Testing VPS connection...').start();
156
+
157
+ try {
158
+ // Test SSH connection
159
+ const connectionTest = await testSSHConnection(deployAnswers.vpsUser, deployAnswers.vpsHost);
160
+
161
+ if (!connectionTest.success) {
162
+ authSpinner.warn('Could not connect to VPS');
163
+ console.log(chalk.yellow('\n⚠️ Warning: Unable to authenticate to GHCR on VPS\n'));
164
+ console.log(chalk.gray(`Error: ${connectionTest.error}`));
165
+ console.log(chalk.gray('\nYou can authenticate manually later by running:'));
166
+ console.log(chalk.white(` ssh ${deployAnswers.vpsUser}@${deployAnswers.vpsHost} "echo 'YOUR_GHCR_TOKEN' | docker login ghcr.io -u ${deployAnswers.githubOrg} --password-stdin"\n`));
167
+ } else {
168
+ authSpinner.text = 'Logging into GitHub Container Registry...';
169
+
170
+ // Login to GHCR on VPS
171
+ await executeSSH(
172
+ deployAnswers.vpsUser,
173
+ deployAnswers.vpsHost,
174
+ `echo '${deployAnswers.ghcrToken}' | docker login ghcr.io -u ${deployAnswers.githubOrg} --password-stdin`
175
+ );
176
+
177
+ authSpinner.succeed('VPS authenticated to GitHub Container Registry');
178
+ console.log(chalk.gray(' Docker can now pull images from ghcr.io\n'));
179
+ }
180
+ } catch (error) {
181
+ authSpinner.warn('Could not authenticate to GHCR');
182
+ console.log(chalk.yellow('\n⚠️ Warning: Failed to authenticate to GHCR on VPS\n'));
183
+ console.log(chalk.gray(`Error: ${error.message}`));
184
+ console.log(chalk.gray('\nYou can authenticate manually later by running:'));
185
+ console.log(chalk.white(` ssh ${deployAnswers.vpsUser}@${deployAnswers.vpsHost} "echo 'YOUR_GHCR_TOKEN' | docker login ghcr.io -u ${deployAnswers.githubOrg} --password-stdin"\n`));
186
+ }
187
+
188
+ console.log(chalk.green.bold('\n✅ Deployment configuration complete!\n'));
189
+ console.log(chalk.white('Configuration saved to:'));
190
+ console.log(chalk.gray(` - .launchframe`));
191
+ if (filesUpdated > 0) {
192
+ console.log(chalk.gray(` - ${filesUpdated} file(s) updated\n`));
193
+ }
194
+
195
+ console.log(chalk.white('Prerequisites:\n'));
196
+ console.log(chalk.white('• Docker must be installed and running locally'));
197
+ console.log(chalk.gray(' (Required for building production images during deploy:init)\n'));
198
+
199
+ console.log(chalk.white('Next steps:\n'));
200
+ console.log(chalk.white('1. Configure GitHub Secrets (required for CI/CD):'));
201
+ console.log(chalk.gray(` Repository: https://github.com/${deployAnswers.githubOrg}/${config.projectName}/settings/secrets/actions`));
202
+ console.log(chalk.gray(` Required secrets:`));
203
+ console.log(chalk.gray(` - VPS_HOST: ${deployAnswers.vpsHost}`));
204
+ console.log(chalk.gray(` - VPS_USER: ${deployAnswers.vpsUser}`));
205
+ console.log(chalk.gray(` - VPS_SSH_KEY: (your private SSH key)`));
206
+ console.log(chalk.gray(` - GHCR_TOKEN: (use the same token you just provided)`));
207
+
208
+ console.log(chalk.white('\n2. Point DNS records to your VPS:'));
209
+ console.log(chalk.gray(` - A record: ${deployAnswers.primaryDomain} → VPS IP`));
210
+ console.log(chalk.gray(` - A record: *.${deployAnswers.primaryDomain} → VPS IP`));
211
+
212
+ console.log(chalk.white('\n3. Configure production environment variables:'));
213
+ console.log(chalk.gray(` Run: launchframe deploy:set-env`));
214
+
215
+ console.log(chalk.white('\n4. Initial VPS deployment:'));
216
+ console.log(chalk.gray(` Run: launchframe deploy:init\n`));
217
+ }
218
+
219
+ module.exports = { deployConfigure };
@@ -0,0 +1,277 @@
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 } = require('../utils/project-helpers');
6
+ const { validateEnvProd } = require('../utils/env-validator');
7
+ const {
8
+ testSSHConnection,
9
+ checkSSHKeys,
10
+ executeSSH,
11
+ copyFileToVPS,
12
+ copyDirectoryToVPS,
13
+ checkRepoPrivacy,
14
+ showDeployKeyInstructions
15
+ } = require('../utils/ssh-helper');
16
+
17
+ /**
18
+ * Initial VPS setup - clone repo and configure environment
19
+ */
20
+ async function deployInit() {
21
+ requireProject();
22
+
23
+ console.log(chalk.blue.bold('\n🚀 LaunchFrame Initial VPS Setup\n'));
24
+
25
+ const config = getProjectConfig();
26
+
27
+ // Validate deployment is configured
28
+ if (!config.deployConfigured || !config.deployment) {
29
+ console.log(chalk.red('❌ Error: Deployment not configured yet\n'));
30
+ console.log(chalk.gray('Run this command first:'));
31
+ console.log(chalk.white(' launchframe deploy:configure\n'));
32
+ process.exit(1);
33
+ }
34
+
35
+ const { vpsHost, vpsUser, vpsAppFolder, githubOrg } = config.deployment;
36
+ const { projectName } = config;
37
+ const projectRoot = process.cwd();
38
+ const envProdPath = path.join(projectRoot, 'infrastructure', '.env.prod');
39
+
40
+ // Step 1: Validate .env.prod exists and has no placeholders
41
+ console.log(chalk.yellow('📋 Step 1: Validating production environment...\n'));
42
+
43
+ const validation = await validateEnvProd(envProdPath);
44
+ if (!validation.valid) {
45
+ console.log(chalk.red(`❌ Error: ${validation.error}\n`));
46
+
47
+ if (validation.placeholders) {
48
+ console.log(chalk.yellow('Placeholder variables found:'));
49
+ validation.placeholders.forEach(p => console.log(chalk.gray(` - ${p}`)));
50
+ console.log();
51
+ }
52
+
53
+ console.log(chalk.gray('Run this command first:'));
54
+ console.log(chalk.white(' launchframe deploy:set-env\n'));
55
+ process.exit(1);
56
+ }
57
+
58
+ console.log(chalk.green('✓ Production environment validated\n'));
59
+
60
+ // Step 2: Check SSH keys
61
+ console.log(chalk.yellow('🔑 Step 2: Checking SSH configuration...\n'));
62
+
63
+ const { hasKeys, keyPaths } = await checkSSHKeys();
64
+ if (!hasKeys) {
65
+ console.log(chalk.yellow('⚠️ Warning: No SSH keys found in ~/.ssh/\n'));
66
+ console.log(chalk.gray('You may need to generate SSH keys:'));
67
+ console.log(chalk.white(' ssh-keygen -t ed25519\n'));
68
+ } else {
69
+ console.log(chalk.green(`✓ Found SSH keys: ${keyPaths.length} key(s)\n`));
70
+ }
71
+
72
+ // Step 3: Test SSH connection
73
+ console.log(chalk.yellow('🔌 Step 3: Testing VPS connection...\n'));
74
+
75
+ const spinner = ora('Connecting to VPS...').start();
76
+ const connectionTest = await testSSHConnection(vpsUser, vpsHost);
77
+
78
+ if (!connectionTest.success) {
79
+ spinner.fail('Failed to connect to VPS');
80
+ console.log(chalk.red(`\n❌ SSH connection failed: ${connectionTest.error}\n`));
81
+ console.log(chalk.gray('Troubleshooting:'));
82
+ console.log(chalk.gray(` - Check VPS is online: ping ${vpsHost}`));
83
+ console.log(chalk.gray(` - Test SSH manually: ssh ${vpsUser}@${vpsHost}`));
84
+ console.log(chalk.gray(' - Verify SSH keys are configured\n'));
85
+ process.exit(1);
86
+ }
87
+
88
+ spinner.succeed('Connected to VPS successfully');
89
+ console.log();
90
+
91
+ // Step 3.5: Build and push Docker images
92
+ console.log(chalk.yellow('🐳 Step 3.5: Building Docker images locally...\n'));
93
+
94
+ // Check if Docker is running
95
+ const {
96
+ checkDockerRunning,
97
+ loginToGHCR,
98
+ buildFullAppImages
99
+ } = require('../utils/docker-helper');
100
+
101
+ const dockerRunning = await checkDockerRunning();
102
+ if (!dockerRunning) {
103
+ console.log(chalk.red('❌ Docker is not running\n'));
104
+ console.log(chalk.gray('Please start Docker Desktop and try again.\n'));
105
+ console.log(chalk.gray('Docker is required to build production images for deployment.\n'));
106
+ process.exit(1);
107
+ }
108
+
109
+ // Validate GHCR token is configured
110
+ const { ghcrToken } = config.deployment || {};
111
+ if (!ghcrToken) {
112
+ console.log(chalk.red('❌ GHCR token not configured\n'));
113
+ console.log(chalk.gray('Run this command first:'));
114
+ console.log(chalk.white(' launchframe deploy:configure\n'));
115
+ process.exit(1);
116
+ }
117
+
118
+ try {
119
+ // Login to GHCR
120
+ await loginToGHCR(githubOrg, ghcrToken);
121
+
122
+ // Build full-app images
123
+ await buildFullAppImages(projectRoot, projectName, githubOrg, envProdPath);
124
+
125
+ console.log(chalk.green.bold('\n✅ All images built and pushed to GHCR!\n'));
126
+ } catch (error) {
127
+ console.log(chalk.red('\n❌ Failed to build Docker images\n'));
128
+ console.log(chalk.gray('Error:'), error.message, '\n');
129
+ console.log(chalk.gray('Common issues:'));
130
+ console.log(chalk.gray(' - Invalid GHCR token (check write:packages permission)'));
131
+ console.log(chalk.gray(' - Dockerfile syntax errors'));
132
+ console.log(chalk.gray(' - Insufficient disk space (~10GB required)'));
133
+ console.log(chalk.gray(' - Network connection issues\n'));
134
+ console.log(chalk.white('To create a valid token:'));
135
+ console.log(chalk.gray(' https://github.com/settings/tokens/new\n'));
136
+ process.exit(1);
137
+ }
138
+
139
+
140
+ // Step 4: Check repository privacy
141
+ console.log(chalk.yellow('🔍 Step 4: Checking repository access...\n'));
142
+
143
+ const repoCheck = await checkRepoPrivacy(githubOrg, projectName);
144
+ if (repoCheck.isPrivate) {
145
+ console.log(chalk.yellow('⚠️ Repository appears to be private or inaccessible\n'));
146
+ showDeployKeyInstructions(vpsUser, vpsHost, githubOrg, projectName);
147
+ process.exit(1);
148
+ }
149
+
150
+ console.log(chalk.green('✓ Repository is accessible (public)\n'));
151
+
152
+ // Step 5: Create app directory and clone repository
153
+ console.log(chalk.yellow('📦 Step 5: Setting up application on VPS...\n'));
154
+
155
+ const cloneSpinner = ora('Creating app directory...').start();
156
+
157
+ try {
158
+ // Create directory
159
+ await executeSSH(vpsUser, vpsHost, `mkdir -p ${vpsAppFolder}`);
160
+ cloneSpinner.text = 'Cloning repository...';
161
+
162
+ // Check if directory is empty or has .git
163
+ const { stdout: lsOutput } = await executeSSH(vpsUser, vpsHost, `ls -A ${vpsAppFolder}`);
164
+
165
+ if (lsOutput.trim()) {
166
+ // Directory not empty
167
+ const { stdout: gitCheck } = await executeSSH(vpsUser, vpsHost, `test -d ${vpsAppFolder}/.git && echo "exists" || echo "missing"`);
168
+
169
+ if (gitCheck.trim() === 'exists') {
170
+ cloneSpinner.text = 'Repository already cloned, pulling latest changes...';
171
+ await executeSSH(vpsUser, vpsHost, `cd ${vpsAppFolder} && git pull origin main || git pull origin master`);
172
+ } else {
173
+ cloneSpinner.warn('Directory not empty and not a git repository');
174
+ console.log(chalk.yellow(`\n⚠️ Warning: ${vpsAppFolder} exists but is not a git repository\n`));
175
+ console.log(chalk.gray('Please clean up the directory or choose a different path.\n'));
176
+ process.exit(1);
177
+ }
178
+ } else {
179
+ // Clone fresh
180
+ await executeSSH(
181
+ vpsUser,
182
+ vpsHost,
183
+ `git clone https://github.com/${githubOrg}/${projectName}.git ${vpsAppFolder}`,
184
+ { timeout: 180000 } // 3 minutes for clone
185
+ );
186
+ }
187
+
188
+ cloneSpinner.succeed('Repository setup complete');
189
+ } catch (error) {
190
+ cloneSpinner.fail('Failed to setup repository');
191
+ console.log(chalk.red(`\n❌ Error: ${error.message}\n`));
192
+ process.exit(1);
193
+ }
194
+
195
+ // Check if waitlist is running and stop it (full-app deployment)
196
+ try {
197
+ const { stdout: psOutput } = await executeSSH(
198
+ vpsUser,
199
+ vpsHost,
200
+ `docker ps --filter "name=${projectName}-waitlist" --format "{{.Names}}"`,
201
+ { timeout: 10000 }
202
+ );
203
+
204
+ if (psOutput.trim()) {
205
+ console.log(chalk.yellow('\n⚠️ Waitlist is currently running. Stopping it...\n'));
206
+
207
+ const stopSpinner = ora('Stopping waitlist containers...').start();
208
+
209
+ try {
210
+ await executeSSH(
211
+ vpsUser,
212
+ vpsHost,
213
+ `cd ${vpsAppFolder}/waitlist && docker-compose -f docker-compose.waitlist.yml down`,
214
+ { timeout: 30000 }
215
+ );
216
+ stopSpinner.succeed('Waitlist stopped');
217
+ } catch (stopError) {
218
+ stopSpinner.warn('Could not stop waitlist automatically');
219
+ console.log(chalk.gray('You may need to stop it manually after deployment.\n'));
220
+ }
221
+ }
222
+ } catch (error) {
223
+ // If error, waitlist probably not running - continue
224
+ }
225
+
226
+ // Step 6: Copy .env.prod to VPS
227
+ console.log(chalk.yellow('\n📄 Step 6: Copying production environment...\n'));
228
+
229
+ const envSpinner = ora('Copying .env.prod to VPS...').start();
230
+
231
+ try {
232
+ const remoteEnvPath = `${vpsAppFolder}/infrastructure/.env`;
233
+ await copyFileToVPS(envProdPath, vpsUser, vpsHost, remoteEnvPath);
234
+ envSpinner.succeed('.env.prod copied successfully');
235
+ } catch (error) {
236
+ envSpinner.fail('Failed to copy .env.prod');
237
+ console.log(chalk.red(`\n❌ Error: ${error.message}\n`));
238
+ process.exit(1);
239
+ }
240
+
241
+ // Step 7: Pull Docker images
242
+ console.log(chalk.yellow('\n🐳 Step 7: Pulling Docker images...\n'));
243
+ console.log(chalk.gray('This may take several minutes...\n'));
244
+
245
+ const dockerSpinner = ora('Pulling Docker images...').start();
246
+
247
+ try {
248
+ await executeSSH(
249
+ vpsUser,
250
+ vpsHost,
251
+ `cd ${vpsAppFolder}/infrastructure && docker-compose -f docker-compose.yml -f docker-compose.prod.yml pull`,
252
+ { timeout: 600000 } // 10 minutes for image pull
253
+ );
254
+ dockerSpinner.succeed('Docker images pulled successfully');
255
+ } catch (error) {
256
+ dockerSpinner.fail('Failed to pull Docker images');
257
+ console.log(chalk.yellow(`\n⚠️ Warning: ${error.message}\n`));
258
+ console.log(chalk.gray('This might mean Docker is not installed on the VPS.'));
259
+ console.log(chalk.gray('Please install Docker and Docker Compose:\n'));
260
+ console.log(chalk.white(' curl -fsSL https://get.docker.com | sh'));
261
+ console.log(chalk.white(' sudo usermod -aG docker ${vpsUser}\n'));
262
+ process.exit(1);
263
+ }
264
+
265
+ // Success!
266
+ console.log(chalk.green.bold('\n✅ Initial VPS setup complete!\n'));
267
+
268
+ console.log(chalk.white('Summary:'));
269
+ console.log(chalk.gray(` - Repository cloned to: ${vpsAppFolder}`));
270
+ console.log(chalk.gray(' - Production .env configured'));
271
+ console.log(chalk.gray(' - Docker images pulled\n'));
272
+
273
+ console.log(chalk.white('Next step:'));
274
+ console.log(chalk.gray(' Run: launchframe deploy:up\n'));
275
+ }
276
+
277
+ module.exports = { deployInit };