@launchframe/cli 1.0.0-beta.3 ā 1.0.0-beta.30
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/settings.local.json +12 -0
- package/CLAUDE.md +27 -0
- package/LICENSE +21 -0
- package/README.md +7 -1
- package/package.json +4 -3
- package/src/commands/cache.js +14 -14
- package/src/commands/database-console.js +84 -0
- package/src/commands/deploy-build.js +76 -0
- package/src/commands/deploy-configure.js +15 -6
- package/src/commands/deploy-init.js +24 -57
- package/src/commands/deploy-set-env.js +17 -7
- package/src/commands/deploy-up.js +4 -3
- package/src/commands/dev-add-user.js +165 -0
- package/src/commands/dev-queue.js +85 -0
- package/src/commands/help.js +29 -11
- package/src/commands/init.js +49 -64
- package/src/commands/migration-create.js +40 -0
- package/src/commands/migration-revert.js +32 -0
- package/src/commands/migration-run.js +32 -0
- 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 +81 -12
- 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/telemetry.js +238 -0
- package/src/utils/variable-replacer.js +18 -23
- 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/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 LaunchFrame
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@launchframe/cli",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.30",
|
|
4
4
|
"description": "Production-ready B2B SaaS boilerplate with subscriptions, credits, and multi-tenancy",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -34,15 +34,16 @@
|
|
|
34
34
|
"url": "https://github.com/launchframe/cli/issues"
|
|
35
35
|
},
|
|
36
36
|
"engines": {
|
|
37
|
-
"node": ">=
|
|
37
|
+
"node": ">=22.0.0"
|
|
38
38
|
},
|
|
39
39
|
"publishConfig": {
|
|
40
40
|
"access": "public"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
+
"bcryptjs": "^2.4.3",
|
|
43
44
|
"chalk": "^4.1.2",
|
|
45
|
+
"dotenv": "^17.3.1",
|
|
44
46
|
"fs-extra": "^11.1.1",
|
|
45
|
-
"glob": "^10.3.10",
|
|
46
47
|
"inquirer": "^8.2.5"
|
|
47
48
|
}
|
|
48
49
|
}
|
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,84 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
const inquirer = require('inquirer');
|
|
5
|
+
const { spawnSync } = require('child_process');
|
|
6
|
+
const { requireProject, getProjectConfig } = require('../utils/project-helpers');
|
|
7
|
+
|
|
8
|
+
async function databaseConsole({ remote = false } = {}) {
|
|
9
|
+
requireProject();
|
|
10
|
+
|
|
11
|
+
const infrastructurePath = path.join(process.cwd(), 'infrastructure');
|
|
12
|
+
|
|
13
|
+
if (!fs.existsSync(infrastructurePath)) {
|
|
14
|
+
console.error(chalk.red('\nā Error: infrastructure/ directory not found'));
|
|
15
|
+
console.log(chalk.gray('Make sure you are in the root of your LaunchFrame project.\n'));
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (remote) {
|
|
20
|
+
// 1. Check deployment is configured
|
|
21
|
+
const config = getProjectConfig();
|
|
22
|
+
|
|
23
|
+
if (!config.deployConfigured || !config.deployment) {
|
|
24
|
+
console.error(chalk.red('\nā Deployment is not configured.'));
|
|
25
|
+
console.log(chalk.gray('Run deploy:configure first.\n'));
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const { vpsUser, vpsHost, vpsAppFolder } = config.deployment;
|
|
30
|
+
|
|
31
|
+
// 2. Warn before connecting to production
|
|
32
|
+
console.log(chalk.yellow.bold('\nā ļø You are about to connect to the PRODUCTION database.\n'));
|
|
33
|
+
console.log(chalk.gray(` Host: ${vpsHost}`));
|
|
34
|
+
console.log(chalk.gray(` Folder: ${vpsAppFolder}\n`));
|
|
35
|
+
|
|
36
|
+
const { confirmed } = await inquirer.prompt([
|
|
37
|
+
{
|
|
38
|
+
type: 'confirm',
|
|
39
|
+
name: 'confirmed',
|
|
40
|
+
message: 'Are you sure you want to open a console to the production database?',
|
|
41
|
+
default: false
|
|
42
|
+
}
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
if (!confirmed) {
|
|
46
|
+
console.log(chalk.gray('\nAborted.\n'));
|
|
47
|
+
process.exit(0);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
console.log(chalk.blue.bold('\nš Connecting to production database...\n'));
|
|
51
|
+
|
|
52
|
+
// 3. Let the shell inside the container expand $POSTGRES_USER / $POSTGRES_DB.
|
|
53
|
+
// Pass the remote command as a single ssh argument (spawnSync array form)
|
|
54
|
+
// so the local shell never touches it.
|
|
55
|
+
const remoteCmd = `cd ${vpsAppFolder}/infrastructure && docker compose -f docker-compose.yml -f docker-compose.prod.yml exec -it database sh -c 'psql -U $POSTGRES_USER $POSTGRES_DB'`;
|
|
56
|
+
|
|
57
|
+
const result = spawnSync('ssh', ['-t', `${vpsUser}@${vpsHost}`, remoteCmd], { stdio: 'inherit' });
|
|
58
|
+
|
|
59
|
+
if (result.status !== 0) {
|
|
60
|
+
console.error(chalk.red('\nā Could not connect to the production database.'));
|
|
61
|
+
console.log(chalk.gray('Check that the VPS is reachable and services are running.\n'));
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
} else {
|
|
65
|
+
console.log(chalk.blue.bold('\nšļø Opening local database console...\n'));
|
|
66
|
+
|
|
67
|
+
// Let the shell inside the container expand $POSTGRES_USER / $POSTGRES_DB
|
|
68
|
+
const psqlCmd = [
|
|
69
|
+
'compose', '-f', 'docker-compose.yml', '-f', 'docker-compose.dev.yml',
|
|
70
|
+
'exec', 'database', 'sh', '-c', 'psql -U $POSTGRES_USER $POSTGRES_DB'
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
const result = spawnSync('docker', psqlCmd, { cwd: infrastructurePath, stdio: 'inherit' });
|
|
74
|
+
|
|
75
|
+
if (result.status !== 0) {
|
|
76
|
+
console.error(chalk.red('\nā Could not connect to the local database container.'));
|
|
77
|
+
console.log(chalk.gray('Make sure services are running:'));
|
|
78
|
+
console.log(chalk.white(' launchframe docker:up\n'));
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
module.exports = { databaseConsole };
|
|
@@ -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
|
|
@@ -205,8 +181,8 @@ async function deployInit() {
|
|
|
205
181
|
// If error, waitlist probably not running - continue
|
|
206
182
|
}
|
|
207
183
|
|
|
208
|
-
// Step
|
|
209
|
-
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'));
|
|
210
186
|
|
|
211
187
|
const envSpinner = ora('Copying .env.prod to VPS...').start();
|
|
212
188
|
|
|
@@ -220,27 +196,18 @@ async function deployInit() {
|
|
|
220
196
|
process.exit(1);
|
|
221
197
|
}
|
|
222
198
|
|
|
223
|
-
// Step
|
|
224
|
-
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'));
|
|
225
201
|
console.log(chalk.gray('This may take several minutes...\n'));
|
|
226
202
|
|
|
227
|
-
const dockerSpinner = ora('Pulling Docker images...').start();
|
|
228
|
-
|
|
229
203
|
try {
|
|
230
|
-
await
|
|
231
|
-
vpsUser,
|
|
232
|
-
vpsHost,
|
|
233
|
-
`cd ${vpsAppFolder}/infrastructure && docker-compose -f docker-compose.yml -f docker-compose.prod.yml pull`,
|
|
234
|
-
{ timeout: 600000 } // 10 minutes for image pull
|
|
235
|
-
);
|
|
236
|
-
dockerSpinner.succeed('Docker images pulled successfully');
|
|
204
|
+
await pullImagesOnVPS(vpsUser, vpsHost, vpsAppFolder);
|
|
237
205
|
} catch (error) {
|
|
238
|
-
dockerSpinner.fail('Failed to pull Docker images');
|
|
239
206
|
console.log(chalk.yellow(`\nā ļø Warning: ${error.message}\n`));
|
|
240
207
|
console.log(chalk.gray('This might mean Docker is not installed on the VPS.'));
|
|
241
208
|
console.log(chalk.gray('Please install Docker and Docker Compose:\n'));
|
|
242
209
|
console.log(chalk.white(' curl -fsSL https://get.docker.com | sh'));
|
|
243
|
-
console.log(chalk.white(
|
|
210
|
+
console.log(chalk.white(` sudo usermod -aG docker ${vpsUser}\n`));
|
|
244
211
|
process.exit(1);
|
|
245
212
|
}
|
|
246
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 };
|