@launchframe/cli 1.0.0-beta.2 → 1.0.0-beta.20
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/CLAUDE.md +27 -0
- package/README.md +7 -1
- package/package.json +1 -1
- package/src/commands/cache.js +14 -14
- package/src/commands/deploy-build.js +76 -0
- package/src/commands/deploy-configure.js +15 -6
- package/src/commands/deploy-init.js +40 -57
- package/src/commands/deploy-set-env.js +17 -7
- package/src/commands/deploy-up.js +4 -3
- package/src/commands/help.js +10 -5
- package/src/commands/init.js +39 -64
- package/src/commands/service.js +12 -6
- package/src/commands/waitlist-deploy.js +1 -0
- package/src/generator.js +34 -44
- package/src/index.js +13 -10
- package/src/services/variant-config.js +36 -25
- package/src/utils/docker-helper.js +116 -2
- package/src/utils/env-generator.js +9 -6
- package/src/utils/env-validator.js +4 -2
- package/src/utils/github-access.js +19 -17
- package/src/utils/logger.js +93 -0
- package/src/utils/project-helpers.js +5 -1
- package/src/utils/{module-cache.js → service-cache.js} +67 -73
- package/src/utils/ssh-helper.js +51 -1
- package/src/utils/variant-processor.js +35 -42
package/CLAUDE.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# LaunchFrame CLI
|
|
2
|
+
|
|
3
|
+
CLI tool for generating new projects from the LaunchFrame template.
|
|
4
|
+
|
|
5
|
+
## Purpose
|
|
6
|
+
|
|
7
|
+
The CLI takes user input (project name, domain, GitHub org, etc.) and:
|
|
8
|
+
1. Copies the `services/` template
|
|
9
|
+
2. Replaces all `{{TEMPLATE_VARIABLES}}`
|
|
10
|
+
3. Generates secrets (DB password, auth secret)
|
|
11
|
+
4. Sets up the project structure
|
|
12
|
+
|
|
13
|
+
## Template Variables
|
|
14
|
+
|
|
15
|
+
The CLI replaces these placeholders in all files:
|
|
16
|
+
- `{{PROJECT_NAME}}` - lowercase project name
|
|
17
|
+
- `{{PROJECT_NAME_UPPER}}` - uppercase project name
|
|
18
|
+
- `{{GITHUB_ORG}}` - GitHub organization/username
|
|
19
|
+
- `{{PRIMARY_DOMAIN}}` - main domain (e.g., mysaas.com)
|
|
20
|
+
- `{{ADMIN_EMAIL}}` - admin email for Let's Encrypt
|
|
21
|
+
- `{{VPS_HOST}}` - VPS hostname/IP
|
|
22
|
+
- `{{BETTER_AUTH_SECRET}}` - auto-generated (32+ chars)
|
|
23
|
+
- `{{DB_PASSWORD}}` - auto-generated
|
|
24
|
+
|
|
25
|
+
## Development
|
|
26
|
+
|
|
27
|
+
TODO: CLI implementation details
|
package/README.md
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
# LaunchFrame CLI
|
|
2
2
|
|
|
3
|
-
> Ship your B2B SaaS to production in hours, not
|
|
3
|
+
> Ship your B2B SaaS to production in hours, not months.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
**🚀 We're looking for beta testers!** Get free lifetime access to LaunchFrame [here](https://launchframe.dev/#beta-signup). Limited spots available.
|
|
8
|
+
|
|
9
|
+
---
|
|
4
10
|
|
|
5
11
|
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
12
|
|
package/package.json
CHANGED
package/src/commands/cache.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
const chalk = require('chalk');
|
|
2
|
-
const { clearCache, getCacheInfo } = require('../utils/
|
|
2
|
+
const { clearCache, getCacheInfo } = require('../utils/service-cache');
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* Clear
|
|
5
|
+
* Clear service cache
|
|
6
6
|
*/
|
|
7
7
|
async function cacheClear() {
|
|
8
|
-
console.log(chalk.yellow('\n⚠️ This will delete all cached
|
|
8
|
+
console.log(chalk.yellow('\n⚠️ This will delete all cached services'));
|
|
9
9
|
console.log(chalk.gray('You will need to re-download on next init or service:add\n'));
|
|
10
10
|
|
|
11
11
|
const inquirer = require('inquirer');
|
|
@@ -30,7 +30,7 @@ async function cacheClear() {
|
|
|
30
30
|
async function cacheInfo() {
|
|
31
31
|
const info = await getCacheInfo();
|
|
32
32
|
|
|
33
|
-
console.log(chalk.blue('\n📦
|
|
33
|
+
console.log(chalk.blue('\n📦 Service Cache Information\n'));
|
|
34
34
|
|
|
35
35
|
console.log(chalk.white('Location:'));
|
|
36
36
|
console.log(chalk.gray(` ${info.path}\n`));
|
|
@@ -54,14 +54,14 @@ async function cacheInfo() {
|
|
|
54
54
|
console.log(chalk.gray(` ${info.lastUpdate.toLocaleString()}\n`));
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
if (info.
|
|
58
|
-
console.log(chalk.white('Cached
|
|
59
|
-
info.
|
|
57
|
+
if (info.services && info.services.length > 0) {
|
|
58
|
+
console.log(chalk.white('Cached Services:'));
|
|
59
|
+
info.services.forEach(mod => {
|
|
60
60
|
console.log(chalk.gray(` • ${mod}`));
|
|
61
61
|
});
|
|
62
62
|
console.log('');
|
|
63
63
|
} else {
|
|
64
|
-
console.log(chalk.gray('No
|
|
64
|
+
console.log(chalk.gray('No services cached yet\n'));
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
console.log(chalk.gray('Commands:'));
|
|
@@ -73,21 +73,21 @@ async function cacheInfo() {
|
|
|
73
73
|
* Force update cache
|
|
74
74
|
*/
|
|
75
75
|
async function cacheUpdate() {
|
|
76
|
-
const { ensureCacheReady, getCacheInfo } = require('../utils/
|
|
76
|
+
const { ensureCacheReady, getCacheInfo } = require('../utils/service-cache');
|
|
77
77
|
|
|
78
78
|
console.log(chalk.blue('\n🔄 Forcing cache update...\n'));
|
|
79
79
|
|
|
80
80
|
try {
|
|
81
81
|
const info = await getCacheInfo();
|
|
82
|
-
const
|
|
82
|
+
const currentServices = info.services || [];
|
|
83
83
|
|
|
84
|
-
if (
|
|
85
|
-
console.log(chalk.yellow('No
|
|
84
|
+
if (currentServices.length === 0) {
|
|
85
|
+
console.log(chalk.yellow('No services in cache yet. Use init or service:add to populate.\n'));
|
|
86
86
|
return;
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
-
// Update cache with current
|
|
90
|
-
await ensureCacheReady(
|
|
89
|
+
// Update cache with current services
|
|
90
|
+
await ensureCacheReady(currentServices);
|
|
91
91
|
console.log(chalk.green('\n✓ Cache updated successfully\n'));
|
|
92
92
|
} catch (error) {
|
|
93
93
|
console.error(chalk.red(`\n❌ Failed to update cache: ${error.message}\n`));
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
const chalk = require('chalk');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { requireProject, getProjectConfig } = require('../utils/project-helpers');
|
|
4
|
+
const { buildAndPushWorkflow } = require('../utils/docker-helper');
|
|
5
|
+
const { pullImagesOnVPS, restartServicesOnVPS } = require('../utils/ssh-helper');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Build, push, and deploy Docker images
|
|
9
|
+
* @param {string} [serviceName] - Optional specific service to build (e.g., 'backend', 'admin-portal')
|
|
10
|
+
*/
|
|
11
|
+
async function deployBuild(serviceName) {
|
|
12
|
+
requireProject();
|
|
13
|
+
|
|
14
|
+
const serviceLabel = serviceName ? `(${serviceName})` : '(all services)';
|
|
15
|
+
console.log(chalk.blue.bold(`\n🔨 LaunchFrame Build & Deploy ${serviceLabel}\n`));
|
|
16
|
+
|
|
17
|
+
const config = getProjectConfig();
|
|
18
|
+
const projectRoot = process.cwd();
|
|
19
|
+
|
|
20
|
+
// Validate deployment is configured
|
|
21
|
+
if (!config.deployConfigured || !config.deployment) {
|
|
22
|
+
console.log(chalk.red('❌ Error: Deployment not configured yet\n'));
|
|
23
|
+
console.log(chalk.gray('Run this command first:'));
|
|
24
|
+
console.log(chalk.white(' launchframe deploy:configure\n'));
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const { vpsHost, vpsUser, vpsAppFolder, githubOrg, ghcrToken } = config.deployment;
|
|
29
|
+
const { projectName, installedServices } = config;
|
|
30
|
+
const envProdPath = path.join(projectRoot, 'infrastructure', '.env.prod');
|
|
31
|
+
|
|
32
|
+
// Step 1-3: Build and push images
|
|
33
|
+
console.log(chalk.yellow('🐳 Step 1: Building and pushing images...\n'));
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
await buildAndPushWorkflow({
|
|
37
|
+
projectRoot,
|
|
38
|
+
projectName,
|
|
39
|
+
githubOrg,
|
|
40
|
+
ghcrToken,
|
|
41
|
+
envProdPath,
|
|
42
|
+
installedServices,
|
|
43
|
+
serviceName
|
|
44
|
+
});
|
|
45
|
+
} catch (error) {
|
|
46
|
+
console.log(chalk.red(`\n❌ ${error.message}\n`));
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Step 4: Pull images on VPS
|
|
51
|
+
console.log(chalk.yellow('🚀 Step 2: Pulling images on VPS...\n'));
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
await pullImagesOnVPS(vpsUser, vpsHost, vpsAppFolder);
|
|
55
|
+
} catch (error) {
|
|
56
|
+
console.log(chalk.red(`\n❌ ${error.message}\n`));
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Step 5: Restart services
|
|
61
|
+
console.log(chalk.yellow('\n🔄 Step 3: Restarting services...\n'));
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
await restartServicesOnVPS(vpsUser, vpsHost, vpsAppFolder);
|
|
65
|
+
} catch (error) {
|
|
66
|
+
console.log(chalk.red(`\n❌ ${error.message}\n`));
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Success!
|
|
71
|
+
console.log(chalk.green.bold('\n✅ Build and deploy complete!\n'));
|
|
72
|
+
|
|
73
|
+
console.log(chalk.gray('Your updated application is now running.\n'));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
module.exports = { deployBuild };
|
|
@@ -59,8 +59,8 @@ async function deployConfigure() {
|
|
|
59
59
|
console.log(chalk.yellow('\n⚙️ Updating configuration files...\n'));
|
|
60
60
|
|
|
61
61
|
// Files that need template variable replacement
|
|
62
|
+
// Note: infrastructure/.env is NOT updated - it's for local development only
|
|
62
63
|
const filesToUpdate = [
|
|
63
|
-
'infrastructure/.env',
|
|
64
64
|
'infrastructure/.env.example',
|
|
65
65
|
'infrastructure/docker-compose.yml',
|
|
66
66
|
'infrastructure/docker-compose.dev.yml',
|
|
@@ -71,16 +71,25 @@ async function deployConfigure() {
|
|
|
71
71
|
'admin-portal/public/env-config.js',
|
|
72
72
|
'admin-portal/src/config/runtime.ts',
|
|
73
73
|
'admin-portal/src/config/pageMetadata.ts',
|
|
74
|
-
'admin-portal/src/pages/FirstProject.tsx',
|
|
75
|
-
'admin-portal/src/components/projects/NewProject.tsx',
|
|
76
|
-
'admin-portal/src/components/settings/CustomDomain.tsx',
|
|
77
74
|
'admin-portal/src/App.tsx',
|
|
78
75
|
'admin-portal/src/components/common/PageTitle.tsx',
|
|
79
76
|
'admin-portal/src/sentry.tsx',
|
|
80
|
-
'admin-portal/src/pages/AppSumo.tsx',
|
|
81
|
-
'customers-portal/src/App.tsx'
|
|
82
77
|
];
|
|
83
78
|
|
|
79
|
+
if (config.variants.tenancy === 'multi-tenant') {
|
|
80
|
+
filesToUpdate.push(
|
|
81
|
+
'admin-portal/src/pages/FirstProject.tsx',
|
|
82
|
+
'admin-portal/src/components/projects/NewProject.tsx',
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (config.variants.userModel === 'b2b2c') {
|
|
87
|
+
filesToUpdate.push(
|
|
88
|
+
'admin-portal/src/components/settings/CustomDomain.tsx',
|
|
89
|
+
'customers-portal/src/App.tsx'
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
|
|
84
93
|
const projectRoot = process.cwd();
|
|
85
94
|
let filesUpdated = 0;
|
|
86
95
|
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
const chalk = require('chalk');
|
|
2
2
|
const path = require('path');
|
|
3
|
-
const fs = require('fs-extra');
|
|
4
3
|
const ora = require('ora');
|
|
5
4
|
const { requireProject, getProjectConfig } = require('../utils/project-helpers');
|
|
6
5
|
const { validateEnvProd } = require('../utils/env-validator');
|
|
@@ -9,8 +8,10 @@ const {
|
|
|
9
8
|
checkSSHKeys,
|
|
10
9
|
executeSSH,
|
|
11
10
|
copyFileToVPS,
|
|
12
|
-
copyDirectoryToVPS
|
|
11
|
+
copyDirectoryToVPS,
|
|
12
|
+
pullImagesOnVPS
|
|
13
13
|
} = require('../utils/ssh-helper');
|
|
14
|
+
const { buildAndPushWorkflow } = require('../utils/docker-helper');
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* Initial VPS setup - copy infrastructure files and configure environment
|
|
@@ -30,8 +31,8 @@ async function deployInit() {
|
|
|
30
31
|
process.exit(1);
|
|
31
32
|
}
|
|
32
33
|
|
|
33
|
-
const { vpsHost, vpsUser, vpsAppFolder, githubOrg } = config.deployment;
|
|
34
|
-
const { projectName } = config;
|
|
34
|
+
const { vpsHost, vpsUser, vpsAppFolder, githubOrg, ghcrToken } = config.deployment;
|
|
35
|
+
const { projectName, installedServices } = config;
|
|
35
36
|
const projectRoot = process.cwd();
|
|
36
37
|
const envProdPath = path.join(projectRoot, 'infrastructure', '.env.prod');
|
|
37
38
|
|
|
@@ -86,42 +87,18 @@ async function deployInit() {
|
|
|
86
87
|
spinner.succeed('Connected to VPS successfully');
|
|
87
88
|
console.log();
|
|
88
89
|
|
|
89
|
-
// Step
|
|
90
|
-
console.log(chalk.yellow('🐳 Step
|
|
91
|
-
|
|
92
|
-
// Check if Docker is running
|
|
93
|
-
const {
|
|
94
|
-
checkDockerRunning,
|
|
95
|
-
loginToGHCR,
|
|
96
|
-
buildFullAppImages
|
|
97
|
-
} = require('../utils/docker-helper');
|
|
98
|
-
|
|
99
|
-
const dockerRunning = await checkDockerRunning();
|
|
100
|
-
if (!dockerRunning) {
|
|
101
|
-
console.log(chalk.red('❌ Docker is not running\n'));
|
|
102
|
-
console.log(chalk.gray('Please start Docker Desktop and try again.\n'));
|
|
103
|
-
console.log(chalk.gray('Docker is required to build production images for deployment.\n'));
|
|
104
|
-
process.exit(1);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// Validate GHCR token is configured
|
|
108
|
-
const { ghcrToken } = config.deployment || {};
|
|
109
|
-
if (!ghcrToken) {
|
|
110
|
-
console.log(chalk.red('❌ GHCR token not configured\n'));
|
|
111
|
-
console.log(chalk.gray('Run this command first:'));
|
|
112
|
-
console.log(chalk.white(' launchframe deploy:configure\n'));
|
|
113
|
-
process.exit(1);
|
|
114
|
-
}
|
|
90
|
+
// Step 4: Build and push Docker images
|
|
91
|
+
console.log(chalk.yellow('🐳 Step 4: Building Docker images locally...\n'));
|
|
115
92
|
|
|
116
93
|
try {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
94
|
+
await buildAndPushWorkflow({
|
|
95
|
+
projectRoot,
|
|
96
|
+
projectName,
|
|
97
|
+
githubOrg,
|
|
98
|
+
ghcrToken,
|
|
99
|
+
envProdPath,
|
|
100
|
+
installedServices: installedServices || ['backend', 'admin-portal', 'website']
|
|
101
|
+
});
|
|
125
102
|
} catch (error) {
|
|
126
103
|
console.log(chalk.red('\n❌ Failed to build Docker images\n'));
|
|
127
104
|
console.log(chalk.gray('Error:'), error.message, '\n');
|
|
@@ -135,16 +112,15 @@ async function deployInit() {
|
|
|
135
112
|
process.exit(1);
|
|
136
113
|
}
|
|
137
114
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
console.log(chalk.yellow('📦 Step 4: Setting up application on VPS...\n'));
|
|
115
|
+
// Step 5: Create app directory and copy infrastructure files
|
|
116
|
+
console.log(chalk.yellow('📦 Step 5: Setting up application on VPS...\n'));
|
|
141
117
|
|
|
142
118
|
const setupSpinner = ora('Creating app directory...').start();
|
|
143
119
|
|
|
144
120
|
try {
|
|
145
121
|
// Create infrastructure directory on VPS
|
|
146
122
|
await executeSSH(vpsUser, vpsHost, `mkdir -p ${vpsAppFolder}/infrastructure`);
|
|
147
|
-
|
|
123
|
+
|
|
148
124
|
setupSpinner.text = 'Copying infrastructure files to VPS...';
|
|
149
125
|
|
|
150
126
|
// Copy entire infrastructure directory to VPS
|
|
@@ -158,6 +134,22 @@ async function deployInit() {
|
|
|
158
134
|
process.exit(1);
|
|
159
135
|
}
|
|
160
136
|
|
|
137
|
+
// Create symlink for docker-compose.override.yml -> docker-compose.prod.yml
|
|
138
|
+
const symlinkSpinner = ora('Creating docker-compose.override.yml symlink...').start();
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
await executeSSH(
|
|
142
|
+
vpsUser,
|
|
143
|
+
vpsHost,
|
|
144
|
+
`cd ${vpsAppFolder}/infrastructure && ln -sf docker-compose.prod.yml docker-compose.override.yml`
|
|
145
|
+
);
|
|
146
|
+
symlinkSpinner.succeed('Docker Compose override symlink created');
|
|
147
|
+
} catch (error) {
|
|
148
|
+
symlinkSpinner.fail('Failed to create symlink');
|
|
149
|
+
console.log(chalk.red(`\n❌ Error: ${error.message}\n`));
|
|
150
|
+
process.exit(1);
|
|
151
|
+
}
|
|
152
|
+
|
|
161
153
|
// Check if waitlist is running and stop it (full-app deployment)
|
|
162
154
|
try {
|
|
163
155
|
const { stdout: psOutput } = await executeSSH(
|
|
@@ -189,8 +181,8 @@ async function deployInit() {
|
|
|
189
181
|
// If error, waitlist probably not running - continue
|
|
190
182
|
}
|
|
191
183
|
|
|
192
|
-
// Step
|
|
193
|
-
console.log(chalk.yellow('\n📄 Step
|
|
184
|
+
// Step 6: Copy .env.prod to VPS (overwrites .env copied from infrastructure/)
|
|
185
|
+
console.log(chalk.yellow('\n📄 Step 6: Configuring production environment...\n'));
|
|
194
186
|
|
|
195
187
|
const envSpinner = ora('Copying .env.prod to VPS...').start();
|
|
196
188
|
|
|
@@ -204,27 +196,18 @@ async function deployInit() {
|
|
|
204
196
|
process.exit(1);
|
|
205
197
|
}
|
|
206
198
|
|
|
207
|
-
// Step
|
|
208
|
-
console.log(chalk.yellow('\n🐳 Step
|
|
199
|
+
// Step 7: Pull Docker images
|
|
200
|
+
console.log(chalk.yellow('\n🐳 Step 7: Pulling Docker images on VPS...\n'));
|
|
209
201
|
console.log(chalk.gray('This may take several minutes...\n'));
|
|
210
202
|
|
|
211
|
-
const dockerSpinner = ora('Pulling Docker images...').start();
|
|
212
|
-
|
|
213
203
|
try {
|
|
214
|
-
await
|
|
215
|
-
vpsUser,
|
|
216
|
-
vpsHost,
|
|
217
|
-
`cd ${vpsAppFolder}/infrastructure && docker-compose -f docker-compose.yml -f docker-compose.prod.yml pull`,
|
|
218
|
-
{ timeout: 600000 } // 10 minutes for image pull
|
|
219
|
-
);
|
|
220
|
-
dockerSpinner.succeed('Docker images pulled successfully');
|
|
204
|
+
await pullImagesOnVPS(vpsUser, vpsHost, vpsAppFolder);
|
|
221
205
|
} catch (error) {
|
|
222
|
-
dockerSpinner.fail('Failed to pull Docker images');
|
|
223
206
|
console.log(chalk.yellow(`\n⚠️ Warning: ${error.message}\n`));
|
|
224
207
|
console.log(chalk.gray('This might mean Docker is not installed on the VPS.'));
|
|
225
208
|
console.log(chalk.gray('Please install Docker and Docker Compose:\n'));
|
|
226
209
|
console.log(chalk.white(' curl -fsSL https://get.docker.com | sh'));
|
|
227
|
-
console.log(chalk.white(
|
|
210
|
+
console.log(chalk.white(` sudo usermod -aG docker ${vpsUser}\n`));
|
|
228
211
|
process.exit(1);
|
|
229
212
|
}
|
|
230
213
|
|
|
@@ -71,7 +71,7 @@ async function deploySetEnv() {
|
|
|
71
71
|
// Generate secure defaults
|
|
72
72
|
const dbPassword = generateSecret(24);
|
|
73
73
|
const redisPassword = generateSecret(24);
|
|
74
|
-
const
|
|
74
|
+
const betterAuthSecret = generateSecret(32);
|
|
75
75
|
const bullAdminToken = generateSecret(32);
|
|
76
76
|
|
|
77
77
|
const answers = await inquirer.prompt([
|
|
@@ -91,9 +91,9 @@ async function deploySetEnv() {
|
|
|
91
91
|
},
|
|
92
92
|
{
|
|
93
93
|
type: 'password',
|
|
94
|
-
name: '
|
|
95
|
-
message: '
|
|
96
|
-
default:
|
|
94
|
+
name: 'betterAuthSecret',
|
|
95
|
+
message: 'Better Auth secret (min 32 chars):',
|
|
96
|
+
default: betterAuthSecret,
|
|
97
97
|
mask: '*'
|
|
98
98
|
},
|
|
99
99
|
{
|
|
@@ -143,7 +143,7 @@ async function deploySetEnv() {
|
|
|
143
143
|
|
|
144
144
|
replacements['DB_PASSWORD'] = answers.dbPassword;
|
|
145
145
|
replacements['REDIS_PASSWORD'] = answers.redisPassword;
|
|
146
|
-
replacements['
|
|
146
|
+
replacements['BETTER_AUTH_SECRET'] = answers.betterAuthSecret;
|
|
147
147
|
replacements['BULL_ADMIN_TOKEN'] = answers.bullAdminToken;
|
|
148
148
|
replacements['RESEND_API_KEY'] = answers.resendApiKey || 're_your_resend_api_key';
|
|
149
149
|
replacements['POLAR_ACCESS_TOKEN'] = answers.polarAccessToken || 'polar_oat_your_token';
|
|
@@ -163,11 +163,21 @@ async function deploySetEnv() {
|
|
|
163
163
|
// Update production URLs based on deployment config
|
|
164
164
|
if (config.deployment?.primaryDomain) {
|
|
165
165
|
const domain = config.deployment.primaryDomain;
|
|
166
|
+
const adminEmail = config.deployment.adminEmail || `admin@${domain}`;
|
|
167
|
+
|
|
168
|
+
// First, replace all {{PRIMARY_DOMAIN}} and {{ADMIN_EMAIL}} placeholders globally
|
|
169
|
+
envContent = envContent.split('{{PRIMARY_DOMAIN}}').join(domain);
|
|
170
|
+
envContent = envContent.split('{{ADMIN_EMAIL}}').join(adminEmail);
|
|
171
|
+
|
|
172
|
+
// Then update specific URL variables for production
|
|
166
173
|
const urlReplacements = {
|
|
174
|
+
'PRIMARY_DOMAIN': domain,
|
|
175
|
+
'NODE_ENV': 'production',
|
|
167
176
|
'API_BASE_URL': `https://api.${domain}`,
|
|
168
177
|
'ADMIN_BASE_URL': `https://admin.${domain}`,
|
|
169
|
-
'FRONTEND_BASE_URL': `https
|
|
170
|
-
'WEBSITE_BASE_URL': `https
|
|
178
|
+
'FRONTEND_BASE_URL': `https://${domain}`,
|
|
179
|
+
'WEBSITE_BASE_URL': `https://www.${domain}`,
|
|
180
|
+
'GOOGLE_REDIRECT_URI': `https://api.${domain}/auth/google/callback`
|
|
171
181
|
};
|
|
172
182
|
|
|
173
183
|
for (const [key, value] of Object.entries(urlReplacements)) {
|
|
@@ -105,8 +105,8 @@ async function deployUp() {
|
|
|
105
105
|
const verifySpinner = ora('Checking service status...').start();
|
|
106
106
|
|
|
107
107
|
try {
|
|
108
|
-
const { stdout: psOutput} = await execAsync(
|
|
109
|
-
`ssh ${vpsUser}@${vpsHost} "cd ${vpsAppFolder}/infrastructure && docker-compose -f docker-compose.yml ps"`,
|
|
108
|
+
const { stdout: psOutput } = await execAsync(
|
|
109
|
+
`ssh ${vpsUser}@${vpsHost} "cd ${vpsAppFolder}/infrastructure && docker-compose -f docker-compose.yml -f docker-compose.prod.yml ps"`,
|
|
110
110
|
{ timeout: 30000 }
|
|
111
111
|
);
|
|
112
112
|
|
|
@@ -114,6 +114,7 @@ async function deployUp() {
|
|
|
114
114
|
|
|
115
115
|
console.log(chalk.gray('\n' + psOutput));
|
|
116
116
|
} catch (error) {
|
|
117
|
+
console.error(chalk.red(`\n❌ Error: ${error.message}\n`));
|
|
117
118
|
verifySpinner.warn('Could not verify services');
|
|
118
119
|
}
|
|
119
120
|
|
|
@@ -138,7 +139,7 @@ async function deployUp() {
|
|
|
138
139
|
console.log(chalk.gray(' Just push to GitHub - CI/CD will handle deployment automatically!\n'));
|
|
139
140
|
|
|
140
141
|
console.log(chalk.white('3. Monitor services:'));
|
|
141
|
-
console.log(chalk.gray(` Run: ssh ${vpsUser}@${vpsHost} "cd ${vpsAppFolder}/infrastructure && docker-compose logs -f"\n`));
|
|
142
|
+
console.log(chalk.gray(` Run: ssh ${vpsUser}@${vpsHost} "cd ${vpsAppFolder}/infrastructure && docker-compose -f docker-compose.yml -f docker-compose.prod.yml logs -f"\n`));
|
|
142
143
|
}
|
|
143
144
|
|
|
144
145
|
module.exports = { deployUp };
|
package/src/commands/help.js
CHANGED
|
@@ -7,16 +7,19 @@ const { isLaunchFrameProject, isWaitlistInstalled } = require('../utils/project-
|
|
|
7
7
|
function help() {
|
|
8
8
|
const inProject = isLaunchFrameProject();
|
|
9
9
|
|
|
10
|
-
console.log(chalk.blue.bold('\
|
|
10
|
+
console.log(chalk.blue.bold('\nLaunchFrame CLI\n'));
|
|
11
11
|
console.log(chalk.white('Usage:'));
|
|
12
|
-
console.log(chalk.gray(' launchframe [command]\n'));
|
|
12
|
+
console.log(chalk.gray(' launchframe [command] [options]\n'));
|
|
13
|
+
console.log(chalk.white('Global options:'));
|
|
14
|
+
console.log(chalk.gray(' --verbose, -v Show detailed output\n'));
|
|
13
15
|
|
|
14
16
|
if (inProject) {
|
|
15
17
|
console.log(chalk.white('Deployment commands:'));
|
|
16
18
|
console.log(chalk.gray(' deploy:configure Configure production deployment settings'));
|
|
17
19
|
console.log(chalk.gray(' deploy:set-env Configure production environment variables'));
|
|
18
20
|
console.log(chalk.gray(' deploy:init Initialize VPS and build Docker images'));
|
|
19
|
-
console.log(chalk.gray(' deploy:up Start services on VPS
|
|
21
|
+
console.log(chalk.gray(' deploy:up Start services on VPS'));
|
|
22
|
+
console.log(chalk.gray(' deploy:build [service] Build, push, and deploy (all or specific service)\n'));
|
|
20
23
|
|
|
21
24
|
// Conditionally show waitlist commands
|
|
22
25
|
if (isWaitlistInstalled()) {
|
|
@@ -40,7 +43,7 @@ function help() {
|
|
|
40
43
|
console.log(chalk.white('Available Services:'));
|
|
41
44
|
console.log(chalk.gray(' waitlist Coming soon page with email collection\n'));
|
|
42
45
|
console.log(chalk.white('Cache Management:'));
|
|
43
|
-
console.log(chalk.gray(' cache:info Show cache location, size, and cached
|
|
46
|
+
console.log(chalk.gray(' cache:info Show cache location, size, and cached services'));
|
|
44
47
|
console.log(chalk.gray(' cache:update Force update cache to latest version'));
|
|
45
48
|
console.log(chalk.gray(' cache:clear Delete cache (re-download on next use)\n'));
|
|
46
49
|
console.log(chalk.white('Other commands:'));
|
|
@@ -75,7 +78,7 @@ function help() {
|
|
|
75
78
|
console.log(chalk.gray(' --user-model <b2b|b2b2c> User model (skips prompt)'));
|
|
76
79
|
console.log(chalk.gray(' help Show this help message\n'));
|
|
77
80
|
console.log(chalk.white('Cache Management:'));
|
|
78
|
-
console.log(chalk.gray(' cache:info Show cache location, size, and cached
|
|
81
|
+
console.log(chalk.gray(' cache:info Show cache location, size, and cached services'));
|
|
79
82
|
console.log(chalk.gray(' cache:update Force update cache to latest version'));
|
|
80
83
|
console.log(chalk.gray(' cache:clear Delete cache (re-download on next use)\n'));
|
|
81
84
|
console.log(chalk.white('Examples:'));
|
|
@@ -83,6 +86,8 @@ function help() {
|
|
|
83
86
|
console.log(chalk.gray(' launchframe init\n'));
|
|
84
87
|
console.log(chalk.gray(' # Non-interactive mode'));
|
|
85
88
|
console.log(chalk.gray(' launchframe init --project-name my-saas --tenancy single --user-model b2b\n'));
|
|
89
|
+
console.log(chalk.gray(' # With verbose output'));
|
|
90
|
+
console.log(chalk.gray(' launchframe init --verbose\n'));
|
|
86
91
|
}
|
|
87
92
|
}
|
|
88
93
|
|