@launchframe/cli 1.0.0-beta.8 ā 1.0.0
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/CODE_OF_CONDUCT.md +128 -0
- package/LICENSE +21 -0
- package/README.md +7 -1
- package/package.json +9 -6
- 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 +10 -3
- package/src/commands/deploy-init.js +24 -57
- package/src/commands/deploy-set-env.js +17 -7
- package/src/commands/deploy-sync-features.js +233 -0
- package/src/commands/deploy-up.js +4 -3
- package/src/commands/dev-add-user.js +165 -0
- package/src/commands/dev-logo.js +160 -0
- package/src/commands/dev-npm-install.js +33 -0
- package/src/commands/dev-queue.js +85 -0
- package/src/commands/docker-build.js +9 -6
- package/src/commands/help.js +35 -11
- package/src/commands/init.js +48 -56
- 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/module.js +146 -0
- package/src/commands/service.js +6 -6
- package/src/commands/waitlist-deploy.js +1 -0
- package/src/generator.js +43 -42
- package/src/index.js +109 -4
- package/src/services/module-config.js +25 -0
- package/src/services/module-registry.js +12 -0
- package/src/services/variant-config.js +24 -13
- 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/module-installer.js +58 -0
- package/src/utils/project-helpers.js +34 -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
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# Contributor Covenant Code of Conduct
|
|
2
|
+
|
|
3
|
+
## Our Pledge
|
|
4
|
+
|
|
5
|
+
We as members, contributors, and leaders pledge to make participation in our
|
|
6
|
+
community a harassment-free experience for everyone, regardless of age, body
|
|
7
|
+
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
|
8
|
+
identity and expression, level of experience, education, socio-economic status,
|
|
9
|
+
nationality, personal appearance, race, religion, or sexual identity
|
|
10
|
+
and orientation.
|
|
11
|
+
|
|
12
|
+
We pledge to act and interact in ways that contribute to an open, welcoming,
|
|
13
|
+
diverse, inclusive, and healthy community.
|
|
14
|
+
|
|
15
|
+
## Our Standards
|
|
16
|
+
|
|
17
|
+
Examples of behavior that contributes to a positive environment for our
|
|
18
|
+
community include:
|
|
19
|
+
|
|
20
|
+
* Demonstrating empathy and kindness toward other people
|
|
21
|
+
* Being respectful of differing opinions, viewpoints, and experiences
|
|
22
|
+
* Giving and gracefully accepting constructive feedback
|
|
23
|
+
* Accepting responsibility and apologizing to those affected by our mistakes,
|
|
24
|
+
and learning from the experience
|
|
25
|
+
* Focusing on what is best not just for us as individuals, but for the
|
|
26
|
+
overall community
|
|
27
|
+
|
|
28
|
+
Examples of unacceptable behavior include:
|
|
29
|
+
|
|
30
|
+
* The use of sexualized language or imagery, and sexual attention or
|
|
31
|
+
advances of any kind
|
|
32
|
+
* Trolling, insulting or derogatory comments, and personal or political attacks
|
|
33
|
+
* Public or private harassment
|
|
34
|
+
* Publishing others' private information, such as a physical or email
|
|
35
|
+
address, without their explicit permission
|
|
36
|
+
* Other conduct which could reasonably be considered inappropriate in a
|
|
37
|
+
professional setting
|
|
38
|
+
|
|
39
|
+
## Enforcement Responsibilities
|
|
40
|
+
|
|
41
|
+
Community leaders are responsible for clarifying and enforcing our standards of
|
|
42
|
+
acceptable behavior and will take appropriate and fair corrective action in
|
|
43
|
+
response to any behavior that they deem inappropriate, threatening, offensive,
|
|
44
|
+
or harmful.
|
|
45
|
+
|
|
46
|
+
Community leaders have the right and responsibility to remove, edit, or reject
|
|
47
|
+
comments, commits, code, wiki edits, issues, and other contributions that are
|
|
48
|
+
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
|
49
|
+
decisions when appropriate.
|
|
50
|
+
|
|
51
|
+
## Scope
|
|
52
|
+
|
|
53
|
+
This Code of Conduct applies within all community spaces, and also applies when
|
|
54
|
+
an individual is officially representing the community in public spaces.
|
|
55
|
+
Examples of representing our community include using an official e-mail address,
|
|
56
|
+
posting via an official social media account, or acting as an appointed
|
|
57
|
+
representative at an online or offline event.
|
|
58
|
+
|
|
59
|
+
## Enforcement
|
|
60
|
+
|
|
61
|
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
|
62
|
+
reported to the community leaders responsible for enforcement at
|
|
63
|
+
.
|
|
64
|
+
All complaints will be reviewed and investigated promptly and fairly.
|
|
65
|
+
|
|
66
|
+
All community leaders are obligated to respect the privacy and security of the
|
|
67
|
+
reporter of any incident.
|
|
68
|
+
|
|
69
|
+
## Enforcement Guidelines
|
|
70
|
+
|
|
71
|
+
Community leaders will follow these Community Impact Guidelines in determining
|
|
72
|
+
the consequences for any action they deem in violation of this Code of Conduct:
|
|
73
|
+
|
|
74
|
+
### 1. Correction
|
|
75
|
+
|
|
76
|
+
**Community Impact**: Use of inappropriate language or other behavior deemed
|
|
77
|
+
unprofessional or unwelcome in the community.
|
|
78
|
+
|
|
79
|
+
**Consequence**: A private, written warning from community leaders, providing
|
|
80
|
+
clarity around the nature of the violation and an explanation of why the
|
|
81
|
+
behavior was inappropriate. A public apology may be requested.
|
|
82
|
+
|
|
83
|
+
### 2. Warning
|
|
84
|
+
|
|
85
|
+
**Community Impact**: A violation through a single incident or series
|
|
86
|
+
of actions.
|
|
87
|
+
|
|
88
|
+
**Consequence**: A warning with consequences for continued behavior. No
|
|
89
|
+
interaction with the people involved, including unsolicited interaction with
|
|
90
|
+
those enforcing the Code of Conduct, for a specified period of time. This
|
|
91
|
+
includes avoiding interactions in community spaces as well as external channels
|
|
92
|
+
like social media. Violating these terms may lead to a temporary or
|
|
93
|
+
permanent ban.
|
|
94
|
+
|
|
95
|
+
### 3. Temporary Ban
|
|
96
|
+
|
|
97
|
+
**Community Impact**: A serious violation of community standards, including
|
|
98
|
+
sustained inappropriate behavior.
|
|
99
|
+
|
|
100
|
+
**Consequence**: A temporary ban from any sort of interaction or public
|
|
101
|
+
communication with the community for a specified period of time. No public or
|
|
102
|
+
private interaction with the people involved, including unsolicited interaction
|
|
103
|
+
with those enforcing the Code of Conduct, is allowed during this period.
|
|
104
|
+
Violating these terms may lead to a permanent ban.
|
|
105
|
+
|
|
106
|
+
### 4. Permanent Ban
|
|
107
|
+
|
|
108
|
+
**Community Impact**: Demonstrating a pattern of violation of community
|
|
109
|
+
standards, including sustained inappropriate behavior, harassment of an
|
|
110
|
+
individual, or aggression toward or disparagement of classes of individuals.
|
|
111
|
+
|
|
112
|
+
**Consequence**: A permanent ban from any sort of public interaction within
|
|
113
|
+
the community.
|
|
114
|
+
|
|
115
|
+
## Attribution
|
|
116
|
+
|
|
117
|
+
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
|
118
|
+
version 2.0, available at
|
|
119
|
+
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
|
120
|
+
|
|
121
|
+
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
|
122
|
+
enforcement ladder](https://github.com/mozilla/diversity).
|
|
123
|
+
|
|
124
|
+
[homepage]: https://www.contributor-covenant.org
|
|
125
|
+
|
|
126
|
+
For answers to common questions about this code of conduct, see the FAQ at
|
|
127
|
+
https://www.contributor-covenant.org/faq. Translations are available at
|
|
128
|
+
https://www.contributor-covenant.org/translations.
|
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
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "Production-ready B2B SaaS boilerplate with subscriptions, credits, and multi-tenancy",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -28,21 +28,24 @@
|
|
|
28
28
|
"homepage": "https://launchframe.dev",
|
|
29
29
|
"repository": {
|
|
30
30
|
"type": "git",
|
|
31
|
-
"url": "git+https://github.com/launchframe/cli.git"
|
|
31
|
+
"url": "git+https://github.com/launchframe-dev/cli.git"
|
|
32
32
|
},
|
|
33
33
|
"bugs": {
|
|
34
|
-
"url": "https://github.com/launchframe/cli/issues"
|
|
34
|
+
"url": "https://github.com/launchframe-dev/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
|
+
"@resvg/resvg-js": "^2.6.2",
|
|
44
|
+
"bcryptjs": "^2.4.3",
|
|
43
45
|
"chalk": "^4.1.2",
|
|
46
|
+
"dotenv": "^17.3.1",
|
|
44
47
|
"fs-extra": "^11.1.1",
|
|
45
|
-
"
|
|
46
|
-
"
|
|
48
|
+
"inquirer": "^8.2.5",
|
|
49
|
+
"to-ico": "^1.1.5"
|
|
47
50
|
}
|
|
48
51
|
}
|
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 };
|
|
@@ -53,14 +53,16 @@ async function deployConfigure() {
|
|
|
53
53
|
'{{PRIMARY_DOMAIN}}': deployAnswers.primaryDomain,
|
|
54
54
|
'{{ADMIN_EMAIL}}': deployAnswers.adminEmail,
|
|
55
55
|
'{{GITHUB_ORG}}': deployAnswers.githubOrg,
|
|
56
|
-
'{{VPS_APP_FOLDER}}': deployAnswers.vpsAppFolder
|
|
56
|
+
'{{VPS_APP_FOLDER}}': deployAnswers.vpsAppFolder,
|
|
57
|
+
'{{PROJECT_NAME}}': config.projectName,
|
|
58
|
+
'{{PROJECT_NAME_UPPER}}': config.projectName.toUpperCase()
|
|
57
59
|
};
|
|
58
60
|
|
|
59
61
|
console.log(chalk.yellow('\nāļø Updating configuration files...\n'));
|
|
60
62
|
|
|
61
63
|
// Files that need template variable replacement
|
|
64
|
+
// Note: infrastructure/.env is NOT updated - it's for local development only
|
|
62
65
|
const filesToUpdate = [
|
|
63
|
-
'infrastructure/.env',
|
|
64
66
|
'infrastructure/.env.example',
|
|
65
67
|
'infrastructure/docker-compose.yml',
|
|
66
68
|
'infrastructure/docker-compose.dev.yml',
|
|
@@ -74,6 +76,10 @@ async function deployConfigure() {
|
|
|
74
76
|
'admin-portal/src/App.tsx',
|
|
75
77
|
'admin-portal/src/components/common/PageTitle.tsx',
|
|
76
78
|
'admin-portal/src/sentry.tsx',
|
|
79
|
+
'backend/.github/workflows/deploy-backend.yml',
|
|
80
|
+
'admin-portal/.github/workflows/deploy-admin-portal.yml',
|
|
81
|
+
'website/.github/workflows/deploy-website.yml',
|
|
82
|
+
'infrastructure/scripts/zero-downtime-deploy.sh',
|
|
77
83
|
];
|
|
78
84
|
|
|
79
85
|
if (config.variants.tenancy === 'multi-tenant') {
|
|
@@ -86,7 +92,8 @@ async function deployConfigure() {
|
|
|
86
92
|
if (config.variants.userModel === 'b2b2c') {
|
|
87
93
|
filesToUpdate.push(
|
|
88
94
|
'admin-portal/src/components/settings/CustomDomain.tsx',
|
|
89
|
-
'customers-portal/src/App.tsx'
|
|
95
|
+
'customers-portal/src/App.tsx',
|
|
96
|
+
'customers-portal/.github/workflows/deploy-customers-portal.yml',
|
|
90
97
|
)
|
|
91
98
|
}
|
|
92
99
|
|
|
@@ -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
|
|