@launchframe/cli 0.1.11 ā 1.0.0-beta.1
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 +40 -11
- package/package.json +5 -5
- package/src/commands/cache.js +102 -0
- package/src/commands/deploy-configure.js +21 -4
- package/src/commands/deploy-init.js +24 -58
- package/src/commands/deploy-set-env.js +68 -91
- package/src/commands/docker-destroy.js +45 -15
- package/src/commands/docker-up.js +42 -16
- package/src/commands/help.js +11 -1
- package/src/commands/init.js +89 -55
- package/src/commands/service.js +64 -40
- package/src/commands/waitlist-deploy.js +2 -2
- package/src/commands/waitlist-logs.js +1 -2
- package/src/commands/waitlist-up.js +50 -15
- package/src/generator.js +13 -4
- package/src/index.js +16 -2
- package/src/prompts.js +12 -0
- package/src/services/registry.js +8 -6
- package/src/services/variant-config.js +135 -37
- package/src/utils/docker-helper.js +56 -44
- package/src/utils/github-access.js +67 -0
- package/src/utils/module-cache.js +274 -0
- package/src/utils/project-helpers.js +1 -1
- package/src/utils/section-replacer.js +32 -15
- package/src/utils/variable-replacer.js +7 -2
- package/src/utils/variant-processor.js +24 -12
package/README.md
CHANGED
|
@@ -4,13 +4,6 @@
|
|
|
4
4
|
|
|
5
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
6
|
|
|
7
|
-
## Status
|
|
8
|
-
|
|
9
|
-
LaunchFrame is currently in **private beta**. This CLI exists but does not yet generate projects.
|
|
10
|
-
|
|
11
|
-
**[Join the waitlist at launchframe.dev](https://launchframe.dev)** to get early access, exclusive updates, and founding member perks.
|
|
12
|
-
|
|
13
|
-
Here's a sneak peek of the CLI experience:
|
|
14
7
|

|
|
15
8
|
|
|
16
9
|
## What You Get
|
|
@@ -30,14 +23,39 @@ Here's a sneak peek of the CLI experience:
|
|
|
30
23
|
## Installation
|
|
31
24
|
|
|
32
25
|
```bash
|
|
33
|
-
|
|
26
|
+
npm install -g @launchframe/cli
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Quick Start
|
|
30
|
+
|
|
31
|
+
Initialize a new LaunchFrame project:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
launchframe init
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Local Development
|
|
38
|
+
|
|
39
|
+
Start the full stack locally with Docker:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
launchframe docker:up
|
|
34
43
|
```
|
|
35
44
|
|
|
36
|
-
|
|
45
|
+
This spins up all services (backend, admin portal, customer portal, website, database, etc.) with hot-reload enabled. Build your domain logic, customize the UI, and test everything locally.
|
|
46
|
+
|
|
47
|
+
### Deployment
|
|
48
|
+
|
|
49
|
+
When you're ready to deploy to your VPS:
|
|
37
50
|
|
|
38
|
-
|
|
51
|
+
```bash
|
|
52
|
+
launchframe deploy:configure # Set up deployment configuration
|
|
53
|
+
launchframe deploy:set-env # Configure environment variables
|
|
54
|
+
launchframe deploy:init # Initialize the VPS
|
|
55
|
+
launchframe deploy:up # Deploy to production
|
|
56
|
+
```
|
|
39
57
|
|
|
40
|
-
|
|
58
|
+
**[Get started at launchframe.dev](https://launchframe.dev)** | **[Read the docs at docs.launchframe.dev](https://docs.launchframe.dev)**
|
|
41
59
|
|
|
42
60
|
## Why LaunchFrame?
|
|
43
61
|
|
|
@@ -53,6 +71,17 @@ Most SaaS boilerplates give you authentication and a database. LaunchFrame gives
|
|
|
53
71
|
|
|
54
72
|
All tested in production. All ready to customize.
|
|
55
73
|
|
|
74
|
+
## Documentation
|
|
75
|
+
|
|
76
|
+
Full documentation is available at **[docs.launchframe.dev](https://docs.launchframe.dev)**, including:
|
|
77
|
+
|
|
78
|
+
- Getting started guides
|
|
79
|
+
- Architecture overview
|
|
80
|
+
- Deployment instructions
|
|
81
|
+
- API reference
|
|
82
|
+
- Feature customization guides
|
|
83
|
+
- Multi-tenancy patterns
|
|
84
|
+
|
|
56
85
|
## License
|
|
57
86
|
|
|
58
87
|
MIT
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@launchframe/cli",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "1.0.0-beta.1",
|
|
4
4
|
"description": "Production-ready B2B SaaS boilerplate with subscriptions, credits, and multi-tenancy",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
7
|
-
"launchframe": "
|
|
7
|
+
"launchframe": "src/index.js"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"start": "node src/index.js"
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"homepage": "https://launchframe.dev",
|
|
29
29
|
"repository": {
|
|
30
30
|
"type": "git",
|
|
31
|
-
"url": "https://github.com/launchframe/cli"
|
|
31
|
+
"url": "git+https://github.com/launchframe/cli.git"
|
|
32
32
|
},
|
|
33
33
|
"bugs": {
|
|
34
34
|
"url": "https://github.com/launchframe/cli/issues"
|
|
@@ -40,9 +40,9 @@
|
|
|
40
40
|
"access": "public"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"inquirer": "^8.2.5",
|
|
44
43
|
"chalk": "^4.1.2",
|
|
45
44
|
"fs-extra": "^11.1.1",
|
|
46
|
-
"glob": "^10.3.10"
|
|
45
|
+
"glob": "^10.3.10",
|
|
46
|
+
"inquirer": "^8.2.5"
|
|
47
47
|
}
|
|
48
48
|
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
const chalk = require('chalk');
|
|
2
|
+
const { clearCache, getCacheInfo } = require('../utils/module-cache');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Clear module cache
|
|
6
|
+
*/
|
|
7
|
+
async function cacheClear() {
|
|
8
|
+
console.log(chalk.yellow('\nā ļø This will delete all cached modules'));
|
|
9
|
+
console.log(chalk.gray('You will need to re-download on next init or service:add\n'));
|
|
10
|
+
|
|
11
|
+
const inquirer = require('inquirer');
|
|
12
|
+
const { confirmed } = await inquirer.prompt([{
|
|
13
|
+
type: 'confirm',
|
|
14
|
+
name: 'confirmed',
|
|
15
|
+
message: 'Continue with cache clear?',
|
|
16
|
+
default: false
|
|
17
|
+
}]);
|
|
18
|
+
|
|
19
|
+
if (!confirmed) {
|
|
20
|
+
console.log('Cancelled');
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
await clearCache();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Show cache information
|
|
29
|
+
*/
|
|
30
|
+
async function cacheInfo() {
|
|
31
|
+
const info = await getCacheInfo();
|
|
32
|
+
|
|
33
|
+
console.log(chalk.blue('\nš¦ Module Cache Information\n'));
|
|
34
|
+
|
|
35
|
+
console.log(chalk.white('Location:'));
|
|
36
|
+
console.log(chalk.gray(` ${info.path}\n`));
|
|
37
|
+
|
|
38
|
+
if (!info.exists) {
|
|
39
|
+
console.log(chalk.yellow('Status: Not initialized'));
|
|
40
|
+
console.log(chalk.gray('Cache will be created on first use\n'));
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
console.log(chalk.green('Status: Active\n'));
|
|
45
|
+
|
|
46
|
+
if (info.size !== undefined) {
|
|
47
|
+
const sizeMB = (info.size / 1024 / 1024).toFixed(2);
|
|
48
|
+
console.log(chalk.white('Size:'));
|
|
49
|
+
console.log(chalk.gray(` ${sizeMB} MB\n`));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (info.lastUpdate) {
|
|
53
|
+
console.log(chalk.white('Last Updated:'));
|
|
54
|
+
console.log(chalk.gray(` ${info.lastUpdate.toLocaleString()}\n`));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (info.modules && info.modules.length > 0) {
|
|
58
|
+
console.log(chalk.white('Cached Modules:'));
|
|
59
|
+
info.modules.forEach(mod => {
|
|
60
|
+
console.log(chalk.gray(` ⢠${mod}`));
|
|
61
|
+
});
|
|
62
|
+
console.log('');
|
|
63
|
+
} else {
|
|
64
|
+
console.log(chalk.gray('No modules cached yet\n'));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
console.log(chalk.gray('Commands:'));
|
|
68
|
+
console.log(chalk.gray(' launchframe cache:clear - Delete cache'));
|
|
69
|
+
console.log(chalk.gray(' launchframe cache:update - Force update\n'));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Force update cache
|
|
74
|
+
*/
|
|
75
|
+
async function cacheUpdate() {
|
|
76
|
+
const { ensureCacheReady, getCacheInfo } = require('../utils/module-cache');
|
|
77
|
+
|
|
78
|
+
console.log(chalk.blue('\nš Forcing cache update...\n'));
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
const info = await getCacheInfo();
|
|
82
|
+
const currentModules = info.modules || [];
|
|
83
|
+
|
|
84
|
+
if (currentModules.length === 0) {
|
|
85
|
+
console.log(chalk.yellow('No modules in cache yet. Use init or service:add to populate.\n'));
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Update cache with current modules
|
|
90
|
+
await ensureCacheReady(currentModules);
|
|
91
|
+
console.log(chalk.green('\nā Cache updated successfully\n'));
|
|
92
|
+
} catch (error) {
|
|
93
|
+
console.error(chalk.red(`\nā Failed to update cache: ${error.message}\n`));
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
module.exports = {
|
|
99
|
+
cacheClear,
|
|
100
|
+
cacheInfo,
|
|
101
|
+
cacheUpdate
|
|
102
|
+
};
|
|
@@ -61,7 +61,24 @@ async function deployConfigure() {
|
|
|
61
61
|
// Files that need template variable replacement
|
|
62
62
|
const filesToUpdate = [
|
|
63
63
|
'infrastructure/.env',
|
|
64
|
-
'infrastructure/.env.example'
|
|
64
|
+
'infrastructure/.env.example',
|
|
65
|
+
'infrastructure/docker-compose.yml',
|
|
66
|
+
'infrastructure/docker-compose.dev.yml',
|
|
67
|
+
'infrastructure/docker-compose.prod.yml',
|
|
68
|
+
'infrastructure/traefik.yml',
|
|
69
|
+
'backend/src/main.ts',
|
|
70
|
+
'admin-portal/.env.example',
|
|
71
|
+
'admin-portal/public/env-config.js',
|
|
72
|
+
'admin-portal/src/config/runtime.ts',
|
|
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
|
+
'admin-portal/src/App.tsx',
|
|
78
|
+
'admin-portal/src/components/common/PageTitle.tsx',
|
|
79
|
+
'admin-portal/src/sentry.tsx',
|
|
80
|
+
'admin-portal/src/pages/AppSumo.tsx',
|
|
81
|
+
'customers-portal/src/App.tsx'
|
|
65
82
|
];
|
|
66
83
|
|
|
67
84
|
const projectRoot = process.cwd();
|
|
@@ -134,14 +151,14 @@ async function deployConfigure() {
|
|
|
134
151
|
|
|
135
152
|
const updatedConfig = {
|
|
136
153
|
...config,
|
|
137
|
-
primaryDomain: deployAnswers.primaryDomain,
|
|
138
|
-
githubOrg: deployAnswers.githubOrg,
|
|
139
|
-
vpsAppFolder: deployAnswers.vpsAppFolder,
|
|
140
154
|
deployConfigured: true,
|
|
141
155
|
deployment: {
|
|
142
156
|
adminEmail: deployAnswers.adminEmail,
|
|
143
157
|
vpsHost: deployAnswers.vpsHost,
|
|
144
158
|
vpsUser: deployAnswers.vpsUser,
|
|
159
|
+
vpsAppFolder: deployAnswers.vpsAppFolder,
|
|
160
|
+
primaryDomain: deployAnswers.primaryDomain,
|
|
161
|
+
githubOrg: deployAnswers.githubOrg,
|
|
145
162
|
ghcrToken: deployAnswers.ghcrToken,
|
|
146
163
|
configuredAt: new Date().toISOString()
|
|
147
164
|
}
|
|
@@ -9,13 +9,11 @@ const {
|
|
|
9
9
|
checkSSHKeys,
|
|
10
10
|
executeSSH,
|
|
11
11
|
copyFileToVPS,
|
|
12
|
-
copyDirectoryToVPS
|
|
13
|
-
checkRepoPrivacy,
|
|
14
|
-
showDeployKeyInstructions
|
|
12
|
+
copyDirectoryToVPS
|
|
15
13
|
} = require('../utils/ssh-helper');
|
|
16
14
|
|
|
17
15
|
/**
|
|
18
|
-
* Initial VPS setup -
|
|
16
|
+
* Initial VPS setup - copy infrastructure files and configure environment
|
|
19
17
|
*/
|
|
20
18
|
async function deployInit() {
|
|
21
19
|
requireProject();
|
|
@@ -119,8 +117,9 @@ async function deployInit() {
|
|
|
119
117
|
// Login to GHCR
|
|
120
118
|
await loginToGHCR(githubOrg, ghcrToken);
|
|
121
119
|
|
|
122
|
-
// Build full-app images
|
|
123
|
-
|
|
120
|
+
// Build full-app images (only for installed services)
|
|
121
|
+
const installedServices = config.installedServices || ['backend', 'admin-portal', 'website'];
|
|
122
|
+
await buildFullAppImages(projectRoot, projectName, githubOrg, envProdPath, installedServices);
|
|
124
123
|
|
|
125
124
|
console.log(chalk.green.bold('\nā
All images built and pushed to GHCR!\n'));
|
|
126
125
|
} catch (error) {
|
|
@@ -137,57 +136,24 @@ async function deployInit() {
|
|
|
137
136
|
}
|
|
138
137
|
|
|
139
138
|
|
|
140
|
-
// Step 4:
|
|
141
|
-
console.log(chalk.yellow('
|
|
139
|
+
// Step 4: Create app directory and copy infrastructure files
|
|
140
|
+
console.log(chalk.yellow('š¦ Step 4: Setting up application on VPS...\n'));
|
|
142
141
|
|
|
143
|
-
const
|
|
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();
|
|
142
|
+
const setupSpinner = ora('Creating app directory...').start();
|
|
156
143
|
|
|
157
144
|
try {
|
|
158
|
-
// Create directory
|
|
159
|
-
await executeSSH(vpsUser, vpsHost, `mkdir -p ${vpsAppFolder}`);
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
}
|
|
145
|
+
// Create infrastructure directory on VPS
|
|
146
|
+
await executeSSH(vpsUser, vpsHost, `mkdir -p ${vpsAppFolder}/infrastructure`);
|
|
147
|
+
|
|
148
|
+
setupSpinner.text = 'Copying infrastructure files to VPS...';
|
|
149
|
+
|
|
150
|
+
// Copy entire infrastructure directory to VPS
|
|
151
|
+
const infrastructurePath = path.join(projectRoot, 'infrastructure');
|
|
152
|
+
await copyDirectoryToVPS(infrastructurePath, vpsUser, vpsHost, `${vpsAppFolder}/infrastructure`);
|
|
187
153
|
|
|
188
|
-
|
|
154
|
+
setupSpinner.succeed('Infrastructure files copied to VPS');
|
|
189
155
|
} catch (error) {
|
|
190
|
-
|
|
156
|
+
setupSpinner.fail('Failed to copy infrastructure files');
|
|
191
157
|
console.log(chalk.red(`\nā Error: ${error.message}\n`));
|
|
192
158
|
process.exit(1);
|
|
193
159
|
}
|
|
@@ -223,23 +189,23 @@ async function deployInit() {
|
|
|
223
189
|
// If error, waitlist probably not running - continue
|
|
224
190
|
}
|
|
225
191
|
|
|
226
|
-
// Step
|
|
227
|
-
console.log(chalk.yellow('\nš Step
|
|
192
|
+
// Step 5: Copy .env.prod to VPS (overwrites .env copied from infrastructure/)
|
|
193
|
+
console.log(chalk.yellow('\nš Step 5: Configuring production environment...\n'));
|
|
228
194
|
|
|
229
195
|
const envSpinner = ora('Copying .env.prod to VPS...').start();
|
|
230
196
|
|
|
231
197
|
try {
|
|
232
198
|
const remoteEnvPath = `${vpsAppFolder}/infrastructure/.env`;
|
|
233
199
|
await copyFileToVPS(envProdPath, vpsUser, vpsHost, remoteEnvPath);
|
|
234
|
-
envSpinner.succeed('.env.prod copied successfully');
|
|
200
|
+
envSpinner.succeed('.env.prod copied as .env successfully');
|
|
235
201
|
} catch (error) {
|
|
236
202
|
envSpinner.fail('Failed to copy .env.prod');
|
|
237
203
|
console.log(chalk.red(`\nā Error: ${error.message}\n`));
|
|
238
204
|
process.exit(1);
|
|
239
205
|
}
|
|
240
206
|
|
|
241
|
-
// Step
|
|
242
|
-
console.log(chalk.yellow('\nš³ Step
|
|
207
|
+
// Step 6: Pull Docker images
|
|
208
|
+
console.log(chalk.yellow('\nš³ Step 6: Pulling Docker images...\n'));
|
|
243
209
|
console.log(chalk.gray('This may take several minutes...\n'));
|
|
244
210
|
|
|
245
211
|
const dockerSpinner = ora('Pulling Docker images...').start();
|
|
@@ -266,7 +232,7 @@ async function deployInit() {
|
|
|
266
232
|
console.log(chalk.green.bold('\nā
Initial VPS setup complete!\n'));
|
|
267
233
|
|
|
268
234
|
console.log(chalk.white('Summary:'));
|
|
269
|
-
console.log(chalk.gray(` -
|
|
235
|
+
console.log(chalk.gray(` - Infrastructure copied to: ${vpsAppFolder}`));
|
|
270
236
|
console.log(chalk.gray(' - Production .env configured'));
|
|
271
237
|
console.log(chalk.gray(' - Docker images pulled\n'));
|
|
272
238
|
|
|
@@ -75,77 +75,65 @@ async function deploySetEnv() {
|
|
|
75
75
|
const bullAdminToken = generateSecret(32);
|
|
76
76
|
|
|
77
77
|
const answers = await inquirer.prompt([
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
type: 'input',
|
|
138
|
-
name: 'googleClientId',
|
|
139
|
-
message: 'Google OAuth Client ID:',
|
|
140
|
-
default: ''
|
|
141
|
-
},
|
|
142
|
-
{
|
|
143
|
-
type: 'input',
|
|
144
|
-
name: 'googleClientSecret',
|
|
145
|
-
message: 'Google OAuth Client Secret:',
|
|
146
|
-
default: ''
|
|
147
|
-
}
|
|
148
|
-
]);
|
|
78
|
+
{
|
|
79
|
+
type: 'password',
|
|
80
|
+
name: 'dbPassword',
|
|
81
|
+
message: 'Database password:',
|
|
82
|
+
default: dbPassword,
|
|
83
|
+
mask: '*'
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
type: 'password',
|
|
87
|
+
name: 'redisPassword',
|
|
88
|
+
message: 'Redis password:',
|
|
89
|
+
default: redisPassword,
|
|
90
|
+
mask: '*'
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
type: 'password',
|
|
94
|
+
name: 'jwtSecret',
|
|
95
|
+
message: 'JWT secret:',
|
|
96
|
+
default: jwtSecret,
|
|
97
|
+
mask: '*'
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
type: 'password',
|
|
101
|
+
name: 'bullAdminToken',
|
|
102
|
+
message: 'Bull Admin token:',
|
|
103
|
+
default: bullAdminToken,
|
|
104
|
+
mask: '*'
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
type: 'input',
|
|
108
|
+
name: 'resendApiKey',
|
|
109
|
+
message: 'Resend API key (email):',
|
|
110
|
+
default: ''
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
type: 'input',
|
|
114
|
+
name: 'polarAccessToken',
|
|
115
|
+
message: 'Polar Access Token:',
|
|
116
|
+
default: ''
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
type: 'input',
|
|
120
|
+
name: 'polarWebhookSecret',
|
|
121
|
+
message: 'Polar Webhook Secret:',
|
|
122
|
+
default: ''
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
type: 'input',
|
|
126
|
+
name: 'googleClientId',
|
|
127
|
+
message: 'Google OAuth Client ID:',
|
|
128
|
+
default: ''
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
type: 'input',
|
|
132
|
+
name: 'googleClientSecret',
|
|
133
|
+
message: 'Google OAuth Client Secret:',
|
|
134
|
+
default: ''
|
|
135
|
+
}
|
|
136
|
+
]);
|
|
149
137
|
|
|
150
138
|
// Read current .env.prod content
|
|
151
139
|
let envContent = await fs.readFile(envProdPath, 'utf8');
|
|
@@ -153,26 +141,15 @@ async function deploySetEnv() {
|
|
|
153
141
|
// Replace values based on deployment mode
|
|
154
142
|
const replacements = {};
|
|
155
143
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
replacements['REDIS_PASSWORD'] = answers.redisPassword;
|
|
166
|
-
replacements['JWT_SECRET'] = answers.jwtSecret;
|
|
167
|
-
replacements['BULL_ADMIN_TOKEN'] = answers.bullAdminToken;
|
|
168
|
-
replacements['RESEND_API_KEY'] = answers.resendApiKey || 're_your_resend_api_key';
|
|
169
|
-
replacements['OPENAI_API_KEY'] = answers.openaiApiKey || 'sk-your_openai_api_key';
|
|
170
|
-
replacements['CLAUDE_API_KEY'] = answers.claudeApiKey || 'sk-ant-your_claude_api_key';
|
|
171
|
-
replacements['POLAR_ACCESS_TOKEN'] = answers.polarAccessToken || 'polar_oat_your_token';
|
|
172
|
-
replacements['POLAR_WEBHOOK_SECRET'] = answers.polarWebhookSecret || 'polar_whs_your_secret';
|
|
173
|
-
replacements['GOOGLE_CLIENT_ID'] = answers.googleClientId || 'YOUR_GOOGLE_CLIENT_ID';
|
|
174
|
-
replacements['GOOGLE_CLIENT_SECRET'] = answers.googleClientSecret || 'YOUR_GOOGLE_CLIENT_SECRET';
|
|
175
|
-
}
|
|
144
|
+
replacements['DB_PASSWORD'] = answers.dbPassword;
|
|
145
|
+
replacements['REDIS_PASSWORD'] = answers.redisPassword;
|
|
146
|
+
replacements['JWT_SECRET'] = answers.jwtSecret;
|
|
147
|
+
replacements['BULL_ADMIN_TOKEN'] = answers.bullAdminToken;
|
|
148
|
+
replacements['RESEND_API_KEY'] = answers.resendApiKey || 're_your_resend_api_key';
|
|
149
|
+
replacements['POLAR_ACCESS_TOKEN'] = answers.polarAccessToken || 'polar_oat_your_token';
|
|
150
|
+
replacements['POLAR_WEBHOOK_SECRET'] = answers.polarWebhookSecret || 'polar_whs_your_secret';
|
|
151
|
+
replacements['GOOGLE_CLIENT_ID'] = answers.googleClientId || 'YOUR_GOOGLE_CLIENT_ID';
|
|
152
|
+
replacements['GOOGLE_CLIENT_SECRET'] = answers.googleClientSecret || 'YOUR_GOOGLE_CLIENT_SECRET';
|
|
176
153
|
|
|
177
154
|
// Update environment variables
|
|
178
155
|
for (const [key, value] of Object.entries(replacements)) {
|
|
@@ -47,36 +47,66 @@ async function dockerDestroy(options = {}) {
|
|
|
47
47
|
|
|
48
48
|
console.log(chalk.yellow('\nšļø Destroying Docker resources...\n'));
|
|
49
49
|
|
|
50
|
-
// Step 1: Stop
|
|
51
|
-
console.log(chalk.gray('Stopping
|
|
50
|
+
// Step 1: Stop all running containers first
|
|
51
|
+
console.log(chalk.gray('Stopping running containers...'));
|
|
52
52
|
try {
|
|
53
|
-
execSync(`docker ps
|
|
53
|
+
const runningContainerIds = execSync(`docker ps --filter "name=${projectName}" -q`, { encoding: 'utf8' }).trim();
|
|
54
|
+
if (runningContainerIds) {
|
|
55
|
+
const ids = runningContainerIds.replace(/\n/g, ' ');
|
|
56
|
+
// Use pipe mode instead of inherit to avoid Windows stdio issues
|
|
57
|
+
const output = execSync(`docker stop ${ids}`, { encoding: 'utf8' });
|
|
58
|
+
if (output) console.log(output);
|
|
59
|
+
}
|
|
60
|
+
} catch (error) {
|
|
61
|
+
// Ignore errors if no running containers
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Step 2: Remove all containers (running and stopped)
|
|
65
|
+
console.log(chalk.gray('Removing containers...'));
|
|
66
|
+
try {
|
|
67
|
+
const containerIds = execSync(`docker ps -a --filter "name=${projectName}" -q`, { encoding: 'utf8' }).trim();
|
|
68
|
+
if (containerIds) {
|
|
69
|
+
const ids = containerIds.replace(/\n/g, ' ');
|
|
70
|
+
const output = execSync(`docker rm -f ${ids}`, { encoding: 'utf8' });
|
|
71
|
+
if (output) console.log(output);
|
|
72
|
+
}
|
|
54
73
|
} catch (error) {
|
|
55
74
|
// Ignore errors if no containers found
|
|
56
75
|
}
|
|
57
76
|
|
|
58
|
-
// Step
|
|
59
|
-
console.log(chalk.gray('Removing
|
|
77
|
+
// Step 3: Remove network (must be after containers are removed)
|
|
78
|
+
console.log(chalk.gray('Removing network...'));
|
|
60
79
|
try {
|
|
61
|
-
execSync(`docker
|
|
80
|
+
const output = execSync(`docker network rm ${projectName}-network`, { encoding: 'utf8' });
|
|
81
|
+
if (output) console.log(output);
|
|
62
82
|
} catch (error) {
|
|
63
|
-
// Ignore errors if
|
|
83
|
+
// Ignore errors if network doesn't exist or has active endpoints
|
|
64
84
|
}
|
|
65
85
|
|
|
66
|
-
// Step
|
|
67
|
-
console.log(chalk.gray('Removing
|
|
86
|
+
// Step 4: Remove all volumes (must be after containers are removed)
|
|
87
|
+
console.log(chalk.gray('Removing volumes...'));
|
|
68
88
|
try {
|
|
69
|
-
execSync(`docker
|
|
89
|
+
const volumeIds = execSync(`docker volume ls --filter "name=${projectName}" -q`, { encoding: 'utf8' }).trim();
|
|
90
|
+
if (volumeIds) {
|
|
91
|
+
const ids = volumeIds.replace(/\n/g, ' ');
|
|
92
|
+
const output = execSync(`docker volume rm ${ids}`, { encoding: 'utf8' });
|
|
93
|
+
if (output) console.log(output);
|
|
94
|
+
}
|
|
70
95
|
} catch (error) {
|
|
71
|
-
// Ignore errors if no
|
|
96
|
+
// Ignore errors if no volumes found
|
|
72
97
|
}
|
|
73
98
|
|
|
74
|
-
// Step
|
|
75
|
-
console.log(chalk.gray('Removing
|
|
99
|
+
// Step 5: Remove all images (do this last)
|
|
100
|
+
console.log(chalk.gray('Removing images...'));
|
|
76
101
|
try {
|
|
77
|
-
execSync(`docker
|
|
102
|
+
const imageIds = execSync(`docker images --filter "reference=${projectName}*" -q`, { encoding: 'utf8' }).trim();
|
|
103
|
+
if (imageIds) {
|
|
104
|
+
const ids = imageIds.replace(/\n/g, ' ');
|
|
105
|
+
const output = execSync(`docker rmi -f ${ids}`, { encoding: 'utf8' });
|
|
106
|
+
if (output) console.log(output);
|
|
107
|
+
}
|
|
78
108
|
} catch (error) {
|
|
79
|
-
// Ignore errors if
|
|
109
|
+
// Ignore errors if no images found
|
|
80
110
|
}
|
|
81
111
|
|
|
82
112
|
console.log(chalk.green.bold('\nā
All Docker resources destroyed successfully!\n'));
|